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-linkedauth.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 -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"
}'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();{
"claim": "eyJhbGciOiJIUzI1NiJ9.eyJhcHBJZCI6ImFwcF81YmQwZjNhOCIsImV4dGVybmFsVXNlcklkIjoiODgxMjA0In0.dm91Y2hlZC1ieS10aGUtYXBw",
"presence": "eyJhbGciOiJIUzI1NiJ9.eyJleHRlcm5hbFVzZXJJZCI6Ijg4MTIwNCIsImF0IjoxNzkyNjI1MjAwfQ.cHJlc2VudC1ub3c"
}{
"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
| 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. |
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. |
UserBanned reason | This person is barred from signing in. | Contact Manifold support. |
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 |
|---|---|---|
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
| 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. |
How refusals work everywhere: Errors.