Keys and safekeeping

A pipeline key is a credential with a fixed list of scopes. It acts for the account that owns it, and it can be limited to one project, an expiry date, a set of addresses and credit caps.

Who may hold a key

Any active account may hold and use a key. Starting a batch needs an active plan on the owning account (Creator, Pro or Max); without one the submit answers 403 batch_subscription_required and every other route keeps working. Nothing is deleted; batches can start again once a plan is active. An inactive or suspended account gets 403 account_inactive or 403 account_frozen.

An account can hold at most 5 active keys.

Creating a key

The panel sends the grant fields below on POST /api/keys, and the rest of this section is the reference for that call. It needs a signed-in account session: an API key cannot create, rotate or revoke keys.

Send any of these fields with name to make a pipeline key: preset, scopes, workspaceId, dailyCreditCap, perBatchCreditCap, maxActiveBatches, allowedCidrs, expiresInDays. Scopes are required, directly through scopes or through a preset.

POST /api/keys
{
  "name": "ci-submit",
  "preset": "submit_only",
  "dailyCreditCap": 100,
  "perBatchCreditCap": 50,
  "expiresInDays": 60,
  "allowedCidrs": ["203.0.113.0/24"]
}

The reply is the only time the full key is shown. It is stored as a hash and cannot be shown again.

response
{
  "id": "...",
  "name": "ci-submit",
  "key": "myrl_...",
  "prefix": "myrl_AbC1234",
  "created_at": "...",
  "message": "Save this key securely. It will not be shown again.",
  "expires_at": "...",
  "grant": {
    "apiKeyId": "...",
    "scopes": ["projects:read", "..."],
    "workspaceId": null,
    "dailyCreditCap": 100,
    "perBatchCreditCap": 50,
    "maxActiveBatches": 2,
    "allowedCidrs": ["203.0.113.0/24"],
    "disabledAt": null
  }
}

Other key routes, all session only: GET /api/keys lists keys with the prefix only, DELETE /api/keys/{id} revokes one, PATCH /api/keys/{id}/grant changes scopes, binding, caps, addresses or switches pipeline access off, POST /api/keys/{id}/rotate rotates, and GET /api/keys/{id}/audit reads the key's audit trail.

Scopes

A key can only hold scopes from this fixed list. Anything else is refused when the key is created. A route that needs a scope the key lacks answers 403 missing_scope and names the scope.

Pipeline key scopes and the routes that need them
ScopeRoutes that need it
projects:readGET /notifications GET /notifications/preferences GET /projects GET /projects/{slug} GET /projects/{slug}/assets GET /projects/{slug}/assets/{assetId} GET /projects/{slug}/assets/{assetId}/history GET /projects/{slug}/progress GET /projects/{slug}/quality
projects:writePUT /notifications/preferences POST /notifications/read-all POST /notifications/{notificationId}/read PUT /projects/{slug}
inputs:readNo route uses this scope yet.
inputs:writePOST /projects/{slug}/inputs
batches:readGET /batches/{batchId} GET /batches/{batchId}/events GET /batches/{batchId}/wait
batches:quoteGET /pricing POST /projects/{slug}/batches/quote POST /projects/{slug}/lint POST /projects/{slug}/resize POST /projects/{slug}/restore
batches:submitPOST /projects/{slug}/batches
batches:retryNo route uses this scope yet.
batches:cancelPOST /batches/{batchId}/cancel
results:readGET /batches/{batchId}/preview.png GET /batches/{batchId}/receipt GET /batches/{batchId}/result-url GET /batches/{batchId}/result.json GET /batches/{batchId}/result.zip GET /files/{fileId} GET /files/{fileId}/download GET /projects/{slug}/receipt GET /projects/{slug}/thumbnails
reviews:writePOST /projects/{slug}/evidence POST /projects/{slug}/reviews

GET /me and GET /capabilities need no scope. A signed-in session carries every scope and no project binding or caps.

Presets

A preset expands to a scope list when the key is created. The key stores the scopes, not the preset name. Extra scopes can be added next to a preset.

Key presets
PresetDefault expiryNeeds a projectScopes
modworks30 daysYes, workspaceIdprojects:read, projects:write, inputs:read, inputs:write, batches:read, batches:quote, batches:submit, batches:retry, batches:cancel, results:read, reviews:write
read_only90 daysNoprojects:read, inputs:read, batches:read, results:read
submit_only90 daysNoprojects:read, inputs:read, inputs:write, batches:read, batches:quote, batches:submit, results:read
  • read_only is for dashboards and status checks: every read scope and nothing else.
  • submit_only is for a CI job that uploads inputs, lints, quotes, submits and reads results. It cannot edit projects, submit reviews, retry or cancel.
  • modworks is the Mod Works integration key: bound to one project, 30 days by default. See Mod Works toolkit and MCP.

