OAuth user verification
Verify the pending end user on your server before activating an OAuth connection.
In Project settings → OAuth user verification, optionally set a callback URL on your server. Live and Sandbox have separate settings. Leave the URL blank to disable verification. Project owner/admin access is required in the console.
Saving generates a new signing key, shown once. Store it securely on your server before allowing new connections. Saving an existing URL also rotates the key; in-flight authorizations using old settings fail and require a new connection link. The key is encrypted at rest and never returned by settings reads.
Business keys can read GET /v1/oauth-user-verification with
connect_settings:read, or save PUT /v1/oauth-user-verification with
connect_settings:manage and { "url": "https://your-server.example/arc0/verify" }
(or {"url": null} to disable). The management response wraps settings in data:
{url, revision}; saving an enabled URL also returns signingSecret once. These
settings are not exposed as management MCP tools. A key authorized to manage
Connect settings can configure this callback.
Request and response contract
After a successful OAuth exchange and provider identity check, Arc0 sends one
HTTPS POST before storing/activating the new connection or updating a reconnect.
The callback runs server to server. It only proves that your server still approves
this externalId. It does not prove which
browser finished OAuth, or bind that browser to your application's signed-in user.
Arc0 sends the connector's linked provider identity (identity.id and
identity.name) so your server can compare accounts. Each field is null when
unavailable; do not treat a missing identity as proof of an account match. No OAuth
code or provider token is sent.
{
"version": "1",
"projectId": "project-id",
"connectionId": "pending-connection-id",
"attemptId": "authorization-attempt-id",
"externalId": "your-user-id",
"providerIdentity": {"id": "provider-account-id", "name": "Account name"},
"challenge": "random-single-attempt-challenge"
}Headers are Content-Type: application/json, Arc0-Timestamp (Unix seconds),
Arc0-Challenge, and Arc0-Signature (lowercase hex HMAC-SHA256).
Use the signing key as its UTF-8 string, without base64 decoding it. The exact UTF-8 string signed is:
arc0.user-verification.v1.request.<timestamp>.<challenge>.<raw-request-body>Validate the HMAC with a constant-time comparison, require a timestamp within 30 seconds, check the header challenge equals the body's challenge, and reject challenges/attempts already accepted (store them for at least 60 seconds). Resolve the pending external ID against your own authoritative user/session records and connection policy. Merely echoing the ID without checking it does not establish user identity. Never trust an unsigned external ID or a caller-supplied user ID.
To approve, return HTTP 200 with exactly:
{"externalId":"your-user-id","challenge":"the-request-challenge"}Include Arc0-Signature, using the same key, timestamp, challenge and your exact
raw response body with a separate response prefix:
arc0.user-verification.v1.response.<timestamp>.<challenge>.<raw-response-body>Arc0 checks the signed response, challenge, and exact case-sensitive external ID match. A response from a different attempt cannot approve this one. Any other status rejects the connection. Group-owned OAuth has no end-user ID and fails when verification is enabled. Static API-key/basic/two-step credential flows are unaffected. Existing active connections and refreshes do not invoke this hook.
Limits and failures
URLs must use HTTPS on port 443, without credentials or fragments. Loopback, private, link-local and reserved destinations are rejected. Dispatch checks all DNS answers and pins a public address while preserving TLS hostname verification; redirects are rejected. The total verification budget is five seconds, including DNS and the response body. Approved response bodies are limited to 4 KiB.
Arc0 fails closed and does not retry verification. Start a new connection flow after fixing the error. A rejected new connection has no active credential; failed reconnects preserve the previous connection. The authorization attempt records one of these error codes:
| Code | Meaning |
|---|---|
user_verification_unsafe_url | Invalid URL or unsafe DNS destination |
user_verification_user_required | No active end user for the pending connection |
user_verification_timeout | Five-second deadline exceeded |
user_verification_unavailable | DNS, TLS or transport failure |
user_verification_rejected | Customer returned a status other than 200 |
user_verification_invalid_signature | Missing or forged response signature |
user_verification_invalid_response | Malformed, oversized or wrong-challenge response |
user_verification_user_mismatch | Customer returned another external ID |
user_verification_settings_changed | Project configuration changed before activation |
Audit records
connection.user_verified records a successful signed approval, with the pending
connection ID, attempt ID and verification revision. It records approval, not
connection activation: a later authority or settings check can still reject the
connection. connection.authorization_failed includes the failure code and
attempt ID. Settings updates record enabled and previousEnabled so operators
can identify toggles. Signing keys, provider identity and callback bodies are not
retained in these audit records.