# AfterPack Cloud API reference

The wire contract for api.afterpack.dev: authentication, the batch obfuscation endpoint, limits, rate limits, the build time budget, and the diagnostic codes a client has to handle.

Source: https://www.afterpack.dev/docs/cloud-api

`@afterpack/core` and every [framework plugin](https://www.afterpack.dev/docs/frameworks) already speak this protocol once a [Pro key](https://www.afterpack.dev/docs/dashboard/api-keys) is configured: auth, multipart streaming, retries, all of it. Read this when wiring a custom CI integration, writing a client outside the official packages, or debugging a response your tooling did not expect.

## Base URL & versioning

Base URL: **`https://api.afterpack.dev/v1/`**

The API is URL-versioned and `v1` is the only version. Within it, response fields are added, never removed or repurposed; a breaking change would ship as `v2` alongside it. `GET /v1/version` returns the deployed version and environment, and `GET /` is an unauthenticated liveness check.

There is no client-version handshake.

## Authentication

Two credentials, and they are not interchangeable.

| Credential | Header | What it reaches |
|---|---|---|
| **A Pro API key** | `Authorization: Bearer ap_v1_<...>` | `POST /v1/obfuscate/batch` and `GET /v1/usage`. The key derives its own workspace, so no workspace id is sent. |
| **A dashboard session** | The session your browser holds after [signing in](https://www.afterpack.dev/docs/dashboard) | Everything that *manages* an account: creating, listing and revoking keys, plans, billing, members, projects. Takes an explicit `workspaceId` and a [workspace role](https://www.afterpack.dev/docs/dashboard/workspaces). |

A key is a build credential. It obfuscates and reports its own usage, and it cannot mint another key, read billing, or change a workspace. That containment is the point: a key sitting in CI can spend your allowance, and nothing else.

Keys are `ap_v1_` followed by 40 base62 characters. A key is **shown exactly once**, at creation. Only a salted HMAC-SHA256 hash is stored server-side, so a raw key is never recoverable later, only revocable. Mint one in the [dashboard](https://www.afterpack.dev/docs/dashboard/api-keys).

*(Pro cloud build request-flow diagram — see the web page)*

## Endpoints

### `POST /v1/obfuscate/batch`

The only obfuscation endpoint. One call is one build. Results stream back as each file finishes.

**Request**: `multipart/form-data`.

| Part | Required | Description |
|---|---|---|
| `batch` | Yes | The batch manifest, as a JSON **string** part. |
| one part per file | Yes | The JS source, under the `partName` its manifest entry declares. |
| one part per source map | Optional | An upstream source map, under that entry's `sourceMapPartName`. |

The manifest is:

```jsonc
{
  "version": "1",
  "files": [
    {
      "filePath": "assets/app-a1b2c3.js",  // echoed back on the result part
      "partName": "file0",                 // names the form part carrying the source
      "sourceMapPartName": "map0",         // optional
      "config": { "regions": [ /* ... */ ] } // optional, per-file
    }
  ],
  "config": { "preset": "hard", "seed": 42 }, // batch-level EngineConfig
  "git": { "commitSha": "…", "ref": "…" }     // optional build attribution
}
```

The batch-level `config` is an engine config object: every key on [Configuration](https://www.afterpack.dev/docs/config), including [`preset`](https://www.afterpack.dev/docs/config#preset), [`seed`](https://www.afterpack.dev/docs/config#seed) and [`regions`](https://www.afterpack.dev/docs/config#regions). `filePath` and `git` are manifest-level fields; see the manifest shape above.

`filePath` may not contain a quote or a CR/LF character. It is echoed verbatim into the result part's `Content-Disposition`, and a malformed path is rejected.

```bash
curl https://api.afterpack.dev/v1/obfuscate/batch \
  -H "Authorization: Bearer ap_v1_xxx" \
  -F 'batch={"version":"1","files":[{"filePath":"dist/app.js","partName":"file0"}],"config":{"preset":"hard"}}' \
  -F file0=@dist/app.js
```

**Response**: `200` with a streaming `multipart/mixed` body. Parts arrive as files finish, so write each one to disk as it lands rather than buffering the batch:

1. One **`obfuscation-result`** part per file: `{ version, filePath, status, output, outputMap, processingManifest }`. On a failure `output` and `outputMap` are `null` and `processingManifest.diagnostics` says why.
2. A final **`batch-summary`** part: `{ version, totalFiles, successCount, failureCount, engineVersion, results, totalDurationMs, totalInputBytes, totalOutputBytes }`.

A single file's failure does not abort the batch. Every other file still streams normally, and the summary reports the split.

### `GET /v1/usage`

Workspace usage and allowance. The one management-shaped read a key can do, so a CI job can check its own remaining allowance. Accepts **either** a Pro API key (which derives its own workspace) **or** a dashboard session plus `?workspaceId=`.

```jsonc
{
  "tier": "indie",
  "workspace": { "id": "…", "name": "…" },
  "allowance": { "remainingMb": 412.5, "totalMb": 500, "resetAt": "…" },
  "credits": { "balanceMb": 20 },
  "subscription": { "status": "active", "plan": "indie_monthly", "renewsAt": "…", "graceUntil": null, "lapsed": false },
  "recentBuilds": [
    {
      "buildId": "…", "builtAt": "…", "fileCount": 61,
      "totalBytes": 3145728, "outputBytes": 9437184,
      "meteredMb": 3.0, "allowanceMb": 3.0, "creditMb": 0, "overageMb": 0,
      "projectId": "…", "cloudMs": 39120
    }
  ]
}
```

`outputBytes` and `cloudMs` are `null` when unknown, never `0` as a stand-in. `allowanceMb` / `creditMb` / `overageMb` are the split of `meteredMb` across the three pools.

## Limits

| Limit | Value |
|---|---|
| Per file | 50 MB |
| Files per batch | 200 |
| Total batch bytes | 500 MB |

Over any of these, the request is rejected wholesale with `413` and `DIAG_FILE_TOO_LARGE` or `DIAG_BATCH_TOO_LARGE`, before anything is metered. The same numbers, from the dashboard's side, are in [API keys](https://www.afterpack.dev/docs/dashboard/api-keys).

## Rate limits

Counted over a rolling **60-second** window, per key and per client IP:

| Scope | Limit |
|---|---|
| Per key, free workspace | 10 requests / min |
| Per key, paid workspace | 100 requests / min |
| Per client IP | 100 requests / min |

Exceeding either scope returns `429` with `DIAG_RATE_LIMITED` and a `Retry-After` header set to the time until the triggering window actually drains. If the rate-limit check itself cannot run, the request fails closed with `503 DIAG_RATE_CHECK_UNAVAILABLE` and `Retry-After: 5`.

## The build time budget

A whole bundle is obfuscated inside one invocation, under a **120-second CPU limit**. Two wall-clock budgets keep a build within that limit.

| Elapsed | What happens |
|---|---|
| **45 s** | [Protection Map](https://www.afterpack.dev/docs/protection-map) capture stops. Obfuscation continues to completion; the stored map is marked partial with a `deadline` skip reason. |
| **90 s** | No further file is *started*. Every remaining file streams an explicit failure with `DIAG_BUILD_DEADLINE` naming how many of N completed, and the stream closes normally. |

If you hit the second budget, split the batch.

## Diagnostic codes

Errors carry a stable `code` alongside the HTTP status, the same `DIAG_*` namespace as the engine's own [diagnostics](https://www.afterpack.dev/docs/diagnostics). The ones a client has to handle:

| Code | Status | Meaning |
|---|---|---|
| `DIAG_INVALID_KEY` | 401 | Missing or unrecognised key. |
| `DIAG_KEY_REVOKED` | 401 | The key was revoked. |
| `DIAG_KEY_EXPIRED` | 401 | The key is past its `expiresAt`. |
| `DIAG_KEY_OUT_OF_SCOPE` | 403 | Valid key, used from an IP outside its `allowedIPs` list. |
| `DIAG_ENTITLEMENT_LAPSED` | 402 | Subscription payment past due. Carries `graceUntil` and `upgradeUrl`. |
| `DIAG_QUOTA_EXHAUSTED` | 402 | [Allowance + credits](https://www.afterpack.dev/docs/dashboard/usage) (+ [overage cap](https://www.afterpack.dev/docs/tiers), if enabled) can't cover this build. Carries `remainingMb`, `requestedMb` and a `reason` distinguishing "out of quota" from "overage cap reached". |
| `DIAG_RATE_LIMITED` | 429 | See rate limits above. Carries `retryAfterSeconds`. |
| `DIAG_RATE_CHECK_UNAVAILABLE` | 503 | Rate-limit check failed; failing closed. Retry. |
| `DIAG_INVALID_MANIFEST` | 400 | Malformed multipart body, missing `batch` part, or a manifest that fails validation. |
| `DIAG_FILE_TOO_LARGE` | 413 | One file over 50 MB. |
| `DIAG_BATCH_TOO_LARGE` | 413 | Over 200 files or 500 MB total. |
| `DIAG_BUILD_DEADLINE` | n/a | Per-file, inside a `200` stream. The build ran out of time budget before this file was started. |
| `DIAG_ENGINE_FAILURE` | n/a | Per-file, inside a `200` stream. The engine threw on this file. |

The last two are **not** HTTP statuses. They arrive as `status: "failure"` on an `obfuscation-result` part inside a successful response. A client that only inspects the HTTP status will mistake a partly-failed build for a clean one.

## Retries & backoff

| Status | Retryable | Behaviour |
|---|:---:|---|
| `400`, `401`, `402`, `403`, `413` | No | Fix the request. |
| `429` | Once | Honor `Retry-After` exactly, capped at 120 s; a second `429` fails closed. |
| `500` | No | Engine bug; report it. |
| `503`, `504` | Yes | 1 s, 4 s, 16 s (three retries, four attempts total), then fail closed. |

The official client also applies a 120-second whole-request timeout, and treats DNS, TLS and connection-reset failures as [unreachable-cloud](https://www.afterpack.dev/docs/deployment-modes) rather than retrying them indefinitely.

## Key lifecycle, from a client's side

Minting, scoping and revoking are [dashboard operations](https://www.afterpack.dev/docs/dashboard/api-keys). Two behaviours matter to code calling this API:

- **Mid-build revocation.** A key revoked while a batch streams returns `401` only on the *next* request; anything already in flight completes normally.
- **A key restricted to an IP list** returns `403 DIAG_KEY_OUT_OF_SCOPE` when used from anywhere else, distinct from the `401` an invalid or revoked key gets, so a client can tell "wrong key" from "right key, wrong place".

> **Metering is atomic, and it runs first**
>
> A batch that does not fit the workspace's pools is rejected wholesale *before* any file is dispatched, with the pools untouched: never partly processed, never partly billed. That is the batch-level form of the [fail-closed rule](https://www.afterpack.dev/docs/diagnostics#fail-closed-no-exceptions).

## Source-code handling

Source sent for obfuscation is processed **in memory only**, for the duration of that one request, and is never written to disk, logs, or a search index. Logs carry request ids, file counts, byte totals and diagnostic codes, never file contents, identifier names, or constant values.

One artifact outlives the request: the [Protection Map](https://www.afterpack.dev/docs/protection-map) stored per build for the dashboard. It contains your original source, coloured by how hard each token is to reverse. It is on by default per project, readable only by members of the owning workspace, retained one year **and** the last 500 builds per project (whichever runs out first), and switchable off in [project settings](https://www.afterpack.dev/docs/dashboard/workspaces). Full detail in [Privacy & data handling](https://www.afterpack.dev/docs/privacy).

## Next

- [Deployment modes](https://www.afterpack.dev/docs/deployment-modes): where this protocol fits relative to a local Free build.
- [Pro](https://www.afterpack.dev/docs/pro): what Pro adds on top of the local engine.
- [Privacy & data handling](https://www.afterpack.dev/docs/privacy): what is stored, for how long, and by whom it is readable.
- [Tiers](https://www.afterpack.dev/docs/tiers): MB allowances, overage, and what each plan unlocks against this API.
- [API keys](https://www.afterpack.dev/docs/dashboard/api-keys): minting, scoping, rotating and revoking the credential this API authenticates.
- [CLI reference](https://www.afterpack.dev/docs/cli): the official client that implements these rules.
- [Configuration](https://www.afterpack.dev/docs/config): the engine config the `config` field carries.
