---
title: "Create an authentication credential"
method: POST
path: "/auth/credentials"
tags: ["Embedded Wallet Auth"]
---

# Create an authentication credential

`POST /auth/credentials`

Register an authentication credential for an Embedded Wallet customer.

**First credential on an internal account**

If the target internal account does not yet have any authentication credential registered, call this endpoint with the credential details. The response is `201` with the created `AuthMethod`. For `EMAIL_OTP` credentials, this call also triggers a one-time password email to the address on the customer record tied to the internal account; the credential must be activated via `POST /auth/credentials/{id}/verify` before it can sign requests. For `OAUTH` credentials, the supplied `oidcToken` is validated inline against the issuer's `.well-known` OpenID configuration (the token's `iat` must be less than 60 seconds before the request); activation still happens via `POST /auth/credentials/{id}/verify`. For `PASSKEY` credentials, the client completes a WebAuthn registration (`navigator.credentials.create()`) using a `challenge` issued by the platform backend and submits the resulting `attestation` here; the credential must still be activated via `POST /auth/credentials/{id}/verify` by completing a WebAuthn assertion. Unlike the registration `challenge` (platform-issued), the challenge for the first authentication is issued by Grid and returned inline on the `201` response alongside the `AuthMethod` fields, plus a `requestId` and challenge `expiresAt` (see `PasskeyAuthChallenge`). The client uses that Grid-issued `challenge` to produce the assertion and submits it with `Request-Id: <requestId>` to `POST /auth/credentials/{id}/verify`. On every subsequent reauthentication the challenge is re-issued via `POST /auth/credentials/{id}/challenge`. Only one `PASSKEY` credential is supported per internal account in v1.

**Adding an additional credential**

Registering an additional credential against an internal account that already has one requires a signature from an existing verified credential. Call this endpoint with the new credential's details; if an existing credential is already registered on the internal account the response is `202` with a `payloadToSign` and a `requestId`. Sign the payload with the session private key of an existing verified credential on the same internal account (decrypted client-side from its `encryptedSessionSigningKey`) and retry the same request with the signature supplied 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 `EMAIL_OTP`, the OTP email is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`.

## Headers

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

## Request body

- union
  - EmailOtpCredentialCreateRequest
    - `type` 'EMAIL_OTP', required — Discriminator value identifying this as an email OTP credential.
    - `accountId` string, required — Identifier of the internal account that this credential will authenticate.
  - OauthCredentialCreateRequest
    - `type` 'OAUTH', required — Discriminator value identifying this as an OAuth credential.
    - `accountId` string, required — Identifier of the internal account that this credential will authenticate.
    - `oidcToken` string, required — OIDC ID token issued by the identity provider (e.g. Google, Apple). Grid fetches the issuer's signing key from the `iss` claim's `.well-known` OpenID configuration and verifies the token signature. The token's `iat` claim must be less than 60 seconds before the request timestamp.
  - PasskeyCredentialCreateRequest
    - `type` 'PASSKEY', required — Discriminator value identifying this as a passkey credential.
    - `accountId` string, required — Identifier of the internal account that this credential will authenticate.
    - `nickname` string, required — Human-readable identifier for the passkey, chosen by the user at registration time (e.g. "iPhone Face-ID", "YubiKey 5C"). Shown back on `AuthMethod` responses and in credential listings.
    - `challenge` string, required — Base64url-encoded WebAuthn challenge issued by the platform backend and passed to the client before `navigator.credentials.create()`. Grid verifies it matches the challenge embedded in the attestation's `clientDataJson`, binding the attestation to this registration. Must be single-use.
    - `attestation` PasskeyAttestation, required
      - `credentialId` string, required — Base64url-encoded credential identifier produced by the authenticator at registration time. Typically the base64url of `PublicKeyCredential.rawId`.
      - `clientDataJson` string, required — Base64url-encoded JSON client data collected by the browser during the WebAuthn `navigator.credentials.create()` call. Corresponds to `AuthenticatorAttestationResponse.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.create"`.
      - `attestationObject` string, required — Base64url-encoded CBOR attestation object produced by the authenticator during registration. Corresponds to `AuthenticatorAttestationResponse.attestationObject`.
      - `transports` string[] — Optional. WebAuthn transports as returned by `AuthenticatorAttestationResponse.getTransports()`. Values follow the W3C `AuthenticatorTransport` enum — pass the raw values through to Grid; provider-specific translation is handled server-side. Some authenticators return an empty array; omit the field or send `[]` in that case.

## Response `201`

