# Oakstack overview

Oakstack is one API key for the infrastructure every app ends up needing:

| Module    | What it does                                                                       | Status      |
| --------- | ---------------------------------------------------------------------------------- | ----------- |
| **clock** | Call a URL in your app on a cron schedule, with retries and a run log              | Available   |
| **hook**  | A permanent inbound URL for webhooks: store every event, forward it, retry, replay | Available   |
| **print** | HTML in, PDF out, with hosted templates                                            | Coming soon |
| **post**  | Transactional email, plus inbound email delivered to a hook                        | Coming soon |

It's built for coding agents. The SDK, the MCP server, and these docs are designed so an agent can set everything up from a one-line request like "run my cleanup every night at 3am Chicago time."

## Setup

1. Create an account and an API key at https://oakstack.dev/dashboard/api-keys.
2. Put the key in your app's environment as `OAKSTACK_API_KEY`. Keys start with `ok_live_` or `ok_test_`; never commit them.
3. Pick one or more ways in:

**SDK (TypeScript/JavaScript):**

```sh
npm install oakstack
```

```ts
import { Oakstack } from "oakstack";
const oakstack = new Oakstack(); // reads OAKSTACK_API_KEY
```

The SDK has zero dependencies and runs in Node 18+, Bun, Deno, Vercel Edge, and Cloudflare Workers.

**MCP (lets your coding agent manage Oakstack directly):**

```sh
# Hosted, nothing to install:
claude mcp add --transport http oakstack https://oakstack.dev/mcp --header "Authorization: Bearer $OAKSTACK_API_KEY"

# Or local over stdio:
claude mcp add oakstack -e OAKSTACK_API_KEY=ok_live_... -- npx -y oakstack-mcp
```

**REST:** every endpoint is under `https://oakstack.dev/api/v1/` and takes `Authorization: Bearer <key>`.

## Quick examples

```ts
// Call your app every weekday at 9:00 Chicago time.
const job = await oakstack.clock.jobs.create({
  name: "Morning digest",
  schedule: "0 9 * * 1-5",
  timezone: "America/Chicago",
  url: "https://yourapp.com/api/cron/digest",
});

// A permanent webhook URL for Stripe that never loses events.
const endpoint = await oakstack.hook.endpoints.create({
  name: "Stripe",
  forwardUrl: "https://yourapp.com/api/webhooks/stripe",
});
// Give endpoint.url to Stripe.
```

Requests Oakstack sends to your app are signed. Verify them with `verifySignature` (see [signatures](https://oakstack.dev/docs/signatures.md)).

## Errors

Every error response is JSON with a machine-readable `name` and a `message` that says what to fix:

```json
{
  "statusCode": 400,
  "name": "invalid_request",
  "message": "schedule: must be a 5-field cron expression ..."
}
```

The SDK throws these as `OakstackError` (`statusCode`, `errorName`, `message`).

| Status | `name`                                     | Meaning                                           | What to do                                                             |
| ------ | ------------------------------------------ | ------------------------------------------------- | ---------------------------------------------------------------------- |
| 400    | `invalid_request`                          | A field is missing or invalid                     | Fix the field named in `message`                                       |
| 400    | `invalid_idempotency_key`                  | Bad Idempotency-Key header                        | Use 1-255 printable characters, such as a UUID                         |
| 401    | `missing_api_key` / `invalid_api_key`      | No key, or the key is wrong, revoked, or expired  | Check `OAKSTACK_API_KEY`                                               |
| 403    | `restricted_api_key`                       | A standard key used on a full-access endpoint     | Use a full-access key                                                  |
| 403    | `workspace_paused` / `workspace_suspended` | The workspace has been stopped                    | See the dashboard                                                      |
| 404    | `not_found`                                | No such object in your workspace                  | Check the id                                                           |
| 409    | `idempotency_in_progress`                  | The same Idempotency-Key is still being processed | Retry shortly (the SDK does this)                                      |
| 422    | `idempotency_key_reused`                   | The same key was sent with a different request    | Use a new key                                                          |
| 429    | `rate_limited`                             | Too many requests this minute                     | Wait for `Retry-After` (the SDK does this)                             |
| 429    | `usage_limit_reached`                      | Your plan's limit is used up                      | Upgrade, free up capacity, or wait for the monthly reset. Don't retry. |
| 500    | `internal_error`                           | Something failed on Oakstack's side               | Retry (the SDK does this)                                              |

## Retries and idempotency

Send an `Idempotency-Key` header on any `POST`, `PATCH`, or `DELETE`. If you retry with the same key, you get the original response back instead of a second job or endpoint. Keys are remembered for 24 hours per workspace.

The SDK does this for you: every write gets a fresh key that's reused across its automatic retries. To make a write safe across process restarts too (for example, a setup script), pass your own key:

```ts
await oakstack.clock.jobs.create(params, { idempotencyKey: "setup-nightly-cleanup-v1" });
```

## Plans and limits

Limits are hard caps. When you reach one, Oakstack stops with `usage_limit_reached` instead of charging you more.

|                          | Free           | Builder ($20/mo) | Studio ($49/mo) |
| ------------------------ | -------------- | ---------------- | --------------- |
| Scheduled jobs           | 3              | 25               | 100             |
| Webhook events per month | 1,000          | 50,000           | 250,000         |
| PDFs per month (soon)    | 25             | 1,000            | 5,000           |
| Emails per month (soon)  | 100, test mode | 10,000           | 50,000          |
| API requests per minute  | 60             | 600              | 1,200           |
| Log retention            | 7 days         | 7 days           | 30 days         |

Monthly limits reset on the 1st (UTC). Check usage any time:

```ts
const { plan, usage } = await oakstack.usage();
```

Every API response also carries `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset` headers.

## What Oakstack will and won't call

Cron jobs and webhook forwarding only call **public `https://` URLs**. Localhost, private networks, and cloud metadata addresses are refused, including hostnames that resolve to them. Redirects are not followed. Your endpoint should answer `2xx` within 15 seconds; for longer work, answer `202` and continue in the background.

To test locally, expose your dev server with a tunnel (for example `cloudflared tunnel` or `ngrok`) and use its https URL.

## More

- [Scheduled jobs (clock)](https://oakstack.dev/docs/clock.md)
- [Webhook relay (hook)](https://oakstack.dev/docs/hook.md)
- [Verifying signatures](https://oakstack.dev/docs/signatures.md)
- Everything in one file: https://oakstack.dev/llms-full.txt
