---
title: "Create auth session"
method: POST
path: "/api/v1/auth/sessions"
tags: ["Auth"]
---

# Create auth session

`POST /api/v1/auth/sessions`

## Request

- Base URL: `https://api.proof.holdings`
- URL: `https://api.proof.holdings/api/v1/auth/sessions`
- Auth: none declared

Create a new passwordless auth session (login). Returns a deep link and QR code for the selected channel. Rate limited: 15 requests per minute per IP.

The email channel additionally honours the account-bootstrap decline cooldown: if the owner of the address used the "this wasn't me" link on a recent message from us, this endpoint answers 429 `identity_declined` for an hour. That covers sign-in mail as well, so a client should offer the other channels (Telegram, WhatsApp, SMS) rather than retrying.

No credential is required. A client that DOES present an API key (`Authorization: Bearer pk_…`) — an MCP connection signing in from inside — has it checked: a key the API no longer accepts (unknown, revoked, expired) is refused with 401 `invalid_api_key` before anything is stored or sent, so a connection whose key was revoked cannot sign back in to the account. A live presented key is recorded on the session, and the user session issued when the sign-in completes is BOUND to it: revoking that key by any path ends the session, and it cannot be refreshed once the key is revoked or expired. If the key is revoked before the person completes the sign-in, no session is issued at all.

ON THE EMAIL CHANNEL A 201 MEANS THE MAIL WAS ACCEPTED FOR DELIVERY. The code and the magic link travel in that message and are returned by nothing, so a session created after a failed send could never be completed — if delivery fails, this endpoint answers 503 `challenge_delivery_failed` and the session is discarded rather than returning a session to poll. Do not look for an `email_sent: false` to decide whether to tell someone to check their inbox: on this route a 201 already carries that answer. A field of that name DOES mean something elsewhere — `POST /api/v1/accounts` (AccountSessionResponse) and `POST /api/v1/accounts/{id}/send` (its own response shape) split creating a session from mailing it, and report a withheld or failed send that way — but those are different endpoints. This one still carries `email_sent` on an email-channel 201, where it is ALWAYS `true` (a failed send never reaches a 201), and it never returns `email_error` at all. Note that `email_sent` is NOT part of the declared 201 schema below: `AuthSessionResponse` is shared with the session-poll and submit operations, which return no such field, so it is documented here rather than declared there — a generated client type for this response will not carry it.

## Request body

- CreateAuthSessionRequest: Request body for creating a new auth session (login)
  - `channel` 'telegram' | 'whatsapp' | 'sms' | 'email', required: Authentication channel
  - `phone_number` string: Required for SMS channel (to determine DID routing)
  - `email` string, email: Required for email channel
  - `return_to` string: Same-origin path on the web app (starting with a single `/`) that the emailed magic link returns to once it signs the person in; `GET /auth/complete/{sessionId}` honours it too. A messenger sign-in returns through the starting tab, which polls, so its "Open Dashboard" button still opens the dashboard. A value that is not such a path (an absolute or protocol-relative URL, a backslash, a scheme, over 2048 characters) is ignored rather than refused, and the sign-in then returns to the dashboard.

## Response `201`

Auth session created

- AuthSessionResponse: Auth session response
  - `id` string, required
  - `type` 'login', required
  - `channel` 'telegram' | 'whatsapp' | 'sms' | 'email', required: Authentication channel
  - `status` 'pending' | 'verified' | 'expired', required: Status of an auth session
  - `deep_link` string
  - `qr_code` string: Base64-encoded QR code image
  - `qr_text` string: UTF-8 text representation of the QR code for terminal/agent display
  - `expires_at` string, date-time, required
  - `instructions` string
  - `verified_at` string, date-time
  - `user_id` string

## Other responses

- `400`: Validation error
- `401`: `invalid_api_key` — the request presented an API key the API no longer accepts (unknown, revoked or expired). Requests without a `pk_` bearer never receive it.
- `429`: Either the IP rate limit, or `identity_declined` — the owner of this address used the "this wasn't me" link recently and email to it is paused. DO NOT RETRY on `identity_declined`; offer another channel instead.
- `503`: Either `identity_check_unavailable` — the decline check could not be completed, so the request is refused rather than allowed through — or, on the email channel, `challenge_delivery_failed`: the verification email could not be sent, so no session was created. Retry shortly, or offer another channel. A third code, `email_service_unavailable`, means no mail provider is configured at all.

---

[API](https://skmtc.dev/proof/apis/proof-holdings-api.md) · [All operations](https://skmtc.dev/proof/apis/proof-holdings-api/llms.txt) · [OpenAPI document](https://skmtc.dev/proof/apis/proof-holdings-api/revisions/b15f506de696?raw)
