Auth API · appIdentity
A signed-in person redeems the identity claim an app handed their browser
POST /api/auth/apps/{appId}/claim
A signed-in person redeems the identity claim an app handed their browser (claim: who they are in the app's system, and the external account the install is for, signed with the app's own key), with the app's presence — its fresh word, fetched by the browser from the app as it redeems, that the same external user of the same account is in front of it now; a claim alone redeems nothing.
Both are verified against the key of the app in the path — 400 ClaimInvalid for one another app signed, one naming another app, a tampered or an expired one, or a missing, stale or mismatched presence; 404 AppNotFound and 403 AppCannotAssertIdentity for an app that is not one that may vouch for a person. A claim whose account the app has bound to an organization admits only that organization's members (403 NotAMember — its owner invites anyone else); an external user already linked to another person is refused before anything is made (409 AppIdentityLinkedElsewhere — a link is never moved). For an unbound account the person chooses: organizationId connects it to an organization they own or administer (403 OrganizationRefused for one they hold neither role in, a sandbox, or one other than the organization the account is already the app's in), and without it the claim creates the organization, named after the account, with the caller as its owner and origin: app. Either way the organization becomes the session's active one, the external user is linked to the person for that app and account, and an email the app's system verified marks the person's matching address verified. Appends auth.org-created and auth.member-added when it creates the organization, and nothing when it joins or is given one; and auth.app-user-linked the first time the external user is linked to the person, in the link's own transaction. No idempotency key: the claim's nonce is not kept here — a repeat for a bound account joins again and appends nothing (the link already stands), and a repeat of an unbound claim, before the app has bound the account, creates another organization.
Access
Requires a signed-in session: The person's BetterAuth session, and a claim that verifies against the key of the app named in the path
How scopes come from roles: Scopes and roles. Getting a token: Authentication and app tokens.
Retries
No idempotency key: the claim's nonce is not kept here — a repeat for a bound account joins again and appends nothing (the link already stands), and a repeat of an unbound claim, before the app has bound the account, creates another organization.
Why every write carries a key: Idempotency and retries.
Facts appended
auth.org-createdauth.member-addedauth.app-user-linked
What a fact is: Facts and the journal.
AI agent tool
No agent tool. Redeems the signed identity claim an app handed a person's browser, under that person's own session: a sign-in step in the admin, never an agent's.
Connecting an agent: AI agents.
Example
curl -X POST 'https://auth.getmanifold.io/api/auth/apps/app_5bd0f3a8-2c4e-4a71-9e6b-0d3f1c2a7e40/claim' \
-H "Authorization: Bearer $MANIFOLD_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"claim": "eyJhbGciOiJIUzI1NiJ9.eyJhcHBJZCI6ImFwcF81YmQwZjNhOCIsImV4dGVybmFsVXNlcklkIjoiODgxMjA0In0.dm91Y2hlZC1ieS10aGUtYXBw",
"presence": "eyJhbGciOiJIUzI1NiJ9.eyJleHRlcm5hbFVzZXJJZCI6Ijg4MTIwNCIsImF0IjoxNzkyNjI1MjAwfQ.cHJlc2VudC1ub3c"
}'const response = await fetch("https://auth.getmanifold.io/api/auth/apps/app_5bd0f3a8-2c4e-4a71-9e6b-0d3f1c2a7e40/claim", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.MANIFOLD_TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({
"claim": "eyJhbGciOiJIUzI1NiJ9.eyJhcHBJZCI6ImFwcF81YmQwZjNhOCIsImV4dGVybmFsVXNlcklkIjoiODgxMjA0In0.dm91Y2hlZC1ieS10aGUtYXBw",
"presence": "eyJhbGciOiJIUzI1NiJ9.eyJleHRlcm5hbFVzZXJJZCI6Ijg4MTIwNCIsImF0IjoxNzkyNjI1MjAwfQ.cHJlc2VudC1ub3c"
}),
});
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
const answer = await response.json();{
"claim": "eyJhbGciOiJIUzI1NiJ9.eyJhcHBJZCI6ImFwcF81YmQwZjNhOCIsImV4dGVybmFsVXNlcklkIjoiODgxMjA0In0.dm91Y2hlZC1ieS10aGUtYXBw",
"presence": "eyJhbGciOiJIUzI1NiJ9.eyJleHRlcm5hbFVzZXJJZCI6Ijg4MTIwNCIsImF0IjoxNzkyNjI1MjAwfQ.cHJlc2VudC1ub3c"
}{
"organizationId": "org_01a156d2-8680-7738-ab2e-ff985e454887",
"created": true,
"resume": "app:shopify"
}The panel writes this call in other languages. It does not send it: Manifold's APIs do not take calls from a web
page on another site, so run the call from your own code or terminal. The whole API as OpenAPI: auth-service.openapi.json. Base URL https://auth.getmanifold.io.
Errors
Every refusal is a status and a JSON body whose _tag names it. A refused call changes nothing.
400
| Refusal | What it means | What to do |
|---|---|---|
ClaimInvalid reason | The app's identity claim is unsigned, expired or for another app. | Get a fresh claim from the app. |
401
| Refusal | What it means | What to do |
|---|---|---|
Unauthorized reason | The token is missing, expired or not one Manifold issued. | Get a fresh token and send the call again. |
TokenAlreadyUsed tokenId | A single-use app token was already spent. | Mint a new token for this call. |
402
| Refusal | What it means | What to do |
|---|---|---|
SubscriptionRequired status | The organization's subscription has lapsed, so writes are refused. | An owner settles billing. Reads still work meanwhile. |
403
| Refusal | What it means | What to do |
|---|---|---|
AppCannotAssertIdentity appId, reason | This app may not vouch for who a person is. | Sign the person in directly. |
NotAMember organizationId, reason | The person is not a member of this organization. | Check the organization, or invite them first. |
OrganizationRefused organizationId, reason | You may not connect this account to that organization. reason says why. | Choose an organization you own or administer, or leave organizationId out. |
TenantRequired userId | The token names no organization. | Choose the organization, then get a new token. |
ScopeRequired required | The token's roles do not grant the scope this call needs. | Ask an owner or admin for a role that grants it. |
404
| Refusal | What it means | What to do |
|---|---|---|
AppNotFound appId | No such record in this organization. | Check the id in the path or the body. |
409
| Refusal | What it means | What to do |
|---|---|---|
AppIdentityLinkedElsewhere reason | The app account is already linked to another person. | Sign in as that person, or ask them to unlink it. |
MirroredFromSource source | This area is fed by your source system until the organization goes live. | Make the change in the source system, or go live first. |
503
| Refusal | What it means | What to do |
|---|---|---|
AppIdentityDisabled reason | Signing in through apps is turned off on this deployment. | Send the same request again shortly. |
RequestLogUnavailable tokenId, reason | The service could not record the request, so it did nothing. | Send the same request again shortly. |
How refusals work everywhere: Errors.