Using Response Headers and Body Signals
In addition to the standard RFC 6749 response, logi attaches signals that your RP server can use to debug and monitor without ever opening the console. This page shows you how to capture those signals at the middleware layer and stream them automatically into your server logs and observability systems.
Signal catalog
Body fields (4xx responses)
| Field | Always present | Purpose |
|---|---|---|
error | ✅ | Machine-readable code (RFC 6749 §5.2) |
error_description | ✅ | One-line description |
error_uri | ✅ | Docs anchor for this code |
request_id | Per response | The same key as the console's request_logs |
Response headers (all responses)
| Header | When | Purpose |
|---|---|---|
X-Logi-Request-Id | Always on 4xx | Trace even when the body is empty (HEAD, or a failure rendering the error) |
X-Logi-Console-Url | On 4xx when the RP authenticates successfully | A direct link to this request in the console's request_logs |
X-Logi-Scope-Drift | On a token 200 response when drift was recorded within the last 7 days | Detects an unregistered scope — for immediate alerting. Under the default block policy, a drift request is itself rejected with invalid_scope; this header applies to apps explicitly set to log_only/alert with ongoing drift, or as an echo of rejected/recorded drift history |
Pattern 1: Auto-log every 4xx response
Node.js / Express + axios
import axios from 'axios'
const logiClient = axios.create({ baseURL: 'https://api.1pass.dev' })
logiClient.interceptors.response.use(
(res) => res,
(err) => {
if (err.response?.status >= 400 && err.response?.status < 500) {
const { data, headers, status } = err.response
console.warn('[logi] 4xx', {
status,
error: data?.error,
error_description: data?.error_description,
error_uri: data?.error_uri,
request_id: data?.request_id || headers['x-logi-request-id'],
console_url: headers['x-logi-console-url'], // an operator can click it right away
})
}
return Promise.reject(err)
}
)Rails + Faraday
class LogiResponseLogger < Faraday::Middleware
def on_complete(env)
return unless env.status >= 400 && env.status < 500
body = JSON.parse(env.body) rescue {}
Rails.logger.warn(
"[logi] 4xx",
status: env.status,
error: body["error"],
error_description: body["error_description"],
error_uri: body["error_uri"],
request_id: body["request_id"] || env.response_headers["x-logi-request-id"],
console_url: env.response_headers["x-logi-console-url"]
)
end
endPython + requests
import logging, requests
def log_logi_4xx(resp: requests.Response):
if not (400 <= resp.status_code < 500):
return
body = resp.json() if "json" in resp.headers.get("content-type", "") else {}
logging.warning(
"[logi] 4xx",
extra={
"status": resp.status_code,
"error": body.get("error"),
"error_description": body.get("error_description"),
"error_uri": body.get("error_uri"),
"request_id": body.get("request_id") or resp.headers.get("X-Logi-Request-Id"),
"console_url": resp.headers.get("X-Logi-Console-Url"),
},
)Pattern 2: Auto-link traces in Sentry / Datadog
If you tie request_id to your RP's distributed tracing key, you can follow a single trace in your own dashboard all the way from "this user's payment failure → logi token issuance failure."
import * as Sentry from '@sentry/node'
logiClient.interceptors.response.use(undefined, (err) => {
if (err.response?.status >= 400) {
const reqId = err.response.data?.request_id ||
err.response.headers['x-logi-request-id']
Sentry.withScope((scope) => {
scope.setTag('logi.request_id', reqId)
scope.setTag('logi.error', err.response.data?.error)
scope.setExtra('logi.console_url', err.response.headers['x-logi-console-url'])
Sentry.captureException(err)
})
}
return Promise.reject(err)
})With this in place, you can jump from a Sentry issue page to the logi console in one click.
Pattern 3: Scope drift alerts (200 responses)
The default scope drift policy is block — a request containing an unregistered scope is rejected with invalid_scope (Scope drift policy). The X-Logi-Scope-Drift header is attached to successful token responses in two cases: when the app is explicitly set to the log_only/alert policy and drift is ongoing (dropped, then proceeding), or as an echo of the drift history recorded within the last 7 days (including block rejections) — the missing scopes arrive as a comma-separated list.
logiClient.interceptors.response.use((res) => {
const drift = res.headers['x-logi-scope-drift']
if (drift) {
console.warn('[logi] scope drift detected:', drift.split(','),
'— update the registered scopes in the console.')
}
return res
})For the 7 days the drift history stays alive, this signal is attached to every token response, so even if you forget to update a scope before deploying, an alert appears automatically in your server logs.
Pattern 4: A one-line message to show the user
error_description can mix Korean and English and is technical. When showing it to users, we recommend that you maintain a mapping table from the error code to a user-facing message in your RP:
const USER_FACING: Record<string, string> = {
invalid_grant: 'Your login expired. Please try again.',
access_denied: 'Login was cancelled.',
rate_limited: 'Too many requests. Please try again in a moment.',
invalid_scope: "We can't log you in right now because of an app configuration issue.",
}
function userFacingMessage(error: string) {
return USER_FACING[error] ?? 'Please try again in a moment.'
}Send the technical information (error, request_id, console_url) to your server logs and show the user only a friendly one-liner — separating the two this way keeps users from ever seeing something like a stack trace.
Operations checklist
- [ ] On a 4xx, the RP server records all three of
error,error_description, andrequest_idat the INFO level - [ ] The
X-Logi-Console-Urlheader is included in alerting messages (a one-click jump for the operator) - [ ] When the
X-Logi-Scope-Driftheader appears, the RP's monitoring system fires a medium-priority alert - [ ]
request_idis mapped to the same field as the RP's trace ID, or at minimum printed on the same line - [ ] User-facing messages are limited to text that has passed through the
errorcode mapping table
Security note
The extra signals in the response headers and body are not PII — they contain only the request_id, error code, and console URL. They never include user identifiers (email, sub), token values, or secrets, so they are safe to leave in ordinary logs.
References
- OAuth error codes — where every
error_uripoints - Scope drift policy — the conditions that fire the header
- Recommended architecture — the recommended structure for RP server integration