Authentication
API keys, the browser login the CLI and MCP use, scopes and presets, actions only a person can take, rate limits and key rotation.
Every call to the Syndai coding API uses an API key. A key belongs to one workspace. Its scopes say what it may do.
Send the key
Put the key in the Authorization header as a bearer token, on every request:
curl -s https://api.syndai.ai/api/v1/external/coding/repos \
-H "Authorization: Bearer $SYNDAI_API_KEY"
There is no query-string or body form. Keys start with sk_syndai_live_. A missing, wrong or revoked key gets 401 with code INVALID_TOKEN.
Get a key
There are two ways.
- In the app. Open Settings, then Developer, then API keys, and click Create key. Name it and pick one access preset. The key is shown once. Syndai keeps only a hash, so a lost key cannot be shown again. Revoke it and make a new one.
- From a browser login.
syndai loginandsyndai mcp installnever ask you to paste a key. A person approves them in the browser, and the key is made then. See Browser login.
GET /api/v1/external/coding/me describes the key you call with: customer_id, key_id, key_name, scopes and expires_at. DELETE /api/v1/external/coding/me revokes it. That is what syndai logout does.
Scopes
A key can only do what its scopes allow. Give each key the fewest it needs. You cannot add scopes to a key later. Make a new key.
| scope | allows |
|---|---|
coding:repos:read | list the workspace's projects |
coding:runs:create | start runs (POST /runs) |
coding:runs:read | read runs, their events and timelines |
coding:runs:respond | answer a run's questions |
coding:runs:cancel | stop or cancel runs |
coding:runs:ratify | confirm a run's acceptance criteria |
coding:criteria:edit | edit a drafted criterion before it is confirmed |
coding:spec:request-changes | mark up a drafted run spec and ask for a revision |
coding:webhooks:read | list webhook endpoints and deliveries |
coding:webhooks:write | create, disable, test and rotate webhook endpoints |
coding:webhooks:redeliver | send a delivery again |
coding:schedules:manage | create and manage schedules that repeat a run |
coding:members:read | list members and invites |
coding:members:write | invite or remove members |
coding:policy:read | read workspace and project policy |
coding:policy:write | change workspace and project policy |
coding:apps:read | read a project's published app |
coding:apps:publish | publish an app and edit a live site's text |
coding:apps:unpublish | take a published app offline |
coding:secrets:read | list environments and secret names, never a value |
coding:secrets:write | set, import and delete secrets |
coding:tools:read | list custom skills and MCP servers |
coding:tools:write | add, edit, rescan and delete custom skills and MCP servers |
coding:secrets:reveal | read secret values (see Secret values) |
Your plan sets which scopes your workspace may use. A key cannot hold a scope the plan leaves out. Each call is checked against the plan at that moment too. So if your plan drops a scope, keys that list it lose that access.
When a call is refused:
- the key lacks the scope:
403 INVALID_SCOPE; - the plan lacks the scope:
403 PLAN_UPGRADE_REQUIRED.
Presets
The app offers presets, so you pick a job and not a list. A preset only grants the scopes your plan allows.
| preset | scopes |
|---|---|
| Run lifecycle | coding:repos:read, coding:runs:create, coding:runs:read, coding:runs:respond, coding:runs:cancel, coding:runs:ratify, coding:criteria:edit |
| Webhook delivery | coding:webhooks:read, coding:webhooks:redeliver |
| Webhook admin | the two above, plus coding:webhooks:write |
| Policy admin | coding:policy:read, coding:policy:write |
| App publishing | coding:apps:read, coding:apps:publish, coding:apps:unpublish |
| Connections (read-only) | coding:secrets:read, coding:tools:read |
| Connections admin | coding:secrets:read, coding:secrets:write, coding:tools:read, coding:tools:write |
| Team admin | coding:members:read, coding:members:write |
| Owner CLI and MCP | coding:repos:read, coding:runs:create, coding:runs:read, coding:runs:respond, coding:runs:cancel, coding:apps:read, coding:apps:publish, coding:policy:read, coding:policy:write, coding:schedules:manage |
| Full entitled access | every scope in the table above except coding:secrets:reveal |
Owner CLI and MCP is the set a browser login grants.
Browser login
The CLI and syndai mcp install log in with a device code (RFC 8628), like gh auth login.
- The client calls
POST /api/v1/cli/device. It needs no key and allows 10 calls a minute per IP. The answer has adevice_code, a shortuser_code,interval,expires_inandverification_uri_complete(https://app.syndai.ai/cli?code=…). The client may sendscopesto ask for more than the owner set. - An owner or admin of the workspace opens that page. It shows the code, the IP address and time Syndai saw, and the client and computer names. The client wrote those names itself, so they do not prove who sent it. Approving picks the workspace. A member or viewer gets
403 ROLE_REQUIRED. - The client polls
POST /api/v1/cli/device/tokenwith{"device_code": "…"}everyintervalseconds (5). Until there is a decision, it gets400with{"error": "authorization_pending"}. Polling too fast getsslow_down, so wait 5 seconds longer. A denial isaccess_denied. A code that expired after 10 minutes, or was already used, isexpired_token. - Once approved, the poll returns
api_key,customer_id,key_idandexpires_at, just once. The key lasts 90 days. It is named after the client and the computer, such asCLI · laptop.
A browser login never grants coding:members:read, coding:members:write or coding:apps:unpublish. Each MCP client gets a key of its own, so you can revoke one without the others.
Secret values
coding:secrets:reveal is in no preset. No key made in the app can hold it. Only a browser login that a person approves can grant it, and the page says so.
POST /api/v1/external/coding/secrets/resolve returns one environment's values, merged the way a run sees them. Each value it returns writes one SECRET_READ audit row that names the key. The response has Cache-Control: no-store.
A production environment needs one more step. A person must approve this key in the browser in the last 10 minutes:
- The key calls
POST /api/v1/external/coding/cli/production-access. It needscoding:secrets:reveal. The answer has adevice_codeandverification_uri_complete. - An owner or admin of the key's workspace opens the page and allows it.
- The key polls
POST /api/v1/external/coding/cli/production-access/tokenwith{"device_code": "…"}. On approval it gets{"allowed_until": "…"}. Waiting, denied and expired codes use the same errors as the browser login. - The resolve call sends that
device_codeasproduction_grant.
Without a fresh grant, resolve answers 403 PRODUCTION_STEP_UP_REQUIRED and returns no values. The CLI keeps that code in memory and never writes it to disk. So another holder of the same key has nothing to send.
Actions only a person can take
A key is access you hand to a tool. The CLI, an MCP client and CI all hold one, and so can any agent that reads the file it is stored in. Some actions need a person signed in at app.syndai.ai:
- landing a held change anyway (merge anyway, or go live anyway);
- making a project's protected files looser, such as removing a pattern or adding a
!exemption; - editing a protected file's text.
A key that tries one gets 403 HUMAN_APPROVAL_REQUIRED with a review_url. That is the page in the app where a person decides. Nothing changes.
Keys can still retry or close a held change, put a change live that is not held, and add protected-file patterns. Buying credits and managing keys stay in the app.
Why: an owner's agent reads text that other people wrote, such as members' requests and run output. A click in the browser is the one thing that agent does not hold.
Rate limits
Each workspace can make 600 keyed calls a minute, across all its keys. More keys do not raise the limit. Each route also has its own cap. A few of them:
| route | per minute |
|---|---|
POST /runs | 120 |
GET /runs/{id} | 240 |
GET /runs/{id}/events | 60 |
POST /runs/{id}/responses | 120 |
POST /webhooks | 30 |
Whichever limit you hit first applies. A limited call gets 429 with code RATE_LIMITED and a Retry-After header in seconds. These limits are separate from your plan's credits and run concurrency. See Errors for how to retry.
Rotate a key
Rotate keys on a schedule, and at once if one may have leaked.
- Make a new key with the same preset.
- Put the new key in your deployment.
- Check that your calls work.
- Revoke the old key in the app.
Revoking takes effect at once. Calls with the old key then get 401 INVALID_TOKEN. A key nobody uses for a year is revoked by Syndai, and the workspace owners are told.
Keep keys out of source code. Use environment variables or a secrets manager.