---
title: "Verify an authentication credential"
method: POST
path: "/auth/credentials/{id}/verify"
tags: ["Embedded Wallet Auth"]
---

# Verify an authentication credential

`POST /auth/credentials/{id}/verify`

Complete the verification step for a previously created authentication credential and issue a session.

For `EMAIL_OTP` and `SMS_OTP` credentials, submit the `encryptedOtpBundle` produced by HPKE-encrypting `{otp_code, public_key}` under the `otpEncryptionTargetBundle` returned from registration when present, or from `POST /auth/credentials/{id}/challenge` when registration omitted it or the OTP must be reissued. The server is a pass-through and never sees the plaintext OTP code. On success the response is `202` with a `payloadToSign` carrying the `verificationToken` bound to the client's TEK public key — sign that token with the matching TEK private key, then retry the same request with the full stamp in `Grid-Wallet-Signature` and the `requestId` echoed in `Request-Id`. The signed retry returns `200` with the issued `AuthSession`. The TEK public key becomes the session API key on successful completion.
In sandbox mode, the OTP flow runs real HPKE end-to-end against a sandbox enclave keypair — clients build a real `encryptedOtpBundle` against the sandbox `otpEncryptionTargetBundle` and sign a real `verificationToken` with their TEK keypair. The only sandbox shortcut is the magic OTP code (`"000000"`) the user "receives" instead of a real email or SMS delivery.

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`. Sending a compressed `clientPublicKey` selects the recommended client-held-key model: the client already holds the session signing key — the private key it generated before authentication — so no key material is returned and the deprecated `encryptedSessionSigningKey` is omitted. Sending an uncompressed `clientPublicKey` selects the deprecated legacy flow, where the session signing key is HPKE-sealed to that key and returned as `encryptedSessionSigningKey` for the client to decrypt. The `expiresAt` timestamp marks when the session expires.

## Path parameters

- `id` string, required

## Headers

- `Grid-Wallet-Signature` string
- `Request-Id` string

## Request body

- union
  - EmailOtpCredentialVerifyRequest — Verify an email-OTP credential via the secure two-leg flow. The client HPKE-encrypts the OTP code (together with its public key) under the `otpEncryptionTargetBundle` returned from registration when present, or from `POST /auth/credentials/{id}/challenge` when registration omitted it or the OTP must be reissued, submits the result here, and receives `202` with a `payloadToSign` carrying a `verificationToken` bound to the client's public key. The client signs that token with the matching private key and retries this request with `Grid-Wallet-Signature` + `Request-Id` headers to obtain the session. Plaintext OTP codes are never sent over the wire.
    - `type` 'EMAIL_OTP', required — Discriminator value identifying this as an email OTP verification.
    - `encryptedOtpBundle` string, required — HPKE-sealed OTP attempt — the OTP code never reaches Grid in plaintext. The client generates a fresh ephemeral P-256 key pair (the session signing key pair it keeps once login completes), HPKE-encrypts `{otp_code, public_key}` (the code the user entered plus that key pair's public key) to the key in `otpEncryptionTargetBundle`, and submits the encrypted result here. The value is the `{encappedPublic, ciphertext}` JSON an HPKE library produces; the Global Accounts client-keys guide has a worked example. On success the response is `202` with a `payloadToSign` carrying a `verificationToken` bound to the public key sealed in this bundle. Sign that token with the matching private key, then retry this request with the full stamp in `Grid-Wallet-Signature` and the `requestId` in `Request-Id` to complete the flow and receive the session. The client keeps that private key as the session signing key, and its public key becomes the session API key.
  - SmsOtpCredentialVerifyRequest — Verify an SMS-OTP credential via the same secure two-leg flow as email OTP. The client HPKE-encrypts the OTP code (together with its public key) under the `otpEncryptionTargetBundle` returned from registration or `POST /auth/credentials/{id}/challenge`, submits the result here, and receives `202` with a `payloadToSign` carrying a `verificationToken` bound to the client's public key. The client signs that token with the matching private key and retries this request with `Grid-Wallet-Signature` + `Request-Id` headers to obtain the session. Plaintext OTP codes are never sent over the wire.
    - `type` 'SMS_OTP', required — Discriminator value identifying this as an SMS OTP verification.
    - `encryptedOtpBundle` string, required — HPKE-sealed OTP attempt. Same format and retry semantics as `EmailOtpCredentialVerifyRequest.encryptedOtpBundle`.
  - OauthCredentialVerifyRequest
    - `type` 'OAUTH', required — Discriminator value identifying this as an OAuth verification.
    - `oidcToken` string, required — OIDC ID token issued by the identity provider. For reauthentication after a prior session expired, supply a fresh token — the token's `iat` claim must be less than 60 seconds before the request timestamp. The token identity (`iss`, `aud`, and `sub`) must match the registered OAuth credential. In production, the provider signature is verified against the issuer's JWKS. In sandbox, the token must still be JWT-shaped with supported `iss`, non-empty `aud` and `sub`, numeric `iat` and `exp`, and a `nonce` equal to `sha256(clientPublicKey)`, but the signature segment may be a dummy value.
    - `clientPublicKey` string, required — Client-generated, ephemeral P-256 public key; the matching private key is retained on the client and is one-time-use per verification request. Send a compressed SEC1 key (`02`/`03` prefix followed by the 32-byte X coordinate; 66 hex characters) for the recommended client-held-key model, where that private key becomes the session signing key. Send an uncompressed SEC1 key (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters) for the deprecated legacy flow, where Grid seals the session signing key to it instead.
  - PasskeyCredentialVerifyRequest
    - `type` 'PASSKEY', required — Discriminator value identifying this as a passkey verification.
    - `assertion` PasskeyAssertion, required — WebAuthn assertion returned by `navigator.credentials.get()`. In sandbox, Grid validates the assertion against the registered passkey credential so the client-side flow can match production. In production, Grid validates the WebAuthn assertion.
      - `credentialId` string, required — Base64url-encoded credential identifier returned during the WebAuthn assertion. Corresponds to `PublicKeyCredential.rawId`.
      - `clientDataJson` string, required — Base64url-encoded JSON client data collected by the browser during the WebAuthn `navigator.credentials.get()` call. Corresponds to `AuthenticatorAssertionResponse.clientDataJSON` from the WebAuthn spec — Grid's field name is intentionally camelCased as `clientDataJson` (lowercase JSON) for consistency with the rest of the API; the value is the same bytes the browser returns. Contains the challenge, origin, and `type: "webauthn.get"`.
      - `authenticatorData` string, required — Base64url-encoded authenticator data returned by the authenticator during the assertion. Corresponds to `AuthenticatorAssertionResponse.authenticatorData`.
      - `signature` string, required — Base64url-encoded signature produced by the authenticator over `authenticatorData || SHA-256(clientDataJSON)`. Corresponds to `AuthenticatorAssertionResponse.signature`. The signature byte format is determined by the credential's public-key algorithm — DER-encoded ECDSA for ES256 (P-256, typical for passkeys), PKCS#1 v1.5 for RS256, or a raw 64-byte signature for EdDSA.
      - `userHandle` string — Base64url-encoded user handle returned by the authenticator. Corresponds to `AuthenticatorAssertionResponse.userHandle`. Populated (and required by the WebAuthn spec) for discoverable credentials — resident keys used in the "Sign in with passkey" autofill flow — and typically present for passkey registrations. Omit this field entirely for non-discoverable credentials specified via `allowCredentials` where the authenticator returns no user handle.

## Response `200`

Authentication credential verified and session issued

- AuthSession — 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). The `clientPublicKey` encoding on the issuing request selects the flow: a compressed key gets the client-held-key model, where the client generates and retains the session signing key and session-issuing responses carry no key material; an uncompressed key gets the deprecated legacy flow, where Grid seals the session signing key to that public key and returns it as `encryptedSessionSigningKey`. Never returned by the list endpoint.
  - `id` string, required — System-generated unique identifier for the session. Pass this value to `DELETE /auth/sessions/{id}` to revoke the session before `expiresAt`. Overrides the `id` inherited from `AuthMethod` so this response identifies the session rather than the authenticating credential.
  - `accountId` string, required — Identifier of the internal account that this credential authenticates.
  - `type` 'OAUTH' | 'EMAIL_OTP' | 'SMS_OTP' | 'PASSKEY', required — The type of authentication credential. - `OAUTH`: OpenID Connect (OIDC) token issued by an identity provider such as Google or Apple. - `EMAIL_OTP`: A one-time password delivered to the user's email address. - `SMS_OTP`: A one-time password delivered to the user's phone number. - `PASSKEY`: A WebAuthn passkey bound to the user's device.
  - `credentialId` string — Base64url-encoded WebAuthn credential identifier for this passkey. Present only for `PASSKEY` authentication credentials. Corresponds to `PublicKeyCredential.rawId`; pass this value as `allowCredentials[].id` when requesting a passkey assertion for this auth method.
  - `nickname` string, required — Human-readable identifier for this credential. For EMAIL_OTP credentials this is the email address; for SMS_OTP credentials this is the E.164 phone number; for OAUTH credentials it is typically the email claim from the OIDC token; for PASSKEY credentials it is the validated nickname provided at registration time.
  - `createdAt` string, date-time, required — Creation timestamp.
  - `updatedAt` string, date-time, required — Last update timestamp.
  - `encryptedSessionSigningKey` string — Deprecated; present only for the legacy flow, selected by sending an uncompressed `clientPublicKey` on the verification or refresh request. Grid seals the session signing key to that public key and returns it here as a base58check string (a 33-byte compressed P-256 encapsulated public key followed by AES-256-GCM ciphertext) for the client to decrypt with its private key. The recommended client-held-key flow sends a compressed `clientPublicKey` instead: the client generates and retains the session signing key itself, so this field is omitted — the same way `EMAIL_OTP` and `SMS_OTP` sessions have always worked. See the "Client keys & signing" guide. Always omitted from list responses (`GET /auth/sessions`).
  - `expiresAt` string, date-time, required — Timestamp after which the session is no longer valid and the session signing key must not be used to sign further requests.

## Other responses

- `202` — Verification challenge issued. Returned only for OTP credentials, on the first leg of the secure OTP login flow. Build an API-key stamp over `payloadToSign` (the `verificationToken`) with the TEK keypair the client generated for this login, then resubmit the same request with that full stamp as `Grid-Wallet-Signature` and `requestId` echoed as `Request-Id` to receive the issued session on the signed retry.
- `400` — Bad request
- `401` — Unauthorized. Returned for an invalid or expired OTP (`EMAIL_OTP` or `SMS_OTP`), for an OIDC token whose signature, issuer, identity, nonce, or `iat` freshness check failed (`OAUTH`), or for a WebAuthn assertion whose signature, challenge, or credential match failed (`PASSKEY`). Also returned for `PASSKEY` when `Request-Id` is missing, does not match an unexpired pending challenge for this credential, or was already consumed. For OTP signed retries, returned when `Grid-Wallet-Signature` is missing, malformed, signed by a public key that does not match the one bound into the `verificationToken`, or when `Request-Id` does not match an unexpired pending verification challenge.
- `404` — Authentication credential not found
- `429` — Too many requests. Returned with `RATE_LIMITED` when verification attempts for this credential happen too frequently (for example, repeated bad OTPs or rapid-fire reauthentication retries). Clients should back off and retry after the interval indicated by the `Retry-After` response header.
- `500` — Internal service error

---

[API](https://skmtc.net/stainless-api/apis/grid-api.md) · [All operations](https://skmtc.net/stainless-api/apis/grid-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/stainless-api/grid-api/revisions/4f1f6b937f93/schema)
