Skip to content

Authentication

Every request to https://app.moondeck.io/api carries a bearer token:

Authorization: Bearer <token>

There are two kinds of token, and the difference matters more than the shared header suggests.

ClientTokenWhere it comes from
Web appSession tokenThe browser session, sent as a cookie
Mobile appSession tokengetToken(), sent as a bearer JWT
CLI, MCP, scriptsAPI keyMinted per user, long-lived and opaque

Session tokens belong to our own apps. Anything you write authenticates with an API key.

Keys are minted from your account page at app.moondeck.io/account — press Manage account, choose API keys, then Add new key.

Name the key and give it an expiry: anything from a day to a year, or Never. The secret is shown once, at creation, and cannot be retrieved afterwards — copy it then. Revoke a key from the same panel at any time.

A key is issued to you and carries your access in whichever workspace the request names.

A workspace-scoped key is rejected:

{
"error": {
"code": "UNAUTHORIZED",
"message": "org-scoped API keys are not supported; use a user-scoped key"
}
}

This is not an oversight. Work on Uai attributes to a person — a task has a creator, a pull request has an author — and a workspace-scoped key has no person behind it to attribute to.

Keys created from your account page are user-scoped already, so you will not run into this by accident. It matters if you mint keys programmatically, where choosing a workspace scope is possible and will be refused here.

Calls authenticated with an API key check whether the workspace owns a paid seat and answer 402 when it does not (API access is not part of the Free tier):

{
"error": { "code": "PLAN_LIMIT", "feature": "apiAccess", "plan": "free" }
}

Session-token calls are never gated this way, so the same operation can succeed in the app and return 402 from your script. If that happens, nothing is wrong with your key — the workspace it resolved to has no paid seats, so it has no API access.

Most resources belong to a workspace, so most requests need to say which one. Send the slug in a header:

x-uai-org: acme

Use me for your personal workspace. For EventSource, which cannot set headers, pass ?org=acme instead — the two are read the same way.

Set it explicitly on every API-key call. If you omit it, the request still resolves to some workspace — for a key, the earliest workspace you joined that is usable — and that is a poor thing to depend on: it can change when your memberships change, without anything about your code changing.

A slug that does not exist and a workspace you are not a member of both answer 404. They are deliberately indistinguishable, because a 403 would confirm that a workspace exists.

Every operation states the permission it needs — the reference shows it as Requires: near the top of the page. Lacking it gives a 403 that names the permission you were missing:

{ "error": { "code": "FORBIDDEN", "permission": "tasks.write" } }

Some operations are restricted to workspace admins, which is a separate axis rather than a stronger permission. No grant substitutes for it, and the failure says so:

{ "error": { "code": "ADMIN_REQUIRED" } }

Cross-origin browser requests are refused by default — the API expects native and server-side clients. Reaching it from a browser on another origin needs that origin allowlisted on the service.

Bear in mind what that would mean: a long-lived key in front-end code is readable by anyone who opens the page. Call the API from your server and keep the key there.

Every failure uses the same envelope — { "error": { "code", "message" } }, with extra keys on some codes as shown above.

StatusCodeMeaning
401UNAUTHORIZEDMissing, invalid, or workspace-scoped credential
402PLAN_LIMITThe workspace has no paid seats, so no API access
403FORBIDDENAuthenticated, but missing the named permission
403ADMIN_REQUIREDThe operation is workspace-admin only
404SCOPE_REQUIREDThe named workspace does not exist, or you are not a member
503SCOPE_REQUIREDMembership is being reconciled; retry shortly

Codes are stable. Branch on error.code, not on the message.

Terminal window
curl https://app.moondeck.io/api/tasks \
-H "Authorization: Bearer $UAI_API_KEY" \
-H "x-uai-org: acme"

From here, the API reference lists every operation with its authentication, required permissions and failure codes.