> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lightspark.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Create an authentication credential

> Register an authentication credential for an Embedded Wallet customer.

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 (`OAUTH` or `PASSKEY`), or to add `EMAIL_OTP` back after it has been removed. Only one `EMAIL_OTP` credential is 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 `EMAIL_OTP`, the OTP email is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`.




## OpenAPI

````yaml https://app.stainless.com/api/spec/documented/grid/openapi.documented.yml post /auth/credentials
openapi: 3.1.0
info:
  title: Grid API
  description: >
    API for managing global payments on the open Money Grid. Built by
    Lightspark. See the full documentation at https://docs.lightspark.com/.
  version: '2025-10-13'
  contact:
    name: Lightspark Support
    email: support@lightspark.com
  license:
    name: Proprietary
    url: https://lightspark.com/terms
servers:
  - url: https://api.lightspark.com/grid/2025-10-13
    description: Production server
security:
  - BasicAuth: []
  - AgentAuth: []
tags:
  - name: Platform Configuration
    description: >-
      Platform configuration endpoints for managing global settings. You can
      also configure these settings in the Grid dashboard.
  - name: Customers
    description: >-
      Customer management endpoints for creating and updating customer
      information
  - name: KYC/KYB Verifications
    description: >-
      Endpoints for Know Your Customer (KYC) and Know Your Business (KYB)
      verification, including managing beneficial owners and triggering
      verification for customers.
  - name: Documents
    description: >-
      Endpoints for uploading and managing verification documents for customers
      and beneficial owners. Supports KYC and KYB document requirements.
  - name: Internal Accounts
    description: >-
      Internal account management endpoints for creating and managing internal
      accounts
  - name: External Accounts
    description: >-
      External account management endpoints for creating and managing external
      bank accounts
  - name: Same-Currency Transfers
    description: >-
      Endpoints for transferring funds between internal and external accounts
      with the same currency
  - name: Cross-Currency Transfers
    description: Endpoints for creating and confirming quotes for cross-currency transfers
  - name: Transactions
    description: Endpoints for retrieving transaction information
  - name: Webhooks
    description: Webhook endpoints and configuration for receiving notifications
  - name: Invitations
    description: Endpoints for creating, claiming and managing UMA invitations
  - name: Sandbox
    description: Endpoints to trigger test cases in sandbox
  - name: API Tokens
    description: Endpoints to programmatically manage API tokens
  - name: Exchange Rates
    description: >-
      Endpoints for retrieving cached foreign exchange rates. Rates are cached
      for approximately 5 minutes and include platform-specific fees.
  - name: Discoveries
    description: >-
      Endpoints for discovering available payment rails, banks, and providers
      for a given country and currency corridor.
  - name: Embedded Wallet Auth
    description: >-
      Endpoints for registering and verifying end-user authentication
      credentials (email OTP, OAuth, passkey) used to sign Embedded Wallet
      actions.
  - name: Agent Management
    description: >-
      Endpoints for creating and managing agents (experimental), called by the
      partner's backend using platform credentials. Covers the full agent
      lifecycle: creation, policy configuration, pausing, deletion, the device
      code installation flow, and approving or rejecting transactions initiated
      by agents.
  - name: Agent Operations
    description: >-
      Endpoints called by the agent itself using its own credentials (obtained
      via device code redemption). Scoped to the agent's associated customer —
      all requests automatically operate on behalf of that customer and are
      subject to the agent's policy. When an action requires approval, the
      resulting transaction enters a pending state and must be approved by the
      platform via `POST /transactions/{transactionId}/approve`.
  - name: Cards
    description: >-
      Card management endpoints. Issue debit cards against an internal account,
      freeze / unfreeze, close, manage card funding sources, and list card
      transactions.
paths:
  /auth/credentials:
    post:
      tags:
        - Embedded Wallet Auth
      summary: Create an authentication credential
      description: >
        Register an authentication credential for an Embedded Wallet customer.


        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 (`OAUTH` or `PASSKEY`), or to add `EMAIL_OTP`
        back after it has been removed. Only one `EMAIL_OTP` credential is
        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 `EMAIL_OTP`, the OTP
        email is triggered on the signed retry, and the credential must then be
        activated via `POST /auth/credentials/{id}/verify`.
      operationId: createAuthCredential
      parameters:
        - name: Grid-Wallet-Signature
          in: header
          required: false
          description: >-
            Full API-key stamp built over the prior `payloadToSign` with the
            session API keypair of an existing verified authentication
            credential on the target internal account. Required on the signed
            retry.
          schema:
            type: string
          example: >-
            eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
        - name: Request-Id
          in: header
          required: false
          description: >-
            The `requestId` returned in a prior `202` response, echoed back
            exactly on the signed retry so the server can correlate it with the
            issued challenge. Required on the signed retry when registering a
            credential; must be paired with `Grid-Wallet-Signature`.
          schema:
            type: string
          example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AuthCredentialCreateRequestOneOf'
            examples:
              emailOtp:
                summary: Add an email OTP credential
                value:
                  type: EMAIL_OTP
                  accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
              oauth:
                summary: Add an OAuth credential
                value:
                  type: OAUTH
                  accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
                  oidcToken: >-
                    eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q
              passkey:
                summary: Add a passkey credential
                value:
                  type: PASSKEY
                  accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
                  nickname: iPhone Face-ID
                  challenge: ArkQi2yAYHPlgnJNFBlneIwchQdWXBOTrdB-AmMUB21Lx
                  attestation:
                    credentialId: >-
                      AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY
                    clientDataJson: >-
                      eyJjaGFsbGVuZ2UiOiJBcktRaTJ5QVlIUGxnbkpORkJsbmVJd2NoUWRXWEJPVHJkQi1BbU1VQjIxTHgiLCJjbGllbnRFeHRlbnNpb25zIjp7fSwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwczovL2Rldi5kb250bmVlZGEucHciLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0
                    attestationObject: >-
                      o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFPdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KBFAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQHSlyRHIdWleVqO24-6ix7JFWODqDWo_arvEz3Se5EgIFHkcVjZ4F5XDSBreIHsWRilRnKmaaqlqK3V2_4XtYs2pQECAyYgASFYID5PQTZQQg6haZFQWFzqfAOyQ_ENsMH8xxQ4GRiNPsqrIlggU8IVUOV8qpgk_Jh-OTaLuZL52KdX1fTht07X4DiQPow
                    transports:
                      - internal
                      - hybrid
      responses:
        '201':
          description: >-
            Authentication credential created successfully. The body is the
            created `AuthMethod` for all three credential types. For
            `EMAIL_OTP`, the email is the customer email tied to the internal
            account. When the response is for adding EMAIL_OTP back to an
            existing wallet through the signed-retry flow, it also carries
            `otpEncryptionTargetBundle` — the HPKE target bundle the client uses
            to encrypt the OTP attempt on the subsequent `POST
            /auth/credentials/{id}/verify`. First-time EMAIL_OTP wallet
            bootstrap responses may omit that bundle; if it is absent, call
            `POST /auth/credentials/{id}/challenge` for the new credential to
            issue a fresh OTP and receive `otpEncryptionTargetBundle` before
            verifying. For `PASSKEY`, the credential must be authenticated for
            the first time via `POST /auth/credentials/{id}/challenge` followed
            by `POST /auth/credentials/{id}/verify` to produce a session — there
            is no inline authentication challenge on the registration response.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthMethodResponse'
              examples:
                emailOtp:
                  summary: Email OTP credential created
                  value:
                    id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
                    accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
                    type: EMAIL_OTP
                    nickname: example@lightspark.com
                    otpEncryptionTargetBundle: >-
                      '{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}'
                    createdAt: '2026-04-08T15:30:01Z'
                    updatedAt: '2026-04-08T15:30:01Z'
                oauth:
                  summary: OAuth credential created
                  value:
                    id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
                    accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
                    type: OAUTH
                    nickname: example@lightspark.com
                    createdAt: '2026-04-08T15:30:01Z'
                    updatedAt: '2026-04-08T15:30:01Z'
                passkey:
                  summary: Passkey credential created
                  value:
                    id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
                    accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
                    type: PASSKEY
                    nickname: iPhone Face-ID
                    createdAt: '2026-04-08T15:30:01Z'
                    updatedAt: '2026-04-08T15:30:01Z'
        '202':
          description: >-
            Challenge issued. Build an API-key stamp over `payloadToSign` with
            the session API keypair of an existing verified credential on the
            same internal account, then send that full stamp as
            `Grid-Wallet-Signature` and echo `requestId` as `Request-Id` on the
            retry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthSignedRequestChallenge'
              examples:
                emailOtp:
                  summary: Additional email OTP credential challenge
                  value:
                    type: EMAIL_OTP
                    payloadToSign: >-
                      {"organizationId":"org_2m9F...","parameters":{"userEmail":"jane@example.com","userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_UPDATE_USER_EMAIL"}
                    requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
                    expiresAt: '2026-04-08T15:35:00Z'
                oauth:
                  summary: Additional OAuth credential challenge
                  value:
                    type: OAUTH
                    payloadToSign: >-
                      {"organizationId":"org_2m9F...","parameters":{"oauthProviders":[{"oidcToken":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9...","providerName":"Google"}],"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_CREATE_OAUTH_PROVIDERS"}
                    requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
                    expiresAt: '2026-04-08T15:35:00Z'
                passkey:
                  summary: Additional passkey credential challenge
                  value:
                    type: PASSKEY
                    payloadToSign: >-
                      {"organizationId":"org_2m9F...","parameters":{"authenticators":[{"attestation":{"attestationObject":"o2NmbXRk...","clientDataJson":"eyJjaGFsbGVuZ2UiOiJBcktRa...","credentialId":"AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY"},"authenticatorName":"iPhone
                      Face-ID","challenge":"ArkQi2yAYHPlgnJNFBlneIwchQdWXBOTrdB-AmMUB21Lx","transports":["internal","hybrid"]}],"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_CREATE_AUTHENTICATORS_V2"}
                    requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
                    expiresAt: '2026-04-08T15:35:00Z'
        '400':
          description: >-
            Bad request. Returned with `EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS`
            when registering an email OTP credential while one already exists,
            or `PASSKEY_CREDENTIAL_ALREADY_EXISTS` when registering a passkey
            whose WebAuthn credentialId is already attached to the internal
            account, or `INVALID_INPUT` when an OAuth `oidcToken` is malformed
            or has an unsupported issuer.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error400'
        '401':
          description: >-
            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, when the
            `Request-Id` does not match an unexpired pending challenge, or when
            OAuth token authentication fails during credential registration.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error401'
        '404':
          description: Internal account not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error404'
        '500':
          description: Internal service error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error500'
      security:
        - BasicAuth: []
      x-codeSamples:
        - lang: JavaScript
          source: |-
            import LightsparkGrid from '@lightsparkdev/grid';

            const client = new LightsparkGrid({
              username: process.env['GRID_CLIENT_ID'], // This is the default and can be omitted
              password: process.env['GRID_CLIENT_SECRET'], // This is the default and can be omitted
            });

            const authMethodResponse = await client.auth.credentials.create({
              AuthCredentialCreateRequest: {
                accountId: 'InternalAccount:019542f5-b3e7-1d02-0000-000000000002',
                type: 'EMAIL_OTP',
              },
            });

            console.log(authMethodResponse);
        - lang: Python
          source: |-
            import os
            from grid import LightsparkGrid

            client = LightsparkGrid(
                username=os.environ.get("GRID_CLIENT_ID"),  # This is the default and can be omitted
                password=os.environ.get("GRID_CLIENT_SECRET"),  # This is the default and can be omitted
            )
            auth_method_response = client.auth.credentials.create(
                auth_credential_create_request={
                    "account_id": "InternalAccount:019542f5-b3e7-1d02-0000-000000000002",
                    "type": "EMAIL_OTP",
                },
            )
            print(auth_method_response)
        - lang: Go
          source: "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/stainless-sdks/grid-go\"\n\t\"github.com/stainless-sdks/grid-go/option\"\n)\n\nfunc main() {\n\tclient := grid.NewClient(\n\t\toption.WithUsername(\"My Username\"),\n\t\toption.WithPassword(\"My Password\"),\n\t)\n\tauthMethodResponse, err := client.Auth.Credentials.New(context.TODO(), grid.AuthCredentialNewParams{\n\t\tAuthCredentialCreateRequest: grid.AuthCredentialCreateRequestOneOfUnionParam{\n\t\t\tOfEmailOtpCredentialCreateRequest: &grid.EmailOtpCredentialCreateRequestParam{\n\t\t\t\tAuthCredentialCreateRequestParam: grid.AuthCredentialCreateRequestParam{\n\t\t\t\t\tAccountID: \"InternalAccount:019542f5-b3e7-1d02-0000-000000000002\",\n\t\t\t\t\tType:      \"EMAIL_OTP\",\n\t\t\t\t},\n\t\t\t\tType: \"EMAIL_OTP\",\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf(\"%+v\\n\", authMethodResponse)\n}\n"
        - lang: Kotlin
          source: >-
            package com.lightspark.grid.example


            import com.lightspark.grid.client.LightsparkGridClient

            import com.lightspark.grid.client.okhttp.LightsparkGridOkHttpClient

            import com.lightspark.grid.core.JsonValue

            import
            com.lightspark.grid.models.auth.credentials.AuthMethodResponse

            import
            com.lightspark.grid.models.auth.credentials.EmailOtpCredentialCreateRequest


            fun main() {
                val client: LightsparkGridClient = LightsparkGridOkHttpClient.fromEnv()

                val params: EmailOtpCredentialCreateRequest = EmailOtpCredentialCreateRequest.builder()
                    .accountId("InternalAccount:019542f5-b3e7-1d02-0000-000000000002")
                    .type(JsonValue.from("EMAIL_OTP"))
                    .build()
                val authMethodResponse: AuthMethodResponse = client.auth().credentials().create(params)
            }
        - lang: Ruby
          source: >-
            require "grid"


            lightspark_grid = Grid::Client.new(username: "My Username",
            password: "My Password")


            auth_method_response = lightspark_grid.auth.credentials.create(
              auth_credential_create_request: {accountId: "InternalAccount:019542f5-b3e7-1d02-0000-000000000002", type: "EMAIL_OTP"}
            )


            puts(auth_method_response)
        - lang: PHP
          source: |-
            <?php

            require_once dirname(__DIR__) . '/vendor/autoload.php';

            use Grid\Client;
            use Grid\Core\Exceptions\APIException;

            $client = new Client(
              username: getenv('GRID_CLIENT_ID') ?: 'My Username',
              password: getenv('GRID_CLIENT_SECRET') ?: 'My Password',
            );

            try {
              $authMethodResponse = $client->auth->credentials->create(
                authCredentialCreateRequest: [
                  'accountID' => 'InternalAccount:019542f5-b3e7-1d02-0000-000000000002',
                  'type' => 'EMAIL_OTP',
                ],
                gridWalletSignature: 'eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9',
                requestID: 'Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21',
              );

              var_dump($authMethodResponse);
            } catch (APIException $e) {
              echo $e->getMessage();
            }
        - lang: C#
          source: >-
            using System;

            using System.Text.Json;

            using Grid;

            using Grid.Models.Auth.Credentials;


            LightsparkGridClient client = new();


            CredentialCreateParams parameters = new()

            {
                AuthCredentialCreateRequest = new EmailOtpCredentialCreateRequest()
                {
                    AccountID = "InternalAccount:019542f5-b3e7-1d02-0000-000000000002",
                    Type = JsonSerializer.SerializeToElement("EMAIL_OTP"),
                },
            };


            var authMethodResponse = await
            client.Auth.Credentials.Create(parameters);


            Console.WriteLine(authMethodResponse);
        - lang: CLI
          source: |-
            grid auth:credentials create \
              --username 'My Username' \
              --password 'My Password' \
              --auth-credential-create-request '{accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002, type: EMAIL_OTP}'
components:
  schemas:
    AuthCredentialCreateRequestOneOf:
      oneOf:
        - $ref: '#/components/schemas/EmailOtpCredentialCreateRequest'
        - $ref: '#/components/schemas/OauthCredentialCreateRequest'
        - $ref: '#/components/schemas/PasskeyCredentialCreateRequest'
      discriminator:
        propertyName: type
        mapping:
          EMAIL_OTP:
            $ref: '#/components/schemas/EmailOtpCredentialCreateRequest'
          OAUTH:
            $ref: '#/components/schemas/OauthCredentialCreateRequest'
          PASSKEY:
            $ref: '#/components/schemas/PasskeyCredentialCreateRequest'
    AuthMethodResponse:
      title: Auth Method Response
      description: >-
        Strict wrapper around `AuthMethod`. Used directly as the registration
        response on `POST /auth/credentials` (all three credential types) and
        inside `AuthCredentialResponseOneOf` for the `EMAIL_OTP` branch of `POST
        /auth/credentials/{id}/challenge`. The only difference from `AuthMethod`
        is `unevaluatedProperties: false`, which disambiguates the oneOf against
        `PasskeyAuthChallenge` — without the strictness, an `AuthMethod` with
        extra fields would ambiguously match both branches.


        For `EMAIL_OTP` credentials, responses that initiate or reissue an OTP
        challenge carry `otpEncryptionTargetBundle` so the client can
        HPKE-encrypt the OTP code in the subsequent `POST
        /auth/credentials/{id}/verify` call without the plaintext code ever
        transiting the server. First-time EMAIL_OTP wallet bootstrap
        registration can omit it; call `POST /auth/credentials/{id}/challenge`
        if it is absent.
      allOf:
        - $ref: '#/components/schemas/AuthMethod'
        - type: object
          properties:
            otpEncryptionTargetBundle:
              type: string
              description: >-
                HPKE encryption target bundle for a freshly initiated OTP
                challenge. Returned only on `EMAIL_OTP` responses that initiate
                or reissue an OTP challenge, such as `POST
                /auth/credentials/{id}/challenge` and the add-EMAIL_OTP
                signed-retry response. It is omitted from first-time EMAIL_OTP
                wallet bootstrap registration; call `POST
                /auth/credentials/{id}/challenge` for the new credential if it
                is absent. The client generates an ephemeral P-256 keypair (the
                Target Encryption Key, or TEK) and uses this bundle as the
                recipient when HPKE-encrypting `{otp_code, public_key}`; the
                encrypted payload is submitted as `encryptedOtpBundle` on `POST
                /auth/credentials/{id}/verify`. The bundle is one-time-use per
                OTP issuance — re-issue via `POST
                /auth/credentials/{id}/challenge` to obtain a fresh bundle. The
                matching TEK private key must remain on the client and is used
                to sign the `verificationToken` returned on the subsequent
                signed-retry. Treat the bundle as opaque and pass it to your
                HPKE library; the Global Accounts client-keys guide shows how.
              example: >-
                {"version":"v1.0.0","data":"7b227461726765745075626c6963...","dataSignature":"30450221...","enclaveQuorumPublic":"04a1b2c3..."}
      unevaluatedProperties: false
    AuthSignedRequestChallenge:
      title: Authentication Signed Request Challenge
      description: >-
        202 response returned from Embedded Wallet Auth endpoints that require a
        signed retry — `POST /auth/credentials` (adding an additional
        credential), `DELETE /auth/credentials/{id}` (revoking a credential),
        `DELETE /auth/sessions/{id}` (revoking a session), and the `EMAIL_OTP`
        branch of `POST /auth/credentials/{id}/verify` (the secure OTP login
        flow, where the client submits an `encryptedOtpBundle` and receives a
        `verificationToken` to sign for the second-leg session issuance).
        Carries the signing fields from `SignedRequestChallenge` plus the `type`
        of the authentication credential involved (being added, revoked, that
        issued the session being revoked, or being authenticated). The client
        already knows the target resource id from the request path / body it
        just sent, so nothing beyond `type` is echoed in the response.


        The keypair used to compute the stamp depends on the operation. For
        credential / session management retries, sign with the session API
        keypair of an existing verified credential on the same internal account.
        For the `EMAIL_OTP` verify retry, sign with the ephemeral Target
        Encryption Key (TEK) the client generated for this login — its public
        key is the one carried inside the `encryptedOtpBundle` and bound into
        the `verificationToken`, and it becomes the client's session API key on
        successful completion.
      allOf:
        - $ref: '#/components/schemas/SignedRequestChallenge'
        - type: object
          required:
            - type
          properties:
            type:
              $ref: '#/components/schemas/AuthMethodType'
              description: >-
                Credential type relevant to this challenge: the credential type
                being added (`POST /auth/credentials`), revoked (`DELETE
                /auth/credentials/{id}`), or authenticated (`EMAIL_OTP` branch
                of `POST /auth/credentials/{id}/verify`). For session
                revocation, this is the type of credential that issued the
                session (`DELETE /auth/sessions/{id}`).
    Error400:
      type: object
      required:
        - message
        - status
        - code
      properties:
        status:
          type: integer
          enum:
            - 400
          description: HTTP status code
        code:
          type: string
          description: >
            | Error Code | Description |

            |------------|-------------|

            | INVALID_INPUT | Invalid input provided |

            | MISSING_MANDATORY_USER_INFO | Required customer information is
            missing |

            | INVITATION_ALREADY_CLAIMED | Invitation has already been claimed |

            | INVITATIONS_NOT_CONFIGURED | Invitations are not configured |

            | INVALID_UMA_ADDRESS | UMA address format is invalid |

            | INVITATION_CANCELLED | Invitation has been cancelled |

            | QUOTE_REQUEST_FAILED | An issue occurred during the quote process;
            this is retryable |

            | INVALID_PAYREQ_RESPONSE | Counterparty Payreq response was invalid
            |

            | INVALID_RECEIVER | Receiver is invalid |

            | PARSE_PAYREQ_RESPONSE_ERROR | Error parsing receiver PayReq
            response |

            | CERT_CHAIN_INVALID | Counterparty certificate chain is invalid |

            | CERT_CHAIN_EXPIRED | Counterparty certificate chain has expired |

            | INVALID_PUBKEY_FORMAT | Counterparty Public key format is invalid
            |

            | MISSING_REQUIRED_UMA_PARAMETERS | Counterparty required UMA
            parameters are missing |

            | SENDER_NOT_ACCEPTED | Sender is not accepted |

            | AMOUNT_OUT_OF_RANGE | Amount is out of range |

            | INVALID_CURRENCY | Currency is invalid |

            | INVALID_TIMESTAMP | Timestamp is invalid |

            | INVALID_NONCE | Nonce is invalid |

            | INVALID_REQUEST_FORMAT | Request format is invalid |

            | INVALID_BANK_ACCOUNT | Bank account is invalid |

            | SELF_PAYMENT | Self payment not allowed |

            | LOOKUP_REQUEST_FAILED | Lookup request failed |

            | PARSE_LNURLP_RESPONSE_ERROR | Error parsing LNURLP response |

            | INVALID_AMOUNT | Amount is invalid |

            | WEBHOOK_ENDPOINT_NOT_SET | Webhook endpoint is not set |

            | WEBHOOK_DELIVERY_ERROR | Webhook delivery error |

            | LOW_QUALITY | Document quality too low to process |

            | DATA_MISMATCH | Document details don't match provided information
            |

            | EXPIRED | Document has expired |

            | SUSPECTED_FRAUD | Document suspected of being forged or edited |

            | UNSUITABLE_DOCUMENT | Document type is not accepted or not
            supported |

            | INCOMPLETE | Document is missing pages or sides |

            | EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS | An EMAIL_OTP credential is
            already registered on the target internal account; only one email
            OTP credential is supported per internal account at this time |

            | PASSKEY_CREDENTIAL_ALREADY_EXISTS | A PASSKEY credential with the
            same WebAuthn credentialId is already registered on the target
            internal account |
          enum:
            - INVALID_INPUT
            - MISSING_MANDATORY_USER_INFO
            - INVITATION_ALREADY_CLAIMED
            - INVITATIONS_NOT_CONFIGURED
            - INVALID_UMA_ADDRESS
            - INVITATION_CANCELLED
            - QUOTE_REQUEST_FAILED
            - INVALID_PAYREQ_RESPONSE
            - INVALID_RECEIVER
            - PARSE_PAYREQ_RESPONSE_ERROR
            - CERT_CHAIN_INVALID
            - CERT_CHAIN_EXPIRED
            - INVALID_PUBKEY_FORMAT
            - MISSING_REQUIRED_UMA_PARAMETERS
            - SENDER_NOT_ACCEPTED
            - AMOUNT_OUT_OF_RANGE
            - INVALID_CURRENCY
            - INVALID_TIMESTAMP
            - INVALID_NONCE
            - INVALID_REQUEST_FORMAT
            - INVALID_BANK_ACCOUNT
            - SELF_PAYMENT
            - LOOKUP_REQUEST_FAILED
            - PARSE_LNURLP_RESPONSE_ERROR
            - INVALID_AMOUNT
            - WEBHOOK_ENDPOINT_NOT_SET
            - WEBHOOK_DELIVERY_ERROR
            - LOW_QUALITY
            - DATA_MISMATCH
            - EXPIRED
            - SUSPECTED_FRAUD
            - UNSUITABLE_DOCUMENT
            - INCOMPLETE
            - EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS
            - PASSKEY_CREDENTIAL_ALREADY_EXISTS
        message:
          type: string
          description: Error message
        details:
          type: object
          description: Additional error details
          additionalProperties: true
    Error401:
      type: object
      required:
        - message
        - status
        - code
      properties:
        status:
          type: integer
          enum:
            - 401
          description: HTTP status code
        code:
          type: string
          description: >
            | Error Code | Description |

            |------------|-------------|

            | UNAUTHORIZED | Issue with API credentials |

            | INVALID_SIGNATURE | Signature header is invalid |

            | WALLET_SIGNATURE_MISSING | The `Grid-Wallet-Signature` header is
            required for this Embedded Wallet action but was not supplied |

            | WALLET_SIGNATURE_MALFORMED | The `Grid-Wallet-Signature` header
            could not be parsed (bad encoding, structure, or fields) |

            | WALLET_SIGNATURE_BODY_MISMATCH | The `Grid-Wallet-Signature` was
            computed over a different request body than the one received |

            | WALLET_SIGNATURE_INVALID | The `Grid-Wallet-Signature` failed
            cryptographic verification against the registered credential |

            | REQUEST_ID_MISSING | The `Request-Id` header is required on the
            signed retry but was not supplied (paired with
            `Grid-Wallet-Signature`) |
          enum:
            - UNAUTHORIZED
            - INVALID_SIGNATURE
            - WALLET_SIGNATURE_MISSING
            - WALLET_SIGNATURE_MALFORMED
            - WALLET_SIGNATURE_BODY_MISMATCH
            - WALLET_SIGNATURE_INVALID
            - REQUEST_ID_MISSING
        message:
          type: string
          description: Error message
        details:
          type: object
          description: Additional error details
          additionalProperties: true
    Error404:
      type: object
      required:
        - message
        - status
        - code
      properties:
        status:
          type: integer
          enum:
            - 404
          description: HTTP status code
        code:
          type: string
          description: >
            | Error Code | Description |

            |------------|-------------|

            | TRANSACTION_NOT_FOUND | Transaction not found |

            | INVITATION_NOT_FOUND | Invitation not found |

            | USER_NOT_FOUND | Customer not found |

            | QUOTE_NOT_FOUND | Quote not found |

            | LOOKUP_REQUEST_NOT_FOUND | Lookup request not found |

            | TOKEN_NOT_FOUND | Token not found |

            | BULK_UPLOAD_JOB_NOT_FOUND | Bulk upload job not found |

            | REFERENCE_NOT_FOUND | Reference not found |

            | UMA_NOT_FOUND | The UMA address is well-formed but no receiver
            exists at the counterparty VASP |
          enum:
            - TRANSACTION_NOT_FOUND
            - INVITATION_NOT_FOUND
            - USER_NOT_FOUND
            - QUOTE_NOT_FOUND
            - LOOKUP_REQUEST_NOT_FOUND
            - TOKEN_NOT_FOUND
            - BULK_UPLOAD_JOB_NOT_FOUND
            - REFERENCE_NOT_FOUND
            - UMA_NOT_FOUND
        message:
          type: string
          description: Error message
        details:
          type: object
          description: Additional error details
          additionalProperties: true
    Error500:
      type: object
      required:
        - message
        - status
        - code
      properties:
        status:
          type: integer
          enum:
            - 500
          description: HTTP status code
        code:
          type: string
          description: |
            | Error Code | Description |
            |------------|-------------|
            | GRID_SWITCH_ERROR | Grid switch error |
            | INTERNAL_ERROR | Internal server or UMA error |
          enum:
            - GRID_SWITCH_ERROR
            - INTERNAL_ERROR
        message:
          type: string
          description: Error message
        details:
          type: object
          description: Additional error details
          additionalProperties: true
    EmailOtpCredentialCreateRequest:
      title: Email OTP Credential Create Request
      allOf:
        - $ref: '#/components/schemas/AuthCredentialCreateRequest'
        - $ref: '#/components/schemas/EmailOtpCredentialCreateRequestFields'
    OauthCredentialCreateRequest:
      title: OAuth Credential Create Request
      allOf:
        - $ref: '#/components/schemas/AuthCredentialCreateRequest'
        - $ref: '#/components/schemas/OauthCredentialCreateRequestFields'
    PasskeyCredentialCreateRequest:
      title: Passkey Credential Create Request
      allOf:
        - $ref: '#/components/schemas/AuthCredentialCreateRequest'
        - $ref: '#/components/schemas/PasskeyCredentialCreateRequestFields'
    AuthMethod:
      type: object
      required:
        - id
        - accountId
        - type
        - nickname
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          description: >-
            System-generated unique identifier for the authentication
            credential.
          example: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
        accountId:
          type: string
          description: >-
            Identifier of the internal account that this credential
            authenticates.
          example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
        type:
          $ref: '#/components/schemas/AuthMethodType'
        credentialId:
          type: string
          description: >-
            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.
          example: >-
            KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew
        nickname:
          type: string
          description: >-
            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 validated nickname provided at registration
            time.
          example: example@lightspark.com
        createdAt:
          type: string
          format: date-time
          description: Creation timestamp.
          example: '2026-04-08T15:30:01Z'
        updatedAt:
          type: string
          format: date-time
          description: Last update timestamp.
          example: '2026-04-08T15:35:00Z'
    SignedRequestChallenge:
      title: Signed Request Challenge
      type: object
      required:
        - payloadToSign
        - requestId
        - expiresAt
      description: >-
        Common base for two-step signed-retry challenge responses on Embedded
        Wallet endpoints (credential registration or revocation, session refresh
        or revocation, wallet export, customer email updates, and similar).
        Holds the signing fields shared across every challenge shape; each
        variant composes this base via `allOf` and adds its own resource `id`
        (and `type`, when applicable) with variant-specific description and
        example.
      properties:
        payloadToSign:
          type: string
          description: >-
            Canonical payload for the retry authorization stamp. Build an
            API-key stamp over this exact value with the session API keypair,
            then send the full base64url-encoded stamp in
            `Grid-Wallet-Signature` on the retry that completes the original
            request.
          example: >-
            {"organizationId":"org_2m9F...","parameters":{"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_EXAMPLE"}
        requestId:
          type: string
          description: >-
            Grid-issued `Request:<uuid>` identifier for this pending request.
            Echo this value exactly in the `Request-Id` header on the signed
            retry so the server can correlate the retry with the issued
            challenge.
          example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
        expiresAt:
          type: string
          format: date-time
          description: >-
            Timestamp after which this challenge is no longer valid. The signed
            retry must be submitted before this time.
          example: '2026-04-08T15:35:00Z'
    AuthMethodType:
      type: string
      enum:
        - OAUTH
        - EMAIL_OTP
        - PASSKEY
      description: >-
        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.
    AuthCredentialCreateRequest:
      type: object
      required:
        - type
        - accountId
      properties:
        type:
          $ref: '#/components/schemas/AuthMethodType'
        accountId:
          type: string
          description: >-
            Identifier of the internal account that this credential will
            authenticate.
          example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
    EmailOtpCredentialCreateRequestFields:
      type: object
      required:
        - type
      properties:
        type:
          type: string
          enum:
            - EMAIL_OTP
          description: Discriminator value identifying this as an email OTP credential.
    OauthCredentialCreateRequestFields:
      type: object
      required:
        - type
        - oidcToken
      properties:
        type:
          type: string
          enum:
            - OAUTH
          description: Discriminator value identifying this as an OAuth credential.
        oidcToken:
          type: string
          description: >-
            OIDC ID token issued by the identity provider (e.g. Google, Apple).
            The token's `iss`, `aud`, and `sub` claims define the OAuth identity
            registered to this 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 `iat` less than 60 seconds before the
            request timestamp, but the signature segment may be a dummy value.
          example: >-
            eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q
    PasskeyCredentialCreateRequestFields:
      type: object
      required:
        - type
        - nickname
        - challenge
        - attestation
      properties:
        type:
          type: string
          enum:
            - PASSKEY
          description: Discriminator value identifying this as a passkey credential.
        nickname:
          type: string
          description: >-
            Human-readable identifier for the passkey, chosen by the user at
            registration time (e.g. "iPhone Face-ID", "YubiKey 5C"). Leading and
            trailing whitespace is ignored. Must be 1-100 characters and may
            contain Unicode letters, numbers, spaces, and the following
            separators: period, underscore, hyphen, apostrophe, and parentheses.
            Shown back on AuthMethod responses and in credential listings.
          example: iPhone Face-ID
        challenge:
          type: string
          description: >-
            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.
          example: ArkQi2yAYHPlgnJNFBlneIwchQdWXBOTrdB-AmMUB21Lx
        attestation:
          $ref: '#/components/schemas/PasskeyAttestation'
    PasskeyAttestation:
      title: Passkey Attestation
      type: object
      required:
        - credentialId
        - clientDataJson
        - attestationObject
      properties:
        credentialId:
          type: string
          description: >-
            Base64url-encoded credential identifier produced by the
            authenticator at registration time. Typically the base64url of
            `PublicKeyCredential.rawId`.
          example: >-
            AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY
        clientDataJson:
          type: string
          description: >-
            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"`.
          example: >-
            eyJjaGFsbGVuZ2UiOiJBcktRaTJ5QVlIUGxnbkpORkJsbmVJd2NoUWRXWEJPVHJkQi1BbU1VQjIxTHgiLCJjbGllbnRFeHRlbnNpb25zIjp7fSwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwczovL2Rldi5kb250bmVlZGEucHciLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0
        attestationObject:
          type: string
          description: >-
            Base64url-encoded CBOR attestation object produced by the
            authenticator during registration. Corresponds to
            `AuthenticatorAttestationResponse.attestationObject`.
          example: >-
            o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFPdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KBFAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQHSlyRHIdWleVqO24-6ix7JFWODqDWo_arvEz3Se5EgIFHkcVjZ4F5XDSBreIHsWRilRnKmaaqlqK3V2_4XtYs2pQECAyYgASFYID5PQTZQQg6haZFQWFzqfAOyQ_ENsMH8xxQ4GRiNPsqrIlggU8IVUOV8qpgk_Jh-OTaLuZL52KdX1fTht07X4DiQPow
        transports:
          type: array
          items:
            type: string
            enum:
              - usb
              - nfc
              - ble
              - internal
              - hybrid
          description: >-
            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.
          example:
            - internal
            - hybrid
  securitySchemes:
    BasicAuth:
      type: http
      scheme: basic
      description: >-
        API token authentication using format `<api token id>:<api client
        secret>`
    AgentAuth:
      type: http
      scheme: bearer
      description: >-
        Bearer token authentication for agent-scoped endpoints. The token is the
        `accessToken` returned when redeeming a device code via `POST
        /agents/device-codes/{code}/redeem`. Agent credentials are user-scoped:
        all requests are automatically bound to the agent's associated customer
        and subject to the agent's policy.

````