Limits on a key

Grant fields that limit a key
FieldWhat it does
workspaceIdBinds the key to one of your projects. A bound key sees only that project; any other slug answers 404 project_not_found.
expiresInDays1 to 365 days. A preset supplies a default. A key made from explicit scopes with no preset and no expiresInDays never expires, so set one.
allowedCidrsUp to 16 addresses or CIDR ranges. When set, a request from any other address gets 403 ip_not_allowed. An address the server cannot read is refused, not allowed.
dailyCreditCapCredits the key may reserve per day, up to 1,000,000. Omitted means 0.
perBatchCreditCapCredits one batch may cost, up to 1,000,000. Omitted means 0.
maxActiveBatchesBatches the key may have active at once, 0 to 20. Omitted means 20.

Sending a key

Send X-API-Key: myrl_... or Authorization: Bearer myrl_.... Sending both is 400 ambiguous_credentials. New keys are myrl_ followed by 40 random letters and digits and 6 checksum characters, 51 characters in all, matching myrl_[0-9A-Za-z]{46}. The first 12 characters are the prefix shown in lists, audit rows and notices. Older myrl_ and mk_live_ keys keep working. A key with a wrong checksum is refused before any lookup.

What Forge does to protect keys

  • Hashed storage. Only a SHA-256 hash of the key is stored. Forge cannot show you a key again.
  • Shown once. The full key appears in the create reply and nowhere else. Notices, logs and audit rows carry the prefix only, never the key, a key hash, an address or a URL.
  • Checksummed format. A scanner can tell a real key shape from a lookalike, and Forge refuses a mistyped or made-up key without a database lookup.
  • GitHub secret scanning. Forge runs a receiver for GitHub's secret scanning reports. When GitHub reports a key, Forge verifies GitHub's signature, revokes the key at once, records the report, and sends the owner a notice if the key was live.
  • Rotation. POST /api/keys/{id}/rotate makes a new key with the same grant. The old key keeps working for 24 hours so you can switch over.
  • Audit view. The owner can read a key's audit rows and lifecycle events for 90 days.
  • Expiry reminders. A key with an expiry gets a notice 7 days and 1 day before it stops working.
  • Watching for misuse. The owner gets a notice the first time a key is used from a new network, and when a known key has 20 refused requests in 10 minutes.
  • Failed guesses are throttled. 20 failed key attempts from one address in 10 minutes gets 429 too_many_failed_auth.

Notices about your keys and batches

Events create a notice on your account. GET /notifications lists them, POST /notifications/{id}/read marks one read, and PUT /notifications/preferences sets email switches. Email copies go out only where email delivery is switched on. Security notices cannot be turned off.

Notice kinds
KindSent whenCan be turned off
batch_finisheda batch reached completed or completed_with_failures (best-effort files ride in the same notice)Email only
batch_faileda batch ended needs_review, failed or expiredEmail only
lane_pauseda fix_exhausted problem paused a lane for the accountEmail only
lane_clearedan admin resolved the problem that paused a laneEmail only
cap_reacheda submit was refused by a key or project credit capEmail only
key_createdan API key was created on the accountNo
key_revokedan API key was revokedNo
key_rotatedan API key was rotated (the old key keeps working for a grace period)No
key_expiringan API key expires in 7 days or 1 dayNo
key_new_networka key was used from a network it had not been used from beforeNo
unusual_usagea known key had repeated refused requestsNo
subscription_neededa key request was refused with pipeline_subscription_requiredEmail only
studio_maintenanceStudio was under maintenance while work of the account needed a Studio builderEmail only
studio_recoveredStudio is taking new work again after a maintenance the account was told aboutEmail only

Email limits: one email per kind per project per 1 hour and 10 a day per account. More than 3 finished-batch notices in 30 minutes become one digest, sent after 30 quiet minutes. Security notices skip those two limits and stop at 30 a day. No message holds a key, a prompt or file contents.

Keeping a key safe on your side

  • Keep keys in environment variables or a secret manager. Never put one in a repository, a script you commit, a browser bundle or front-end code. The pipeline API is meant to be called from a server or a CI job.
  • Use one key per integration, so you can revoke one without breaking the others and the audit trail shows which one acted.
  • Give every key the fewest scopes it needs. read_only and submit_only exist for that.
  • Set an expiry. Keys expire at most 365 days out, and a short life limits what a leak is worth.
  • Use an address allowlist when the caller has fixed addresses.
  • Rotate a key after any exposure: a laptop lost, a log shared, a teammate who left. Revoke the old one when the new one is in place.
  • Do not paste a key into a ticket, a chat or a screenshot. If you did, revoke it now. Anything shaped like a key in a public repository may be reported by GitHub and revoked.
  • Keep keys out of logs and error reports. The example client never prints the key.