Users and credentials

Recovery codes API

One-time backup codes for users who lose their authenticator. Generates, counts, validates and deletes an identity's codes, and holds the tenant's recovery code settings.

https://{tenant-host}/recovery-code-service/api/v1audience recovery-code-service8 operations
Last checked against a live tenant: 28 October 2026checked live called on a tenantfrom source read in the service codenot confirmed not confirmed

When to use it

  • Give the user a set of backup codes right after they enroll an authenticator.
  • Let a user sign in with a backup code when the authenticator app isn't available.
  • Show how many codes are left, or replace the set.
  • Import existing codes when you migrate users from another system.

Connect

Base URL
https://{tenant-host}/recovery-code-service/api/v1
Token
POST https://{tenant-host}/oauth/v1/token with grant_type=client_credentials. The client ID and secret go in the form body.checked live
Client to create
Access admin → Web clients → a client with the grant type Client credentials. Not an API client: those are only for Access's own configuration API.checked live
Default audiences
must include recovery-code-servicechecked live
Default scopes
The recovery-codes:* scope each operation lists below. A tenant may not define these scopes in Access yet: then even the config read answers 403 "scope missing", and an Access admin must create the scopes first. Keep the client's scopes, joined with spaces, within 1024 characters.checked live
Tenant-Id header
Send Tenant-Id: <tenant-uuid> as for the other platform services. Tenant context comes from a shared library, so the exact header rules aren't confirmed.not confirmed
curl -s https://{tenant-host}/oauth/v1/token \
  -d grant_type=client_credentials \
  -d client_id=$CLIENT_ID \
  -d client_secret=$CLIENT_SECRET

curl -s https://{tenant-host}/recovery-code-service/api/v1/recovery-codes/config \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Tenant-Id: $TENANT_ID"

Operations

Open an operation to see its parameters and code, and send it to your tenant. The token needs the listed scope.

Connect to your tenant

Then open any operation below and send it.

Sends grant_type=client_credentials to https://{tenant-host}/oauth/v1/token through this site's server. The secret and token stay in this tab only; host and client ID are remembered.

What trips people up

403 "scope missing" with the audience present

checked live

The token endpoint accepts recovery-code-service and the service answers 403, not 401. The client lacks the recovery-codes:* scope, often because Access doesn't define these scopes on that tenant yet. Create them in Access, then add them to the client.

Codes are shown once

from source

POST and PUT return the plaintext codes in recoveryCodes, with count. The service stores only hashes: GET returns a count, never the codes. Show the set to the user right away. Don't retry PUT to get the codes again: each PUT makes a new set.

Generate refuses while any code is left

from source

POST answers 409 RCS-104 if even one code remains. PUT replaces the set and the old codes stop working. Ask the user before you replace a set: they may have printed it.

No codes left is a 404, not a count of 0

from source

GET answers 404 RCS-103 once every code is used or deleted. Treat that 404 as zero codes left.

A wrong code is HTTP 422, unlike TOTP

from source

Validate answers 200 with valid: true only on success. A wrong code is 422 RCS-106, and so is a user with no codes at all. TOTP returns 200 valid: false instead, so don't share one error branch between them.

Lockout blocks the right code too

from source

After maxValidationAttempts wrong codes, validate answers 422 RCS-105 before it checks the code, until autoUnlockSeconds pass. The unlock happens on the next request after that time.

autoUnlockSeconds 0 doesn't lock forever

from source

The setting's description says 0 means never unlock. The service adds 0 seconds, so the lock ends right away. Use a large value if you want a long lockout.

Codes match exactly

from source

Codes mix upper- and lower-case letters and digits. The service doesn't trim, change case or remove separators. Send exactly what the user typed, and don't apply TOTP's digits-only rule.

Import takes plaintext and skips blanks

from source

Send {"codes": [...]} with the codes themselves, not hashes. Blank entries are dropped without an error and duplicates are kept. The reply is 201 with an empty body: check the count afterwards. Import also answers 409 RCS-104 if codes remain.

Paused stops new codes, not sign-in

from source

With recovery codes paused for the tenant, generate, regenerate and config changes answer 422 RCS-108, while validate still works. Disabled also blocks validate, with 422 RCS-109.

Errors

Error bodies carry id (trace id), code, message and details. 401 and 403 come from the security layer and have no RCS code.

400RCS-100Invalid parameters, e.g. a blank code. Field errors are in details.
400RCS-101Invalid tenant.
404RCS-102Recovery code config not found for the tenant.
404RCS-103No codes left for this identity (GET).
409RCS-104Codes already exist (generate, import).
422RCS-105Locked after too many wrong codes.
422RCS-106Wrong code, or no codes left (validate).
422RCS-108Recovery codes paused or disabled: no new codes or config changes.
422RCS-109Recovery codes disabled: validation refused.
404RCS-110Authenticator state missing for the tenant.

When a call fails

ReplyMeansWhat to change
The service answers 401The token lacks this service's audience, or it expired.Add the audience to the client's Default audiences (Access admin → Web clients → the client), then get a new token.
The service answers 403 and the token has the audienceA scope the operation needs is missing.Add one of the operation's scopes to the client's Default scopes. Use the scope names your tenant lists under Access admin → Scopes.
Token endpoint: 401 invalid_clientUnknown client ID or wrong secret.Check the client ID and paste the secret again.
Token endpoint: 400 unauthorized_clientThe client may not use client credentials.Add the grant type Client credentials to the web client.
Token endpoint: 400 invalid_request about an audienceYou asked for an audience the client doesn't have.Add it to Default audiences, or stop asking for it.
Token endpoint: 500 right after scopes were addedThe client's Default scopes, joined with spaces, are over about 1024 characters.Move scopes to Additional scopes and request them per call, or split the work over two clients.
{"code":"not_found","message":"The requested location was not found"}That service isn't deployed on this tenant.Nothing to change in the client. Ask whoever runs the tenant.

Two kinds of client exist in Access. Service APIs like this one take a web client with the Client credentials grant. Access's own configuration API takes an API client: a web client sent there gets 401 invalid_credentials.