Authentication and app tokens
Every call carries a bearer token. Here is how to get one, for a person and for an app.
Who can call
- A person
- Signs in, chooses an organization, and gets a token for it. The token carries their roles.
- An AI agent
- Acts as the person who connected it, with only the scopes that person chose. See AI agents.
- An app
- Acts as itself, never as a person, with the scopes its installation was granted.
A person's token
Sign in, make an organization the session's active one, then ask for a token. The quickstart walks through it, and Signing in from code below lists each call's answer.
curl 'https://auth.getmanifold.io/api/auth/token' \
-H "Authorization: Bearer $SESSION_TOKEN"The token names:
- the organization (
activeOrganizationId). A token with no organization is refused403TenantRequired; - your roles, which decide the scopes you hold (see Scopes and roles);
- whether the subscription is current. A lapsed organization's writes are refused
402SubscriptionRequired; - whether the organization is read-only, and which system it is copying from, if any (see Read-only mode and the mirror lock).
It lasts 15 minutes. When a call answers 401, ask for a new one with
the same session token. A change to your roles shows in your next token.
Signing in from code
Four calls to the auth service at https://auth.getmanifold.io. Each answers JSON. Send the session
token as Authorization: Bearer on every call after the first. A refusal answers a
body with a code and a message.
POST /api/auth/sign-in/email
Body { "email": …, "password": … }. Answers 200 with the person, and
the session token in the set-auth-token header (the body's token is the same value).
{
"redirect": false,
"token": "Kq3v9X…",
"user": {
"id": "usr_0199f3a2-6b3d-7a09-8c2d-4e5f6a7b8c9d",
"email": "integrations@yourcompany.com",
"name": "Warehouse integration",
"emailVerified": true,
"image": null,
"createdAt": "2026-10-20T06:00:00.000Z",
"updatedAt": "2026-10-20T06:00:00.000Z"
}
}401INVALID_EMAIL_OR_PASSWORD- The email or password is wrong. Check both. Sending it again unchanged gets the same answer.
429- More than three sign-ins from your address in ten seconds. Wait the seconds the
X-Retry-Afterheader gives.
An unconfirmed email does not stop sign-in. A person who signs in only with Google or a passkey has no password to send: use an agent, or give the integration its own member (below).
GET /api/auth/organization/list
Answers an array of the organizations you belong to. Note the id of the one you want.
[
{
"id": "org_0199f3a2-7c4e-7b1a-9d3e-5f6a7b8c9d0e",
"name": "Saltbush Homewares",
"slug": "saltbush-homewares",
"logo": null,
"createdAt": "2026-09-01T02:00:00.000Z"
}
]POST /api/auth/organization/set-active
Body { "organizationId": "org_…" }. Makes it the session's organization and answers 200 with the organization, its members and its pending invitations.
403USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION- You are not a member. Check the id against the list above.
403MEMBERSHIP_DEACTIVATED- Your access to it was ended. An owner or admin can reactivate you.
400ORGANIZATION_NOT_FOUND- No organization has that id.
GET /api/auth/token
Answers the bearer token every other service takes. It lasts 15 minutes.
{ "token": "eyJhbGciOiJFZERTQSIs…" }401- The session has ended. Sign in again.
How long each lasts. The token: 15 minutes, so ask for a new one when a call
answers 401. The session token: seven days from its last use, renewed at most once a
day as you use it.
Rate limits. Only the auth service limits how fast you call it: 100 calls in any
ten seconds from one address, and three sign-ins in ten seconds. Over that it answers 429 with X-Retry-After, in seconds: wait that long, then send the call again. Ask for a token
when the last one expires, not before every call. The other services have no rate limit today.
Integrating a back-end system today
App tokens and webhooks are not open to partners yet (below). A warehouse system or other integration that runs unattended signs in as a member of its own. Set it up once:
- Invite an address of its own. An owner or admin opens Settings › Users,
enters an address such as
integrations@yourcompany.comunder Invite by email, leaves Role at member and chooses Invite. - Accept the invitation with a password. Create an account with that address and a password. Do not sign up with Google: an account without a password cannot sign in from code. Confirm the address from the email Manifold sends, then choose Accept on the organizations screen.
- Give it only the roles its calls need. On its row, choose Edit under Roles. To take orders, ship them and record payments: Fulfilment (
orders:write,shipping:write) and Finance (payments:write). Add Receiving (receiving:write) if it books deliveries in. Each operation's page names the scope it needs. See Roles. - Keep the email and password in your secret store. The integration signs in, keeps the session token and chooses the organization (Signing in from code).
- Refresh on
401. Ask for a token, use it until a call answers401, then ask again. When the token request itself answers401, sign in again. - Poll for new work. Without webhooks, read a service's journal from where you
left off:
GET /events?since=…on fulfillment for new orders, and keep the lastsequenceyou handled. See Paging, events and as-of reads.
The member's changes show under its own name, and deactivating it ends its access in one step. Members are not billed: the plan charges by sales order.
App tokens
Partner apps are not open yet. Today only Manifold's own apps, such as the Shopify, Amazon and Xero apps, are issued app tokens. To build on Manifold now, call the API with a person's token, or connect as an agent. This section describes how app tokens work.
Installing
An owner or admin installs an app from its page in the App Store (Settings › Apps › Browse the App Store). The app gets everything it asks for, or nothing. A person who
does not hold every scope the app asks for is refused 403 GrantExceedsYours.
The grant is never cut down to fit them.
An app is removed in Settings › Apps, or in Settings › Sales channels for a sales-channel app. No new token is then issued for it, and the app is sent the app.uninstalled webhook. A token already issued works until it expires, at most a minute.
Two kinds of token
A Manifold app asks for its tokens with POST /api/auth/app-token, under its own service
credential. Partner apps have no such credential yet. Both kinds last one minute and carry exactly the installation's scopes.
- Read token
- Good for any number of
GETandHEADcalls in its minute, and for the fewPOSTs that only read, such as a shipping rate quote. Refused on any other write. Ask for one with the organization and the app alone, withoutjobs. - Write token
- Good for one request, once. It is spent before the service acts on the request.
{
"tenantId": "org_0199f3a2-7c4e-7b1a-9d3e-5f6a7b8c9d0e",
"appId": "app_0199f3a2-8a10-7c2b-8e4f-1a2b3c4d5e6f"
}{
"token": "eyJhbGciOiJFZERTQSIs…",
"expiresAt": "2026-10-23T09:14:05.000Z"
}For write tokens, send jobs: one entry per request you are about to make, up to 100.
Each entry names:
op: the method and the real path, with the SKU or id filled in. A token for one SKU cannot change another;key: the idempotency key the request carries. Leave it out when the request carries none, such as aPUTthat replaces. The token is then good only for a request with no key;aud: the service the request is for.
{
"tenantId": "org_0199f3a2-7c4e-7b1a-9d3e-5f6a7b8c9d0e",
"appId": "app_0199f3a2-8a10-7c2b-8e4f-1a2b3c4d5e6f",
"jobs": [
{
"op": "POST /items/WIDGET-BLUE/receive",
"key": "dock-note-8841",
"aud": "inventory-manager"
},
{
"op": "PUT /items/WIDGET-BLUE/cost",
"aud": "inventory-manager"
}
]
}{
"tokens": [
{
"tokenId": "atk_0199f3a2-9b21-7d3c-8f50-2b3c4d5e6f70",
"op": "POST /items/WIDGET-BLUE/receive",
"key": "dock-note-8841",
"aud": "inventory-manager",
"token": "eyJhbGciOiJFZERTQSIs…",
"expiresAt": "2026-10-23T09:14:05.000Z"
},
{
"tokenId": "atk_0199f3a2-9b21-7d3c-8f50-2b3c4d5e6f71",
"op": "PUT /items/WIDGET-BLUE/cost",
"aud": "inventory-manager",
"token": "eyJhbGciOiJFZERTQSIs…",
"expiresAt": "2026-10-23T09:14:05.000Z"
}
]
}The tokens come back in tokens, in the order of jobs.
When no token is issued
403AppNotInstalled- The organization has not installed the app, or removed it. Asking again will not help.
403AppRetired- The organization retired this app when it went live. It will not be issued tokens again.
{
"_tag": "AppNotInstalled",
"appId": "app_0199f3a2-8a10-7c2b-8e4f-1a2b3c4d5e6f"
}When a write token is refused
401Unauthorized- The token was made for another method, path, key or service, or a read token was used to write. Nothing happened.
401TokenAlreadyUsed- The token was spent by an earlier request. The service did not act on this one.
503RequestLogUnavailable- The service could not record the token as spent. Nothing was spent and nothing happened. Try again with a fresh token.
To retry a write, ask for a new token and send the same key. The key makes the second request a retry, not the token. The service answers it with the first result and records nothing new. See Idempotency and retries.
An app's own page
An app can have a page that the admin shows under Apps. The page is served from the app's own address, and never sees the person's token. When it opens, the admin asks the auth service to launch it, and hands the page an app session token:
- it names the person (
sub) and the organization (activeOrganizationId); - its
appSessionclaim holds the app, the installation and the person's own scopes; - its audience is
app-session:followed by the app's id; - it lasts five minutes; the page asks the admin for a new one as it runs out;
- only the app's own service accepts it. Every Manifold service refuses it.
What the app then does in Manifold, it does with its own app token.
Who the record says acted
Every fact records its actor, taken from the token: the person (user), the app
(app), or work Manifold ran on its own, named by service and job (system).
{"_tag":"user","id":"usr_0199f3a2-a1b2-7c3d-8e4f-5a6b7c8d9e0f"}
{"_tag":"app","id":"app_0199f3a2-8a10-7c2b-8e4f-1a2b3c4d5e6f","installationId":"ins_0199f3a2-b2c3-7d4e-8f50-6a7b8c9d0e1f"}
{"_tag":"system","id":"fulfillment-manager:reallocator"}Checking a token
Every token is a JWT signed by the auth service. No other service can make one. Check the signature
against the auth service's public keys at https://auth.getmanifold.io/api/auth/jwks, and allow five
seconds of clock difference on when it was issued and when it expires.
An app's service checks an app session token the same way, and also checks that the audience is app-session: and its own app id, and that appSession.appId is its app. It
then acts for the person and organization the token names, within the scopes in appSession.
Reference
POST /api/auth/app-token: issue app tokens. Only Manifold's own apps call it, under their service credential, so it is not in the public reference.PUT /api/auth/organizations/{organizationId}/installations/{appId}: install an app.DELETE /api/auth/organizations/{organizationId}/installations/{appId}: remove one.POST …/installations/{appId}/launch: open an app's page.- What happens when you get it wrong, or send it twice.