OAuth error codes
Every error response follows the RFC 6749 §5.2 format.
{
"error": "<machine-readable code>",
"error_description": "<human-readable description>",
"error_uri": "https://docs.1pass.dev/oauth/errors#<error>",
"request_id": "8f0c1e7b-..."
}Response signals at a glance
The signals your RP server receives come over two channels: the response body and the response headers. They are designed to be connected to the console (web) by the same request_id, which makes it easy for an RP to cross-reference them in its own logs.
| Channel | Field | Purpose |
|---|---|---|
| body | error | Machine-readable code (RFC 6749 §5.2) |
| body | error_description | One-line human-readable description |
| body | error_uri | The anchor in this document for the code |
| body | request_id | The same key as the console request log |
| header | X-Logi-Request-Id | Lets you trace even when the body is empty (e.g. HEAD) |
| header | X-Logi-Console-Url | A deep link to the console request_logs, when the RP is authenticated in production |
| header | X-Logi-Scope-Drift | Included on token 200 responses — when drift was recorded within the last 7 days. Under the default block policy a drift request is itself rejected with invalid_scope; the header applies to apps explicitly set to log_only/alert, or as an echo of rejected/recorded history |
If you're integrating for the first time
Just record the response body's error_description in your RP server log at INFO level. Most integration mistakes are explained by one of two things — redirect_uri mismatch or PKCE verifier mismatch — and the error_description carries the exact cause.
Reference by code
Each code anchor is where the error_uri of an OAuth response points. URL format: https://docs.1pass.dev/oauth/errors#<error>
invalid_request
Meaning: The request itself violates the RFC 6749 format.
Common causes:
- The
redirect_uridoes not exactly match the whitelist in the app registration (scheme/host/path/query, all of them) code_challenge_methodis notS256(plainis not supported)- A required parameter is missing (
response_type,client_id,redirect_uri,code_challenge)
Quick diagnosis:
- Console → app detail → "Redirect URIs", and compare it character by character with the URL the RP sent
- Pay special attention to a trailing slash, a query string, or a fragment — any of these makes the two URIs differ and fail the exact match
- Confirm the PKCE library outputs
S256(Buffer.from(sha256(verifier)).toString('base64url'))
invalid_client
Meaning: Client authentication failed — client_id / client_secret mismatch or a missing secret.
HTTP: 401
Common causes:
- The
client_secretenvironment variable is wired up incorrectly between dev and prod - A base64 encoding mistake in the HTTP Basic auth header (
client_id:client_secretform) - The RP server was not redeployed after rotating the secret → it's using the old secret
Quick diagnosis:
- Console → app detail → "Rotate Secret" → update the RP env with the new secret
- A PKCE-only RP does not need a secret at all (PKCE guide)
unauthorized_client
Meaning: The client itself is authenticated, but it lacks permission for the current operation.
Common causes:
- The app is in
pendingstatus (awaiting admin approval — for domains other thanlocalhost) - The app is in
suspendedstatus (suspended by operations — check the console → audit log) - The
grant_typethe app used was not allowed at registration
Quick diagnosis:
- Console → app detail → check the Status pill. If it's
pending, you'll see the stepper for the Apply for production promotion steps.
invalid_grant
Meaning: The authorization code or refresh token is not valid.
HTTP: 400
Common causes + the exact error_description:
code not found— the code was already exchanged once, or never existedcode already used— a second exchange attempt with the same code. An authorization code is single-usecode expired— more than 10 minutes elapsed after the code was issuedredirect_uri mismatch— the URI at/oauth/authorizediffers from the one at/oauth/tokenPKCE verifier mismatch— thecode_verifierdoes not match thecode_challengesubmitted at the startrefresh token not found— the RT was revoked or was issued to a different clientrefresh token reuse detected; chain revoked— reuse attack detected — the entire token chain is revoked. Force the user to log in againrefresh token expired— the RT passed its 30-day lifetime
Quick diagnosis:
- A lost PKCE verifier is the most common — when using
sessionStorage, check whether it evaporates on a tab switch or refresh - The
redirect_urimust be exactly identical at authorize and at token — even a single trailing slash difference fails - Console → app detail → error log → find the exact cause by
request_id
invalid_scope
Meaning: The requested scope is not registered on the app, or is empty.
Common causes:
- The default scope drift policy (
block) — at least one unregistered scope was in the request - A scope parameter was sent while
allowed_scopesis unset - A typo (e.g.
email↔email_address) - A missing namespace prefix on a custom scope (
<client_id>:reviewer_roleform)
Quick diagnosis:
- Console → app detail → check the exact names registered under "Allowed Scopes"
- If a Scope drift header (
X-Logi-Scope-Drift) appeared too, see Scope drift handling
unsupported_grant_type
Meaning: An unsupported grant_type was used.
Supported values:
authorization_coderefresh_tokenurn:ietf:params:oauth:grant-type:device_code(RFC 8628)
password, implicit, and client_credentials are not supported (intentionally omitted for security).
unsupported_response_type
Meaning: The response_type at /oauth/authorize is not code.
logi supports only the Authorization Code Flow. token (implicit) and a standalone id_token are not supported.
access_denied
Meaning: The user chose "Deny" on the consent screen, or it was denied in the device flow.
HTTP: 302 (authorize) or 400 (token)
Response: The RP should give the user a "Login cancelled" UX and surface a retry entry point.
invalid_token
Meaning: Bearer access_token verification failed at /oauth/userinfo.
HTTP: 401 + WWW-Authenticate: Bearer error="invalid_token"
Common causes:
- JWT signature verification failed (the RP used the wrong JWKS)
- The token expired (
expires_inelapsed) - The token was explicitly revoked
- The user account was soft-deleted
Quick diagnosis:
- Invalidate the JWKS cache and retry
- Compare the
expires_inat issuance with the current time
authorization_pending / slow_down / expired_token
Meaning: Polling responses for the Device Authorization Grant (RFC 8628).
| error | Meaning | Response |
|---|---|---|
authorization_pending | The user has not yet approved the device_code | Wait for interval, then poll again |
slow_down | Polling is too fast | Increase interval by 5 seconds and poll again |
expired_token | The device_code expired | Start over from the beginning |
rate_limited
Meaning: A rate-limit threshold was exceeded.
HTTP: 429
Limits:
| Endpoint | Limit | Key |
|---|---|---|
/session | 5/min (Cloudflare) · 10/3min (Rails) | IP |
/oauth/token | 20/min | client_id |
/oauth/device_authorization | 30/min | client_id |
/api/v1/me/merge/otp | 10/min | user_id |
Response: Respect the Retry-After header, use exponential backoff, and aggressively cache tokens for a single user.
Error triage workflow
The procedure to follow when a 4xx occurs in a new integration:
- Log the response body's
error_descriptionverbatim in your RP server log. — The cause of the most common mistakes is written there directly. - Record the
request_idorX-Logi-Request-Idheader alongside it. — It matches exactly in the console request_logs by the same key. - In production, click the URL in the
X-Logi-Console-Urlheader. — That request is shown in the console immediately. - Follow the
error_urianchor in this document. — The per-code sections above list common causes + quick diagnosis.
Logging caveats
Never log:
password,client_secret,code_verifier,refresh_token,access_token(including JWTs),logi_pak_*- logi itself never logs
password_digest,otp_secret_encrypted, JWTs, or PAK plaintext, not even once.
Safe to log:
error,error_description,error_urirequest_id,X-Logi-Request-Idclient_id,redirect_uri(not PII)