Authentication credential created successfully. For `EMAIL_OTP` and `OAUTH`, the body is a plain `AuthMethod`. For `PASSKEY`, the body is a `PasskeyAuthChallenge` — an `AuthMethod` with the Grid-issued `challenge`, `requestId`, and `expiresAt` that drive the first authentication assertion.

- union — Discriminated response shape returned from `POST /auth/credentials` (on successful registration) and `POST /auth/credentials/{id}/challenge` (on challenge re-issue). For `EMAIL_OTP` and `OAUTH` credentials the body is a plain `AuthMethod` (wrapped as `AuthMethodResponse` to disambiguate the oneOf). For `PASSKEY` credentials the body is a `PasskeyAuthChallenge` — the base `AuthMethod` fields plus the Grid-issued `challenge`, `requestId`, and `expiresAt` that drive the subsequent assertion.
  - AuthMethodResponse
    - `id` string, required — System-generated unique identifier for the authentication credential.
    - `accountId` string, required — Identifier of the internal account that this credential authenticates.
    - `type` 'OAUTH' | 'EMAIL_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. - `PASSKEY`: A WebAuthn passkey bound to the user's device.
    - `nickname` string, required — Human-readable identifier for this credential. For EMAIL_OTP credentials this is the email address; for OAUTH credentials it is typically the email claim from the OIDC token; for PASSKEY credentials it is the nickname provided at registration time.
    - `createdAt` string, date-time, required — Creation timestamp.
    - `updatedAt` string, date-time, required — Last update timestamp.
  - PasskeyAuthChallenge — Extended `AuthMethod` shape returned for `PASSKEY` credentials from `POST /auth/credentials` (first-authentication case) and `POST /auth/credentials/{id}/challenge` (reauthentication case). Adds a Grid-issued `challenge`, the corresponding `requestId`, and the challenge's `expiresAt` to the base `AuthMethod` fields. The client signs the challenge with the passkey to produce the assertion submitted to `POST /auth/credentials/{id}/verify`.
    - `id` string, required — System-generated unique identifier for the authentication credential.
    - `accountId` string, required — Identifier of the internal account that this credential authenticates.
    - `type` 'OAUTH' | 'EMAIL_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. - `PASSKEY`: A WebAuthn passkey bound to the user's device.
    - `nickname` string, required — Human-readable identifier for this credential. For EMAIL_OTP credentials this is the email address; for OAUTH credentials it is typically the email claim from the OIDC token; for PASSKEY credentials it is the nickname provided at registration time.
    - `createdAt` string, date-time, required — Creation timestamp.
    - `updatedAt` string, date-time, required — Last update timestamp.
    - `challenge` string, required — Base64url-encoded challenge issued by Grid for the pending passkey authentication. The client passes it into `navigator.credentials.get()` as the WebAuthn challenge; the resulting assertion is submitted to `POST /auth/credentials/{id}/verify`. Single-use; a new challenge is issued on the next call to `POST /auth/credentials/{id}/challenge`.
    - `requestId` string, required — Unique identifier for this pending passkey authentication request. Must be echoed as the `Request-Id` header on the subsequent `POST /auth/credentials/{id}/verify` call so Grid can correlate the assertion with the issued challenge.
    - `expiresAt` string, date-time, required — Timestamp after which the issued challenge is no longer valid. The assertion must reach `POST /auth/credentials/{id}/verify` before this time; otherwise the client must request a fresh challenge via `POST /auth/credentials/{id}/challenge`.

## Other responses

- `202` — An existing authentication credential is already registered on the internal account. The response contains a `payloadToSign` that must be signed with the session private key of an existing verified credential on the same internal account, along with a `requestId` that must be echoed back on the retry. The signature is passed as the `Grid-Wallet-Signature` header and the `requestId` as the `Request-Id` header on a retry of this request to complete registration.
- `400` — Bad request. Returned with `EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an `EMAIL_OTP` credential on an internal account that already has one — only one email OTP credential is supported per internal account at this time. Returned with `PASSKEY_CREDENTIAL_ALREADY_EXISTS` when registering a `PASSKEY` credential on an internal account that already has one — only one passkey credential is supported per internal account in v1.
- `401` — Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending challenge for an additional credential on the target internal account, or when the `Request-Id` does not match an unexpired pending challenge.
- `404` — Internal account not found
- `500` — Internal service error

## Changes

- **2026-04-27** `5b8a8161eeb3` — 1 info
  - endpoint added

[Change history](https://skmtc.dev/lightsparkdev/apis/grid-api/changes/auth/credentials/post.md)

---

[API](https://skmtc.dev/lightsparkdev/apis/grid-api.md) · [All operations](https://skmtc.dev/lightsparkdev/apis/grid-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/lightsparkdev/grid-api/revisions/5b8a8161eeb3/schema)
