# Progressive profiling with Hybrid ID

Canonical guide: https://hybrid-id.com/developers/progressive-profiling
Machine contract: https://hybrid-id.com/integrations/progressive-profiling.openapi.json

## Account creation and application admission are separate

First and last names are optional at Hybrid ID signup. Email, password and terms
acceptance remain required. Missing names are not replaced with invented legal
names. Email ownership is not verified merely by signing up. Existing service
admission, developer quotas, scopes and MFA requirements are unchanged.

A game can use the stable validated `(iss, sub)` without requiring a name. Do not
use email or a name as an identity key. Self-declared profile completion is not
KYC, legal-name verification, bank admission or permission to access documents.
Names do not universally follow a first-name/last-name structure: request only the
fields your service actually needs.

## Configure an application

In **Developer Apps → Edit app settings → Profile information requests**, select
`first_name`, `last_name`, or both and explain the purpose. Save with the existing
owner MFA flow. Agent-delegated `applications:write` can also configure the same
metadata through the documented application update API:

```json
{
  "action": "update",
  "client_id": "YOUR_CLIENT_ID",
  "name": "Your application",
  "redirect_uris": ["https://your-app.example/auth/hybrid/callback"],
  "profile_requirements": {
    "fields": ["first_name"],
    "reason": "Address your account correspondence by name."
  }
}
```

Only these two fields are supported in this milestone. An empty `fields` array
disables requests. Omitting `profile_requirements` on update preserves it. The
server derives the application name and origin from its registration. Clients
cannot invent a requesting company, ask for arbitrary fields, or supply a user ID.
The immutable request snapshot binds the configured fields and purpose by digest;
changed requirements need a new user approval.

## Request information after sign-in

1. Add `profile:request` to your normal `openid` authorization scope. Keep all
   existing scopes your integration needs. Obtain explicit OIDC consent; the new
   scope permits requests, not automatic disclosure of first/last-name fields.
2. On your backend, use the app client ID/secret and the signed-in user's current
   access token to call `POST https://auth.hybrid-id.com/profile/requests`.
3. Show the returned `review_url` as a **Complete your profile in Hybrid ID** link.
   The user reviews your purpose, adds or confirms missing fields, and approves
   sharing, declines, or closes the page. Closing does not approve the request.
4. When the user returns to your app, read the request again. Enforce your own
   onboarding/feature requirement on your backend until `status` is `approved`.
   Sign-in success alone must not open a name-dependent feature.

```http
POST /profile/requests HTTP/1.1
Host: auth.hybrid-id.com
Authorization: Basic BASE64(CLIENT_ID:CLIENT_SECRET)
Content-Type: application/x-www-form-urlencoded

operation=create&access_token=USER_ACCESS_TOKEN
```

Read uses the same endpoint and authentication:

```text
operation=read&access_token=USER_ACCESS_TOKEN&request_id=RETURNED_REQUEST_ID
```

Never put either credential in a URL, browser bundle, logs or an agent prompt.
No refresh tokens are issued by this integration. On an expired access token,
start a fresh Authorization Code + PKCE flow; the same request is found while its
app, identity, requirements and consent generation remain the same. The request
survives browser sessions. There is no public unauthenticated status endpoint.
Do not continuously poll; check on the user's return or explicit refresh and use
bounded backoff for transient errors.

## Response and disclosure

```json
{
  "request_id": "opaque-64-hex-id",
  "app_id": "YOUR_CLIENT_ID",
  "app_name": "Your application",
  "app_origin": "https://your-app.example",
  "fields": ["first_name"],
  "reason": "Address your account correspondence by name.",
  "policy_digest": "64-hex-digest",
  "status": "pending",
  "assurance": "self_declared",
  "created_at": 1791280000,
  "expires_at": 1793872000,
  "review_url": "https://hybrid-id.com/account/service-access?request=opaque-id"
}
```

Only an approved response adds `approved_at` and `values`, containing exactly the
requested fields. It is a snapshot of the values the person approved, not a live
unrestricted profile feed. No native identity UUID, upstream session, unrequested
field, document, verified-name claim or approval for another app is returned.
Even pre-existing names require this approval.

Pending requests expire after at most 30 days. Approval expires after at most one
year and no later than its underlying connection consent. Every read checks the
current app, user token, native identity, grant, consent generation and policy.
A new sign-in does not resurrect a revoked approval. Request retries are
idempotent and do not create duplicate notices or reopen declined requests.
Changing policy requires review; withdrawing and reauthorizing connection consent
creates a new request generation rather than restoring old approval.

Possible statuses: `pending`, `approved`, `declined`, `revoked`, `expired`,
`requirements_changed`, `connection_revoked`, `application_disabled`. Some changes
invalidate the access token or current request lookup instead; handle 401/403/404
as unavailable access, never as approval. Only `approved` permits disclosure.
The user can review and withdraw approval in Access requests. Withdrawal stops
future retrieval but cannot erase a copy already received by your application.
Encrypted request records are retained up to 366 days after their last action;
revocation removes the stored name values immediately. This does not prescribe
the recipient application's retention obligations.

Saving names updates the central Hybrid ID profile. The consent screen explicitly
explains that existing `profile` permissions to read the display name may reflect
that change; this workflow does not withdraw those separate existing permissions.
`first_name` and `last_name` are returned by this request API only after approval;
this release does not add `given_name` or `family_name` to OIDC UserInfo/ID tokens.

## Errors, notifications and boundaries

HTTP 400: invalid input; 401: invalid app credential; 403: missing/revoked user
scope or authority; 404: no matching request; 405: wrong method; 409: stale action;
429: throttle (30 requests per app/user/minute, existing exact-owner exemption
preserved); 503: unavailable dependency. Do not treat errors as approval.

Users receive in-portal notices when requested, approved, declined or revoked.
Email-ready content uses the existing outbox, but delivery remains unconfigured:
no email delivery is promised. There are no webhooks in this milestone.

The application developer's automation credential may configure its requirements;
it cannot complete a user's profile, consent, impersonate a user, or approve on
their behalf. API client authentication alone is never sufficient to read names.
Requests are private and are not published to Explorer or IPFS.

## Integration-agent handoff

Implement Hybrid ID progressive profiling using this guide and its linked OpenAPI.
Keep login and application admission separate. Names are optional at signup; do
not assume they are present or verified. Configure only necessary profile fields
and a truthful purpose. Request `profile:request` consent through Authorization
Code + PKCE, then create/read the request from your backend using client Basic
authentication plus the current user's access token. Send the user to the returned
Hybrid ID review URL. Unlock the dependent feature only after your backend reads
`approved`; handle decline, expiry, revocation, unavailable services and changed
requirements without inventing data or bypassing approval. Test two users and two
apps for isolation. Never publish credentials or user profile values in logs.
