API Documentation

Everything you need to integrate with AccorID’s OAuth identity platform.

Quickstart

Get up and running with AccorID OAuth in four steps.

1Create an app in the Developer Portal

Head to the Developer Portal, create a new application, and note your Client ID and Client Secret. Configure at least one Redirect URI.

2Build the authorization URL

Redirect the user’s browser to the authorization endpoint:

GET /oauth/authorize?client_id=YOUR_CLIENT_ID
    &redirect_uri=YOUR_CALLBACK
    &scope=profile email
    &response_type=code
    &state=RANDOM

cURL example:

curl -G "https://accorid.com/oauth/authorize" \
  --data-urlencode "client_id=YOUR_CLIENT_ID" \
  --data-urlencode "redirect_uri=YOUR_CALLBACK" \
  --data-urlencode "scope=profile email" \
  --data-urlencode "response_type=code" \
  --data-urlencode "state=RANDOM"
3Exchange the code for tokens

Once the user approves, they’re redirected to your callback with a code parameter. Exchange it for tokens:

POST /api/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=YOUR_CALLBACK
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

cURL example:

curl -X POST "https://accorid.com/api/oauth/token" \
  -d "grant_type=authorization_code" \
  -d "code=AUTH_CODE" \
  -d "redirect_uri=YOUR_CALLBACK" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"

Response:

{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "dGhpcyBpcyBh...",
  "scope": "profile email"
}
4Fetch user info

Use the access token to retrieve the user’s profile:

GET /api/oauth/userinfo
Authorization: Bearer ACCESS_TOKEN

cURL example:

curl "https://accorid.com/api/oauth/userinfo" \
  -H "Authorization: Bearer ACCESS_TOKEN"

Every userinfo response includes sub, id, and context. All other fields are scope-gated. The example below shows the response for a token holding profile email phone:

{
  "sub": "usr_abc123",
  "id": "usr_abc123",
  "context": { "type": "profile", "id": "prf_xyz" },
  "username": "janedoe",
  "display_name": "Jane Doe",
  "avatar": "https://accorid.com/api/users/usr_abc123/avatar",
  "email": "jane@example.com",
  "phone_verified": true
}

Verified-flag scope — if your app only needs a trust signal (not the number itself), request phone.verified. It returns only the boolean; the phone number is not shared.

AccorID.js — auth in five minutes

AccorID.js gives you drop-in sign-in, One Tap, and account management with a few lines of markup. Components render inside AccorID-hosted iframes (the same model Stripe uses for card fields), so credentials and passkeys never touch your page. Try everything live in the playground.

1Register your app

