diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml
index 2a1df65c4..fbf5d7994 100644
--- a/mintlify/openapi.yaml
+++ b/mintlify/openapi.yaml
@@ -5534,7 +5534,7 @@ paths:
Embedded Wallet internal accounts are initialized with an `EMAIL_OTP` credential tied to the customer email on the account. Use this endpoint to add another credential (`SMS_OTP`, `OAUTH`, or `PASSKEY`), or to add `EMAIL_OTP` / `SMS_OTP` back after it has been removed. Only one `EMAIL_OTP` and one `SMS_OTP` credential are supported per internal account; multiple distinct `PASSKEY` credentials may be registered.
- Adding a credential requires a signature from an existing verified credential on the same account. Call this endpoint with the new credential's details to receive `202` with `payloadToSign` and `requestId`. Use the session API keypair of an existing verified credential (decrypted client-side from its `encryptedSessionSigningKey`) to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `201` with the created `AuthMethod`. For OTP credentials, the one-time password is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`.
+ Adding a credential requires a signature from an existing verified credential on the same account. Call this endpoint with the new credential's details to receive `202` with `payloadToSign` and `requestId`. Use the session signing key the client holds for an existing verified credential on the same account to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `201` with the created `AuthMethod`. For OTP credentials, the one-time password is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`.
operationId: createAuthCredential
tags:
- Embedded Wallet Auth
@@ -5852,7 +5852,7 @@ paths:
For `OAUTH` credentials, supply a fresh OIDC token (`iat` must be less than 60 seconds before the request) along with the client-generated public key; this is also the reauthentication path after a prior session expired. The token identity (`iss`, `aud`, and `sub`) must match the OAuth credential being verified. In sandbox, the token's `nonce` must equal `sha256(clientPublicKey)`. For `PASSKEY` credentials, the client completes a WebAuthn assertion (`navigator.credentials.get()`) against the Grid-issued `challenge` returned from `POST /auth/credentials/{id}/challenge`, and submits the resulting `assertion` with the `Request-Id` header. The `clientPublicKey` for `PASSKEY` credentials is supplied on the challenge call, where it is bound into the pending session-creation request.
- On success for `OAUTH` and `PASSKEY`, and on the signed retry for OTP credentials, the response contains an `AuthSession`. For `OAUTH` and `PASSKEY` the session signing key is delivered as `encryptedSessionSigningKey` (HPKE-sealed to the supplied `clientPublicKey`); for OTP credentials the client already holds the session signing key (the TEK private key it generated) and that field is omitted from the response. The `expiresAt` timestamp marks when the session expires.
+ On success for `OAUTH` and `PASSKEY`, and on the signed retry for OTP credentials, the response contains an `AuthSession`. In the recommended client-held-key flow the client sends a compressed `clientPublicKey` and retains the corresponding private key as the session signing key, so the response omits `encryptedSessionSigningKey` — the same way OTP credentials have always worked. In the deprecated legacy flow the client sends an uncompressed `clientPublicKey` and the session signing key is returned as `encryptedSessionSigningKey` sealed to that key for the client to decrypt. The `expiresAt` timestamp marks when the session expires.
operationId: verifyAuthCredential
tags:
- Embedded Wallet Auth
@@ -5907,7 +5907,7 @@ paths:
value:
type: OAUTH
oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q
- clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ clientPublicKey: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31
passkey:
summary: Verify a passkey credential
value:
@@ -6201,9 +6201,9 @@ paths:
description: |
Refresh an active Embedded Wallet auth session and create a new session signing key. Session refresh is a two-step signed-retry flow:
- 1. Call `POST /auth/sessions/{id}/refresh` with the request body `{ "clientPublicKey": "04..." }` and no signature headers. Grid builds a Grid session-refresh payload, binds the supplied `clientPublicKey` into that payload, persists it as a pending request, and returns `202` with `payloadToSign`, `requestId`, and `expiresAt`.
+ 1. Call `POST /auth/sessions/{id}/refresh` with the request body `{ "clientPublicKey": "02..." }` and no signature headers. Send a freshly generated client public key and retain its private key. Grid binds the supplied `clientPublicKey` to the pending refresh and returns `202` with `payloadToSign`, `requestId`, and `expiresAt`.
- 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in the request body. On success, Grid returns a new `AuthSession` with an `encryptedSessionSigningKey` sealed to that client public key.
+ 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in the request body. On success, Grid returns a new `AuthSession`. In the recommended client-held-key flow (compressed `clientPublicKey`) the client retains the new session signing key and the response omits `encryptedSessionSigningKey`; the deprecated legacy flow (uncompressed `clientPublicKey`) instead returns the new key as `encryptedSessionSigningKey` sealed to that key.
The original session must still be active on both steps so it can authorize the refresh. If the session has already expired, use the credential reauthentication flow instead.
operationId: refreshAuthSession
@@ -6243,7 +6243,7 @@ paths:
refresh:
summary: Refresh an active session
value:
- clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ clientPublicKey: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31
responses:
'201':
description: New authentication session created successfully.
@@ -6258,7 +6258,6 @@ paths:
id: Session:019542f5-b3e7-1d02-0000-000000000011
accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
type: EMAIL_OTP
- encryptedSessionSigningKey: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf
nickname: example@lightspark.com
createdAt: '2026-04-08T15:30:01Z'
updatedAt: '2026-04-08T15:35:00Z'
@@ -22783,8 +22782,8 @@ components:
example: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q
clientPublicKey:
type: string
- description: Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (0x04 prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key must remain on the client. Grid encrypts the session signing key returned in the response to this public key. The key is ephemeral and one-time-use per verification request.
- example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ description: Client-generated, ephemeral P-256 public key; the matching private key must remain on the client and is one-time-use per verification request. In the recommended client-held-key flow, send the key in compressed SEC1 format (`02` or `03` prefix followed by the 32-byte X coordinate; 66 hex characters) and retain the private key as the session signing key — the response omits `encryptedSessionSigningKey`. Sending the key in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters) selects the deprecated legacy flow, in which Grid returns the session signing key as `encryptedSessionSigningKey` sealed to this public key. See the "Client keys & signing" guide.
+ example: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31
OauthCredentialVerifyRequest:
title: OAuth Credential Verify Request
allOf:
@@ -22853,7 +22852,7 @@ components:
PASSKEY: '#/components/schemas/PasskeyCredentialVerifyRequest'
AuthSession:
title: Authentication Session
- description: An authentication session on an Embedded Wallet internal account. Returned from `GET /auth/sessions` (list) and `POST /auth/credentials/{id}/verify` (on credential verification) or `POST /auth/sessions/{id}/refresh` (on mid-session refresh). Only session-issuing responses include `encryptedSessionSigningKey` — it is delivered exactly once at the moment the session is issued and is never returned by the list endpoint.
+ description: An authentication session on an Embedded Wallet internal account. Returned from `GET /auth/sessions` (list) and `POST /auth/credentials/{id}/verify` (on credential verification) or `POST /auth/sessions/{id}/refresh` (on mid-session refresh). In the recommended client-held-key flow the client generates and retains the session signing key, so session-issuing responses carry no signing key. The deprecated `encryptedSessionSigningKey` field is present only for the legacy flow and is never returned by the list endpoint.
allOf:
- $ref: '#/components/schemas/AuthMethod'
- type: object
@@ -22867,28 +22866,31 @@ components:
example: Session:019542f5-b3e7-1d02-0000-000000000003
encryptedSessionSigningKey:
type: string
+ deprecated: true
description: |-
- HPKE-encrypted session signing key, sealed to the `clientPublicKey` supplied on the verification or refresh request. Encoded as a base58check string: the decoded payload is a 33-byte compressed P-256 encapsulated public key followed by AES-256-GCM ciphertext. The client decrypts this key with its private key and uses it to sign subsequent Embedded Wallet requests until `expiresAt`.
+ Deprecated; returned only for the legacy flow. When the client sends an uncompressed `clientPublicKey` on the verification or refresh request, Grid seals the session signing key to that key and returns it here for the client to decrypt and use until `expiresAt`.
- Returned only by session-issuing responses for `OAUTH` and `PASSKEY` credentials. `EMAIL_OTP` and `SMS_OTP` sessions omit this field — the client generates a TEK keypair before verification and retains the private key throughout, so the server has nothing to deliver. Always omitted from list responses (`GET /auth/sessions`) since Grid does not retain the plaintext key after the client has decrypted it.
+ In the recommended client-held-key flow the client sends a compressed `clientPublicKey`, generates and retains the session signing key itself, and this field is absent — the same way `EMAIL_OTP` and `SMS_OTP` sessions have always behaved. New integrations should adopt the client-held-key flow; see the "Client keys & signing" guide. Always omitted from list responses (`GET /auth/sessions`).
+
+ When present, the value is a base58check string whose decoded payload is a 33-byte compressed P-256 encapsulated public key followed by AES-256-GCM ciphertext.
example: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf
expiresAt:
type: string
format: date-time
- description: Timestamp after which the session is no longer valid and the `encryptedSessionSigningKey` must not be used to sign further requests.
+ description: Timestamp after which the session is no longer valid and its session signing key must not be used to sign further requests.
example: '2026-04-09T15:30:01Z'
AuthCredentialChallengeRequest:
title: Auth Credential Challenge Request
- description: Request body for `POST /auth/credentials/{id}/challenge`. Required when re-challenging a `PASSKEY` credential — must carry `clientPublicKey` so Grid can bake it into the session-creation payload the returned challenge is computed from. Ignored for `EMAIL_OTP` and `SMS_OTP`, where the credential type alone is sufficient because the OTP is delivered out-of-band. OAuth credentials do not use this endpoint; authenticate or reauthenticate them with `POST /auth/credentials/{id}/verify`.
+ description: Request body for `POST /auth/credentials/{id}/challenge`. Required when re-challenging a `PASSKEY` credential — must carry `clientPublicKey` so Grid can bind it to the session the returned challenge issues. Ignored for `EMAIL_OTP` and `SMS_OTP`, where the credential type alone is sufficient because the OTP is delivered out-of-band. OAuth credentials do not use this endpoint; authenticate or reauthenticate them with `POST /auth/credentials/{id}/verify`.
type: object
properties:
clientPublicKey:
type: string
- pattern: ^04[0-9a-fA-F]{128}$
- minLength: 130
+ pattern: ^(0[23][0-9a-fA-F]{64}|04[0-9a-fA-F]{128})$
+ minLength: 66
maxLength: 130
- description: Required for `PASSKEY` credentials. Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key must remain on the client. Grid bakes this key into the session-creation payload that the returned `challenge` is computed from, so the resulting session signing key is sealed to the client. Ignored for `EMAIL_OTP` and `SMS_OTP`.
- example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ description: Required for `PASSKEY` credentials; the matching private key must remain on the client. In the recommended client-held-key flow, send the key in compressed SEC1 format (`02` or `03` prefix followed by the 32-byte X coordinate; 66 hex characters) and retain the private key as the session signing key — the verification response omits `encryptedSessionSigningKey`. Sending the key in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters) selects the deprecated legacy flow, in which the session signing key is returned as `encryptedSessionSigningKey` sealed to this key. Ignored for `EMAIL_OTP` and `SMS_OTP`. See the "Client keys & signing" guide.
+ example: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31
PasskeyAuthChallenge:
title: Passkey Auth Challenge
description: Extended `AuthMethod` shape returned for `PASSKEY` credentials from `POST /auth/credentials/{id}/challenge`. Includes the WebAuthn `credentialId` needed to target the passkey, plus the Grid-issued `challenge`, corresponding `requestId`, and challenge `expiresAt`. The `challenge` value is the lowercase hex-encoded SHA-256 digest of the canonical session-creation request body, not a base64url string. The client UTF-8 encodes this string as the WebAuthn challenge and signs it with the passkey to produce the assertion submitted to `POST /auth/credentials/{id}/verify`.
@@ -22942,16 +22944,16 @@ components:
$ref: '#/components/schemas/AuthSession'
AuthSessionRefreshRequest:
title: Auth Session Refresh Request
- description: Request body for refreshing an active authentication session. The `clientPublicKey` is required on both steps of the signed-retry flow. On the initial call, Grid binds this key into the session-creation payload returned as `payloadToSign`; on the signed retry, the client echoes the same key back and Grid uses it to encrypt the newly issued session signing key.
+ description: 'Request body for refreshing an active authentication session. The `clientPublicKey` is required on both steps of the signed-retry flow and must match on both. Its SEC1 encoding selects how the refreshed session signing key is delivered: send a compressed key for the recommended client-held-key flow (the client retains the new signing key) or an uncompressed key for the deprecated legacy flow (Grid returns the new key as `encryptedSessionSigningKey`).'
type: object
required:
- clientPublicKey
properties:
clientPublicKey:
type: string
- pattern: ^04[0-9a-fA-F]{128}$
- description: Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key must remain on the client. Grid binds this key into the session-creation payload on the initial call and seals the returned `encryptedSessionSigningKey` to it on the signed retry.
- example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ pattern: ^(0[23][0-9a-fA-F]{64}|04[0-9a-fA-F]{128})$
+ description: Client-generated P-256 public key; the matching private key must remain on the client. In the recommended client-held-key flow, send the key in compressed SEC1 format (`02` or `03` prefix followed by the 32-byte X coordinate; 66 hex characters) and retain the private key as the new session signing key — the response omits `encryptedSessionSigningKey`. Sending the key in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters) selects the deprecated legacy flow, in which the new session signing key is returned as `encryptedSessionSigningKey` sealed to this key. See the "Client keys & signing" guide.
+ example: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31
DelegatedKeyStatus:
type: string
enum:
diff --git a/mintlify/snippets/global-accounts/client-keys.mdx b/mintlify/snippets/global-accounts/client-keys.mdx
index af68aacc2..2a60465e9 100644
--- a/mintlify/snippets/global-accounts/client-keys.mdx
+++ b/mintlify/snippets/global-accounts/client-keys.mdx
@@ -2,14 +2,24 @@ Every signed Global Account action uses two key pairs:
| Key pair | Where it lives | What it does |
|---|---|---|
-| **Client key pair** (P-256) | On the customer's device, generated fresh per session-issuing or export request | Used as the HPKE recipient key so Grid can encrypt session keys or wallet export credentials to the client. Ephemeral — one pair per authentication, session refresh, or wallet export. |
-| **Session signing key** (P-256) | Issued by Grid, encrypted to the client public key, decrypted and held on the device | Signs every account action for the lifetime of the session (default 15 minutes). |
+| **Client key pair** (P-256) | On the customer's device, generated fresh per session-issuing or export request | In the recommended client-held-key flow, its private key becomes the session signing key directly. In the legacy flow, its public key is the recipient key Grid encrypts the session key (or wallet export credentials) to. Ephemeral — one pair per authentication, session refresh, or wallet export. |
+| **Session signing key** (P-256) | Held on the customer's device | Signs every account action for the lifetime of the session (default 15 minutes). In the client-held-key flow it is the client key pair's private key; in the legacy flow Grid issues it encrypted to the client public key for the client to decrypt. |
-This page covers generating the client key pair, sending the public key to your backend, decrypting the session signing key, and signing payloads. Everything here runs **on the client**; your integrator backend only relays opaque byte strings.
+This page covers generating the client key pair, sending the public key to your backend, holding (or, in the legacy flow, decrypting) the session signing key, and signing payloads. Everything here runs **on the client**; your integrator backend only relays opaque byte strings.
+
+## Client-held session key
+
+In the **recommended client-held-key flow**, the key pair you generate in step 1 *is* your session key: you keep the private key on the device and use it directly to sign account actions. There is no session key to receive or decrypt, and Grid never transmits one — so the verify, refresh, and challenge responses omit `encryptedSessionSigningKey`. This is how `EMAIL_OTP` and `SMS_OTP` have always worked; `OAUTH` and `PASSKEY` now follow the same model.
+
+To use it, send `clientPublicKey` in **compressed** SEC1 form — a 66-character hex string starting with `02` or `03` (the prefix followed by the 32-byte X coordinate). Grid treats a compressed key as a request to adopt it as the session signing key and returns an `AuthSession` with no `encryptedSessionSigningKey`. You then skip steps 2–3 and sign each `payloadToSign` with the private key you kept, exactly as in step 4. Because this key signs directly, generate it as a P-256 **signing** key (ECDSA) — not the ECDH recipient key the step 1 samples below create for the legacy decrypt flow.
+
+
+ Sending `clientPublicKey` in **uncompressed** SEC1 form (130 hex characters, `04` prefix) selects the deprecated legacy flow, in which Grid seals the session signing key to your public key and returns it as `encryptedSessionSigningKey` for you to decrypt — steps 2 and 3 below. New integrations should send the compressed key and hold their own session key.
+
## 1. Generate a client key pair
-Generate a fresh P-256 key pair for every authentication, session refresh, and wallet export. The public key is sent to Grid as `clientPublicKey` — for `PASSKEY` credentials this happens on `POST /auth/credentials/{id}/challenge`; for `EMAIL_OTP` and `OAUTH` it happens on `POST /auth/credentials/{id}/verify`; for session refresh it goes on both `/auth/sessions/{id}/refresh` calls; for wallet export it goes on both `/export` calls. Keep the private key in device-local secure storage (browser `IndexedDB` gated by Web Crypto's non-extractable flag, iOS Keychain, Android Keystore). Send the public key hex-encoded — a 130-character string starting with `04` — through your integrator backend. The Web Crypto, iOS, and Android APIs shown below all produce this format natively.
+Generate a fresh P-256 key pair for every authentication, session refresh, and wallet export. The public key is sent to Grid as `clientPublicKey` — for `PASSKEY` credentials this happens on `POST /auth/credentials/{id}/challenge`; for `EMAIL_OTP` and `OAUTH` it happens on `POST /auth/credentials/{id}/verify`; for session refresh it goes on both `/auth/sessions/{id}/refresh` calls; for wallet export it goes on both `/export` calls. Keep the private key in device-local secure storage (browser `IndexedDB` gated by Web Crypto's non-extractable flag, iOS Keychain, Android Keystore). Send the public key hex-encoded through your integrator backend. For the recommended client-held-key flow, send the **compressed** form — a 66-character string starting with `02` or `03` (see [Client-held session key](#client-held-session-key)). For the legacy flow, send the **uncompressed** form — a 130-character string starting with `04`, which the Web Crypto, iOS, and Android APIs below produce natively.
For local development, you can generate a P-256 key pair from the command line:
@@ -166,6 +176,10 @@ The private key of the pair you generated in step 1 stays on the device — it b
## 2. Verify the credential and receive the encrypted session signing key
+
+ Steps 2 and 3 apply to the **legacy** flow only (uncompressed `clientPublicKey`). In the recommended [client-held-key flow](#client-held-session-key) the response has no `encryptedSessionSigningKey` to receive or decrypt — skip to step 4.
+
+
Your client sends `publicKeyHex` to your integrator backend along with whatever the credential type requires (OTP value, OIDC token, or WebAuthn assertion — see Authentication). Your backend calls `POST /auth/credentials/{id}/verify` and returns the `encryptedSessionSigningKey` from Grid's response to the client.
Grid encrypts the session signing key with **HPKE** (RFC 9180) using the suite:
diff --git a/openapi.yaml b/openapi.yaml
index 2a1df65c4..fbf5d7994 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -5534,7 +5534,7 @@ paths:
Embedded Wallet internal accounts are initialized with an `EMAIL_OTP` credential tied to the customer email on the account. Use this endpoint to add another credential (`SMS_OTP`, `OAUTH`, or `PASSKEY`), or to add `EMAIL_OTP` / `SMS_OTP` back after it has been removed. Only one `EMAIL_OTP` and one `SMS_OTP` credential are supported per internal account; multiple distinct `PASSKEY` credentials may be registered.
- Adding a credential requires a signature from an existing verified credential on the same account. Call this endpoint with the new credential's details to receive `202` with `payloadToSign` and `requestId`. Use the session API keypair of an existing verified credential (decrypted client-side from its `encryptedSessionSigningKey`) to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `201` with the created `AuthMethod`. For OTP credentials, the one-time password is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`.
+ Adding a credential requires a signature from an existing verified credential on the same account. Call this endpoint with the new credential's details to receive `202` with `payloadToSign` and `requestId`. Use the session signing key the client holds for an existing verified credential on the same account to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `201` with the created `AuthMethod`. For OTP credentials, the one-time password is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`.
operationId: createAuthCredential
tags:
- Embedded Wallet Auth
@@ -5852,7 +5852,7 @@ paths:
For `OAUTH` credentials, supply a fresh OIDC token (`iat` must be less than 60 seconds before the request) along with the client-generated public key; this is also the reauthentication path after a prior session expired. The token identity (`iss`, `aud`, and `sub`) must match the OAuth credential being verified. In sandbox, the token's `nonce` must equal `sha256(clientPublicKey)`. For `PASSKEY` credentials, the client completes a WebAuthn assertion (`navigator.credentials.get()`) against the Grid-issued `challenge` returned from `POST /auth/credentials/{id}/challenge`, and submits the resulting `assertion` with the `Request-Id` header. The `clientPublicKey` for `PASSKEY` credentials is supplied on the challenge call, where it is bound into the pending session-creation request.
- On success for `OAUTH` and `PASSKEY`, and on the signed retry for OTP credentials, the response contains an `AuthSession`. For `OAUTH` and `PASSKEY` the session signing key is delivered as `encryptedSessionSigningKey` (HPKE-sealed to the supplied `clientPublicKey`); for OTP credentials the client already holds the session signing key (the TEK private key it generated) and that field is omitted from the response. The `expiresAt` timestamp marks when the session expires.
+ On success for `OAUTH` and `PASSKEY`, and on the signed retry for OTP credentials, the response contains an `AuthSession`. In the recommended client-held-key flow the client sends a compressed `clientPublicKey` and retains the corresponding private key as the session signing key, so the response omits `encryptedSessionSigningKey` — the same way OTP credentials have always worked. In the deprecated legacy flow the client sends an uncompressed `clientPublicKey` and the session signing key is returned as `encryptedSessionSigningKey` sealed to that key for the client to decrypt. The `expiresAt` timestamp marks when the session expires.
operationId: verifyAuthCredential
tags:
- Embedded Wallet Auth
@@ -5907,7 +5907,7 @@ paths:
value:
type: OAUTH
oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q
- clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ clientPublicKey: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31
passkey:
summary: Verify a passkey credential
value:
@@ -6201,9 +6201,9 @@ paths:
description: |
Refresh an active Embedded Wallet auth session and create a new session signing key. Session refresh is a two-step signed-retry flow:
- 1. Call `POST /auth/sessions/{id}/refresh` with the request body `{ "clientPublicKey": "04..." }` and no signature headers. Grid builds a Grid session-refresh payload, binds the supplied `clientPublicKey` into that payload, persists it as a pending request, and returns `202` with `payloadToSign`, `requestId`, and `expiresAt`.
+ 1. Call `POST /auth/sessions/{id}/refresh` with the request body `{ "clientPublicKey": "02..." }` and no signature headers. Send a freshly generated client public key and retain its private key. Grid binds the supplied `clientPublicKey` to the pending refresh and returns `202` with `payloadToSign`, `requestId`, and `expiresAt`.
- 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in the request body. On success, Grid returns a new `AuthSession` with an `encryptedSessionSigningKey` sealed to that client public key.
+ 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in the request body. On success, Grid returns a new `AuthSession`. In the recommended client-held-key flow (compressed `clientPublicKey`) the client retains the new session signing key and the response omits `encryptedSessionSigningKey`; the deprecated legacy flow (uncompressed `clientPublicKey`) instead returns the new key as `encryptedSessionSigningKey` sealed to that key.
The original session must still be active on both steps so it can authorize the refresh. If the session has already expired, use the credential reauthentication flow instead.
operationId: refreshAuthSession
@@ -6243,7 +6243,7 @@ paths:
refresh:
summary: Refresh an active session
value:
- clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ clientPublicKey: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31
responses:
'201':
description: New authentication session created successfully.
@@ -6258,7 +6258,6 @@ paths:
id: Session:019542f5-b3e7-1d02-0000-000000000011
accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
type: EMAIL_OTP
- encryptedSessionSigningKey: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf
nickname: example@lightspark.com
createdAt: '2026-04-08T15:30:01Z'
updatedAt: '2026-04-08T15:35:00Z'
@@ -22783,8 +22782,8 @@ components:
example: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q
clientPublicKey:
type: string
- description: Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (0x04 prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key must remain on the client. Grid encrypts the session signing key returned in the response to this public key. The key is ephemeral and one-time-use per verification request.
- example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ description: Client-generated, ephemeral P-256 public key; the matching private key must remain on the client and is one-time-use per verification request. In the recommended client-held-key flow, send the key in compressed SEC1 format (`02` or `03` prefix followed by the 32-byte X coordinate; 66 hex characters) and retain the private key as the session signing key — the response omits `encryptedSessionSigningKey`. Sending the key in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters) selects the deprecated legacy flow, in which Grid returns the session signing key as `encryptedSessionSigningKey` sealed to this public key. See the "Client keys & signing" guide.
+ example: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31
OauthCredentialVerifyRequest:
title: OAuth Credential Verify Request
allOf:
@@ -22853,7 +22852,7 @@ components:
PASSKEY: '#/components/schemas/PasskeyCredentialVerifyRequest'
AuthSession:
title: Authentication Session
- description: An authentication session on an Embedded Wallet internal account. Returned from `GET /auth/sessions` (list) and `POST /auth/credentials/{id}/verify` (on credential verification) or `POST /auth/sessions/{id}/refresh` (on mid-session refresh). Only session-issuing responses include `encryptedSessionSigningKey` — it is delivered exactly once at the moment the session is issued and is never returned by the list endpoint.
+ description: An authentication session on an Embedded Wallet internal account. Returned from `GET /auth/sessions` (list) and `POST /auth/credentials/{id}/verify` (on credential verification) or `POST /auth/sessions/{id}/refresh` (on mid-session refresh). In the recommended client-held-key flow the client generates and retains the session signing key, so session-issuing responses carry no signing key. The deprecated `encryptedSessionSigningKey` field is present only for the legacy flow and is never returned by the list endpoint.
allOf:
- $ref: '#/components/schemas/AuthMethod'
- type: object
@@ -22867,28 +22866,31 @@ components:
example: Session:019542f5-b3e7-1d02-0000-000000000003
encryptedSessionSigningKey:
type: string
+ deprecated: true
description: |-
- HPKE-encrypted session signing key, sealed to the `clientPublicKey` supplied on the verification or refresh request. Encoded as a base58check string: the decoded payload is a 33-byte compressed P-256 encapsulated public key followed by AES-256-GCM ciphertext. The client decrypts this key with its private key and uses it to sign subsequent Embedded Wallet requests until `expiresAt`.
+ Deprecated; returned only for the legacy flow. When the client sends an uncompressed `clientPublicKey` on the verification or refresh request, Grid seals the session signing key to that key and returns it here for the client to decrypt and use until `expiresAt`.
- Returned only by session-issuing responses for `OAUTH` and `PASSKEY` credentials. `EMAIL_OTP` and `SMS_OTP` sessions omit this field — the client generates a TEK keypair before verification and retains the private key throughout, so the server has nothing to deliver. Always omitted from list responses (`GET /auth/sessions`) since Grid does not retain the plaintext key after the client has decrypted it.
+ In the recommended client-held-key flow the client sends a compressed `clientPublicKey`, generates and retains the session signing key itself, and this field is absent — the same way `EMAIL_OTP` and `SMS_OTP` sessions have always behaved. New integrations should adopt the client-held-key flow; see the "Client keys & signing" guide. Always omitted from list responses (`GET /auth/sessions`).
+
+ When present, the value is a base58check string whose decoded payload is a 33-byte compressed P-256 encapsulated public key followed by AES-256-GCM ciphertext.
example: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf
expiresAt:
type: string
format: date-time
- description: Timestamp after which the session is no longer valid and the `encryptedSessionSigningKey` must not be used to sign further requests.
+ description: Timestamp after which the session is no longer valid and its session signing key must not be used to sign further requests.
example: '2026-04-09T15:30:01Z'
AuthCredentialChallengeRequest:
title: Auth Credential Challenge Request
- description: Request body for `POST /auth/credentials/{id}/challenge`. Required when re-challenging a `PASSKEY` credential — must carry `clientPublicKey` so Grid can bake it into the session-creation payload the returned challenge is computed from. Ignored for `EMAIL_OTP` and `SMS_OTP`, where the credential type alone is sufficient because the OTP is delivered out-of-band. OAuth credentials do not use this endpoint; authenticate or reauthenticate them with `POST /auth/credentials/{id}/verify`.
+ description: Request body for `POST /auth/credentials/{id}/challenge`. Required when re-challenging a `PASSKEY` credential — must carry `clientPublicKey` so Grid can bind it to the session the returned challenge issues. Ignored for `EMAIL_OTP` and `SMS_OTP`, where the credential type alone is sufficient because the OTP is delivered out-of-band. OAuth credentials do not use this endpoint; authenticate or reauthenticate them with `POST /auth/credentials/{id}/verify`.
type: object
properties:
clientPublicKey:
type: string
- pattern: ^04[0-9a-fA-F]{128}$
- minLength: 130
+ pattern: ^(0[23][0-9a-fA-F]{64}|04[0-9a-fA-F]{128})$
+ minLength: 66
maxLength: 130
- description: Required for `PASSKEY` credentials. Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key must remain on the client. Grid bakes this key into the session-creation payload that the returned `challenge` is computed from, so the resulting session signing key is sealed to the client. Ignored for `EMAIL_OTP` and `SMS_OTP`.
- example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ description: Required for `PASSKEY` credentials; the matching private key must remain on the client. In the recommended client-held-key flow, send the key in compressed SEC1 format (`02` or `03` prefix followed by the 32-byte X coordinate; 66 hex characters) and retain the private key as the session signing key — the verification response omits `encryptedSessionSigningKey`. Sending the key in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters) selects the deprecated legacy flow, in which the session signing key is returned as `encryptedSessionSigningKey` sealed to this key. Ignored for `EMAIL_OTP` and `SMS_OTP`. See the "Client keys & signing" guide.
+ example: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31
PasskeyAuthChallenge:
title: Passkey Auth Challenge
description: Extended `AuthMethod` shape returned for `PASSKEY` credentials from `POST /auth/credentials/{id}/challenge`. Includes the WebAuthn `credentialId` needed to target the passkey, plus the Grid-issued `challenge`, corresponding `requestId`, and challenge `expiresAt`. The `challenge` value is the lowercase hex-encoded SHA-256 digest of the canonical session-creation request body, not a base64url string. The client UTF-8 encodes this string as the WebAuthn challenge and signs it with the passkey to produce the assertion submitted to `POST /auth/credentials/{id}/verify`.
@@ -22942,16 +22944,16 @@ components:
$ref: '#/components/schemas/AuthSession'
AuthSessionRefreshRequest:
title: Auth Session Refresh Request
- description: Request body for refreshing an active authentication session. The `clientPublicKey` is required on both steps of the signed-retry flow. On the initial call, Grid binds this key into the session-creation payload returned as `payloadToSign`; on the signed retry, the client echoes the same key back and Grid uses it to encrypt the newly issued session signing key.
+ description: 'Request body for refreshing an active authentication session. The `clientPublicKey` is required on both steps of the signed-retry flow and must match on both. Its SEC1 encoding selects how the refreshed session signing key is delivered: send a compressed key for the recommended client-held-key flow (the client retains the new signing key) or an uncompressed key for the deprecated legacy flow (Grid returns the new key as `encryptedSessionSigningKey`).'
type: object
required:
- clientPublicKey
properties:
clientPublicKey:
type: string
- pattern: ^04[0-9a-fA-F]{128}$
- description: Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key must remain on the client. Grid binds this key into the session-creation payload on the initial call and seals the returned `encryptedSessionSigningKey` to it on the signed retry.
- example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ pattern: ^(0[23][0-9a-fA-F]{64}|04[0-9a-fA-F]{128})$
+ description: Client-generated P-256 public key; the matching private key must remain on the client. In the recommended client-held-key flow, send the key in compressed SEC1 format (`02` or `03` prefix followed by the 32-byte X coordinate; 66 hex characters) and retain the private key as the new session signing key — the response omits `encryptedSessionSigningKey`. Sending the key in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters) selects the deprecated legacy flow, in which the new session signing key is returned as `encryptedSessionSigningKey` sealed to this key. See the "Client keys & signing" guide.
+ example: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31
DelegatedKeyStatus:
type: string
enum:
diff --git a/openapi/components/schemas/auth/AuthCredentialChallengeRequest.yaml b/openapi/components/schemas/auth/AuthCredentialChallengeRequest.yaml
index 254c798bd..ca60134d3 100644
--- a/openapi/components/schemas/auth/AuthCredentialChallengeRequest.yaml
+++ b/openapi/components/schemas/auth/AuthCredentialChallengeRequest.yaml
@@ -2,8 +2,8 @@ title: Auth Credential Challenge Request
description: >-
Request body for `POST /auth/credentials/{id}/challenge`. Required when
re-challenging a `PASSKEY` credential — must carry `clientPublicKey` so
- Grid can bake it into the session-creation payload the returned
- challenge is computed from. Ignored for `EMAIL_OTP` and `SMS_OTP`, where
+ Grid can bind it to the session the returned challenge issues. Ignored
+ for `EMAIL_OTP` and `SMS_OTP`, where
the credential type alone is sufficient because the OTP is delivered
out-of-band. OAuth credentials do not use this endpoint; authenticate or
reauthenticate them with `POST /auth/credentials/{id}/verify`.
@@ -11,16 +11,19 @@ type: object
properties:
clientPublicKey:
type: string
- pattern: "^04[0-9a-fA-F]{128}$"
- minLength: 130
+ pattern: "^(0[23][0-9a-fA-F]{64}|04[0-9a-fA-F]{128})$"
+ minLength: 66
maxLength: 130
description: >-
- Required for `PASSKEY` credentials. Client-generated P-256 public
- key, hex-encoded in uncompressed SEC1 format (`04` prefix followed
- by the 32-byte X and 32-byte Y coordinates; 130 hex characters
- total). The matching private key must remain on the client. Grid
- bakes this key into the session-creation payload that the
- returned `challenge` is computed from, so the resulting session
- signing key is sealed to the client. Ignored for `EMAIL_OTP` and
- `SMS_OTP`.
- example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ Required for `PASSKEY` credentials; the matching private key must
+ remain on the client. In the recommended client-held-key flow, send
+ the key in compressed SEC1 format (`02` or `03` prefix followed by the
+ 32-byte X coordinate; 66 hex characters) and retain the private key as
+ the session signing key — the verification response omits
+ `encryptedSessionSigningKey`. Sending the key in uncompressed SEC1
+ format (`04` prefix followed by the 32-byte X and 32-byte Y
+ coordinates; 130 hex characters) selects the deprecated legacy flow,
+ in which the session signing key is returned as
+ `encryptedSessionSigningKey` sealed to this key. Ignored for
+ `EMAIL_OTP` and `SMS_OTP`. See the "Client keys & signing" guide.
+ example: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31
diff --git a/openapi/components/schemas/auth/AuthSession.yaml b/openapi/components/schemas/auth/AuthSession.yaml
index 58c43698a..fff5c7490 100644
--- a/openapi/components/schemas/auth/AuthSession.yaml
+++ b/openapi/components/schemas/auth/AuthSession.yaml
@@ -4,9 +4,10 @@ description: >-
Returned from `GET /auth/sessions` (list) and
`POST /auth/credentials/{id}/verify` (on credential verification) or
`POST /auth/sessions/{id}/refresh` (on mid-session refresh).
- Only session-issuing responses include `encryptedSessionSigningKey` —
- it is delivered exactly once at the moment the session is issued and is
- never returned by the list endpoint.
+ In the recommended client-held-key flow the client generates and retains
+ the session signing key, so session-issuing responses carry no signing key.
+ The deprecated `encryptedSessionSigningKey` field is present only for the
+ legacy flow and is never returned by the list endpoint.
allOf:
- $ref: ./AuthMethod.yaml
- type: object
@@ -25,30 +26,31 @@ allOf:
example: Session:019542f5-b3e7-1d02-0000-000000000003
encryptedSessionSigningKey:
type: string
+ deprecated: true
description: >-
- HPKE-encrypted session signing key, sealed to the
- `clientPublicKey` supplied on the verification or refresh request.
- Encoded as
- a base58check string: the decoded payload is a 33-byte
- compressed P-256 encapsulated public key followed by
- AES-256-GCM ciphertext. The client decrypts this key with
- its private key and uses it to sign subsequent Embedded
- Wallet requests until `expiresAt`.
+ Deprecated; returned only for the legacy flow. When the client
+ sends an uncompressed `clientPublicKey` on the verification or
+ refresh request, Grid seals the session signing key to that key and
+ returns it here for the client to decrypt and use until `expiresAt`.
- Returned only by session-issuing responses for `OAUTH` and
- `PASSKEY` credentials. `EMAIL_OTP` and `SMS_OTP` sessions omit this
- field — the client generates a TEK keypair before verification and
- retains the private key throughout, so the server has nothing
- to deliver. Always omitted from list responses
- (`GET /auth/sessions`) since Grid does not retain the
- plaintext key after the client has decrypted it.
+ In the recommended client-held-key flow the client sends a
+ compressed `clientPublicKey`, generates and retains the session
+ signing key itself, and this field is absent — the same way
+ `EMAIL_OTP` and `SMS_OTP` sessions have always behaved. New
+ integrations should adopt the client-held-key flow; see the
+ "Client keys & signing" guide. Always omitted from list responses
+ (`GET /auth/sessions`).
+
+
+ When present, the value is a base58check string whose decoded
+ payload is a 33-byte compressed P-256 encapsulated public key
+ followed by AES-256-GCM ciphertext.
example: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf
expiresAt:
type: string
format: date-time
description: >-
- Timestamp after which the session is no longer valid and the
- `encryptedSessionSigningKey` must not be used to sign further
- requests.
+ Timestamp after which the session is no longer valid and its
+ session signing key must not be used to sign further requests.
example: '2026-04-09T15:30:01Z'
diff --git a/openapi/components/schemas/auth/AuthSessionRefreshRequest.yaml b/openapi/components/schemas/auth/AuthSessionRefreshRequest.yaml
index bd170bb59..bce723758 100644
--- a/openapi/components/schemas/auth/AuthSessionRefreshRequest.yaml
+++ b/openapi/components/schemas/auth/AuthSessionRefreshRequest.yaml
@@ -1,22 +1,28 @@
title: Auth Session Refresh Request
description: >-
Request body for refreshing an active authentication session. The
- `clientPublicKey` is required on both steps of the signed-retry flow. On the
- initial call, Grid binds this key into the session-creation payload
- returned as `payloadToSign`; on the signed retry, the client echoes the same
- key back and Grid uses it to encrypt the newly issued session signing key.
+ `clientPublicKey` is required on both steps of the signed-retry flow and
+ must match on both. Its SEC1 encoding selects how the refreshed session
+ signing key is delivered: send a compressed key for the recommended
+ client-held-key flow (the client retains the new signing key) or an
+ uncompressed key for the deprecated legacy flow (Grid returns the new key
+ as `encryptedSessionSigningKey`).
type: object
required:
- clientPublicKey
properties:
clientPublicKey:
type: string
- pattern: "^04[0-9a-fA-F]{128}$"
+ pattern: "^(0[23][0-9a-fA-F]{64}|04[0-9a-fA-F]{128})$"
description: >-
- Client-generated P-256 public key, hex-encoded in uncompressed SEC1
- format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates;
- 130 hex characters total). The matching private key must remain on the
- client. Grid binds this key into the session-creation payload on the
- initial call and seals the returned `encryptedSessionSigningKey` to it on
- the signed retry.
- example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ Client-generated P-256 public key; the matching private key must remain
+ on the client. In the recommended client-held-key flow, send the key in
+ compressed SEC1 format (`02` or `03` prefix followed by the 32-byte X
+ coordinate; 66 hex characters) and retain the private key as the new
+ session signing key — the response omits `encryptedSessionSigningKey`.
+ Sending the key in uncompressed SEC1 format (`04` prefix followed by the
+ 32-byte X and 32-byte Y coordinates; 130 hex characters) selects the
+ deprecated legacy flow, in which the new session signing key is returned
+ as `encryptedSessionSigningKey` sealed to this key. See the
+ "Client keys & signing" guide.
+ example: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31
diff --git a/openapi/components/schemas/auth/OauthCredentialVerifyRequestFields.yaml b/openapi/components/schemas/auth/OauthCredentialVerifyRequestFields.yaml
index b1145e8c7..6977c5714 100644
--- a/openapi/components/schemas/auth/OauthCredentialVerifyRequestFields.yaml
+++ b/openapi/components/schemas/auth/OauthCredentialVerifyRequestFields.yaml
@@ -25,10 +25,15 @@ properties:
clientPublicKey:
type: string
description: >-
- Client-generated P-256 public key, hex-encoded in uncompressed SEC1
- format (0x04 prefix followed by the 32-byte X and 32-byte Y
- coordinates; 130 hex characters total). The matching private key
- must remain on the client. Grid encrypts the session signing key
- returned in the response to this public key. The key is ephemeral
- and one-time-use per verification request.
- example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ Client-generated, ephemeral P-256 public key; the matching private key
+ must remain on the client and is one-time-use per verification request.
+ In the recommended client-held-key flow, send the key in compressed SEC1
+ format (`02` or `03` prefix followed by the 32-byte X coordinate; 66 hex
+ characters) and retain the private key as the session signing key — the
+ response omits `encryptedSessionSigningKey`. Sending the key in
+ uncompressed SEC1 format (`04` prefix followed by the 32-byte X and
+ 32-byte Y coordinates; 130 hex characters) selects the deprecated legacy
+ flow, in which Grid returns the session signing key as
+ `encryptedSessionSigningKey` sealed to this public key. See the
+ "Client keys & signing" guide.
+ example: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31
diff --git a/openapi/paths/auth/auth_credentials.yaml b/openapi/paths/auth/auth_credentials.yaml
index 7a16ed121..dfa447652 100644
--- a/openapi/paths/auth/auth_credentials.yaml
+++ b/openapi/paths/auth/auth_credentials.yaml
@@ -15,10 +15,9 @@ post:
Adding a credential requires a signature from an existing verified
credential on the same account. Call this endpoint with the new
credential's details to receive `202` with `payloadToSign` and
- `requestId`. Use the session API keypair of an existing verified
- credential (decrypted client-side from its
- `encryptedSessionSigningKey`) to build an API-key stamp over
- `payloadToSign`, then retry the same request with that full stamp
+ `requestId`. Use the session signing key the client holds for an
+ existing verified credential on the same account to build an API-key
+ stamp over `payloadToSign`, then retry the same request with that full stamp
as the `Grid-Wallet-Signature` header and the `requestId`
echoed back as the `Request-Id` header. The signed retry returns
`201` with the created `AuthMethod`. For OTP credentials, the one-time
diff --git a/openapi/paths/auth/auth_credentials_{id}_verify.yaml b/openapi/paths/auth/auth_credentials_{id}_verify.yaml
index af9d90931..2488a0166 100644
--- a/openapi/paths/auth/auth_credentials_{id}_verify.yaml
+++ b/openapi/paths/auth/auth_credentials_{id}_verify.yaml
@@ -43,13 +43,15 @@ post:
On success for `OAUTH` and `PASSKEY`, and on the signed retry for
- OTP credentials, the response contains an `AuthSession`. For `OAUTH`
- and `PASSKEY` the session signing key is delivered as
- `encryptedSessionSigningKey` (HPKE-sealed to the supplied
- `clientPublicKey`); for OTP credentials the client already holds the
- session signing key (the TEK private key it generated) and that
- field is omitted from the response. The `expiresAt` timestamp
- marks when the session expires.
+ OTP credentials, the response contains an `AuthSession`. In the
+ recommended client-held-key flow the client sends a compressed
+ `clientPublicKey` and retains the corresponding private key as the
+ session signing key, so the response omits `encryptedSessionSigningKey`
+ — the same way OTP credentials have always worked. In the deprecated
+ legacy flow the client sends an uncompressed `clientPublicKey` and the
+ session signing key is returned as `encryptedSessionSigningKey` sealed
+ to that key for the client to decrypt. The `expiresAt` timestamp marks
+ when the session expires.
operationId: verifyAuthCredential
tags:
- Embedded Wallet Auth
@@ -125,7 +127,7 @@ post:
value:
type: OAUTH
oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q
- clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ clientPublicKey: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31
passkey:
summary: Verify a passkey credential
value:
diff --git a/openapi/paths/auth/auth_sessions_{id}_refresh.yaml b/openapi/paths/auth/auth_sessions_{id}_refresh.yaml
index 7248f7337..63359e09e 100644
--- a/openapi/paths/auth/auth_sessions_{id}_refresh.yaml
+++ b/openapi/paths/auth/auth_sessions_{id}_refresh.yaml
@@ -6,17 +6,21 @@ post:
1. Call `POST /auth/sessions/{id}/refresh` with the request body
- `{ "clientPublicKey": "04..." }` and no signature headers. Grid builds a
- Grid session-refresh payload, binds the supplied
- `clientPublicKey` into that payload, persists it as a pending request, and
- returns `202` with `payloadToSign`, `requestId`, and `expiresAt`.
+ `{ "clientPublicKey": "02..." }` and no signature headers. Send a freshly
+ generated client public key and retain its private key. Grid binds the
+ supplied `clientPublicKey` to the pending refresh and returns `202` with
+ `payloadToSign`, `requestId`, and `expiresAt`.
2. Sign `payloadToSign` with the current session signing key, then retry the
same request with the full API-key stamp as `Grid-Wallet-Signature`, the
`requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in
- the request body. On success, Grid returns a new `AuthSession` with an
- `encryptedSessionSigningKey` sealed to that client public key.
+ the request body. On success, Grid returns a new `AuthSession`. In the
+ recommended client-held-key flow (compressed `clientPublicKey`) the client
+ retains the new session signing key and the response omits
+ `encryptedSessionSigningKey`; the deprecated legacy flow (uncompressed
+ `clientPublicKey`) instead returns the new key as
+ `encryptedSessionSigningKey` sealed to that key.
The original session must still be active on both steps so it can authorize
@@ -66,7 +70,7 @@ post:
refresh:
summary: Refresh an active session
value:
- clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ clientPublicKey: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31
responses:
'201':
description: New authentication session created successfully.
@@ -81,7 +85,6 @@ post:
id: Session:019542f5-b3e7-1d02-0000-000000000011
accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
type: EMAIL_OTP
- encryptedSessionSigningKey: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf
nickname: example@lightspark.com
createdAt: '2026-04-08T15:30:01Z'
updatedAt: '2026-04-08T15:35:00Z'