Auth API · appIdentity

A session from the claim and the app's fresh presence

POST /api/auth/apps/{appId}/session

A session from the claim and the app's fresh presence — no password, no sign-up form — verified exactly as POST /api/auth/apps/{appId}/claim verifies it.

The person is the one the claim's external user is linked to, for this app and account; an external user linked to nobody yet is linked here to the person whose address is the email the app's system verified (verifiedEmail), or to a person created for it (that address, verified, no password: the app's sign-in is their way in until they add another); a claim whose address the app's system has not verified (email) creates a person for it, unverified, only while no person holds that address — it never signs anyone in to an existing account. The session opens in the organization the account is bound to, or that registered it, when the person is its member; otherwise in none, and the admin goes on to choose or create one (organizationId absent) — or, for an account bound to an organization the person is not a member of, says which (notMemberOf, its id and name) so they can ask its owner to admit them (POST /api/auth/apps/{appId}/join-requests). Opened from inside the app's system, the app's user decides who the person is: the admin replaces any session it held for someone else with this one. 409 NotLinked when the external user is linked to nobody and the claim carries no address, or an unverified one a person already holds: the admin offers sign-in or sign-up instead, and nothing is created. 403 UserBanned when the person is banned (this route mints sessions outside BetterAuth's endpoints, so it asks itself). Appends auth.app-user-linked when it links the external user, and auth.signed-in in the organization the session opens in (nothing when it opens in none). Idempotent by state: a person is created once per address and an external user linked once, so a repeat mints a fresh session for the same person; a claim stops working when it expires.

Access

Public: The app's signed identity claim and its fresh presence are the credential: verified against the key of the app in the path before any session is made

No token: the link, the claim or the first message is the credential, as the description says.

How scopes come from roles: Scopes and roles. Getting a token: Authentication and app tokens.

Retries

Idempotent by state: a person is created once per address and an external user linked once, so a repeat mints a fresh session for the same person; a claim stops working when it expires.

Why every write carries a key: Idempotency and retries.

Facts appended

  • auth.app-user-linked
  • auth.signed-in

What a fact is: Facts and the journal.

AI agent tool

No agent tool. Mints a person's session from an app's signed identity claim that only their browser holds; a session is a credential and is never handed to an agent.

Connecting an agent: AI agents.

Example

curl
curl -X POST 'https://auth.getmanifold.io/api/auth/apps/app_5bd0f3a8-2c4e-4a71-9e6b-0d3f1c2a7e40/session' \
  -H 'Content-Type: application/json' \
  -d '{
  "claim": "eyJhbGciOiJIUzI1NiJ9.eyJhcHBJZCI6ImFwcF81YmQwZjNhOCIsImV4dGVybmFsVXNlcklkIjoiODgxMjA0In0.dm91Y2hlZC1ieS10aGUtYXBw",
  "presence": "eyJhbGciOiJIUzI1NiJ9.eyJleHRlcm5hbFVzZXJJZCI6Ijg4MTIwNCIsImF0IjoxNzkyNjI1MjAwfQ.cHJlc2VudC1ub3c"
}'
JavaScript (fetch)
const response = await fetch("https://auth.getmanifold.io/api/auth/apps/app_5bd0f3a8-2c4e-4a71-9e6b-0d3f1c2a7e40/session", {
  method: "POST",
  headers: { "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();
Request body · application/json
{
  "claim": "eyJhbGciOiJIUzI1NiJ9.eyJhcHBJZCI6ImFwcF81YmQwZjNhOCIsImV4dGVybmFsVXNlcklkIjoiODgxMjA0In0.dm91Y2hlZC1ieS10aGUtYXBw",
  "presence": "eyJhbGciOiJIUzI1NiJ9.eyJleHRlcm5hbFVzZXJJZCI6Ijg4MTIwNCIsImF0IjoxNzkyNjI1MjAwfQ.cHJlc2VudC1ub3c"
}
Response · 200 · application/json
{
  "sessionToken": "Xq3mV8tR2pLw6zN1cB5hK9dF4sJ7gA0e",
  "organizationId": "org_01a156d2-8680-7738-ab2e-ff985e454887"
}

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

RefusalWhat it meansWhat to do
ClaimInvalid reason The app's identity claim is unsigned, expired or for another app.Get a fresh claim from the app.

403

RefusalWhat it meansWhat to do
AppCannotAssertIdentity appId, reason This app may not vouch for who a person is.Sign the person in directly.
UserBanned reason This person is barred from signing in.Contact Manifold support.

404

RefusalWhat it meansWhat to do
AppNotFound appId No such record in this organization.Check the id in the path or the body.

409

RefusalWhat it meansWhat to do
NotLinked reason The app account is linked to nobody, and the claim cannot create a person.Sign in or sign up, then try again.

503

RefusalWhat it meansWhat to do
AppIdentityDisabled reason Signing in through apps is turned off on this deployment.Send the same request again shortly.

How refusals work everywhere: Errors.