API Documentation
Everything you need to integrate with AccorID’s OAuth identity platform.
Quickstart
Get up and running with AccorID OAuth in four steps.
Head to the Developer Portal, create a new application, and note your Client ID and Client Secret. Configure at least one Redirect URI.
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"
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"
}
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.
Create an app in the developer dashboard, add your site under
Allowed Origins (e.g. https://yourapp.com), and register a
redirect_uri.
<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>
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
Components
| Element | What it does | Key events |
|---|---|---|
<accorid-signin> | Email/username + password sign-in with passkey button, 2FA, and reset built in | accorid:code, accorid:consent_required |
<accorid-signup> | Account creation with email verification | accorid:code, accorid:event |
<accorid-password-reset> | Standalone password reset flow | accorid:event |
<accorid-account-chooser> | Pick between AccorID accounts signed in on this device | accorid:code, accorid:event |
<accorid-user-button> | Avatar button with account menu, switcher, and sign-out | accorid: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 AccorID | accorid:event |
<accorid-mfa> | Step-up verification (passkey / TOTP / password) for sensitive actions | accorid:verified |
<accorid-connected-accounts> | The user's linked external accounts | — |
<accorid-verify-email> | Email verification prompt | accorid: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
- Each embedded surface is served with a
frame-ancestorsCSP naming exactly one origin — validated against your app's registered Allowed Origins (plus your redirect-URI origins). Nobody can frame surfaces configured for your app from anywhere else. - Credentials, passkeys, and AccorID cookies exist only inside the accorid.com iframe. Your page
exchanges structured
accorid:*messages with it — never secrets. - Silent sign-in (
POST /api/oauth/authorize/auto) succeeds only when a prior grant covers every requested scope; scope escalation always routes through the full consent screen.
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.
| Scope | Field |
|---|---|
app_identity | Everything below |
app_identity.display_name | Display name |
app_identity.avatar | Avatar (authenticated URL) |
app_identity.banner | Profile 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.
| Event | Sent when | data |
|---|---|---|
ping | You press "Send test ping" | { app_id } |
identity.updated | The user changes their display name or avatar for your app | { user_id, fields } |
sessions.revoked | The user signs out of your app from the settings embed or dashboard | { user_id, session_id? , all? } |
grant.revoked | The 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.deleted | The user's AccorID account deletion is finalized | { user_id } |
org.member.added / org.member.removed / org.member.left | Org-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.transferred | Org-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));
}
Theming reference
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
Starts the OAuth authorization flow. Redirect the user’s browser to this URL.
| Parameter | In | Description |
|---|---|---|
| client_id | query | Your application’s client ID |
| redirect_uri | query | Must match a registered redirect URI |
| scope | query | Space-separated list of scopes |
| response_type | query | Must be code |
| state | query | Opaque value for CSRF protection |
| code_challenge | query | PKCE code challenge (recommended for public clients) |
| code_challenge_method | query | S256 (required when code_challenge is present) |
Token Exchange
Exchange an authorization code or refresh token for access and refresh tokens.
| Parameter | In | Description |
|---|---|---|
| grant_type | body | authorization_code or refresh_token |
| code | body | Authorization code (when grant_type is authorization_code) |
| refresh_token | body | Refresh token (when grant_type is refresh_token) |
| redirect_uri | body | Must match the URI used in the authorize request |
| client_id | body | Your application’s client ID |
| client_secret | body | Your application’s client secret (not needed for PKCE public clients) |
| code_verifier | body | PKCE 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
Returns user data for the authenticated user. Some fields are always included; others depend on granted scopes.
| Parameter | In | Description |
|---|---|---|
| Authorization | header | Bearer ACCESS_TOKEN |
Always-included fields (returned regardless of scopes):
| Field | Type | Description |
|---|---|---|
sub / id | string | The user’s unique AccorID user ID |
context | object | The authorization context (type and id) |
Scope-gated fields (only included if the matching scope was granted):
| Field | Scope | Description |
|---|---|---|
username | profile or profile.username | The user’s username |
display_name | profile or profile.display_name | Display name |
avatar | profile or profile.avatar | Authenticated avatar URL — fetch it with your Bearer token (uses profile-specific avatar if set, otherwise falls back to user avatar) |
email | email | Primary email address (always verified — emails aren’t shared until the user verifies them) |
phone_verified | phone | Whether the user has a verified phone number |
app_identity | app_identity or app_identity.* sub-scopes | The identity the user curated for your app (see Per-app identity) |
accounts | Provider scopes (e.g. discord, github) | Linked accounts grouped by provider |
identity | identity or identity.* sub-scopes | Verified 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"
}
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
Returns public information about a registered application.
| Parameter | In | Description |
|---|---|---|
| client_id | query | The 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
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.
| Parameter | In | Description |
|---|---|---|
| Authorization | header | Bearer ACCESS_TOKEN |
| scope | query | Custom permission key to check (e.g. billing.view or billing.*) |
| org_id | query | Organization 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"
}
orgs.list selection. Anything else returns 403.Batch Permission Check
Check multiple custom permission keys in a single request. Same requirements and access rules as the single check above.
| Parameter | In | Description |
|---|---|---|
| Authorization | header | Bearer ACCESS_TOKEN |
| scopes | JSON body | Array of custom permission keys to check |
| org_id | JSON body | Organization 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
Revokes an access or refresh token.
| Parameter | In | Description |
|---|---|---|
| token | body | The token to revoke |
| client_id | body | Your application’s client ID |
| client_secret | body | Your 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.
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
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.
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):
| Claim | Required scope | Description |
|---|---|---|
iss | always | https://accorid.com |
sub | always | The user’s stable AccorID ID |
aud | always | Your app’s client_id — reject if it doesn’t match yours |
exp / iat | always | Standard JWT timestamps. Default lifetime: 1 hour |
auth_time | always | Unix seconds at which the user originally authenticated to AccorID (session establishment time). Not the id_token's iat. |
nonce | always (if supplied) | The exact nonce value your authorize URL sent |
name | profile or profile.display_name | User’s display name |
preferred_username | profile or profile.username | User’s username |
picture | profile or profile.avatar | Authenticated URL to the user’s avatar — fetch server-side with your Bearer token and re-serve it yourself |
email / email_verified | email | User’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:
issequalshttps://accorid.comaudequals yourclient_idexpis in the futurenonceequals the value you generated and stored before redirecting (mitigates replay)
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.
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.
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
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
List all objects stored by your app for the authenticated user.
| Parameter | In | Description |
|---|---|---|
| type | query | Filter by type: kv, json, or file |
| name | query | Search by name (partial match) |
| limit | query | Max results (default 50, max 200) |
| offset | query | Pagination offset |
Get Object
Retrieve an object by ID. For KV/JSON types, returns JSON. For file types, streams the file binary with appropriate content headers.
Encrypt / Decrypt
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" }
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
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:
- Managed Sessions — AccorID stores sessions; your app calls AccorID to verify on each request.
- JWT Mode — AccorID issues signed JWTs; your app verifies them locally using the public JWKS.
/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
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
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
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
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:
- User-explicit — The user is redirected to AccorID, sees what they’re approving, and explicitly generates the signature. Verification responses mark these as
user_explicit. - App-delegated — The app generates signatures in the background using a pre-authorized scope. Verification responses mark these as
app_delegated, so relying parties know the user did not explicitly approve that specific action.
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.
Requires a Bearer token with signatures.request scope, or client credentials (client_id + client_secret in the JSON body).
| Parameter | In | Description |
|---|---|---|
| payload_hash | body | SHA-256 (or similar) hash of the data to be signed. Min 16 chars. |
| label | body | Human-readable description shown to the user (e.g. “Employment Contract #4821”) |
| metadata | body | Optional key-value object displayed on the consent page |
| redirect_uri | body | Where to redirect after the user approves or declines. Must be registered. |
| state | body | Optional 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
}
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.
On approval, the user is redirected to your redirect_uri with ?signature_id=...&state=.... On decline: ?error=declined&state=....
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" }
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.
Creates a signature with type app_delegated. Verification will clearly indicate the user did not explicitly approve this specific signature.
| Parameter | In | Description |
|---|---|---|
| payload_hash | body | Hash of the data to sign. Min 16 chars. |
| label | body | Description of what was signed |
| metadata | body | Optional 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
List signatures created by your app for the authenticated user. Requires any signatures scope (signatures.read, signatures.delegate, or signatures.request).
| Parameter | In | Description |
|---|---|---|
| limit | query | Max results (default 50, max 100) |
| offset | query | Pagination offset |
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.
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"
}
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
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
Creates a Stripe verification session and returns the URL to redirect the user to. Requires authentication.
Query parameters:
| Parameter | Description |
|---|---|
return_to | Path 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
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
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.
| Type | Required Scope |
|---|---|
front, back | identity.document_images or identity |
selfie | identity.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
| Scope | Data Returned | Sensitive |
|---|---|---|
identity | All identity data (parent scope) | Yes |
identity.status | Verification status and timestamp | No |
identity.name | Verified legal first/last name | No |
identity.dob | Date of birth | Yes |
identity.address | Verified legal address | Yes |
identity.document_type | Type of document used (passport, etc.) | No |
identity.document_number | Last 4 digits of document number | Yes |
identity.document_images | Photos of the ID document | Yes |
identity.selfie | Verification selfie | Yes |
identity.country | Issuing country of the document | No |
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:
identity.verification_session.verified— verification succeededidentity.verification_session.requires_input— additional input neededidentity.verification_session.canceled/expired— verification failed
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.
Role Management
List all roles in an organization, including their permissions.
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"]
}
Replace the permission set for a custom role.
{ "permissions": ["apps.view", "apps.edit", "apps.create"] }
Role Assignment
Assign a role to a user in an organization.
{ "role_id": "role_abc123" }
Assign a role to a team. All team members inherit the role’s permissions.
{ "role_id": "role_abc123" }
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
Before you ship a scope list, walk through it field by field and ask the hard questions:
- Are you actually using that data? If nothing in your code reads a field, drop the scope. Every extra item on the consent screen is a chance for the user to decline.
- Do you really need all those scopes from one provider? If your app has a Discord integration, ask whether you need the Discord ID and username and display name and email and avatar. Most apps that “connect Discord” only need
discord.id— the rest are nice-to-haves you can fetch later if you ever need them. - Is there a narrower scope that fits? If you only need a trust signal (e.g. for a fraud check), use
phone.verified— you get a boolean, the user doesn’t hand over the number. - Would a requirement work better than a scope? If you just need to gate access on “user has linked Discord,” use the
linked_discordrequirement (see below). Requirements check server-side and return a pass/fail without sharing any underlying data. - Avoid
accounts.allunless you genuinely consume everything. It expands to every provider scope and shows on the consent screen as a wide ask. Users decline it more often than narrowly-scoped requests.
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.
| Category | Scopes |
|---|---|
| Profile | profile profile.username profile.display_name profile.avatar |
| App Identity | app_identity app_identity.display_name app_identity.avatar app_identity.banner |
email | |
| Phone | phone phone.verified |
| Discord | discord discord.id discord.username discord.display_name discord.email discord.avatar |
| GitHub | github github.id github.username github.email github.avatar |
| GitLab | gitlab gitlab.id gitlab.username gitlab.email gitlab.avatar |
google google.id google.email google.name google.avatar | |
| YouTube | youtube youtube.channel_id youtube.title youtube.handle youtube.avatar youtube.stats |
| Steam | steam steam.id steam.username steam.avatar |
| Spotify | spotify spotify.id spotify.email spotify.display_name spotify.avatar |
| Twitch | twitch twitch.id twitch.username twitch.display_name twitch.email twitch.avatar |
linkedin linkedin.id linkedin.name linkedin.email linkedin.avatar linkedin.headline linkedin.profile_url linkedin.verified | |
| X | x x.id x.username x.name x.avatar x.verified x.metrics |
| Minecraft | minecraft minecraft.uuid minecraft.username |
| Telegram | telegram telegram.id telegram.username |
| Network | ip |
| Meta | accounts.all |
| Organization | orgs.list org.membership org.profile org.teams org.permissions |
| Team | team.membership team.permissions |
| Roles | roles roles.read roles.permissions |
| Signatures | signatures.request signatures.delegate signatures.read |
| Identity | identity identity.status identity.name identity.dob identity.address identity.document_type identity.document_number identity.document_images identity.selfie identity.country |
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.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.
| Requirement | Description |
|---|---|
| verified_email | User must have a verified email address |
| verified_phone | User must have a verified phone number |
| email_or_phone | User must have at least a verified email or phone |
| account_age_7d | Account must be at least 7 days old |
| account_age_30d | Account must be at least 30 days old |
| linked_discord | User must have a linked Discord account |
| linked_github | User must have a linked GitHub account |
| linked_google | User must have a linked Google account |
| linked_steam | User must have a linked Steam account |
| linked_minecraft | User must have a linked Minecraft account |
| verified_identity | User must have completed Stripe identity verification |
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.
| Status | Code | Description |
|---|---|---|
| 400 | invalid_grant | The authorization code is invalid, expired, or has already been used |
| 400 | unsupported_grant_type | The grant_type parameter is not supported |
| 401 | invalid_client | Client authentication failed (bad client_id or client_secret) |
| 401 | invalid_token | The access token is invalid, expired, or revoked |
| 403 | access_denied | The user denied the authorization request |
| 403 | unmet_requirements | The 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)