59 endpoints. Send your API key as Authorization: Bearer <key> on each request; see The API and AI assistants for keys, errors, idempotency and the conventions every endpoint shares.
GET /api/health
Liveness check
Responses:
| Status | Meaning |
|---|---|
| 200 | Server is up |
GET /api/me
The signed-in user, the organisation this session acts in, and every organisation they belong to
user is null for an API key or the shared password.
Responses:
| Status | Meaning |
|---|---|
| 200 | The user and organisations |
PATCH /api/me
Change your name
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Up to 200 characters. |
Responses:
| Status | Meaning |
|---|---|
| 200 | Updated |
| 403 | Not a user session |
POST /api/me/email
Start a change of sign-in address; a link goes to the new address
The current password is checked. The account keeps its current address until the link sent to the new one is used (/auth/confirm-email-change). The answer is the same whether or not the new address belongs to another account; an address that bounced for good or complained is refused.
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | |
current_password | string | Yes |
Responses:
| Status | Meaning |
|---|---|
| 202 | If the address can be used |
| 400 | Current password wrong |
| 403 | Not a user session |
POST /api/me/export
Download your own data as a JSON file
Account details, memberships, passkey names, API key labels, sessions, invitations sent, what you own and what you did. Passwords, authenticator secrets, recovery codes, keys and session ids are never included. The current password is required again; ten attempts per hour.
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
current_password | string | Yes |
Responses:
| Status | Meaning |
|---|---|
| 200 | The file |
| 400 | Current password wrong |
| 403 | Not a user session |
| 429 | Too many attempts |
POST /api/me/confirm-age
Confirm you are 18 or over (accounts that have not yet)
Records, once, the time the person confirmed it, in the same column as at sign-up. Until it is given, a browser session can only read /api/me, confirm, export its data or start deleting the account; everything else answers 403 with code age_confirmation_required. Only true is accepted.
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
over_18 | boolean | Yes |
Responses:
| Status | Meaning |
|---|---|
| 200 | The user |
| 400 | over_18 was not true |
| 403 | Not a user session |
GET /api/me/policies
Updated terms and notices waiting for you (the group's policy versions feed)
Only when the server has the policy check (policy_check in /auth/options; 404 otherwise). Reads the Tweed Tyne Technologies legal site's versions.json (cached; a feed that cannot be read gives available: false and nothing is asked). agreements are the changed agreements you are asked to accept (the Terms of Service once for the organisation, asked of an owner; the End User Licence Agreement for each person); notices are shown for information (a member sees the organisation's Terms as a notice). Each document has slug, title, version, effective_date, url, what_changed, line and read_link, already worded. show is true once per sign-in while something is outstanding (false after I do not accept in this session). versions is what to send back. text and decline hold the approved wording; limited is true when, from the feed's enforcement date, you declined an agreement that has taken effect: every other /api call then answers 403 with code policy_acceptance_required and the api_blocked_message, except /api/me, the data export, billing and these routes. Works from the website and the app (X-Client: app); released app builds are never limited for not answering.
Responses:
| Status | Meaning |
|---|---|
| 200 | The outstanding documents and the wording |
| 403 | Not a user session |
| 404 | The policy check is not configured |
POST /api/me/policies/accept
I accept - records accepted for the outstanding agreements and seen for the notices
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
versions | object | Yes | The { slug: version } that GET /api/me/policies returned in versions |
Responses:
| Status | Meaning |
|---|---|
| 200 | The new state |
| 400 | versions missing |
| 409 | code policy_versions_changed: the documents changed since they were shown (nothing recorded; policies is the new state) |
| 503 | code policy_feed_unavailable: the feed cannot be read just now |
POST /api/me/policies/seen
Got it - records seen for the outstanding notices (only when no agreement is waiting)
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
versions | object | Yes |
Responses:
| Status | Meaning |
|---|---|
| 200 | The new state |
| 409 | code agreement_answer_required (an agreement is waiting) or policy_versions_changed |
POST /api/me/policies/decline
I do not accept - records declined for the outstanding agreements and seen for the notices
Nothing is deleted or cancelled and the session is not ended. The answer has declined true and the decline wording. Before the feed's enforcement date the person carries on; from it they have limited access until they accept.
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
versions | object | Yes |
Responses:
| Status | Meaning |
|---|---|
| 200 | The new state and the decline wording |
| 409 | code nothing_to_decline or policy_versions_changed |
GET /api/me/deletion-blockers
The organisations that stop you deleting your account
The organisations in which you are the only owner, with how many other members each has. Empty when you may delete your account.
Responses:
| Status | Meaning |
|---|---|
| 200 | The list |
| 403 | Not a user session |
POST /api/me/delete-request
Start deleting your account; a one-hour link goes to your address (kept for older pages: new requests use the Tweed Tyne Technologies Privacy Centre)
The account page now links to the Tweed Tyne Technologies Privacy Centre (GET /delete-account redirects there); this call is kept for pages and clients loaded before that change. Needs the current password, the authenticator or recovery code when two-factor is on, and the account's email address typed again. Ten attempts an hour (shared with exports). The account is deleted only when the emailed link is used, at once and for good. 409 when you are the only owner of an organisation (details.organisations lists them).
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
current_password | string | Yes | |
code | string | No | |
confirm_email | string | Yes |
Responses:
| Status | Meaning |
|---|---|
| 202 | The link is on its way |
| 400 | Password |
| 403 | Not a user session |
| 409 | You are the only owner of an organisation |
| 429 | Too many attempts |
POST /api/me/password
Change your password; signs out your other sessions
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
current_password | string | Yes | |
password | string | Yes | At least 10 characters. |
Responses:
| Status | Meaning |
|---|---|
| 200 | Changed |
| 400 | Current password wrong or new one invalid |
POST /api/me/organisation
Move this session to another organisation you belong to
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
org_id | integer | Yes |
Responses:
| Status | Meaning |
|---|---|
| 200 | Switched; the same body as GET /api/me |
| 404 | Not a member of that organisation |
GET /api/me/sessions
Your signed-in sessions (id is a short prefix of the stored hash)
Responses:
| Status | Meaning |
|---|---|
| 200 | Sessions |
DELETE /api/me/sessions/{sid}
Sign out one of your sessions
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
sid | path | string | Yes |
Responses:
| Status | Meaning |
|---|---|
| 204 | Signed out |
| 404 | Not one of your sessions |
POST /api/me/sessions/revoke-others
Sign out every session but this one
Responses:
| Status | Meaning |
|---|---|
| 200 | revoked is how many were signed out |
POST /api/me/two-factor/setup
Start setting up an authenticator app; returns the secret and an otpauth URL
Responses:
| Status | Meaning |
|---|---|
| 200 | Secret and otpauth_url |
| 409 | Already on |
POST /api/me/two-factor/enable
Turn two-factor sign-in on with a code from the app and your password; returns ten recovery codes, shown once
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | |
password | string | Yes | Your current password (not needed for an account with no password) |
Responses:
| Status | Meaning |
|---|---|
| 200 | recovery_codes |
| 400 | Wrong code or password |
POST /api/me/two-factor/disable
Turn two-factor sign-in off (needs your password)
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
password | string | Yes |
Responses:
| Status | Meaning |
|---|---|
| 200 | Off |
| 400 | Wrong password |
GET /api/me/passkeys
Your passkeys (name, whether synced, when added and last used)
Responses:
| Status | Meaning |
|---|---|
| 200 | Array of { id, name, backed_up, created_at, last_used_at } |
POST /api/me/passkeys
Save a passkey made from the options of POST /api/me/passkeys/options
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Defaults to "Passkey". Up to 100 characters. |
response | object | Yes | The credential from navigator.credentials.create(), as WebAuthn JSON (base64url fields) |
Responses:
| Status | Meaning |
|---|---|
| 201 | The saved passkey |
| 400 | Not verified: wrong or used challenge, wrong site, or the person was not verified on the device |
| 409 | Already registered |
POST /api/me/passkeys/options
WebAuthn options for adding a passkey (a one-time challenge valid for 5 minutes); needs your password
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
password | string | Yes |
Responses:
| Status | Meaning |
|---|---|
| 200 | PublicKeyCredentialCreationOptions as JSON: discoverable, user verification required |
| 400 | Wrong password |
| 409 | The account already has 20 passkeys |
DELETE /api/me/passkeys/{pid}
Remove a passkey; it stops working at once
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
pid | path | integer | Yes |
Responses:
| Status | Meaning |
|---|---|
| 204 | Removed |
| 404 | Not one of your passkeys |
GET /api/me/identities
Provider accounts (Google, Microsoft, Apple, GitHub, Slack) linked to your account
Responses:
| Status | Meaning |
|---|---|
| 200 | Array of { id, provider, label, created_at, last_used_at }; the label is a masked form of the provider-verified email |
POST /api/me/identities/{provider}/start
Start linking a provider account (google, microsoft, apple, github or slack); needs your password
Returns the address to send the browser to. Needs a browser session (not an API key). The browser-binding cookie af_oauth is set and the link is finished by the provider's callback. A provider that is not set up answers 400 provider_off.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
provider | path | string | Yes | One of: google, microsoft, apple, github, slack. |
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
current_password | string | Yes |
Responses:
| Status | Meaning |
|---|---|
| 200 | The provider address: { url } |
| 400 | Wrong password |
DELETE /api/me/identities/{id}
Unlink a provider account; needs your password
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | Yes |
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
current_password | string | Yes |
Responses:
| Status | Meaning |
|---|---|
| 204 | Unlinked |
| 400 | Wrong password |
| 404 | Not one of your linked sign-ins |
| 409 | It is the account's only way to sign in |
GET /api/meta
Allowed enum values and the caller's actor name
Responses:
| Status | Meaning |
|---|---|
| 200 | Metadata |
| 401 | Missing or invalid API key (Error) |
GET /api/me/notices
Your switch for emails about your work
Only when the server has WORK_EMAILS=on; otherwise this route does not exist. The emails are "A task was assigned to you", "A task is due today" and "A task is overdue" (once each per task and due date, by the UTC date; never for done or cancelled tasks or free-text owners): a fixed subject and a link, never any task text, sent only to a current member of the same organisation. A key that is not tied to a person gets a 400.
Responses:
| Status | Meaning |
|---|---|
| 200 | The setting (WorkEmailSetting) |
| 400 | The request body is invalid or is not valid JSON (Error) |
| 401 | Missing or invalid API key (Error) |
PUT /api/me/notices
Switch emails about your work on or off
Read before every email, so switching off works before the next one.
Request body (application/json, required): WorkEmailSetting.
| Field | Type | Required | Description |
|---|---|---|---|
work_emails | boolean | Yes | On by default. |
Responses:
| Status | Meaning |
|---|---|
| 200 | The setting (WorkEmailSetting) |
| 400 | The request body is invalid or is not valid JSON (Error) |
| 401 | Missing or invalid API key (Error) |
GET /api/me/devices
Your phones and tablets in the current organisation
Only when the server has MOBILE_PUSH=on; otherwise this route does not exist. Never includes the push tokens. Needs a person's sign-in: a key that is not tied to a person gets a 400.
Responses:
| Status | Meaning |
|---|---|
| 200 | Your devices, newest first (array of Device) |
| 400 | The request body is invalid or is not valid JSON (Error) |
| 401 | Missing or invalid API key (Error) |
POST /api/me/devices
Register this app install's push token
The label is your own words; by default it says only the kind of device. Every notification choice starts off. The token is deleted when this sign-in ends, when you leave the organisation, when the account or organisation is deleted, when you remove the device, when a push provider reports it invalid, and after 90 days unused. Registering a token last used by someone else moves it to you with every choice off.
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | At least 8 characters. Up to 4096 characters. |
platform | string | Yes | One of: ios, android, web. |
label | string | No | At least 1 character. Up to 60 characters. |
Responses:
| Status | Meaning |
|---|---|
| 201 | The device (Device) |
| 400 | The request body is invalid or is not valid JSON (Error) |
| 401 | Missing or invalid API key (Error) |
PATCH /api/me/devices/{id}
Change a device's label and notification choices
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | Yes | From 1 to 2147483647. |
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
label | string | No | At least 1 character. Up to 60 characters. |
notify_down | boolean | No | |
notify_overdue | boolean | No | |
notify_mentions | boolean | No | A push when someone @mentions you in a comment (generic wording: no names or text). |
notify_assigned | boolean | No | A push when a task is assigned to you (generic wording). |
summary_hour | integer or null | No | The hour of a morning "due today" summary, or null for none. From 0 to 23. |
time_zone | string | No | Up to 64 characters. |
Responses:
| Status | Meaning |
|---|---|
| 200 | The device (Device) |
| 400 | The request body is invalid or is not valid JSON (Error) |
| 401 | Missing or invalid API key (Error) |
| 404 | Item (or its parent) does not exist (Error) |
DELETE /api/me/devices/{id}
Remove a device (its token is deleted at once)
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | Yes | From 1 to 2147483647. |
Responses:
| Status | Meaning |
|---|---|
| 204 | Removed |
| 401 | Missing or invalid API key (Error) |
| 404 | Item (or its parent) does not exist (Error) |
POST /api/me/devices/{id}/test
Send a test notification to one of your phones
Goes through Apple (APNs) or Google (FCM) to the device. At most 5 an hour per person.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | Yes | From 1 to 2147483647. |
Responses:
| Status | Meaning |
|---|---|
| 200 | Sent |
| 401 | Missing or invalid API key (Error) |
| 404 | Item (or its parent) does not exist (Error) |
| 429 | Too many test messages |
| 502 | The push service did not accept the test |
| 503 | Phone notifications are not set up on the server |
GET /api/me/digest
Your optional daily summary email
Only when the server has WORK_EMAILS=on; otherwise this route does not exist. The summary is off until you turn it on. It is sent at most once a day, in the hour and time zone you choose, only when you have tasks due today or overdue, and holds counts only (never task names) and a link to My work. Switching off all work emails (/api/me/notices) stops it too. A key that is not tied to a person gets a 400.
Responses:
| Status | Meaning |
|---|---|
| 200 | The setting (DailySummarySetting) |
| 400 | The request body is invalid or is not valid JSON (Error) |
| 401 | Missing or invalid API key (Error) |
PUT /api/me/digest
Change your daily summary (any of enabled, hour, time_zone)
Request body (application/json, required): DailySummarySetting.
| Field | Type | Required | Description |
|---|---|---|---|
enabled | boolean | No | Default false. |
hour | integer | No | Default 8. From 0 to 23. |
time_zone | string | No | An IANA time zone name. Default "Europe/London". Up to 64 characters. |
Responses:
| Status | Meaning |
|---|---|
| 200 | The setting (DailySummarySetting) |
| 400 | The request body is invalid or is not valid JSON (Error) |
| 401 | Missing or invalid API key (Error) |
POST /auth/login
Sign in to the web UI
With email and password (and code when two-factor sign-in is on), signs in to an account and sets a 30-day HttpOnly af_session cookie for a server-side session in the organisation used last. Without email, accepts the old shared WEB_AUTH password (or an API key) for organisation 1, while WEB_AUTH is set. Cookie-authenticated /api requests must also send X-Client: web. A right password with two-factor on and no code answers 401 with two_factor_required: true; an unverified address answers 403 with email_unverified: true and a new link is emailed. After 10 failed attempts from one IP address, further attempts are refused for 15 minutes.
A service that checks a password on behalf of its own user (the MCP connector's sign-in page) can send its API key with X-Relayed-Client-IP: <user's IP>, so the lockout applies to that user instead of the service. The header is ignored without a valid API key.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Relayed-Client-IP | header | string | No | The end user's IP address. Honoured only with a valid API key. |
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
email | string (email) | No | |
password | string | Yes | |
code | string | No | 6-digit authenticator code or a recovery code. |
Responses:
| Status | Meaning |
|---|---|
| 200 | Signed in |
| 401 | Incorrect password (Error) |
| 429 | Too many failed attempts (Error) |
POST /auth/app/login
Sign in from the native app (returns the session token instead of a cookie)
For the AperioFlow iOS and Android app (mobile/). The same checks, limits and lockouts as /auth/login with email and password (and code when two-factor sign-in is on), but the 30-day server-side session's token is returned once in token and no cookie is set. The app keeps it in the phone's secure storage and sends it on every /api call as X-Session-Token, together with X-Client: app (a browser cannot add either header to a cross-site request without a CORS preflight, so this is not open to CSRF). Everything a signed-in session can do works the same way, including /api/me/devices. POST /auth/logout with the same two headers ends the session. Failures carry a code: bad (wrong email or password), two_factor (a code is needed, or it was wrong), unverified, locked.
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
email | string (email) | Yes | |
password | string | Yes | |
code | string | No | 6-digit authenticator code or a recovery code. |
Responses:
| Status | Meaning |
|---|---|
| 200 | Signed in |
| 401 | Incorrect email or password, or a two-factor code is needed (Error) |
| 403 | The address is not verified yet (a new link is emailed) (Error) |
| 429 | Too many failed attempts (Error) |
POST /auth/logout
Sign out (ends the server-side session and clears the cookie)
Responses:
| Status | Meaning |
|---|---|
| 204 | Signed out |
POST /auth/signup
Create an account and an organisation it owns
Only when the server has SIGNUP_OPEN=true (404 otherwise). The answer is the same whether or not the address has an account; the address gets a confirmation link.
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
email | string (email) | Yes | |
password | string | Yes | At least 10 characters. Up to 200 characters. |
name | string | Yes | |
organisation | string | Yes | |
accept_terms | boolean | Yes | The Terms tick: I agree to the Terms of Service, including the End User Licence Agreement and the Data Processing Addendum, for myself or for the organisation I represent, and I confirm I am authorised to act for it. Records accepted for those three and seen for the Privacy Policy and Cookie Notice at the current versions. Must be true. |
over_18 | boolean | Yes | Must be true: the person confirms they are 18 or over. The time is stored on the account; no date of birth is asked for. |
Responses:
| Status | Meaning |
|---|---|
| 202 | Done |
| 400 | Invalid fields or link (Error) |
| 429 | More than 10 of these requests from one IP in 15 minutes |
POST /auth/confirm-email-change
Use the link sent to a new address to change the account's sign-in address
The link is valid for 48 hours and works once. The new address becomes the account's address, verified.
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes |
Responses:
| Status | Meaning |
|---|---|
| 200 | Done |
| 400 | Invalid or expired link (Error) |
| 409 | The address now belongs to another account |
POST /auth/confirm-account-deletion
Use the emailed link to delete the account, at once and for good
The link is valid for one hour and works once. Removes the account, its memberships, sessions, passkeys, API keys and sign-in links; the person's work stays with the organisation, shown as "Deleted user". A last-owner check runs again here and a refusal leaves the link unused. Copies in backups are overwritten within 28 days.
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes |
Responses:
| Status | Meaning |
|---|---|
| 200 | Deleted |
| 400 | Invalid or expired link (Error) |
| 409 | You are now the only owner of an organisation |
POST /auth/confirm-organisation-deletion
Use the emailed link to delete the organisation, at once and for good
The link is valid for one hour and works once. The owner and subscription checks run again; a refusal leaves the link unused. Everything under the organisation is removed, every member is emailed, their accounts stay. Copies in backups are overwritten within 28 days.
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes |
Responses:
| Status | Meaning |
|---|---|
| 200 | Deleted |
| 400 | Invalid or expired link (Error) |
| 403 | The person is no longer an owner |
| 409 | A subscription is now active |
POST /auth/verify-email
Confirm an email address with the emailed token
The link is valid for 48 hours and works once.
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes |
Responses:
| Status | Meaning |
|---|---|
| 200 | Done |
| 400 | Invalid fields or link (Error) |
| 429 | More than 10 of these requests from one IP in 15 minutes |
POST /auth/resend-verification
Send a new confirmation link
Says nothing about whether the address has an account.
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
email | string (email) | Yes |
Responses:
| Status | Meaning |
|---|---|
| 202 | Done |
| 400 | Invalid fields or link (Error) |
| 429 | More than 10 of these requests from one IP in 15 minutes |
POST /auth/forgot-password
Email a password reset link
The link is valid for an hour and works once. Says nothing about whether the address has an account.
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
email | string (email) | Yes |
Responses:
| Status | Meaning |
|---|---|
| 202 | Done |
| 400 | Invalid fields or link (Error) |
| 429 | More than 10 of these requests from one IP in 15 minutes |
POST /auth/reset-password
Set a new password with the emailed token
Signs out every session of the account and confirms the address.
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | |
password | string | Yes | At least 10 characters. Up to 200 characters. |
Responses:
| Status | Meaning |
|---|---|
| 200 | Done |
| 400 | Invalid fields or link (Error) |
| 429 | More than 10 of these requests from one IP in 15 minutes |
POST /auth/passkey/options
WebAuthn options for signing in with a passkey (a one-time challenge valid for 5 minutes)
Responses:
| Status | Meaning |
|---|---|
| 200 | PublicKeyCredentialRequestOptions as JSON; no allowCredentials, so the browser offers the passkeys it holds for this site |
| 429 | More than 30 of these requests from one IP in 15 minutes |
POST /auth/passkey
Sign in with a passkey
Checks the browser's answer to POST /auth/passkey/options and sets the af_session cookie, as /auth/login does. A passkey needs no email, password or authenticator code: the device has checked the person. A failure counts towards the same 10-per-15-minutes lockout.
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
response | object | Yes | The credential from navigator.credentials.get(), as WebAuthn JSON. |
Responses:
| Status | Meaning |
|---|---|
| 200 | Signed in |
| 401 | Unknown passkey |
| 403 | Address not verified (email_unverified: true) |
| 429 | Locked out after failed sign-ins |
GET /auth/provider/{provider}/start
Sign in with Google, Microsoft, Apple, GitHub or Slack (browser navigation)
Redirects the browser to the provider (302) and sets the af_oauth browser-binding cookie. Only the OAuth request is sent to the provider. A provider that is not set up, or any failure, redirects to /#/provider-error/<code>.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
provider | path | string | Yes | One of: google, microsoft, apple, github, slack. |
client | query | string | No | From the iOS or Android app, opened in the phone's secure browser. The flow then ends at aperioflow://provider-sign-in with pending, needs_terms and needs_second_factor, or with error; finish it with POST /auth/app/provider/confirm. One of: app. |
challenge | query | string | No | With client=app: base64url SHA-256 of a random value only the app holds. |
Responses:
| Status | Meaning |
|---|---|
| 302 | To the provider |
| 429 | Too many requests or recent failures from this IP |
POST /auth/app/provider/confirm
Finish a provider sign-in started in the app
The pending sign-in handed back to the app, the value behind the challenge the app sent at the start (verifier), and what the pending sign-in still needs: over_18 and accept_terms on the first sign-in through that identity, code (authenticator or recovery code) when two-factor is on. Returns the session token once, for X-Session-Token with X-Client: app. A pending sign-in started in a browser cannot be finished here, and one started in the app cannot be finished by a browser.
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
pending | string | Yes | |
verifier | string | Yes | |
over_18 | boolean | No | |
accept_terms | boolean | No | |
code | string | No |
Responses:
| Status | Meaning |
|---|---|
| 200 | { ok: true, token } |
| 400 | Expired, already used, the wrong verifier (bad_state), or the 18+ and Terms box not ticked |
| 401 | The authenticator code is missing or wrong (two_factor) |
| 429 | Too many requests or recent failures from this IP |
GET /auth/provider/{provider}/callback
The provider's redirect back (browser navigation); finishes a link or a sign-in
Uses the one-time state (10 minutes, one use, same browser). A link made from a signed-in account redirects to /#/account. A sign-in finds the account by the provider's user id only: an unlinked provider account signs nobody in and creates nothing (/#/provider-error/not_linked). A linked account is signed in at once, or, on its first sign-in through that identity or when two-factor is on, redirected to /#/provider-confirm (cookie af_pending, 5 minutes) for the 18+ and terms box and/or the authenticator code.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
provider | path | string | Yes | One of: google, microsoft, apple, github, slack. |
Responses:
| Status | Meaning |
|---|---|
| 302 | To / (signed in), /#/account (linked), /#/provider-confirm or /#/provider-error/<code> |
| 429 | Too many requests or recent failures from this IP |
POST /auth/provider/{provider}/callback
Apple's form POST back (browser navigation); carried on as the GET callback
Apple answers with a form POST (response_mode=form_post). Only code, state and error are kept, each a short plain value, and the browser is sent on to the GET callback (303), which does every check. Apple's ID token and user field are dropped unread. Any other provider redirects to /#/provider-error/provider_off.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
provider | path | string | Yes | One of: apple. |
Request body (application/x-www-form-urlencoded)
Responses:
| Status | Meaning |
|---|---|
| 302 | To /#/provider-error/provider_off |
| 303 | To the GET callback |
GET /auth/provider/{provider}/signup
Create an account with Google, Microsoft, Apple, GitHub or Slack (browser navigation; only while sign-up is open)
Like the sign-in start route, but a provider account that is not yet linked leads to /#/provider-confirm to give a name and an organisation name and confirm 18+ and the Terms. An address that already has an account is never merged: it redirects to /#/provider-error/account_exists. Closed sign-up redirects to /#/provider-error/signup_closed.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
provider | path | string | Yes | One of: google, microsoft, apple, github, slack. |
Responses:
| Status | Meaning |
|---|---|
| 302 | To the provider |
| 429 | Too many requests or recent failures from this IP |
POST /auth/provider/signup
Finish a provider sign-up
Creates a verified account with no password, owner of a new organisation, linked to the provider, and emails a set-a-password link.
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | |
organisation | string | Yes | |
over_18 | boolean | Yes | |
accept_terms | boolean | Yes |
Responses:
| Status | Meaning |
|---|---|
| 201 | Account made and signed in; sets the af_session cookie |
| 400 | A field is missing or a box was not ticked |
| 429 | Locked out after failed sign-ins |
GET /auth/provider/pending
What a pending provider sign-in or sign-up still needs
Responses:
| Status | Meaning |
|---|---|
| 200 | { needs_terms, needs_second_factor }, plus sign_up: true and provider for a sign-up |
| 404 | None |
POST /auth/provider/confirm
Finish a pending provider sign-in
Request body (application/json, required).
| Field | Type | Required | Description |
|---|---|---|---|
code | string | No | Authenticator or recovery code, when two-factor is on. |
over_18 | boolean | No | |
accept_terms | boolean | No |
Responses:
| Status | Meaning |
|---|---|
| 200 | Signed in; sets the af_session cookie |
| 400 | The box was not ticked |
| 401 | Wrong code (two_factor_required: true); five wrong codes end the pending sign-in |
| 429 | Locked out after failed sign-ins |
GET /auth/options
Which ways of signing in the server allows
Responses:
| Status | Meaning |
|---|---|
| 200 | signup is SIGNUP_OPEN; password_only is whether the shared WEB_AUTH password is on; passkeys is whether passkey sign-in is available. |