From 9bdfe1fe1520609d15b95f717dd6cff289a1ea80 Mon Sep 17 00:00:00 2001 From: Dhruv Pareek Date: Fri, 17 Apr 2026 16:32:11 -0700 Subject: [PATCH 1/2] feat: add Embedded Wallet Auth endpoints for Email OTP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New `Embedded Wallet Auth` endpoints for registering and verifying end-user authentication credentials. EMAIL_OTP is wired today; OAuth and Passkey will land as additional branches in the discriminated `oneOf`. **Resources defined** - `AuthMethodType` — enum: `OAUTH`, `EMAIL_OTP`, `PASSKEY` - `AuthMethod` — base credential response (id, accountId, type, nickname, timestamps) - `AuthSession` — verified credential (`AuthMethod` via `allOf` + `encryptedSessionSigningKey` + `expiresAt`) - `AuthCredentialCreateRequest` / `AuthCredentialVerifyRequest` — discriminated `oneOf` envelopes keyed on `type`, EMAIL_OTP branch only today - `EmailOtpCredentialCreateRequest` / `EmailOtpCredentialVerifyRequest` — EMAIL_OTP branch schemas **Endpoints defined** - `POST /auth/credentials` — registers a credential and triggers the OTP email; returns an unverified `AuthMethod` (201) - `POST /auth/credentials/{id}/verify` — exchanges the OTP for a session; returns an `AuthSession` (200). `{id}` is the `AuthMethod.id` returned from `POST /auth/credentials` **Request shapes** - Create: `{ type: "EMAIL_OTP", accountId }` - Verify: `{ type: "EMAIL_OTP", otp, clientPublicKey }` **Response shapes** - Create → `AuthMethod` (no session yet) - Verify → `AuthSession` (adds `encryptedSessionSigningKey`, `expiresAt`) **Validation** - Only one `EMAIL_OTP` credential is supported per internal account for now; duplicate registration returns `400 EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS`. **Implementation notes** - Request bodies modeled as discriminated `oneOf` on `type` from day one so OAuth/Passkey arrive as additive branch files — no refactor of the EMAIL_OTP code paths - Split `AuthMethod` (unverified) and `AuthSession` (verified, `allOf` extension) rather than a single `AuthMethod` with optional `encryptedSessionSigningKey` / `expiresAt` — the "verify always issues a session" contract is enforced by the schema rather than runtime checks on optional fields - Security: `BasicAuth` on both — platform calls or forwards on behalf of the user - New `Embedded Wallet Auth` tag added to `openapi/openapi.yaml`; bundled `openapi.yaml` and `mintlify/openapi.yaml` regenerated via `make build` **Next in stack** - `POST /auth/credentials/{id}/challenge` (resend OTP) — follow-up PR --- .stainless/stainless.yml | 48 +++ mintlify/openapi.yaml | 344 ++++++++++++++++++ openapi.yaml | 344 ++++++++++++++++++ .../AuthCredentialAdditionalChallenge.yaml | 33 ++ ...uthCredentialAdditionalChallengeOneOf.yaml | 6 + .../auth/AuthCredentialCreateRequest.yaml | 12 + .../AuthCredentialCreateRequestOneOf.yaml | 6 + .../auth/AuthCredentialVerifyRequest.yaml | 6 + .../AuthCredentialVerifyRequestOneOf.yaml | 6 + .../components/schemas/auth/AuthMethod.yaml | 37 ++ .../schemas/auth/AuthMethodType.yaml | 14 + .../components/schemas/auth/AuthSession.yaml | 23 ++ ...EmailOtpCredentialAdditionalChallenge.yaml | 4 + ...tpCredentialAdditionalChallengeFields.yaml | 20 + .../auth/EmailOtpCredentialCreateRequest.yaml | 4 + ...EmailOtpCredentialCreateRequestFields.yaml | 9 + .../auth/EmailOtpCredentialVerifyRequest.yaml | 4 + ...EmailOtpCredentialVerifyRequestFields.yaml | 25 ++ .../components/schemas/errors/Error400.yaml | 2 + openapi/openapi.yaml | 9 + openapi/paths/auth/auth_credentials.yaml | 138 +++++++ .../auth/auth_credentials_{id}_verify.yaml | 71 ++++ 22 files changed, 1165 insertions(+) create mode 100644 openapi/components/schemas/auth/AuthCredentialAdditionalChallenge.yaml create mode 100644 openapi/components/schemas/auth/AuthCredentialAdditionalChallengeOneOf.yaml create mode 100644 openapi/components/schemas/auth/AuthCredentialCreateRequest.yaml create mode 100644 openapi/components/schemas/auth/AuthCredentialCreateRequestOneOf.yaml create mode 100644 openapi/components/schemas/auth/AuthCredentialVerifyRequest.yaml create mode 100644 openapi/components/schemas/auth/AuthCredentialVerifyRequestOneOf.yaml create mode 100644 openapi/components/schemas/auth/AuthMethod.yaml create mode 100644 openapi/components/schemas/auth/AuthMethodType.yaml create mode 100644 openapi/components/schemas/auth/AuthSession.yaml create mode 100644 openapi/components/schemas/auth/EmailOtpCredentialAdditionalChallenge.yaml create mode 100644 openapi/components/schemas/auth/EmailOtpCredentialAdditionalChallengeFields.yaml create mode 100644 openapi/components/schemas/auth/EmailOtpCredentialCreateRequest.yaml create mode 100644 openapi/components/schemas/auth/EmailOtpCredentialCreateRequestFields.yaml create mode 100644 openapi/components/schemas/auth/EmailOtpCredentialVerifyRequest.yaml create mode 100644 openapi/components/schemas/auth/EmailOtpCredentialVerifyRequestFields.yaml create mode 100644 openapi/paths/auth/auth_credentials.yaml create mode 100644 openapi/paths/auth/auth_credentials_{id}_verify.yaml diff --git a/.stainless/stainless.yml b/.stainless/stainless.yml index 74641ef5..aea5328d 100644 --- a/.stainless/stainless.yml +++ b/.stainless/stainless.yml @@ -320,6 +320,29 @@ resources: list: get /tokens retrieve: get /tokens/{tokenId} delete: delete /tokens/{tokenId} + + auth: + subresources: + credentials: + methods: + create: post /auth/credentials + verify: post /auth/credentials/{id}/verify + models: + auth_method_type: '#/components/schemas/AuthMethodType' + auth_method: '#/components/schemas/AuthMethod' + auth_session: '#/components/schemas/AuthSession' + auth_credential_create_request: '#/components/schemas/AuthCredentialCreateRequest' + auth_credential_verify_request: '#/components/schemas/AuthCredentialVerifyRequest' + auth_credential_create_request_one_of: '#/components/schemas/AuthCredentialCreateRequestOneOf' + auth_credential_verify_request_one_of: '#/components/schemas/AuthCredentialVerifyRequestOneOf' + auth_credential_additional_challenge: '#/components/schemas/AuthCredentialAdditionalChallenge' + auth_credential_additional_challenge_one_of: '#/components/schemas/AuthCredentialAdditionalChallengeOneOf' + email_otp_credential_create_request: '#/components/schemas/EmailOtpCredentialCreateRequest' + email_otp_credential_verify_request: '#/components/schemas/EmailOtpCredentialVerifyRequest' + email_otp_credential_create_request_fields: '#/components/schemas/EmailOtpCredentialCreateRequestFields' + email_otp_credential_verify_request_fields: '#/components/schemas/EmailOtpCredentialVerifyRequestFields' + email_otp_credential_additional_challenge: '#/components/schemas/EmailOtpCredentialAdditionalChallenge' + email_otp_credential_additional_challenge_fields: '#/components/schemas/EmailOtpCredentialAdditionalChallengeFields' exchange_rates: methods: list: @@ -807,6 +830,31 @@ openapi: - "$.components.schemas.ExternalAccountDetailsTransactionDestination.allOf[0]" keys: [ "$ref" ] + # ── type: auth credential base schemas ── + - command: remove + reason: >- + Remove type $ref from auth credential base schemas so the inline + single-value enums in each *CredentialCreateRequestFields / + *CredentialVerifyRequestFields / *CredentialAdditionalChallengeFields + variant become the sole definition, avoiding allOf type conflicts + args: + target: + - "$.components.schemas.AuthCredentialCreateRequest.properties" + - "$.components.schemas.AuthCredentialVerifyRequest.properties" + - "$.components.schemas.AuthCredentialAdditionalChallenge.properties" + keys: [ "type" ] + + # ── Remove $ref to AuthCredentialVerifyRequest from verify variants ── + - command: remove + reason: >- + Remove allOf $ref to AuthCredentialVerifyRequest from verify variants + because the base schema becomes an empty object after stripping the + type discriminator (no other shared fields on verify) + args: + target: + - "$.components.schemas.EmailOtpCredentialVerifyRequest.allOf[0]" + keys: [ "$ref" ] + codeflow: detect_breaking_changes: true release_environment: npm diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 91b22fea..2f5e4563 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -46,6 +46,8 @@ tags: 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. paths: /config: get: @@ -3454,6 +3456,160 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /auth/credentials: + post: + summary: Create an authentication credential + description: | + 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. + + **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`. + operationId: createAuthCredential + tags: + - Embedded Wallet Auth + security: + - BasicAuth: [] + parameters: + - name: Grid-Wallet-Signature + in: header + required: false + description: Signature over the `payloadToSign` returned in a prior `202` response, produced with the session private key of an existing verified authentication credential on the target internal account and base64-encoded. Required when registering an additional credential on an internal account that already has one; ignored when the internal account has no existing credentials. + schema: + type: string + example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE= + - name: Request-Id + in: header + required: false + description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry when registering an additional credential; must be paired with `Grid-Wallet-Signature`. + schema: + type: string + example: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AuthCredentialCreateRequestOneOf' + examples: + emailOtp: + summary: Register an email OTP credential + value: + type: EMAIL_OTP + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + responses: + '201': + description: Authentication credential created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AuthMethod' + '202': + description: 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. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthCredentialAdditionalChallengeOneOf' + examples: + emailOtp: + summary: Additional email OTP credential challenge + value: + type: EMAIL_OTP + email: example@lightspark.com + payloadToSign: Y2hhbGxlbmdlLXBheWxvYWQtdG8tc2lnbg== + requestId: 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 on an internal account that already has one — only one email OTP credential is supported per internal account at this time. + 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, or when the `Request-Id` does not match an unexpired pending challenge. + 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' + /auth/credentials/{id}/verify: + post: + summary: Verify an authentication credential + description: | + Complete the verification step for a previously created authentication credential and issue a session signing key. + + For `EMAIL_OTP` credentials, supply the one-time password that was emailed to the user along with a client-generated public key. On success, the response contains an `encryptedSessionSigningKey` that is encrypted to the supplied `clientPublicKey`, along with an `expiresAt` timestamp marking when the session expires. The `clientPublicKey` is ephemeral and one-time-use per verification request. + operationId: verifyAuthCredential + tags: + - Embedded Wallet Auth + security: + - BasicAuth: [] + parameters: + - name: id + in: path + description: The id of the authentication credential to verify (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`). + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AuthCredentialVerifyRequestOneOf' + examples: + emailOtp: + summary: Verify an email OTP credential + value: + type: EMAIL_OTP + otp: '123456' + clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + responses: + '200': + description: Authentication credential verified and session issued + content: + application/json: + schema: + $ref: '#/components/schemas/AuthSession' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized - invalid or expired OTP + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Authentication credential not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' webhooks: incoming-payment: post: @@ -4555,6 +4711,7 @@ components: | 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 | enum: - INVALID_INPUT - MISSING_MANDATORY_USER_INFO @@ -4589,6 +4746,7 @@ components: - SUSPECTED_FRAUD - UNSUITABLE_DOCUMENT - INCOMPLETE + - EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS message: type: string description: Error message @@ -12631,6 +12789,192 @@ components: description: A list of permissions to grant to the token items: $ref: '#/components/schemas/Permission' + 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. + EmailOtpCredentialCreateRequest: + title: Email OTP Credential Create Request + allOf: + - $ref: '#/components/schemas/AuthCredentialCreateRequest' + - $ref: '#/components/schemas/EmailOtpCredentialCreateRequestFields' + AuthCredentialCreateRequestOneOf: + oneOf: + - $ref: '#/components/schemas/EmailOtpCredentialCreateRequest' + discriminator: + propertyName: type + mapping: + EMAIL_OTP: '#/components/schemas/EmailOtpCredentialCreateRequest' + 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' + 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 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' + AuthCredentialAdditionalChallenge: + type: object + required: + - type + - payloadToSign + - requestId + - expiresAt + properties: + type: + $ref: '#/components/schemas/AuthMethodType' + payloadToSign: + type: string + description: Payload that must be signed with the session private key of an existing verified authentication credential on the internal account. The resulting signature is passed as the `Grid-Wallet-Signature` header on the retry of `POST /auth/credentials` to complete registration of the additional credential. + example: Y2hhbGxlbmdlLXBheWxvYWQtdG8tc2lnbg== + requestId: + type: string + description: Unique identifier for this additional-credential registration request. Must be echoed in the `Request-Id` header on the signed retry of `POST /auth/credentials` so the server can correlate the retry with the issued challenge. + example: 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' + EmailOtpCredentialAdditionalChallengeFields: + type: object + required: + - type + - email + properties: + type: + type: string + enum: + - EMAIL_OTP + description: Discriminator value identifying this as an additional-credential challenge for an email OTP credential. + email: + type: string + format: email + description: Email address associated with the internal account's customer record, returned here so the client knows which mailbox will receive the OTP on the signed retry. + example: example@lightspark.com + EmailOtpCredentialAdditionalChallenge: + title: Email OTP Credential Additional Challenge + allOf: + - $ref: '#/components/schemas/AuthCredentialAdditionalChallenge' + - $ref: '#/components/schemas/EmailOtpCredentialAdditionalChallengeFields' + AuthCredentialAdditionalChallengeOneOf: + oneOf: + - $ref: '#/components/schemas/EmailOtpCredentialAdditionalChallenge' + discriminator: + propertyName: type + mapping: + EMAIL_OTP: '#/components/schemas/EmailOtpCredentialAdditionalChallenge' + AuthCredentialVerifyRequest: + type: object + required: + - type + properties: + type: + $ref: '#/components/schemas/AuthMethodType' + EmailOtpCredentialVerifyRequestFields: + type: object + required: + - type + - otp + - clientPublicKey + properties: + type: + type: string + enum: + - EMAIL_OTP + description: Discriminator value identifying this as an email OTP verification. + otp: + type: string + description: The one-time password received by the user via email. + example: '123456' + 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 + EmailOtpCredentialVerifyRequest: + title: Email OTP Credential Verify Request + allOf: + - $ref: '#/components/schemas/AuthCredentialVerifyRequest' + - $ref: '#/components/schemas/EmailOtpCredentialVerifyRequestFields' + AuthCredentialVerifyRequestOneOf: + oneOf: + - $ref: '#/components/schemas/EmailOtpCredentialVerifyRequest' + discriminator: + propertyName: type + mapping: + EMAIL_OTP: '#/components/schemas/EmailOtpCredentialVerifyRequest' + AuthSession: + allOf: + - $ref: '#/components/schemas/AuthMethod' + - type: object + required: + - encryptedSessionSigningKey + - expiresAt + properties: + encryptedSessionSigningKey: + type: string + description: Session signing key encrypted to the `clientPublicKey` supplied when the credential was created. The client decrypts this key with its private key and uses it to sign subsequent Embedded Wallet requests until `expiresAt`. + example: '-----BEGIN ENCRYPTED PRIVATE KEY----- MIIBvTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIabc... -----END ENCRYPTED PRIVATE KEY-----' + expiresAt: + type: string + format: date-time + description: Timestamp after which the session signing key is no longer valid. + example: '2026-04-08T15:35:00Z' WebhookType: type: string enum: diff --git a/openapi.yaml b/openapi.yaml index 91b22fea..2f5e4563 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -46,6 +46,8 @@ tags: 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. paths: /config: get: @@ -3454,6 +3456,160 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /auth/credentials: + post: + summary: Create an authentication credential + description: | + 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. + + **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`. + operationId: createAuthCredential + tags: + - Embedded Wallet Auth + security: + - BasicAuth: [] + parameters: + - name: Grid-Wallet-Signature + in: header + required: false + description: Signature over the `payloadToSign` returned in a prior `202` response, produced with the session private key of an existing verified authentication credential on the target internal account and base64-encoded. Required when registering an additional credential on an internal account that already has one; ignored when the internal account has no existing credentials. + schema: + type: string + example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE= + - name: Request-Id + in: header + required: false + description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry when registering an additional credential; must be paired with `Grid-Wallet-Signature`. + schema: + type: string + example: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AuthCredentialCreateRequestOneOf' + examples: + emailOtp: + summary: Register an email OTP credential + value: + type: EMAIL_OTP + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + responses: + '201': + description: Authentication credential created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AuthMethod' + '202': + description: 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. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthCredentialAdditionalChallengeOneOf' + examples: + emailOtp: + summary: Additional email OTP credential challenge + value: + type: EMAIL_OTP + email: example@lightspark.com + payloadToSign: Y2hhbGxlbmdlLXBheWxvYWQtdG8tc2lnbg== + requestId: 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 on an internal account that already has one — only one email OTP credential is supported per internal account at this time. + 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, or when the `Request-Id` does not match an unexpired pending challenge. + 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' + /auth/credentials/{id}/verify: + post: + summary: Verify an authentication credential + description: | + Complete the verification step for a previously created authentication credential and issue a session signing key. + + For `EMAIL_OTP` credentials, supply the one-time password that was emailed to the user along with a client-generated public key. On success, the response contains an `encryptedSessionSigningKey` that is encrypted to the supplied `clientPublicKey`, along with an `expiresAt` timestamp marking when the session expires. The `clientPublicKey` is ephemeral and one-time-use per verification request. + operationId: verifyAuthCredential + tags: + - Embedded Wallet Auth + security: + - BasicAuth: [] + parameters: + - name: id + in: path + description: The id of the authentication credential to verify (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`). + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AuthCredentialVerifyRequestOneOf' + examples: + emailOtp: + summary: Verify an email OTP credential + value: + type: EMAIL_OTP + otp: '123456' + clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + responses: + '200': + description: Authentication credential verified and session issued + content: + application/json: + schema: + $ref: '#/components/schemas/AuthSession' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized - invalid or expired OTP + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Authentication credential not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' webhooks: incoming-payment: post: @@ -4555,6 +4711,7 @@ components: | 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 | enum: - INVALID_INPUT - MISSING_MANDATORY_USER_INFO @@ -4589,6 +4746,7 @@ components: - SUSPECTED_FRAUD - UNSUITABLE_DOCUMENT - INCOMPLETE + - EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS message: type: string description: Error message @@ -12631,6 +12789,192 @@ components: description: A list of permissions to grant to the token items: $ref: '#/components/schemas/Permission' + 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. + EmailOtpCredentialCreateRequest: + title: Email OTP Credential Create Request + allOf: + - $ref: '#/components/schemas/AuthCredentialCreateRequest' + - $ref: '#/components/schemas/EmailOtpCredentialCreateRequestFields' + AuthCredentialCreateRequestOneOf: + oneOf: + - $ref: '#/components/schemas/EmailOtpCredentialCreateRequest' + discriminator: + propertyName: type + mapping: + EMAIL_OTP: '#/components/schemas/EmailOtpCredentialCreateRequest' + 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' + 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 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' + AuthCredentialAdditionalChallenge: + type: object + required: + - type + - payloadToSign + - requestId + - expiresAt + properties: + type: + $ref: '#/components/schemas/AuthMethodType' + payloadToSign: + type: string + description: Payload that must be signed with the session private key of an existing verified authentication credential on the internal account. The resulting signature is passed as the `Grid-Wallet-Signature` header on the retry of `POST /auth/credentials` to complete registration of the additional credential. + example: Y2hhbGxlbmdlLXBheWxvYWQtdG8tc2lnbg== + requestId: + type: string + description: Unique identifier for this additional-credential registration request. Must be echoed in the `Request-Id` header on the signed retry of `POST /auth/credentials` so the server can correlate the retry with the issued challenge. + example: 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' + EmailOtpCredentialAdditionalChallengeFields: + type: object + required: + - type + - email + properties: + type: + type: string + enum: + - EMAIL_OTP + description: Discriminator value identifying this as an additional-credential challenge for an email OTP credential. + email: + type: string + format: email + description: Email address associated with the internal account's customer record, returned here so the client knows which mailbox will receive the OTP on the signed retry. + example: example@lightspark.com + EmailOtpCredentialAdditionalChallenge: + title: Email OTP Credential Additional Challenge + allOf: + - $ref: '#/components/schemas/AuthCredentialAdditionalChallenge' + - $ref: '#/components/schemas/EmailOtpCredentialAdditionalChallengeFields' + AuthCredentialAdditionalChallengeOneOf: + oneOf: + - $ref: '#/components/schemas/EmailOtpCredentialAdditionalChallenge' + discriminator: + propertyName: type + mapping: + EMAIL_OTP: '#/components/schemas/EmailOtpCredentialAdditionalChallenge' + AuthCredentialVerifyRequest: + type: object + required: + - type + properties: + type: + $ref: '#/components/schemas/AuthMethodType' + EmailOtpCredentialVerifyRequestFields: + type: object + required: + - type + - otp + - clientPublicKey + properties: + type: + type: string + enum: + - EMAIL_OTP + description: Discriminator value identifying this as an email OTP verification. + otp: + type: string + description: The one-time password received by the user via email. + example: '123456' + 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 + EmailOtpCredentialVerifyRequest: + title: Email OTP Credential Verify Request + allOf: + - $ref: '#/components/schemas/AuthCredentialVerifyRequest' + - $ref: '#/components/schemas/EmailOtpCredentialVerifyRequestFields' + AuthCredentialVerifyRequestOneOf: + oneOf: + - $ref: '#/components/schemas/EmailOtpCredentialVerifyRequest' + discriminator: + propertyName: type + mapping: + EMAIL_OTP: '#/components/schemas/EmailOtpCredentialVerifyRequest' + AuthSession: + allOf: + - $ref: '#/components/schemas/AuthMethod' + - type: object + required: + - encryptedSessionSigningKey + - expiresAt + properties: + encryptedSessionSigningKey: + type: string + description: Session signing key encrypted to the `clientPublicKey` supplied when the credential was created. The client decrypts this key with its private key and uses it to sign subsequent Embedded Wallet requests until `expiresAt`. + example: '-----BEGIN ENCRYPTED PRIVATE KEY----- MIIBvTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIabc... -----END ENCRYPTED PRIVATE KEY-----' + expiresAt: + type: string + format: date-time + description: Timestamp after which the session signing key is no longer valid. + example: '2026-04-08T15:35:00Z' WebhookType: type: string enum: diff --git a/openapi/components/schemas/auth/AuthCredentialAdditionalChallenge.yaml b/openapi/components/schemas/auth/AuthCredentialAdditionalChallenge.yaml new file mode 100644 index 00000000..f5a5eb03 --- /dev/null +++ b/openapi/components/schemas/auth/AuthCredentialAdditionalChallenge.yaml @@ -0,0 +1,33 @@ +type: object +required: + - type + - payloadToSign + - requestId + - expiresAt +properties: + type: + $ref: ./AuthMethodType.yaml + payloadToSign: + type: string + description: >- + Payload that must be signed with the session private key of an existing + verified authentication credential on the internal account. The resulting + signature is passed as the `Grid-Wallet-Signature` header on the retry of + `POST /auth/credentials` to complete registration of the additional + credential. + example: Y2hhbGxlbmdlLXBheWxvYWQtdG8tc2lnbg== + requestId: + type: string + description: >- + Unique identifier for this additional-credential registration request. + Must be echoed in the `Request-Id` header on the signed retry of + `POST /auth/credentials` so the server can correlate the retry with the + issued challenge. + example: 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' diff --git a/openapi/components/schemas/auth/AuthCredentialAdditionalChallengeOneOf.yaml b/openapi/components/schemas/auth/AuthCredentialAdditionalChallengeOneOf.yaml new file mode 100644 index 00000000..717a103b --- /dev/null +++ b/openapi/components/schemas/auth/AuthCredentialAdditionalChallengeOneOf.yaml @@ -0,0 +1,6 @@ +oneOf: + - $ref: ./EmailOtpCredentialAdditionalChallenge.yaml +discriminator: + propertyName: type + mapping: + EMAIL_OTP: ./EmailOtpCredentialAdditionalChallenge.yaml diff --git a/openapi/components/schemas/auth/AuthCredentialCreateRequest.yaml b/openapi/components/schemas/auth/AuthCredentialCreateRequest.yaml new file mode 100644 index 00000000..265a0685 --- /dev/null +++ b/openapi/components/schemas/auth/AuthCredentialCreateRequest.yaml @@ -0,0 +1,12 @@ +type: object +required: + - type + - accountId +properties: + type: + $ref: ./AuthMethodType.yaml + accountId: + type: string + description: >- + Identifier of the internal account that this credential will authenticate. + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 diff --git a/openapi/components/schemas/auth/AuthCredentialCreateRequestOneOf.yaml b/openapi/components/schemas/auth/AuthCredentialCreateRequestOneOf.yaml new file mode 100644 index 00000000..b0d9bef1 --- /dev/null +++ b/openapi/components/schemas/auth/AuthCredentialCreateRequestOneOf.yaml @@ -0,0 +1,6 @@ +oneOf: + - $ref: ./EmailOtpCredentialCreateRequest.yaml +discriminator: + propertyName: type + mapping: + EMAIL_OTP: ./EmailOtpCredentialCreateRequest.yaml diff --git a/openapi/components/schemas/auth/AuthCredentialVerifyRequest.yaml b/openapi/components/schemas/auth/AuthCredentialVerifyRequest.yaml new file mode 100644 index 00000000..72e8558b --- /dev/null +++ b/openapi/components/schemas/auth/AuthCredentialVerifyRequest.yaml @@ -0,0 +1,6 @@ +type: object +required: + - type +properties: + type: + $ref: ./AuthMethodType.yaml diff --git a/openapi/components/schemas/auth/AuthCredentialVerifyRequestOneOf.yaml b/openapi/components/schemas/auth/AuthCredentialVerifyRequestOneOf.yaml new file mode 100644 index 00000000..03eee7dd --- /dev/null +++ b/openapi/components/schemas/auth/AuthCredentialVerifyRequestOneOf.yaml @@ -0,0 +1,6 @@ +oneOf: + - $ref: ./EmailOtpCredentialVerifyRequest.yaml +discriminator: + propertyName: type + mapping: + EMAIL_OTP: ./EmailOtpCredentialVerifyRequest.yaml diff --git a/openapi/components/schemas/auth/AuthMethod.yaml b/openapi/components/schemas/auth/AuthMethod.yaml new file mode 100644 index 00000000..e3534628 --- /dev/null +++ b/openapi/components/schemas/auth/AuthMethod.yaml @@ -0,0 +1,37 @@ +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: ./AuthMethodType.yaml + 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 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' diff --git a/openapi/components/schemas/auth/AuthMethodType.yaml b/openapi/components/schemas/auth/AuthMethodType.yaml new file mode 100644 index 00000000..05504aff --- /dev/null +++ b/openapi/components/schemas/auth/AuthMethodType.yaml @@ -0,0 +1,14 @@ +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. diff --git a/openapi/components/schemas/auth/AuthSession.yaml b/openapi/components/schemas/auth/AuthSession.yaml new file mode 100644 index 00000000..1fdad37d --- /dev/null +++ b/openapi/components/schemas/auth/AuthSession.yaml @@ -0,0 +1,23 @@ +allOf: + - $ref: ./AuthMethod.yaml + - type: object + required: + - encryptedSessionSigningKey + - expiresAt + properties: + encryptedSessionSigningKey: + type: string + description: >- + Session signing key encrypted to the `clientPublicKey` supplied when + the credential was created. The client decrypts this key with its + private key and uses it to sign subsequent Embedded Wallet requests + until `expiresAt`. + example: >- + -----BEGIN ENCRYPTED PRIVATE KEY----- + MIIBvTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIabc... + -----END ENCRYPTED PRIVATE KEY----- + expiresAt: + type: string + format: date-time + description: Timestamp after which the session signing key is no longer valid. + example: '2026-04-08T15:35:00Z' diff --git a/openapi/components/schemas/auth/EmailOtpCredentialAdditionalChallenge.yaml b/openapi/components/schemas/auth/EmailOtpCredentialAdditionalChallenge.yaml new file mode 100644 index 00000000..6ad4091a --- /dev/null +++ b/openapi/components/schemas/auth/EmailOtpCredentialAdditionalChallenge.yaml @@ -0,0 +1,4 @@ +title: Email OTP Credential Additional Challenge +allOf: + - $ref: ./AuthCredentialAdditionalChallenge.yaml + - $ref: ./EmailOtpCredentialAdditionalChallengeFields.yaml diff --git a/openapi/components/schemas/auth/EmailOtpCredentialAdditionalChallengeFields.yaml b/openapi/components/schemas/auth/EmailOtpCredentialAdditionalChallengeFields.yaml new file mode 100644 index 00000000..4de4257c --- /dev/null +++ b/openapi/components/schemas/auth/EmailOtpCredentialAdditionalChallengeFields.yaml @@ -0,0 +1,20 @@ +type: object +required: + - type + - email +properties: + type: + type: string + enum: + - EMAIL_OTP + description: >- + Discriminator value identifying this as an additional-credential + challenge for an email OTP credential. + email: + type: string + format: email + description: >- + Email address associated with the internal account's customer record, + returned here so the client knows which mailbox will receive the OTP on + the signed retry. + example: example@lightspark.com diff --git a/openapi/components/schemas/auth/EmailOtpCredentialCreateRequest.yaml b/openapi/components/schemas/auth/EmailOtpCredentialCreateRequest.yaml new file mode 100644 index 00000000..efbac75b --- /dev/null +++ b/openapi/components/schemas/auth/EmailOtpCredentialCreateRequest.yaml @@ -0,0 +1,4 @@ +title: Email OTP Credential Create Request +allOf: + - $ref: ./AuthCredentialCreateRequest.yaml + - $ref: ./EmailOtpCredentialCreateRequestFields.yaml diff --git a/openapi/components/schemas/auth/EmailOtpCredentialCreateRequestFields.yaml b/openapi/components/schemas/auth/EmailOtpCredentialCreateRequestFields.yaml new file mode 100644 index 00000000..5e27decf --- /dev/null +++ b/openapi/components/schemas/auth/EmailOtpCredentialCreateRequestFields.yaml @@ -0,0 +1,9 @@ +type: object +required: + - type +properties: + type: + type: string + enum: + - EMAIL_OTP + description: Discriminator value identifying this as an email OTP credential. diff --git a/openapi/components/schemas/auth/EmailOtpCredentialVerifyRequest.yaml b/openapi/components/schemas/auth/EmailOtpCredentialVerifyRequest.yaml new file mode 100644 index 00000000..40d6b601 --- /dev/null +++ b/openapi/components/schemas/auth/EmailOtpCredentialVerifyRequest.yaml @@ -0,0 +1,4 @@ +title: Email OTP Credential Verify Request +allOf: + - $ref: ./AuthCredentialVerifyRequest.yaml + - $ref: ./EmailOtpCredentialVerifyRequestFields.yaml diff --git a/openapi/components/schemas/auth/EmailOtpCredentialVerifyRequestFields.yaml b/openapi/components/schemas/auth/EmailOtpCredentialVerifyRequestFields.yaml new file mode 100644 index 00000000..cf853bf0 --- /dev/null +++ b/openapi/components/schemas/auth/EmailOtpCredentialVerifyRequestFields.yaml @@ -0,0 +1,25 @@ +type: object +required: + - type + - otp + - clientPublicKey +properties: + type: + type: string + enum: + - EMAIL_OTP + description: Discriminator value identifying this as an email OTP verification. + otp: + type: string + description: The one-time password received by the user via email. + example: '123456' + 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 diff --git a/openapi/components/schemas/errors/Error400.yaml b/openapi/components/schemas/errors/Error400.yaml index bfea1a23..5b0f2e25 100644 --- a/openapi/components/schemas/errors/Error400.yaml +++ b/openapi/components/schemas/errors/Error400.yaml @@ -47,6 +47,7 @@ properties: | 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 | enum: - INVALID_INPUT - MISSING_MANDATORY_USER_INFO @@ -81,6 +82,7 @@ properties: - SUSPECTED_FRAUD - UNSUITABLE_DOCUMENT - INCOMPLETE + - EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS message: type: string description: Error message diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 113747c9..9faef3dc 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -52,6 +52,11 @@ tags: 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. servers: - url: https://api.lightspark.com/grid/2025-10-13 description: Production server @@ -167,6 +172,10 @@ paths: $ref: paths/tokens/tokens.yaml /tokens/{tokenId}: $ref: paths/tokens/tokens_{tokenId}.yaml + /auth/credentials: + $ref: paths/auth/auth_credentials.yaml + /auth/credentials/{id}/verify: + $ref: paths/auth/auth_credentials_{id}_verify.yaml webhooks: incoming-payment: $ref: webhooks/incoming-payment.yaml diff --git a/openapi/paths/auth/auth_credentials.yaml b/openapi/paths/auth/auth_credentials.yaml new file mode 100644 index 00000000..a936225a --- /dev/null +++ b/openapi/paths/auth/auth_credentials.yaml @@ -0,0 +1,138 @@ +post: + summary: Create an authentication credential + description: > + 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. + + + **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`. + operationId: createAuthCredential + tags: + - Embedded Wallet Auth + security: + - BasicAuth: [] + parameters: + - name: Grid-Wallet-Signature + in: header + required: false + description: >- + Signature over the `payloadToSign` returned in a prior `202` + response, produced with the session private key of an existing + verified authentication credential on the target internal account + and base64-encoded. Required when registering an additional + credential on an internal account that already has one; ignored + when the internal account has no existing credentials. + schema: + type: string + example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE= + - name: Request-Id + in: header + required: false + description: >- + The `requestId` returned in a prior `202` response, echoed back on + the signed retry so the server can correlate it with the issued + challenge. Required on the signed retry when registering an + additional credential; must be paired with `Grid-Wallet-Signature`. + schema: + type: string + example: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/auth/AuthCredentialCreateRequestOneOf.yaml + examples: + emailOtp: + summary: Register an email OTP credential + value: + type: EMAIL_OTP + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + responses: + '201': + description: Authentication credential created successfully + content: + application/json: + schema: + $ref: ../../components/schemas/auth/AuthMethod.yaml + '202': + description: >- + 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. + content: + application/json: + schema: + $ref: ../../components/schemas/auth/AuthCredentialAdditionalChallengeOneOf.yaml + examples: + emailOtp: + summary: Additional email OTP credential challenge + value: + type: EMAIL_OTP + email: example@lightspark.com + payloadToSign: Y2hhbGxlbmdlLXBheWxvYWQtdG8tc2lnbg== + requestId: 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 on an internal account that + already has one — only one email OTP credential is supported per + internal account at this time. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '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, or when + the `Request-Id` does not match an unexpired pending challenge. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Internal account not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '500': + description: Internal service error + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error500.yaml diff --git a/openapi/paths/auth/auth_credentials_{id}_verify.yaml b/openapi/paths/auth/auth_credentials_{id}_verify.yaml new file mode 100644 index 00000000..f361e7df --- /dev/null +++ b/openapi/paths/auth/auth_credentials_{id}_verify.yaml @@ -0,0 +1,71 @@ +post: + summary: Verify an authentication credential + description: > + Complete the verification step for a previously created authentication + credential and issue a session signing key. + + + For `EMAIL_OTP` credentials, supply the one-time password that was + emailed to the user along with a client-generated public key. On + success, the response contains an `encryptedSessionSigningKey` that is + encrypted to the supplied `clientPublicKey`, along with an `expiresAt` + timestamp marking when the session expires. The `clientPublicKey` is + ephemeral and one-time-use per verification request. + operationId: verifyAuthCredential + tags: + - Embedded Wallet Auth + security: + - BasicAuth: [] + parameters: + - name: id + in: path + description: >- + The id of the authentication credential to verify (the `id` field of + the `AuthMethod` returned from `POST /auth/credentials`). + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/auth/AuthCredentialVerifyRequestOneOf.yaml + examples: + emailOtp: + summary: Verify an email OTP credential + value: + type: EMAIL_OTP + otp: '123456' + clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + responses: + '200': + description: Authentication credential verified and session issued + content: + application/json: + schema: + $ref: ../../components/schemas/auth/AuthSession.yaml + '400': + description: Bad request + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized - invalid or expired OTP + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Authentication credential not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '500': + description: Internal service error + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error500.yaml From 2dc7aa8d0d0c0c7e1583fe94941dab55c532248d Mon Sep 17 00:00:00 2001 From: Dhruv Pareek Date: Tue, 21 Apr 2026 18:53:50 -0700 Subject: [PATCH 2/2] docs: fix AuthSession.encryptedSessionSigningKey example format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace misleading PEM-wrapped PKCS#8 example with a base58check-encoded Turnkey-style HPKE bundle — the actual on-the-wire format. Add format details to the description (33-byte compressed P-256 encapsulated key + AES-256-GCM ciphertext). Co-Authored-By: Claude Opus 4.7 (1M context) --- mintlify/openapi.yaml | 4 ++-- openapi.yaml | 4 ++-- openapi/components/schemas/auth/AuthSession.yaml | 15 +++++++-------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 2f5e4563..cbcf97f1 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -12968,8 +12968,8 @@ components: properties: encryptedSessionSigningKey: type: string - description: Session signing key encrypted to the `clientPublicKey` supplied when the credential was created. The client decrypts this key with its private key and uses it to sign subsequent Embedded Wallet requests until `expiresAt`. - example: '-----BEGIN ENCRYPTED PRIVATE KEY----- MIIBvTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIabc... -----END ENCRYPTED PRIVATE KEY-----' + description: 'HPKE-encrypted session signing key, sealed to the `clientPublicKey` supplied when the credential was created. 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`.' + example: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf expiresAt: type: string format: date-time diff --git a/openapi.yaml b/openapi.yaml index 2f5e4563..cbcf97f1 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -12968,8 +12968,8 @@ components: properties: encryptedSessionSigningKey: type: string - description: Session signing key encrypted to the `clientPublicKey` supplied when the credential was created. The client decrypts this key with its private key and uses it to sign subsequent Embedded Wallet requests until `expiresAt`. - example: '-----BEGIN ENCRYPTED PRIVATE KEY----- MIIBvTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIabc... -----END ENCRYPTED PRIVATE KEY-----' + description: 'HPKE-encrypted session signing key, sealed to the `clientPublicKey` supplied when the credential was created. 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`.' + example: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf expiresAt: type: string format: date-time diff --git a/openapi/components/schemas/auth/AuthSession.yaml b/openapi/components/schemas/auth/AuthSession.yaml index 1fdad37d..ab072707 100644 --- a/openapi/components/schemas/auth/AuthSession.yaml +++ b/openapi/components/schemas/auth/AuthSession.yaml @@ -8,14 +8,13 @@ allOf: encryptedSessionSigningKey: type: string description: >- - Session signing key encrypted to the `clientPublicKey` supplied when - the credential was created. The client decrypts this key with its - private key and uses it to sign subsequent Embedded Wallet requests - until `expiresAt`. - example: >- - -----BEGIN ENCRYPTED PRIVATE KEY----- - MIIBvTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIabc... - -----END ENCRYPTED PRIVATE KEY----- + HPKE-encrypted session signing key, sealed to the `clientPublicKey` + supplied when the credential was created. 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`. + example: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf expiresAt: type: string format: date-time