Create an app in the developer dashboard, add your site under Allowed Origins (e.g. https://yourapp.com), and register a redirect_uri.

2Add the SDK and a component
<script src="https://accorid.com/sdk/v1/accorid.js" defer></script>
<script>
  window.addEventListener('DOMContentLoaded', function () {
    AccorID.init({
      clientId: 'YOUR_CLIENT_ID',
      redirectUri: 'https://yourapp.com/auth/callback',
      scope: 'openid profile email',
      oneTap: true,            // returning users get one-click sign-in
    });
  });
</script>

<accorid-signin></accorid-signin>
3Receive the code

When the user finishes, the component emits an auth code. By default the browser is sent to your redirect_uri; or intercept it and hand it to your backend yourself:

document.querySelector('accorid-signin')
  .addEventListener('accorid:code', function (e) {
    // e.detail = { code, state, redirect }
    fetch('/auth/exchange', { method: 'POST', body: JSON.stringify(e.detail) });
  });

Exchange the code server-side at POST /api/oauth/token as usual — or, for SPAs, let the SDK do a PKCE exchange in the browser (no client secret needed):

// on https://yourapp.com/auth/callback
const session = await AccorID.handleRedirectCallback();
// session.accessToken, session.idToken, session.user
Fallbacks are automatic. If a browser blocks third-party cookies, embedded sign-in still works (the frame keeps its session in memory for the flow), and anything that can't complete inline falls back to a popup or full-page redirect to accorid.com.

Components

ElementWhat it doesKey events
<accorid-signin>Email/username + password sign-in with passkey button, 2FA, and reset built inaccorid:code, accorid:consent_required
<accorid-signup>Account creation with email verificationaccorid:code, accorid:event
<accorid-password-reset>Standalone password reset flowaccorid:event
<accorid-account-chooser>Pick between AccorID accounts signed in on this deviceaccorid:code, accorid:event
<accorid-user-button>Avatar button with account menu, switcher, and sign-outaccorid:user, accorid:signed_out
<accorid-identity>Lets the user edit the identity your app sees (per-app identity)accorid:event
<accorid-settings>Identity editor plus the user's sessions with your app (revocable) and a security link-out to AccorIDaccorid:event
<accorid-mfa>Step-up verification (passkey / TOTP / password) for sensitive actionsaccorid:verified
<accorid-connected-accounts>The user's linked external accounts—
<accorid-verify-email>Email verification promptaccorid:event
<accorid-signin-button>Branded "Continue with AccorID" button (no iframe)—
<accorid-badge verified>Compact verified-identity badge—

All components are responsive, keyboard-accessible, and themed with your app's consent theme, so your sign-in box and consent page read as one system. Events bubble, carry their payload in event.detail, and every surface handles its own loading, error, and success states.

Styling components to match your app

Pass a theme globally, per component, or with shorthand attributes — all validated server-side (same rules as the consent theme, so nothing can break accessibility):

// Global — every component + One Tap
AccorID.init({
  clientId: '…', redirectUri: '…',
  theme: { accent: '#0ea5e9', background: '#ffffff', radius: 8, google_font: 'Space Grotesk' },
  variant: 'flat',   // chromeless: blends straight into your page
});

<!-- Per component: shorthand attributes… -->
<accorid-signin accent="#0ea5e9" background="#ffffff" radius="8" shadow="sm"
  density="compact" font-size="15" google-font="Space Grotesk" variant="flat"></accorid-signin>

<!-- …or a full theme object -->
<accorid-user-button theme='{"accent":"#0ea5e9","background":"#ffffff","border_width":0}'></accorid-user-button>

With variant="flat" the card chrome disappears and the component sits directly on your page — set theme.background to your page color so the derived input and text colors match exactly. Theme keys: accent, mode, background, surface, radius, font, button_style — plus full-control keys: text, text_secondary, text_muted, border (rgba hex ok), border_width (0 removes borders), input_background, input_border, input_text, button_text, shadow (none/sm/md/lg), density (compact/comfortable/spacious), font_size (12–18), font_family (any installed stack), google_font (loaded by name from Google Fonts), and branding (minimal drops the AccorID wording from embedded surfaces — "Sign in to YourApp" instead of "Use your AccorID account"; the full-page consent screen keeps AccorID branding regardless). Explicit values are honored verbatim; anything unset derives from your colors with contrast math. Sizing is yours too: components fill their host element (height auto-fits), and max-width accepts any CSS length or "none". The simplest seamless setup stays: background = your page color, accent = your brand, variant="flat" — themed correctly, users can't tell the component isn't native to your app. Account management (password, sessions, passkeys, linked accounts) is deliberately not embeddable: those change the user's AccorID everywhere, so they live on the AccorID dashboard where that's unmistakable.

One Tap

One Tap shows a small corner prompt (bottom sheet on mobile) to visitors who already have an AccorID account in their browser: avatar, name, email, and a one-click Continue. If they previously granted your requested scopes, the click silently issues a fresh auth code — no consent screen, no navigation. Anything new falls back to the standard consent page in a popup.

AccorID.init({ clientId: '…', redirectUri: '…' });
AccorID.oneTap({
  signedIn: Boolean(currentUser),        // stay quiet when your app has a session
  onCode: ({ code }) => exchangeOnServer(code),
  onDismiss: () => {},                   // dismissals back off automatically (2h → 7d)
});

The prompt only renders when appropriate: never for signed-in users (per your signedIn flag), never without an AccorID device account, never during the post-dismiss cooldown, and never with a flash — eligibility is checked before anything becomes visible. Account switching is built in when multiple accounts are signed in on the device.

React, Vue, Svelte

// React
import { AccorIDProvider, SignIn, UserButton, OneTap } from 'https://accorid.com/sdk/v1/react.js';

<AccorIDProvider clientId="…" redirectUri="…">
  <SignIn onCode={({ code }) => exchange(code)} />
  <UserButton />
  <OneTap signedIn={!!user} onCode={({ code }) => exchange(code)} />
</AccorIDProvider>

// Vue 3
import AccorIDVue from 'https://accorid.com/sdk/v1/vue.js';
app.use(AccorIDVue, { clientId: '…', redirectUri: '…' });
// <accorid-signin @accorid:code="onCode" />

// Svelte
import { initAccorID, accorid } from 'https://accorid.com/sdk/v1/svelte.js';
initAccorID({ clientId: '…', redirectUri: '…' });
// <accorid-signin use:accorid={{ 'accorid:code': onCode }} />

TypeScript definitions: https://accorid.com/sdk/v1/accorid.d.ts. Vanilla JS needs no wrapper — the Web Components and window.AccorID are the whole API.

Allowed origins & security

Per-app identity

Users can curate how they present to each application: a distinct display name and avatar per app (a professional name on a work tool, a casual one on a game). Your app requests it with the app_identity scope (or individual sub-scopes) and only ever sees the identity curated for your app. Usernames are deliberately not per-app — the username your app sees is always the user's real, registered AccorID username, so it can't be impersonated or squatted through app identities.

ScopeField
app_identityEverything below
app_identity.display_nameDisplay name
app_identity.avatarAvatar (authenticated URL)
app_identity.bannerProfile banner (authenticated URL, optional)

Userinfo then includes:

"app_identity": {
  "display_name": "Vex",
  "username": "vex_plays",
  "avatar": "https://accorid.com/api/users/usr_abc123/avatar",
  "banner": "https://accorid.com/api/users/usr_abc123/banner",
  "pronouns": "she/her",
  "bio": "Support main.",
  "customized": ["display_name", "username", "avatar"]
}

Fields the user hasn't customized fall back to their regular profile, so the object is always complete; customized tells you which fields were set deliberately for your app. The avatar endpoint automatically resolves to the app-specific avatar when your credentials fetch it. Embed <accorid-identity> to let users edit this identity without leaving your app — they can also manage every app's identity at accorid.com/account/identities.

Webhooks

Instead of learning about changes at the user's next sign-in, register a webhook and AccorID pushes them to your backend the moment they happen. Configure the endpoint per app in the developer portal (Webhook card) — saving generates a signing secret, shown once.

EventSent whendata
pingYou press "Send test ping"{ app_id }
identity.updatedThe user changes their display name or avatar for your app{ user_id, fields }
sessions.revokedThe user signs out of your app from the settings embed or dashboard{ user_id, session_id? , all? }
grant.revokedThe user revokes your app's access — or an organization context your token used is gone (member removed/left, org blocked your app, org deleted; see reason){ user_id?, org_id?, reason? }
user.deletedThe user's AccorID account deletion is finalized{ user_id }
org.member.added / org.member.removed / org.member.leftOrg-owned apps only: membership changes in the owning organization{ org_id, user_id, via? }
org.role.changed, org.invite.created, org.domain.verified, org.app_policy.updated, org.ownership.transferredOrg-owned apps only: directory and policy lifecycle of the owning organization{ org_id, … }

Deliveries are JSON POSTs: { id, event, created_at, data }, with one automatic retry on network failure or 5xx. Treat them as hints and re-fetch authoritative state from the API — after identity.updated, call /api/oauth/userinfo; after sessions.revoked, grant.revoked, or user.deleted, end your app-side session for that user.

Verifying signatures

Every delivery carries X-AccorID-Signature: t=<unix_ms>,v1=<hex> where v1 is HMAC-SHA256 of t + '.' + rawBody with your signing secret. Recompute over the raw request body, compare with a constant-time check, and reject stale timestamps (±5 minutes) to block replays:

import crypto from 'node:crypto';

function verify(rawBody, header, secret) {
  const m = /^t=(d+),v1=([0-9a-f]+)$/.exec(header || '');
  if (!m) return false;
  const [ , t, sig ] = m;
  if (Math.abs(Date.now() - Number(t)) > 5 * 60 * 1000) return false;
  const expected = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

One theme drives your consent screen and every embedded component. It's a structured object — never CSS — where each field is validated (hex colors, clamped numbers, enums, sanitized font names), so themes can't inject styles or reach third-party hosts. Your explicit values are honored verbatim; anything you leave unset is derived from your colors with contrast math, and the API returns non-fatal warnings when an explicit choice has low contrast.

PUT /api/developer/apps/:id/theme
{
  "theme": {
    // Colors — set any, derive the rest
    "accent": "#0d9488",           // buttons, links, focus rings
    "background": "#faf7f2",       // page color; light/dark derives from luminance
    "surface": "#ffffff",          // card color
    "text": "#1a2e2b",             // primary text
    "text_secondary": "#4a5f5b",
    "text_muted": "#8a9995",
    "border": "#0d948833",         // #rrggbb or #rrggbbaa
    "input_background": "#f4f1ea",
    "input_border": "#0d948855",
    "input_text": "#1a2e2b",
    "button_text": "#ffffff",

    // Shape & depth
    "radius": 8,                   // 0–24
    "border_width": 1,             // 0–3 (0 removes borders)
    "shadow": "sm",                // none | sm | md | lg
    "button_style": "solid",       // solid | outline | soft

    // Typography & spacing
    "font_size": 15,               // 12–18, scales the whole component
    "density": "comfortable",      // compact | comfortable | spacious
    "google_font": "Space Grotesk",// loaded from Google Fonts by name
    "font_family": "Avenir, sans-serif", // or: your own installed stack
    "font": "system",              // or: built-in shorthand

    // Consent card only
    "hero_image_url": "/api/image/app-theme/APP_ID/hero",

    // White-label embeds
    "branding": "minimal",         // full (default) | minimal — hide AccorID wording

    "mode": "light"                // base palette when no colors are set
  }
}

Sizing is controlled by your page: components fill their host element and auto-fit their height; cap width with the max-width attribute (any CSS length, or "none"). Upload the optional consent hero with POST /api/developer/apps/:id/theme-hero (raw image body, 4 MB max). Edit visually with live preview in the developer dashboard, or experiment in the playground.

Endpoints

Authorization

GET /oauth/authorize

Starts the OAuth authorization flow. Redirect the user’s browser to this URL.

ParameterInDescription
client_idqueryYour application’s client ID
redirect_uriqueryMust match a registered redirect URI
scopequerySpace-separated list of scopes
response_typequeryMust be code
statequeryOpaque value for CSRF protection
code_challengequeryPKCE code challenge (recommended for public clients)
code_challenge_methodqueryS256 (required when code_challenge is present)

Token Exchange

POST /api/oauth/token

Exchange an authorization code or refresh token for access and refresh tokens.

ParameterInDescription
grant_typebodyauthorization_code or refresh_token
codebodyAuthorization code (when grant_type is authorization_code)
refresh_tokenbodyRefresh token (when grant_type is refresh_token)
redirect_uribodyMust match the URI used in the authorize request
client_idbodyYour application’s client ID
client_secretbodyYour application’s client secret (not needed for PKCE public clients)
code_verifierbodyPKCE code verifier (required if code_challenge was used during authorization)

Example response:

{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "dGhpcyBpcyBh...",
  "scope": "profile email"
}

User Info

GET /api/oauth/userinfo

Returns user data for the authenticated user. Some fields are always included; others depend on granted scopes.

ParameterInDescription
AuthorizationheaderBearer ACCESS_TOKEN

Always-included fields (returned regardless of scopes):

FieldTypeDescription
sub / idstringThe user’s unique AccorID user ID
contextobjectThe authorization context (type and id)

Scope-gated fields (only included if the matching scope was granted):

FieldScopeDescription
usernameprofile or profile.usernameThe user’s username
display_nameprofile or profile.display_nameDisplay name
avatarprofile or profile.avatarAuthenticated avatar URL — fetch it with your Bearer token (uses profile-specific avatar if set, otherwise falls back to user avatar)
emailemailPrimary email address (always verified — emails aren’t shared until the user verifies them)
phone_verifiedphoneWhether the user has a verified phone number
app_identityapp_identity or app_identity.* sub-scopesThe identity the user curated for your app (see Per-app identity)
accountsProvider scopes (e.g. discord, github)Linked accounts grouped by provider
identityidentity or identity.* sub-scopesVerified identity data

Example response (with profile email scopes):

{
  "sub": "usr_abc123",
  "id": "usr_abc123",
  "context": { "type": "profile", "id": "prf_xyz" },
  "username": "janedoe",
  "display_name": "Jane Doe",
  "avatar": "https://accorid.com/api/users/usr_abc123/avatar",
  "email": "jane@example.com"
}
Important — image URLs are authenticated AccorID URLs. Image URLs returned by the API (avatars, identity documents, etc.) point at AccorID’s servers and require authentication to fetch: include your OAuth Bearer token (Authorization: Bearer ACCESS_TOKEN) in the request. For the user avatar endpoint (/api/users/<user_id>/avatar) your app credentials (Authorization: Basic client_id:client_secret) also work for as long as the user’s grant stands. Because these URLs are not publicly fetchable, do not put them directly in an <img> tag — fetch the image server-side and serve (or cache) it through your own backend.

App Info

GET /api/oauth/app-info

Returns public information about a registered application.

ParameterInDescription
client_idqueryThe application’s client ID

Example response:

{
  "client_id": "app_xyz789",
  "name": "My Cool App",
  "icon": "https://accorid.com/app-icons/xyz789.png",
  "homepage": "https://mycoolapp.com"
}

Check Permission

GET /api/oauth/permissions

Checks whether the authenticated user holds a specific custom org permission (a key the organization defined under Admin Console → Permissions, e.g. billing.view). The token must carry the org.permissions or team.permissions OAuth scope. Parent keys imply children: a grant of billing satisfies a check for billing.view. End a scope with .* to list every granted key under a prefix.

ParameterInDescription
AuthorizationheaderBearer ACCESS_TOKEN
scopequeryCustom permission key to check (e.g. billing.view or billing.*)
org_idqueryOrganization ID — optional when the token's acting context is already an org or team

Example response:

{
  "scope": "billing.view",
  "granted": true,
  "data": { "limit": 5000 },
  "matched_key": "billing"
}
Access rules: the queried org must be one the user is a live member of, whose policy allows your app, and that this token was actually granted — its acting context, or covered by an orgs.list selection. Anything else returns 403.

Batch Permission Check

POST /api/oauth/permissions/batch

Check multiple custom permission keys in a single request. Same requirements and access rules as the single check above.

ParameterInDescription
AuthorizationheaderBearer ACCESS_TOKEN
scopesJSON bodyArray of custom permission keys to check
org_idJSON bodyOrganization ID — optional when the token's acting context is already an org or team

Example request body:

{
  "scopes": ["billing.view", "projects.deploy"],
  "org_id": "org_abc"
}

Example response:

{
  "results": {
    "billing.view": { "granted": true, "data": { "limit": 5000 } },
    "projects.deploy": { "granted": false, "data": null }
  }
}

Revoke Token

POST /api/oauth/revoke

Revokes an access or refresh token.

ParameterInDescription
tokenbodyThe token to revoke
client_idbodyYour application’s client ID
client_secretbodyYour application’s client secret

Returns 200 OK with an empty body on success.

OpenID Connect

AccorID is a standards-compliant OpenID Connect (OIDC) provider on top of the OAuth flows above. When your app includes openid in its requested scopes, the token response carries an additional id_token — an RS256-signed JWT that cryptographically proves the user’s AccorID identity.

This is what you want when one of your apps needs to hand a user’s AccorID identity to another backend (yours or a third party’s). The receiving service verifies the JWT against AccorID’s public JWKS — no network round-trip to AccorID required, and no way for a malicious caller to forge another user’s identity.

The openid scope only issues the id_token. To include profile or email claims, request openid profile email. Granted scopes determine which OIDC claims are populated, exactly matching the OIDC Core spec.

Discovery + JWKS

GET /.well-known/openid-configuration

Standard OIDC discovery document. Any OIDC client library can be pointed at https://accorid.com as the issuer and will auto-configure from this endpoint.

GET /.well-known/jwks.json

Public JSON Web Key Set used to verify id_token signatures. Cached for an hour. Same key as /api/auth/app/jwks — both Full-Auth JWTs and OIDC id_tokens are signed by it under the same kid.

id_token Claims

Request openid (plus any optional scopes) in the authorize URL:

GET /oauth/authorize?client_id=YOUR_CLIENT_ID
    &redirect_uri=YOUR_CALLBACK
    &scope=openid profile email
    &response_type=code
    &state=RANDOM
    &nonce=RANDOM_NONCE

The token exchange response now includes an id_token alongside access_token:

{
  "access_token": "...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "...",
  "scope": "openid profile email",
  "id_token": "eyJhbGciOiJSUzI1NiIs..."
}

Decoded payload (claims are scope-gated):

ClaimRequired scopeDescription
issalwayshttps://accorid.com
subalwaysThe user’s stable AccorID ID
audalwaysYour app’s client_id — reject if it doesn’t match yours
exp / iatalwaysStandard JWT timestamps. Default lifetime: 1 hour
auth_timealwaysUnix seconds at which the user originally authenticated to AccorID (session establishment time). Not the id_token's iat.
noncealways (if supplied)The exact nonce value your authorize URL sent
nameprofile or profile.display_nameUser’s display name
preferred_usernameprofile or profile.usernameUser’s username
pictureprofile or profile.avatarAuthenticated URL to the user’s avatar — fetch server-side with your Bearer token and re-serve it yourself
email / email_verifiedemailUser’s primary email address

Verifying id_tokens

Use any standard JWT/JOSE library. Algorithm: RS256. JWKS URI: https://accorid.com/.well-known/jwks.json. Validate:

Node example (using jose):

import { jwtVerify, createRemoteJWKSet } from 'jose';

const JWKS = createRemoteJWKSet(new URL('https://accorid.com/.well-known/jwks.json'));

const { payload } = await jwtVerify(id_token, JWKS, {
  issuer: 'https://accorid.com',
  audience: YOUR_CLIENT_ID,
});
// payload.sub is now the proven AccorID user ID.
Common pattern: app A wants to push data to app B keyed on a user’s AccorID. A obtains an id_token for the user (via openid scope), forwards it to B in the request body, and B verifies it via JWKS before trusting the sub. B never needs an AccorID client_id of its own.

Vault

The Vault provides per-app, per-user secure storage for key-value pairs, structured JSON, and binary files. It also offers encryption-as-a-service where AccorID encrypts data for your app to store externally.

Vault endpoints require a valid OAuth access token (Authorization: Bearer ACCESS_TOKEN). The user and app are identified from the token automatically. No special scopes are needed — vault is available to any authenticated user of your app.

Create Object

POST /api/vault/objects

Store a KV pair, JSON object, or file. For files, use multipart/form-data with a file field. For KV/JSON, send JSON with name, value, and optional type (kv or json).

JSON example:

POST /api/vault/objects
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json

{
  "name": "user_preferences",
  "type": "json",
  "value": { "theme": "dark", "lang": "en" },
  "metadata": { "version": 2 }
}

File upload example:

curl -X POST "https://accorid.com/api/vault/objects" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -F "file=@document.pdf" \
  -F "name=my-document" \
  -F 'metadata={"category":"docs"}'

List Objects

GET /api/vault/objects

List all objects stored by your app for the authenticated user.

ParameterInDescription
typequeryFilter by type: kv, json, or file
namequerySearch by name (partial match)
limitqueryMax results (default 50, max 200)
offsetqueryPagination offset

Get Object

GET /api/vault/objects/:id

Retrieve an object by ID. For KV/JSON types, returns JSON. For file types, streams the file binary with appropriate content headers.

Encrypt / Decrypt

POST /api/vault/encrypt

Encrypt arbitrary data using a per-user-per-app key managed by AccorID. The encrypted ciphertext is returned for your app to store on its own servers. AccorID holds the key, not the data.

POST /api/vault/encrypt
Authorization: Bearer ACCESS_TOKEN

{ "data": "sensitive information" }

Response: { "encrypted": "iv_hex:ciphertext_hex" }
POST /api/vault/decrypt

Decrypt data previously encrypted via the encrypt endpoint.

POST /api/vault/decrypt
Authorization: Bearer ACCESS_TOKEN

{ "encrypted": "iv_hex:ciphertext_hex" }

Response: { "data": "sensitive information" }

Cross-App Sharing

POST /api/vault/objects/:id/share

Share a vault object with another AccorID application. The target app can read (or read/write) the shared object via GET /api/vault/objects/:id and GET /api/vault/shared.

{
  "target_client_id": "OTHER_APP_CLIENT_ID",
  "permissions": "read"
}

Full Auth Services

AccorID can manage your app’s entire authentication layer. Instead of building your own session system after OAuth, let AccorID handle it.

Two modes are available:

Full Auth endpoints live under /api/auth/app/. No special scopes are needed. Session creation and JWT issuance use the user’s OAuth access token (Bearer). Server-to-server calls (verify, revoke, list, introspect) use app credentials (Basic client_id:client_secret).

Create Managed Session

POST /api/auth/app/sessions/create

After a user completes OAuth, call this to create a managed session. Store the returned session_token in the user’s cookie.

POST /api/auth/app/sessions/create
Authorization: Bearer ACCESS_TOKEN

{
  "expires_in": 86400,
  "metadata": { "device": "mobile" }
}

Response:
{
  "session_id": "ses_abc123",
  "session_token": "hex_token...",
  "expires_at": 1700086400000,
  "expires_in": 86400
}

Verify Session

GET /api/auth/app/sessions/verify

On each request your app receives, call this endpoint to verify the session and get user data. Authenticate with your app credentials (Basic auth).

GET /api/auth/app/sessions/verify?token=SESSION_TOKEN
Authorization: Basic base64(client_id:client_secret)

Response:
{
  "active": true,
  "session": { "id": "ses_abc", "expires_at": 1700086400000 },
  "user": { "id": "usr_abc", "username": "janedoe", "display_name": "Jane" },
  "roles": [{ "org_id": "org_abc", "role": "Admin" }]
}

Issue JWT

POST /api/auth/app/token/create

Issue an RS256-signed JWT containing user claims, roles, and permissions. Your app can verify this locally using the JWKS endpoint.

POST /api/auth/app/token/create
Authorization: Bearer ACCESS_TOKEN

{
  "expires_in": 3600,
  "include_roles": true,
  "include_permissions": true,
  "org_id": "org_abc",
  "custom_claims": { "plan": "pro" }
}

Response:
{
  "token": "eyJhbGciOiJSUzI1NiIs...",
  "expires_in": 3600
}

JWKS Endpoint

GET /api/auth/app/jwks

Public endpoint — no authentication required. Returns the JSON Web Key Set (JWKS) for verifying JWTs issued by AccorID. Use any standard JWT library to verify tokens locally.

GET /api/auth/app/jwks

Response:
{
  "keys": [{
    "kty": "RSA",
    "use": "sig",
    "alg": "RS256",
    "kid": "key_abc123",
    "n": "...",
    "e": "AQAB"
  }]
}

Digital Signatures

AccorID provides a cryptographic digital signature system based on ECDSA P-256 (ES256). Applications can generate signatures tied to a user’s identity, which can then be independently verified by any third party.

Two trust levels are supported:

All signatures are immutable — they are never hard-deleted. Every operation is logged with full request context (IP, user-agent, country). Signatures can be revoked but remain verifiable, with the revocation clearly marked.

User-Explicit Flow

Similar to the OAuth consent flow. The app creates a signature request, redirects the user, and receives a signature_id on success.

1Create a signature request
POST /api/signatures/request

Requires a Bearer token with signatures.request scope, or client credentials (client_id + client_secret in the JSON body).

ParameterInDescription
payload_hashbodySHA-256 (or similar) hash of the data to be signed. Min 16 chars.
labelbodyHuman-readable description shown to the user (e.g. “Employment Contract #4821”)
metadatabodyOptional key-value object displayed on the consent page
redirect_uribodyWhere to redirect after the user approves or declines. Must be registered.
statebodyOptional opaque value returned in the redirect

Example request:

POST /api/signatures/request
Authorization: Bearer ACCESS_TOKEN

{
  "payload_hash": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
  "label": "Terms of Service v2.1",
  "metadata": { "document_id": "doc_123", "version": "2.1" },
  "redirect_uri": "https://yourapp.com/signed",
  "state": "random_csrf_value"
}

Response:

{
  "request_id": "req_abc123",
  "sign_url": "https://accorid.com/signatures/sign?request_id=req_abc123",
  "expires_at": 1700001500000
}
2Redirect the user

Send the user to the sign_url. They’ll see a consent page showing the label, payload hash, and metadata. If they’re not logged in, they’ll authenticate first.

3Handle the redirect

On approval, the user is redirected to your redirect_uri with ?signature_id=...&state=.... On decline: ?error=declined&state=....

POST /api/signatures/approve

Called by the consent page (user session auth). Creates the ECDSA signature and redirects.

{ "request_id": "req_abc123" }

Response: { "redirect": "https://yourapp.com/signed?signature_id=sig_xyz&state=random_csrf_value" }
POST /api/signatures/decline

Called by the consent page when the user declines.

{ "request_id": "req_abc123" }

Response: { "redirect": "https://yourapp.com/signed?error=declined&state=random_csrf_value" }

App-Delegated Flow

For background signature generation without per-action user consent. Requires a Bearer token with the signatures.delegate scope.

POST /api/signatures/create

Creates a signature with type app_delegated. Verification will clearly indicate the user did not explicitly approve this specific signature.

ParameterInDescription
payload_hashbodyHash of the data to sign. Min 16 chars.
labelbodyDescription of what was signed
metadatabodyOptional key-value metadata
POST /api/signatures/create
Authorization: Bearer ACCESS_TOKEN

{
  "payload_hash": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6...",
  "label": "Session activity log batch #892"
}

Response:
{
  "id": "sig_abc123",
  "type": "app_delegated",
  "payload_hash": "a1b2c3d4...",
  "label": "Session activity log batch #892",
  "signature": "MEUCIQDx...",
  "signed_data": "{\"payload_hash\":\"a1b2...\",\"user_id\":\"usr_...\",...}",
  "created_at": 1700000000000
}

List & Get Signatures

GET /api/signatures

List signatures created by your app for the authenticated user. Requires any signatures scope (signatures.read, signatures.delegate, or signatures.request).

ParameterInDescription
limitqueryMax results (default 50, max 100)
offsetqueryPagination offset
GET /api/signatures/:id

Get a single signature’s details. Scoped to your app + authenticated user.

Verification

Verification endpoints are public — no authentication required. Anyone can verify a signature.

GET /api/signatures/:id/verify

Verify a signature and get full details including signer identity, app info, type, and the public key for offline verification.

GET /api/signatures/sig_abc123/verify

Response:
{
  "valid": true,
  "revoked": false,
  "signature_id": "sig_abc123",
  "type": "user_explicit",
  "payload_hash": "a1b2c3d4e5f6...",
  "label": "Terms of Service v2.1",
  "metadata": { "document_id": "doc_123" },
  "signer": {
    "id": "usr_abc123",
    "username": "janedoe",
    "display_name": "Jane Doe"
  },
  "app": {
    "name": "My App",
    "client_id": "app_xyz"
  },
  "signature": "MEUCIQDx...",
  "signed_data": "{\"payload_hash\":\"a1b2...\"}",
  "created_at": 1700000000000,
  "revoked_at": null,
  "public_key": { "kty": "EC", "crv": "P-256", ... },
  "algorithm": "ES256"
}
POST /api/signatures/verify

Verify that a signature matches a specific payload hash. Useful when you want to confirm the signed data corresponds to your document.

POST /api/signatures/verify

{
  "signature_id": "sig_abc123",
  "payload_hash": "a1b2c3d4e5f6..."
}

Response:
{
  "valid": true,
  "crypto_valid": true,
  "payload_match": true,
  "revoked": false,
  "type": "user_explicit",
  "label": "Terms of Service v2.1",
  "created_at": 1700000000000
}

Public Signing Keys

GET /api/signatures/keys

Public endpoint. Returns the current signing key(s) as JWK for offline signature verification.

GET /api/signatures/keys

Response:
{
  "keys": [{
    "kid": "key_abc123",
    "algorithm": "ES256",
    "public_key": {
      "kty": "EC",
      "crv": "P-256",
      "x": "...",
      "y": "..."
    },
    "created_at": 1700000000000
  }]
}

Identity Verification

AccorID integrates with Stripe Identity to let users verify their real-world identity using a government-issued photo ID and a selfie. Once verified, applications can request access to the verified data through granular OAuth scopes.

The verification flow is always user-initiated — the user starts verification from the AccorID dashboard or when an app requires it. Applications can require identity verification (via the verified_identity requirement) and can request identity data through scopes, but they cannot trigger verification on behalf of a user.

Start Verification

POST/api/identity/verify

Creates a Stripe verification session and returns the URL to redirect the user to. Requires authentication.

Query parameters:

ParameterDescription
return_toPath to redirect to after verification completes (default: /dashboard)

Response:

{
  "verification_id": "abc123...",
  "verification_url": "https://verify.stripe.com/..."
}

Returns 400 if identity is already verified or a verification is already pending.

Check Status

GET/api/identity/status

Returns the current identity verification status for the authenticated user.

{
  "verified": true,
  "status": "verified",
  "verification_id": "abc123...",
  "verified_at": 1700000000000,
  "created_at": 1700000000000
}

Possible status values: pending, verified, requires_input, failed, or null if never started.

Document Images

GET/api/identity/documents/:type

Serves identity document images from R2 storage. Requires a valid OAuth token with the appropriate scope. The :type parameter must be one of: front, back, or selfie.

TypeRequired Scope
front, backidentity.document_images or identity
selfieidentity.selfie or identity

Userinfo Data

When an OAuth token includes identity scopes, the /api/oauth/userinfo endpoint returns an identity object:

{
  "id": "user_abc...",
  "identity": {
    "verified": true,
    "verified_at": 1700000000000,
    "first_name": "Jane",
    "last_name": "Doe",
    "date_of_birth": "1990-01-15",
    "address": {
      "line1": "123 Main St",
      "city": "San Francisco",
      "state": "CA",
      "postal_code": "94102",
      "country": "US"
    },
    "document_type": "driving_license",
    "document_number_last4": "1234",
    "issuing_country": "US",
    "document_front_url": "/api/identity/documents/front",
    "document_back_url": "/api/identity/documents/back",
    "selfie_url": "/api/identity/documents/selfie"
  }
}

Each field is only included when the corresponding scope is present.

Identity Scopes

ScopeData ReturnedSensitive
identityAll identity data (parent scope)Yes
identity.statusVerification status and timestampNo
identity.nameVerified legal first/last nameNo
identity.dobDate of birthYes
identity.addressVerified legal addressYes
identity.document_typeType of document used (passport, etc.)No
identity.document_numberLast 4 digits of document numberYes
identity.document_imagesPhotos of the ID documentYes
identity.selfieVerification selfieYes
identity.countryIssuing country of the documentNo

Sensitive scopes display a confirmation checkbox on the consent screen. The user must acknowledge the risks before authorizing.

Webhook

Stripe sends verification results via a webhook to POST /api/stripe/webhook. AccorID verifies the signature using HMAC-SHA256, processes the verification result, downloads and stores document images in R2, and updates the database record. Events handled:

Role-Based Access Control (RBAC)

RBAC extends AccorID’s organization system with named roles that bundle permissions. Roles can be assigned to individual users or entire teams. When a team has a role, all team members inherit those permissions.

System-generated default roles (Admin, Member, Viewer) are seeded when an organization is created. Custom roles can be created via the API or developer portal.

Permission resolution order: Owner → Admin role → User roles → Team roles → Legacy team permissions. The first match wins.

Role Management

GET /api/orgs/:orgId/roles

List all roles in an organization, including their permissions.

POST /api/orgs/:orgId/roles

Create a custom role with an initial set of permissions.

{
  "name": "Editor",
  "description": "Can edit apps and manage team members",
  "permissions": ["apps.view", "apps.edit", "teams.view", "teams.manage"]
}
PUT /api/orgs/:orgId/roles/:roleId/permissions

Replace the permission set for a custom role.

{ "permissions": ["apps.view", "apps.edit", "apps.create"] }

Role Assignment

POST /api/orgs/:orgId/members/:userId/roles

Assign a role to a user in an organization.

{ "role_id": "role_abc123" }
POST /api/orgs/:orgId/teams/:teamId/roles

Assign a role to a team. All team members inherit the role’s permissions.

{ "role_id": "role_abc123" }
GET /api/orgs/:orgId/roles/effective/:userId

Resolve all effective permissions for a user from all sources (direct roles, team roles, legacy permissions).

Response:
{
  "role": "member",
  "permissions": ["apps.view", "teams.view", "teams.manage"],
  "details": [
    { "permission": "apps.view", "sources": ["role:Editor"] },
    { "permission": "teams.view", "sources": ["role:Member", "team_role:Engineering/Editor"] }
  ]
}

Choosing Scopes

Only request scopes you actually use. This is the single most important rule. Every scope on the consent screen is a chance for the user to decline — and a permanent commitment to handle that data responsibly. If you can’t point to a line of code that reads a field, do not request the scope.

Before you ship a scope list, walk through it field by field and ask the hard questions:

A good test: if a user reads your consent screen and asks “why does this app need my Discord email?”, can you answer in one sentence? If not, that scope is probably extra.

Scopes Reference

Request only the scopes your application needs. Users see exactly which data you’re requesting on the consent screen.

CategoryScopes
Profileprofile profile.username profile.display_name profile.avatar
App Identityapp_identity app_identity.display_name app_identity.avatar app_identity.banner
Emailemail
Phonephone phone.verified
Discorddiscord discord.id discord.username discord.display_name discord.email discord.avatar
GitHubgithub github.id github.username github.email github.avatar
GitLabgitlab gitlab.id gitlab.username gitlab.email gitlab.avatar
Googlegoogle google.id google.email google.name google.avatar
YouTubeyoutube youtube.channel_id youtube.title youtube.handle youtube.avatar youtube.stats
Steamsteam steam.id steam.username steam.avatar
Spotifyspotify spotify.id spotify.email spotify.display_name spotify.avatar
Twitchtwitch twitch.id twitch.username twitch.display_name twitch.email twitch.avatar
LinkedInlinkedin linkedin.id linkedin.name linkedin.email linkedin.avatar linkedin.headline linkedin.profile_url linkedin.verified
Xx x.id x.username x.name x.avatar x.verified x.metrics
Minecraftminecraft minecraft.uuid minecraft.username
Telegramtelegram telegram.id telegram.username
Networkip
Metaaccounts.all
Organizationorgs.list org.membership org.profile org.teams org.permissions
Teamteam.membership team.permissions
Rolesroles roles.read roles.permissions
Signaturessignatures.request signatures.delegate signatures.read
Identityidentity identity.status identity.name identity.dob identity.address identity.document_type identity.document_number identity.document_images identity.selfie identity.country
Profile Avatars: Users can set different avatars for each profile (personal, work, etc.). When the profile.avatar scope is granted, the avatar returned in /api/oauth/userinfo uses the profile-specific avatar if one is set, otherwise it falls back to the user’s global avatar. Provider scopes like discord.avatar return the avatar from that linked provider.
Verified-flag scope: phone.verified returns only a boolean (whether the user has any verified phone) without sharing the number itself. Use it when you need a trust signal but not the contact info.

Requirements

Applications can enforce requirements that users must meet before authorizing. These are checked during the consent flow and none of this data is shared with the developer.

RequirementDescription
verified_emailUser must have a verified email address
verified_phoneUser must have a verified phone number
email_or_phoneUser must have at least a verified email or phone
account_age_7dAccount must be at least 7 days old
account_age_30dAccount must be at least 30 days old
linked_discordUser must have a linked Discord account
linked_githubUser must have a linked GitHub account
linked_googleUser must have a linked Google account
linked_steamUser must have a linked Steam account
linked_minecraftUser must have a linked Minecraft account
verified_identityUser must have completed Stripe identity verification
Requirement checks happen server-side during consent. The developer only learns whether the user passed or failed — the underlying data (email, phone, etc.) is never exposed.

Organization Context

The AccorID OAuth flow supports authorizing as a personal profile, an organization, or a team. When your app requests any org.* or team.* scope, the consent screen shows an organization picker and the whole token is scoped to the user's choice — the acting context. Rich org data (teams, custom permission grants) only ever flows for that one acting org.

The chosen context is stored alongside the token and is returned in the /api/oauth/userinfo response:

{
  "sub": "usr_abc123",
  "username": "janedoe",
  "context": {
    "type": "org",
    "id": "org_abc"
  },
  "org": { "id": "org_abc", "name": "Acme", "icon_url": null, "role": "admin" }
}

Listing all memberships — orgs.list

To see which organizations a user belongs to (rather than acting as one), request the orgs.list scope. Consent shows a checklist of the user's organizations (or "all, including ones they join later"); userinfo then carries membership cards only:

"orgs": [
  { "id": "org_abc", "name": "Acme", "username": "acme", "icon_url": null, "banner_url": null, "role": "member" }
]

Membership cards (and the acting-org card) carry the org's name, handle, square logo (icon_url) and wide wordmark banner (banner_url) — use the banner in your own headers the way Gmail shows a Workspace logo, if you like.

The list is resolved when you read it: joining or leaving an org updates it on the next call with no re-consent, and it never includes teams, permissions, or custom grant data. Always handle an empty array — users may be in no organizations, or none they chose to share.

Org app policy

Organization admins can allow or block specific apps for their org (Admin Console → Applications). A blocked org is silently absent from every response your app sees — it is unpickable at consent, excluded from orgs.list, and an existing org-context token dies immediately: API calls return 401, refresh returns invalid_grant, and silent re-auth returns consent_required. Handle those by re-running the authorization flow; the user can continue personally.

Error Codes

The API returns standard HTTP status codes with a JSON error body.

StatusCodeDescription
400invalid_grantThe authorization code is invalid, expired, or has already been used
400unsupported_grant_typeThe grant_type parameter is not supported
401invalid_clientClient authentication failed (bad client_id or client_secret)
401invalid_tokenThe access token is invalid, expired, or revoked
403access_deniedThe user denied the authorization request
403unmet_requirementsThe user does not meet the application’s requirements

Error response body format:

{
  "error": "invalid_grant",
  "error_description": "The authorization code has expired."
}

Code Examples

Full OAuth flow examples for common languages.

// Exchange authorization code for tokens
const tokenRes = await fetch("https://accorid.com/api/oauth/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "authorization_code",
    code: authCode,
    redirect_uri: "https://yourapp.com/callback",
    client_id: "YOUR_CLIENT_ID",
    client_secret: "YOUR_CLIENT_SECRET",
  }),
});
const tokens = await tokenRes.json();

// Fetch user info
const userRes = await fetch("https://accorid.com/api/oauth/userinfo", {
  headers: { Authorization: `Bearer ${tokens.access_token}` },
});
const user = await userRes.json();
console.log(user);
import requests

# Exchange authorization code for tokens
token_res = requests.post("https://accorid.com/api/oauth/token", data={
    "grant_type": "authorization_code",
    "code": auth_code,
    "redirect_uri": "https://yourapp.com/callback",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
})
tokens = token_res.json()

# Fetch user info
user_res = requests.get("https://accorid.com/api/oauth/userinfo", headers={
    "Authorization": f"Bearer {tokens['access_token']}",
})
user = user_res.json()
print(user)