Conventions
The rules every endpoint shares. The endpoint reference lists each operation, with the scope it needs and its request and answer fields.
Base URL and authentication
Every endpoint is under https://api.forgeling.io/v1. Requests and answers are JSON
(Content-Type: application/json), over HTTPS only: plain HTTP is not served, and not redirected.
Authenticate every request with an API key as a bearer token:
curl https://api.forgeling.io/v1/app \
-H "Authorization: Bearer $FORGELING_API_KEY"
GET /v1/app works with any valid key. It answers your app's tier, its quotas and current usage, and
the calling key's scopes and restrictions, so it is a good first call for checking a key.
Every answer carries a Request-Id header (req_…). Include it when you ask Forgeling about a request.
API keys
An API key has the form fgk_<kid>_<secret>: the prefix fgk_, a 16-character key id in lower-case
Crockford base32, _, and a 43-character base64url secret (256 bits). Forgeling stores only the key id
and a hash of the secret. The fixed prefix is registered with secret scanners.
- Management is in the console only, where your app's members create, roll and revoke keys. No scope can create, roll or revoke keys.
- The secret is shown once, when the key is created. Keep it in your server's secret store. A key never belongs in a browser, a mobile app or a repository.
- Rolling a key gives a new secret, and the old one keeps working for up to 24 hours while you deploy the new one.
- Revoking a key stops it at once; an event stream opened with it closes within a minute.
- Expiry. A key may have an
expires_at, after which it stops working.
Missing, malformed, revoked and expired keys all get the same answer, 401 invalid_api_key.
Scopes
A key carries a list of scopes, and each operation needs one of them; the reference
names it. GET /v1/app needs none.
| Scope | Grants |
|---|---|
workspaces:read |
Reading workspaces. |
workspaces:write |
Creating, changing, suspending, resuming and deleting workspaces. |
credentials:read |
Reading a workspace's model credential: its type, status and last four characters, never the key. |
credentials:write |
Storing and deleting a workspace's model credential. |
bundles:read |
Reading bundles and their versions. |
bundles:write |
Publishing and yanking bundle versions. |
runs:read |
Reading runs and turns. |
runs:write |
Starting, changing, completing, cancelling and deleting runs; sending and cancelling turns; run secrets. |
approvals:read |
Reading approvals. |
approvals:write |
Deciding approvals. |
events:read |
Reading events: a run's history, its live stream and the app-wide feed. |
files:read |
Reading output files and downloading their content. |
webhooks:read |
Reading webhook endpoints, the delivery log and the credential-refresh URL. |
webhooks:write |
Creating, changing, testing and deleting webhook endpoints, rotating their secrets and retrying deliveries. It also covers the credential-refresh URL, /v1/app/credential_refresh. |
usage:read |
Reading usage. |
A write scope does not imply read. A key with runs:write alone can start a run but cannot read it
back; give it runs:read as well. Give each key only the scopes its code uses.
Restricted keys
A key may be restricted to some workspaces, by workspace_ids or by an external_id_prefix (one key for
your development workspaces and another for production, say). Such a key gets 404 on
/v1/webhook_endpoints*, /v1/webhook_deliveries* and /v1/app/credential_refresh*, which are
app-level. List routes and GET /v1/events return only its workspaces (GET /v1/events leaves out
app-level events such as bundle.* and ping). Bundles belong to the whole app, so bundle routes work as
for any key. Creating a workspace whose external_id is outside the key's prefix, or with a key restricted
to workspace_ids, answers 404. A resource outside its restriction answers 404, never 403.
An app has at most 20 active keys.
Errors
An error answer has a 4xx or 5xx status and this body:
{"error": {"type": "permission", "code": "missing_scope", "message": "This key does not have the runs:write scope.", "request_id": "req_01M6H2C4Q8ZV3N7RT5WXKB9JDE", "details": {"required_scope": "runs:write"}}}
typeis the class of error andcodethe exact reason. Branch oncode;messageis for people and may change.request_idmatches the answer'sRequest-Idheader.paramnames the request field at fault, when there is one.detailsadds facts for some codes, as the table says.
| Status | Type | Codes |
|---|---|---|
| 400 | invalid_request |
malformed_json: the body is not valid JSON. idempotency_key_required: a POST …/runs or POST …/turns without an Idempotency-Key. unsupported_version: a Forgeling-Version other than 2026-11-01. |
| 401 | authentication |
invalid_api_key: the key is missing, malformed, revoked or expired. |
| 403 | permission |
missing_scope: the key lacks the operation's scope (details.required_scope). approval_required: your app is not approved yet, so it cannot start runs. |
| 404 | not_found |
not_found: no such route, or a resource that does not exist, belongs to another app, was deleted or is outside the key's restrictions. |
| 409 | conflict |
suspended: the app or the workspace is suspended. idempotency_conflict: an Idempotency-Key reused with a different body. credential_missing: the workspace has no valid model credential. quota_exceeded: a quota is used up (details.quota). run_not_active: the run has ended or is completing. turn_not_cancellable: the turn has already ended. bundle_version_exists: that version is already published. approval_not_pending: a decision on an approval that is no longer pending. |
| 422 | invalid_request |
validation_failed: the body breaks a rule (details.errors, a list of {path, message}). unsupported_credential_type: a model credential type other than anthropic_api_key. credential_invalid: Anthropic refused the model key. tier_limit: a value above your tier's maximum (details.field, details.max). bundle_version_unavailable: the bundle version is not ready, or is yanked. repo_host_unsupported: the repository is not on github.com. repo_policy_ack_required: a repository set to allow_unprotected without repo_policy_ack, the record of its admin's opt-in. |
| 429 | rate_limit |
rate_limited: too many requests, or too many open event streams; wait as long as Retry-After says. |
| 500, 502, 503 | api_error |
internal: something failed on Forgeling's side. unavailable: Forgeling cannot serve the request right now; try again shortly. |
A request that fails several checks gets the first of these: a bad key (401), the rate limit (429), an unknown route (404), a missing scope (403), a resource the key cannot reach (404), a suspended app or workspace (409). A resource you cannot reach answers 404, never 403, so an answer never tells you whether another app's resource exists.
Retry a 429 after Retry-After, and a 500, 502 or 503 with backoff; send a retried POST with the
same Idempotency-Key. Any other 4xx fails the same way until the request changes.
Idempotency
Idempotency-Key is required on POST …/runs and POST …/turns and accepted on every other POST.
Use a value from your own system that names the action, such as pipeline-<id>, not a fresh random
value on each attempt: a retry must carry the same key.
- Keys are kept 24 hours, per app.
- A repeat with the same body gets the original answer again, with the header
Idempotent-Replayed: true, and does nothing more. Two attempts that overlap still act once. - A repeat with a different body gets 409
idempotency_conflict. - On turns the key is also stored on the turn, as its
idempotency_key, unique per run and kept for 7 days. If both the answer and the webhook are lost, find the turn withGET /v1/turns?idempotency_key=(addrun=to look in one run).
Pagination
List endpoints take ?after= and ?limit= (default 20, max 100) and answer:
{"object": "list", "data": [], "has_more": false, "next_after": null}
While has_more is true, pass next_after back as after to get the next page:
curl "https://api.forgeling.io/v1/workspaces?limit=100&after=$NEXT_AFTER" \
-H "Authorization: Bearer $FORGELING_API_KEY"
Treat next_after as an opaque cursor and pass it back unchanged; never build one yourself. One list
differs: the after of GET /v1/runs/{run}/events is a stream.seq, so you can read a run's history
from the last event you applied (Order and reconcile).
Ids, timestamps and money
- Ids are a type prefix and a ULID, such as
run_01M6GYS6C0XNXDF714KDYE7SQ2:app_,key_,ws_,run_,trn_,apr_,evt_,file_,whe_,whd_andreq_. Store them as opaque strings. - Timestamps are RFC 3339 in UTC, with milliseconds:
2026-11-03T15:00:00.000Z. - Money is a JSON number of US dollars with at most 6 decimals, such as
"cost_usd": 0.412873. Token counts andcost_usdcome from the sandbox, which the agent controls; Anthropic's own usage figures for the key are the ones to trust (Security model).
Rate limits
Requests are limited per key, as a rate with a burst; the values for each tier are on the
limits page, and GET /v1/app returns yours in quotas. A request over the limit gets 429
rate_limited with a Retry-After header, in seconds. Wait that long before the next request.
Versioning
The major version is in the path. /v1 changes only by adding: new endpoints, fields, event types,
enum values and error codes.
A dated Forgeling-Version header selects the version of the API's behaviour. There is one date so far,
Forgeling-Version: 2026-11-01; any other value is 400 unsupported_version. Your app is pinned to the
date of its first call, and each webhook endpoint to the api_version it was created with, so
deliveries keep the shape your receiver was written for.
A breaking change ships only under a new date or as /v2. An old version stays supported for at least
12 months, and its end is announced at least 6 months ahead, with Deprecation and Sunset headers on
its answers, a changelog entry and an email to your app's members.
Beta features sit behind the Forgeling-Beta header, a comma-separated list of features, such as
Forgeling-Beta: runtime-next.
The runtime. Runs pin the Claude Code CLI through their run image, which a run keeps for its whole
life. A new run image whose CLI changes how tool rules match is announced at least 30 days ahead. You can
try it early by starting a run with "runtime": "next" and the header Forgeling-Beta: runtime-next, and
keep the previous image for 90 days after the switch with "runtime": "previous".
Runtime pinning has the details.
Unknown fields
Clients must ignore unknown fields, event types and enum values. Additions arrive within /v1 without a
new version, so read the fields you know and skip the rest, and handle an error code you do not
recognise by its type and HTTP status.