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
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 refused 403 TenantRequired;
  • your roles, which decide the scopes you hold (see Scopes and roles);
  • whether the subscription is current. A lapsed organization's writes are refused 402 SubscriptionRequired;
  • 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).

Response · 200
{
  "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"
  }
}
401 INVALID_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-After header 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.

Response · 200
[
  {
    "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.

403 USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION
You are not a member. Check the id against the list above.
403 MEMBERSHIP_DEACTIVATED
Your access to it was ended. An owner or admin can reactivate you.
400 ORGANIZATION_NOT_FOUND
No organization has that id.

GET /api/auth/token

Answers the bearer token every other service takes. It lasts 15 minutes.

Response · 200
{ "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:

  1. Invite an address of its own. An owner or admin opens Settings › Users, enters an address such as integrations@yourcompany.com under Invite by email, leaves Role at member and chooses Invite.
  2. 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.
  3. 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.
  4. 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).
  5. Refresh on 401. Ask for a token, use it until a call answers 401, then ask again. When the token request itself answers 401, sign in again.
  6. 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 last sequence you 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 GET and HEAD calls in its minute, and for the few POSTs that only read, such as a shipping rate quote. Refused on any other write. Ask for one with the organization and the app alone, without jobs.
Write token
Good for one request, once. It is spent before the service acts on the request.
POST /api/auth/app-token · body · read token
{
  "tenantId": "org_0199f3a2-7c4e-7b1a-9d3e-5f6a7b8c9d0e",
  "appId": "app_0199f3a2-8a10-7c2b-8e4f-1a2b3c4d5e6f"
}
Response · 200
{
  "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 a PUT that replaces. The token is then good only for a request with no key;
  • aud: the service the request is for.
POST /api/auth/app-token · body · write tokens
{
  "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"
    }
  ]
}
Response · 200
{
  "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

403 AppNotInstalled
The organization has not installed the app, or removed it. Asking again will not help.
403 AppRetired
The organization retired this app when it went live. It will not be issued tokens again.
Response · 403
{
  "_tag": "AppNotInstalled",
  "appId": "app_0199f3a2-8a10-7c2b-8e4f-1a2b3c4d5e6f"
}

When a write token is refused

401 Unauthorized
The token was made for another method, path, key or service, or a read token was used to write. Nothing happened.
401 TokenAlreadyUsed
The token was spent by an earlier request. The service did not act on this one.
503 RequestLogUnavailable
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 appSession claim 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).

actor
{"_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"}

See Facts and the journal.

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