Errors
Every error code returned by the third-party authorization API, with its HTTP status, meaning and recommended handling, plus the Swift SDK error mapping.
Error format
Every application error is returned as JSON:
{ "error": "signature_invalid" }
Error codes are stable, machine-readable strings. Branch on the code, not on the HTTP status or any message text.
Important A few errors are not JSON. A body over 400,000 bytes returns
413. Endpoints that parse a fixed structure —POST /api/oauth/clients,POST /api/oauth/authorize,POST /api/oauth/token— return the framework's plain-text400,415or422for malformed JSON, missing required fields or a missingContent-Type: application/json. On signed endpoints (/api/oauth/requests…,/api/oauth/revoke) any body parsing failure returns400 invalid_input.
Signatures and tokens
Applies to every signed call (/api/oauth/account, /api/oauth/requests…, /api/oauth/vaults…).
| HTTP | Code | Meaning | What to do |
|---|---|---|---|
| 401 | invalid_token | No Authorization header, or the access token is unknown | Refresh; if that fails, authorize again |
| 401 | token_expired | The access token has expired (3600-second lifetime) | Refresh, then retry once |
| 401 | grant_revoked | The grant was revoked by the user or by you, or its whole token family was revoked after refresh-token reuse | Stop retrying, wipe local credentials, ask the user to authorize again |
| 401 | client_disabled | The platform operator disabled the client | Stop calling; contact the platform |
| 403 | client_disabled | The developer disabled the client | Stop calling |
| 401 | signature_required | x-pm-timestamp, x-pm-nonce or x-pm-signature is missing | Check your signing code |
| 401 | signature_invalid | The timestamp is not an integer, the nonce is not 8–128 characters, or the signature does not verify | Check the canonical string: method, path without query, SHA-256 of the exact body bytes; do not modify the body after signing |
| 401 | timestamp_out_of_window | The timestamp is more than 300 seconds from server time | Fix the device clock and retry |
| 401 | nonce_replayed | The nonce was already used within this grant | Generate a fresh random nonce per request |
Client registration
POST /api/oauth/clients, DELETE /api/oauth/clients/{id}.
| HTTP | Code | Meaning | What to do |
|---|---|---|---|
| 400 | invalid_input | name is empty or longer than 100 characters | Fix the name |
| 400 | invalid_client_type | client_type is not public or confidential | Fix the type |
| 400 | invalid_redirect_uris | Not 1–10 redirect URLs, or one violates the rules for the client type | See redirect URL rules |
| 400 | invalid_public_key | seal_public_key is not 32 bytes of base64url | Fix the key |
| 400 | invalid_platforms | platforms is not an array, has more than 10 entries, or contains invalid characters | Use lowercase letters, digits, _ and - |
| 400 | invalid_brand | brand is not an object, or the color, icon URL or homepage is malformed | Use #RRGGBB for the color and https for the icon |
| 400 | mode_retired | A non-empty service_public_key was sent (mode 2 is retired) | Remove the field |
| 404 | client_not_found | The client to disable does not exist, is not yours, or is already disabled | — |
Authorization and consent
GET /api/oauth/authorize/info, POST /api/oauth/authorize (called by the MimiKey app). When these occur in MimiKey, the integrator usually receives the callback error=invalid_request.
| HTTP | Code | Meaning | What to do |
|---|---|---|---|
| 404 | client_not_found | The client_id does not exist | Check the client_id |
| 403 | client_disabled | The client was disabled by its developer or the platform | — |
| 400 | redirect_uri_mismatch | The redirect URL is not registered | Make it match a registered value exactly |
| 400 | invalid_approval_key | The approval public key is not 32 bytes | Check the approval_key deep-link parameter |
| 400 | invalid_public_key | The seal public key is not 32 bytes | Check the seal_key deep-link parameter |
| 400 | seal_key_required | A public client did not supply a seal key | Supply seal_key |
| 400 | code_challenge_required | A public client did not supply a PKCE challenge | Supply code_challenge |
| 400 | invalid_code_challenge | The method is not S256, or the challenge is not 43–128 base64url characters | Fix the PKCE values |
| 400 | invalid_policy | policy is not ask or trust | — |
| 400 | invalid_selections | No vault selected, more than 32, duplicates, or an invalid mode | — |
| 400 | mode_retired | Mode 2 was selected | Use mode 1 only |
| 403 | vault_not_owned | A selected vault is not owned by the user | — |
Tokens
POST /api/oauth/token.
| HTTP | Code | Meaning | What to do |
|---|---|---|---|
| 400 | unsupported_grant_type | grant_type is not authorization_code or refresh_token | — |
| 400 | invalid_client | A public client sent a client_secret | Remove client_secret |
| 401 | invalid_client_secret | The client_id is unknown, or a confidential client's secret is wrong | Check the client_id and secret |
| 403 | client_disabled | The client is disabled | — |
| 400 | invalid_code | The code is unknown or already used | Authorize again |
| 400 | code_expired | The code is older than 10 minutes | Authorize again |
| 400 | code_client_mismatch | The code belongs to a different client | — |
| 400 | redirect_uri_mismatch | redirect_uri differs from the one used at authorization | — |
| 400 | invalid_grant | PKCE verification failed or code_verifier is missing | Send the same verifier the challenge was made from; the code is now spent, so authorize again |
| 401 | invalid_refresh_token | The refresh token is missing, unknown, or belongs to another client | Authorize again |
| 401 | refresh_expired | The refresh token went unused for more than 30 days | Authorize again |
| 401 | refresh_token_reused | A rotated refresh token was presented again; every token of the grant has been revoked | Stop automatic retries immediately, wipe local credentials, ask the user to authorize again; check for concurrent refreshes or token leakage |
| 401 | grant_revoked | The grant has been revoked | Authorize again |
Revocation
POST /api/oauth/revoke. An unknown token returns { "ok": true } rather than an error.
| HTTP | Code | Meaning | What to do |
|---|---|---|---|
| 400 | invalid_input | The body is not valid JSON or lacks fields | — |
| 400 | invalid_client | A public client sent a client_secret | Remove client_secret and sign instead |
| 401 | invalid_client_secret | The client_id is unknown, or a confidential client's secret is wrong | — |
| 401 | signature_required and related | A public client revoking a live token sent no signature or a bad one | Sign with that grant's approval private key |
Requests
POST /api/oauth/requests, PUT /api/oauth/requests/{id}/patch, GET /api/oauth/requests/{id}, POST /api/oauth/requests/{id}/cancel.
| HTTP | Code | Meaning | What to do |
|---|---|---|---|
| 400 | invalid_input | The body is not valid JSON, hint is not an object, or an update_item was created with patch_enc | Fix the body |
| 400 | invalid_op | op is not a supported operation | — |
| 400 | item_id_required | get_item or update_item lacks item_id | — |
| 400 | unsupported_kind | The create_item kind is not one of the six kinds | — |
| 400 | fields_required | update_item lacks fields | — |
| 400 | invalid_fields | fields is empty, has more than 32 entries, or contains an invalid name | Use lowercase letters, digits and _, starting with a letter |
| 400 | invalid_patch | patch_enc is missing, larger than 64 KiB, or not a well-formed sealed box | Check your sealing code |
| 403 | vault_not_granted | The target vault is not in the grant (for a cross-vault pick_item, the grant has no vaults) | Use only vaults returned by the grant |
| 403 | mode_not_granted | The vault is in the grant but not in the mode this operation needs | — |
| 404 | request_not_found | The request does not exist or does not belong to this grant | — |
| 409 | request_not_draft | Uploading a patch to a request that is no longer a draft: the 60-second draft expired, the patch was already uploaded, or it is not an update_item | Create a new update_item and upload the patch within 60 seconds |
| 409 | request_not_pending | Cancelling a request that has already finished | GET it to read the final state |
| 429 | too_many_requests | The grant already has 100 unfinished requests (drafts included) | Cancel requests you no longer need, or wait for them to expire |
Device and user side
Used by the MimiKey apps; integrators normally never see these.
| HTTP | Code | Meaning |
|---|---|---|
| 401 | locked | The account session is missing or expired |
| 401 | device_revoked_or_rotated | The session's device was revoked or re-keyed |
| 403 | untrusted_origin | The request's Origin does not match the service |
| 404 | grant_not_found | The grant does not exist or is not the current user's |
| 409 | request_not_pending | The request already finished, or approve was sent for a pick, create or update request |
| 409 | request_not_approved | A read result was uploaded before the request was approved |
Operator side
| HTTP | Code | Meaning |
|---|---|---|
| 401 | admin_locked | Missing or invalid administrator session |
| 400 | reason_required | No reason (1–500 characters) given for disable or enable |
| 404 | client_not_found | The client does not exist |
Server errors
| HTTP | Code | Meaning | What to do |
|---|---|---|---|
| 500 | database_error | Server storage is temporarily unavailable | Retry with exponential backoff |
Retired codes
These codes belong to mode 2. In v3, mode_retired is checked first, so they no longer occur in practice: service_key_missing, wrap_missing.
Callback errors
When MimiKey returns to your redirect URL it may include an error parameter:
| Error | When | What to do |
|---|---|---|
access_denied | The user tapped Deny on the consent card | Respect the decision |
not_signed_in | MimiKey has no signed-in account | Ask the user to sign in to MimiKey first |
invalid_request | Malformed deep link, unknown or disabled client, or unregistered redirect URL | Check the client_id and redirect URL |
request_not_found | The request does not exist or belongs to a different MimiKey account | Check that the user is signed in to the same account they authorized with |
Request callbacks also carry status: completed, denied, cancelled or expired. These are request outcomes, not errors; see the callback summary.
SDK error mapping
The Swift SDK maps server error codes, request states and callbacks onto MimiKeyError:
MimiKeyError | Source |
|---|---|
.notInstalled(downloadURL:) | mimikey:// cannot be opened (not installed, or mimikey missing from LSApplicationQueriesSchemes on iOS) |
.notAuthorized | No grant in the device keychain |
.cancelled | cancelPending() was called or the Task was cancelled; the request state is cancelled |
.denied | Callback error=access_denied; the request state is denied |
.conflict | The request state is denied with deny_reason conflict |
.expired | The request state is expired; no result 15 seconds past expires_at; authorization waited longer than authorizeTimeout; 150 consecutive polling network failures |
.notSignedIn | Callback error=not_signed_in |
.revoked | grant_revoked, client_disabled, refresh_token_reused, invalid_refresh_token, refresh_expired, invalid_client, invalid_client_secret, or invalid_token persisting after a refresh. The SDK wipes local credentials |
.network(Error) | A URLSession failure (polling failures while waiting are retried automatically) |
.protocol(String) | Any other server code (such as vault_not_granted or invalid_grant), any other callback error (such as invalid_request or request_not_found), and the SDK-internal codes below |
token_expired and invalid_token are never thrown directly: the SDK refreshes and retries once first.
SDK-internal .protocol codes:
| Code | Meaning |
|---|---|
nothing_pending | retryOpen() was called with no flow waiting |
invalid_callback | The authorization callback carried no code |
no_granted_vault | createItem had no vault and the grant has none |
empty_patch | updateItem was called with an empty patch |
updated_item_missing | An update completed but the server returned no updated_item |
result_missing | A request completed without result_enc |
result_open_failed | The result could not be opened with the local seal private key (key or AAD mismatch) |
result_malformed | The result plaintext did not have the expected structure |
malformed_response | The server response was not the expected JSON |
http_<status> | A non-2xx response without a JSON error body |