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.

@afterpack/core and every framework plugin already speak this protocol once a Pro key 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.

CredentialHeaderWhat it reaches
A Pro API keyAuthorization: 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 sessionThe session your browser holds after signing inEverything that manages an account: creating, listing and revoking keys, plans, billing, members, projects. Takes an explicit workspaceId and a workspace role.

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.

Pro cloud batch-obfuscation request flowYour buildplugin / CLIPOST /v1/obfuscate/batchAuthorization: Bearer ap_…AfterPack cloudobfuscates each fileobfuscation is in-memoryper-file resultsBatch summarycounts + timingYour build outputprotected & written backFiles stream back one by one; the source sent for obfuscation is processed in memory, not stored.

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.

PartRequiredDescription
batchYesThe batch manifest, as a JSON string part.
one part per fileYesThe JS source, under the partName its manifest entry declares.
one part per source mapOptionalAn upstream source map, under that entry's sourceMapPartName.

The manifest is:

{
  "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, including preset, seed and 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.

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=.

{
  "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

LimitValue
Per file50 MB
Files per batch200
Total batch bytes500 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.

Rate limits

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

ScopeLimit
Per key, free workspace10 requests / min
Per key, paid workspace100 requests / min
Per client IP100 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.

ElapsedWhat happens
45 sProtection Map capture stops. Obfuscation continues to completion; the stored map is marked partial with a deadline skip reason.
90 sNo 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. The ones a client has to handle:

CodeStatusMeaning
DIAG_INVALID_KEY401Missing or unrecognised key.
DIAG_KEY_REVOKED401The key was revoked.
DIAG_KEY_EXPIRED401The key is past its expiresAt.
DIAG_KEY_OUT_OF_SCOPE403Valid key, used from an IP outside its allowedIPs list.
DIAG_ENTITLEMENT_LAPSED402Subscription payment past due. Carries graceUntil and upgradeUrl.
DIAG_QUOTA_EXHAUSTED402Allowance + credits (+ overage cap, if enabled) can't cover this build. Carries remainingMb, requestedMb and a reason distinguishing "out of quota" from "overage cap reached".
DIAG_RATE_LIMITED429See rate limits above. Carries retryAfterSeconds.
DIAG_RATE_CHECK_UNAVAILABLE503Rate-limit check failed; failing closed. Retry.
DIAG_INVALID_MANIFEST400Malformed multipart body, missing batch part, or a manifest that fails validation.
DIAG_FILE_TOO_LARGE413One file over 50 MB.
DIAG_BATCH_TOO_LARGE413Over 200 files or 500 MB total.
DIAG_BUILD_DEADLINEn/aPer-file, inside a 200 stream. The build ran out of time budget before this file was started.
DIAG_ENGINE_FAILUREn/aPer-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

StatusRetryableBehaviour
400, 401, 402, 403, 413NoFix the request.
429OnceHonor Retry-After exactly, capped at 120 s; a second 429 fails closed.
500NoEngine bug; report it.
503, 504Yes1 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 rather than retrying them indefinitely.

Key lifecycle, from a client's side

Minting, scoping and revoking are dashboard operations. 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.

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 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. Full detail in Privacy & data handling.

Next

  • Deployment modes: where this protocol fits relative to a local Free build.
  • Pro: what Pro adds on top of the local engine.
  • Privacy & data handling: what is stored, for how long, and by whom it is readable.
  • Tiers: MB allowances, overage, and what each plan unlocks against this API.
  • API keys: minting, scoping, rotating and revoking the credential this API authenticates.
  • CLI reference: the official client that implements these rules.
  • Configuration: the engine config the config field carries.