RP Active Health Check
The 1pass IdP periodically confirms that registered RPs' integrations are actually alive. Traffic-based signals (waiting for first request / token issuance passed) cannot tell whether an RP is simply unused or actually dead. The active health check removes this ambiguity: the IdP calls the RP's standard endpoint directly and verifies the response.
Key points
- Opt-in: Newly registered RPs are active by default; existing RPs toggle it on in the developer console.
- One GET per hour to
/.well-known/logi-rp-health. - HMAC-SHA256 signature + client_id echo verification + a ±5-minute clock-drift check.
- The result is shown as 🟢 / 🟡 / 🔴 on a card in the developer console.
Why it's needed
| Situation | Traffic-based signal | Active health check |
|---|---|---|
| Just-registered RP with a typo in redirect_uri | ⏳ Waiting for first request (indistinguishable) | 🔴 Detected immediately |
| Deployed, but the OAuth route is missing | ⏳ Waiting for first request | 🔴 Detected immediately |
| RP cert expired | First discovered when a user clicks | 🔴 Detected within 1 hour |
| Actually integrated correctly | ⏳ Waiting for first request (when there are no users) | 🟢 alive |
| The RP itself is quiet (e.g., a side project) | ⏳ Waiting for first request (forever) | 🟢 alive (no misunderstanding) |
Protocol
Request
The IdP sends a GET request to the RP endpoint as follows:
GET /.well-known/logi-rp-health HTTP/1.1
Host: example.com
User-Agent: logi-healthcheck/1.0
Accept: application/json
X-Logi-Timestamp: 1748345678
X-Logi-Client-Id: logi_xxxxxxxxxxxxxxxx
X-Logi-Signature: 7a91...64-char hex...X-Logi-Timestamp: Unix epoch seconds (when the request was created).X-Logi-Client-Id: the client_id being verified.X-Logi-Signature:HMAC-SHA256(secret, "{timestamp}.{client_id}"), hex-encoded.secret: theLOGI_RP_HEALTH_SECRETthe RP was shown once right after app registration (store it as an environment variable).
Verification on the RP side
The RP handler must verify all four of the following before responding:
1. X-Logi-Client-Id == your registered client_id → otherwise 401
2. |now() - X-Logi-Timestamp| ≤ 5 min (300 s) → otherwise 401 (time_drift)
3. constant-time compare:
hex(HMAC-SHA256(LOGI_RP_HEALTH_SECRET,
"{X-Logi-Timestamp}.{X-Logi-Client-Id}"))
== X-Logi-Signature → otherwise 401 (hmac_invalid)
4. (optional) IP allowlist — allow only the 1pass IdP's outbound rangeWhen verification passes, return 200 OK + a JSON body:
{
"status": "ok",
"client_id": "logi_xxxxxxxxxxxxxxxx",
"timestamp": "2026-05-27T12:34:56Z",
"sdk_version": "logi-rp-integrate/1.0"
}| Field | Required | Description |
|---|---|---|
status | ✅ | "ok" or "degraded" (when the RP wants to signal that it is intentionally serving only partial functionality) |
client_id | ✅ | Must be echoed. The IdP strict-matches it against the registered value. This proves the RP host really is mapped to this client_id. |
timestamp | ✅ | The ISO 8601 time on the RP server. The IdP compares it to its own time to detect clock drift on the RP side (if >5 min, it records rp_time_drift_Ns). |
sdk_version | (optional) | For log tracing |
Verification on the IdP side
The IdP verifies the response in this order:
- HTTP 200 (
Net::HTTPSuccess) — otherwise it recordshttp_<code>. - The body is JSON-parseable — otherwise it records
body_not_json. body.client_id== the registeredclient_id— otherwise it recordsclient_id_mismatch.|now() - body.timestamp|≤ 5 min — otherwise it recordsrp_time_drift_Ns.
If all pass, it transitions to rp_health_status: healthy.
Timing
- Interval: once per hour (a keep-warm side effect for RPs on Render's free tier).
- Timeout: open 5 s, read 15 s (to accommodate Render free-tier cold starts).
- Retries: only once per ping. On failure, it waits until the next hourly ping.
- Traffic: 1 request per RP per hour. With 11 RPs, that's 264 requests/day.
State machine
| State | Meaning | Transition condition |
|---|---|---|
unknown | Never pinged (new RP or grandfathered) | — (initial value) |
healthy | Previous ping succeeded | Any single success from any state |
degraded | 1–2 consecutive failures | Was healthy and failed once, or failed from unknown |
unreachable | 3 consecutive failures | Consecutive-failure counter ≥ 3 |
skipped | Mobile-only redirect_uri + no health_url configured | redirect_uri is a deeplink scheme + no override URL |
Alerting policy
A push notification fires to the operator only on the healthy → unreachable transition. unknown → unreachable cannot be distinguished from a grandfathered RP that hasn't applied the SDK, so it stays silent — only the ping result is shown in the console.
Mobile RPs (deeplink redirect)
If the redirect_uri is a custom scheme such as com.example.app://oauth/callback, there is no host for the IdP to ping. Two ways to handle this:
- A mobile RP that has a backend: Enter the backend host in the developer console's health endpoint URL field (for example,
https://api.example.com). The IdP always calls<health_url>/.well-known/logi-rp-health. - A pure mobile RP with no backend: Leave health_url empty and it is automatically marked
skipped. The console shows 🔇 and no ping occurs at all.
Secret issuance + rotation
- New RP: Right after registration, the developer console's "client_secret issued" page also shows
LOGI_RP_HEALTH_SECRETonce. - Existing RP (grandfathered): When you toggle on RP active health check in the developer console, a secret is freshly issued at that moment and shown once on the next page.
- Rotation: If the secret is leaked or lost, use the console's rotate button to re-issue it immediately (the old secret is invalidated immediately).
Security
- Do not put
LOGI_RP_HEALTH_SECRETin client-side code. If it ends up in a mobile app / SPA bundle, it loses its meaning. - Store it only in server-side environment variables (Render env / Vault / 1Password / AWS Secrets Manager, etc.).
Integration examples
Drop-in code per stack:
- Rails 8: integrations/rails#health-check-endpoint
- Next.js (App Router): integrations/nextjs#health-check-endpoint
- Express / Node.js: integrations/express#health-check-endpoint
Self-diagnosis
# Locally simulate whether your own RP passes the health check:
TS=$(date +%s)
CID=logi_xxxxxxxxxxxxxxxx
SECRET=$LOGI_RP_HEALTH_SECRET
SIG=$(printf "%s.%s" "$TS" "$CID" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')
curl -i \
-H "User-Agent: logi-healthcheck/1.0" \
-H "X-Logi-Timestamp: $TS" \
-H "X-Logi-Client-Id: $CID" \
-H "X-Logi-Signature: $SIG" \
"https://example.com/.well-known/logi-rp-health"
# Expected: 200 OK
# body.client_id == $CIDFrequently asked questions
Does the health check access user data?
No. /.well-known/logi-rp-health is a public static endpoint. The response contains only client_id, status, and timestamp, and no user information at all. The HMAC signature serves only to prove the RP really is integrated with 1pass — it is not a traffic limit or an authentication gate.
Does a failed health check automatically suspend the RP?
No. Even when the state becomes unreachable, the OAuth flow keeps working normally. The health check is purely an observability tool, not an enforcement mechanism. A logi operator reviews the console and decides manually.
Do I need to tell 1pass our server's IP?
Optional. If your RP-side handler wants extra security via an outbound IP allowlist, allow only the range from https://api.1pass.dev/.well-known/outbound-ips (to be published). HMAC verification alone is, however, already secure enough.
What happens if I send status: "degraded" in the response?
The console shows 🟡, but the OAuth flow keeps working as-is. Use it when the RP wants to signal "the integration is alive, but some functionality has a problem."
Does every RP have to implement the health endpoint?
It's recommended for newly registered RPs. If you turn off the health_check_enabled toggle in the console, you won't receive any pings. Cases that have no backend of their own, such as mobile-only RPs, are automatically handled as skipped.
Related documents
- App registration guide — how to obtain
LOGI_RP_HEALTH_SECRET - Authorization Code Flow — the user authentication flow, separate from the health check
- Public vs Confidential — both types are issued the same health-check secret