From 6b3905ed07a83faf72503f10cc3a8d6e52124dac Mon Sep 17 00:00:00 2001 From: Dhruv Pareek Date: Fri, 22 May 2026 10:04:34 -0700 Subject: [PATCH] docs: add sandbox email OTP inbox endpoint --- mintlify/global-accounts/index.mdx | 2 +- .../platform-tools/sandbox-testing.mdx | 6 +- mintlify/openapi.yaml | 165 ++++++++++++++---- .../snippets/sandbox-global-account-magic.mdx | 22 ++- openapi.yaml | 165 ++++++++++++++---- .../sandbox/SandboxEmailOtpResponse.yaml | 42 +++++ openapi/openapi.yaml | 2 + .../sandbox/sandbox_email-otps_latest.yaml | 60 +++++++ 8 files changed, 385 insertions(+), 79 deletions(-) create mode 100644 openapi/components/schemas/sandbox/SandboxEmailOtpResponse.yaml create mode 100644 openapi/paths/sandbox/sandbox_email-otps_latest.yaml diff --git a/mintlify/global-accounts/index.mdx b/mintlify/global-accounts/index.mdx index 921d1379..a6b8b3fe 100644 --- a/mintlify/global-accounts/index.mdx +++ b/mintlify/global-accounts/index.mdx @@ -73,6 +73,6 @@ Some Global Accounts capabilities require platform enablement before you can bui Generate the P-256 key pair, decrypt the session signing key, and sign payloads on Web, iOS, and Android. - Magic values for OTP and signatures, plus sandbox OIDC token rules that exercise the full request shape without standing up real auth providers. + Sandbox OTP inbox, signature magic values, and OIDC token rules that exercise the full request shape. diff --git a/mintlify/global-accounts/platform-tools/sandbox-testing.mdx b/mintlify/global-accounts/platform-tools/sandbox-testing.mdx index b6b9be96..918f1aae 100644 --- a/mintlify/global-accounts/platform-tools/sandbox-testing.mdx +++ b/mintlify/global-accounts/platform-tools/sandbox-testing.mdx @@ -1,13 +1,13 @@ --- title: "Sandbox testing" -description: "Exercise the Global Account auth, signing, and funding flows without standing up real OTP delivery, WebAuthn, or OIDC providers" +description: "Exercise the Global Account auth, signing, and funding flows without real OTP delivery or WebAuthn ceremony" icon: "/images/icons/hammer.svg" "og:image": "/images/og/og-global-accounts.webp" --- import SandboxGlobalAccountMagic from '/snippets/sandbox-global-account-magic.mdx'; -The Grid sandbox lets you exercise the full Global Accounts integration — customer creation, account lookup, credential registration, funding, and signed withdrawals — without moving real money or standing up real auth providers. All API endpoints work the same way as in production, but money movements are simulated. OTP, passkey, and wallet signatures use sandbox-only magic values, while OAuth uses JWT-shaped sandbox OIDC tokens with claim, freshness, identity, and nonce checks. +The Grid sandbox lets you exercise the full Global Accounts integration — customer creation, account lookup, credential registration, funding, and signed withdrawals — without moving real money. All API endpoints work the same way as in production, but money movements are simulated. Email OTP uses a sandbox test inbox, passkey and wallet signatures use sandbox-only magic values, and OAuth validates OIDC token claims against the registered auth credential. ## Sandbox setup @@ -52,7 +52,7 @@ All webhook events fire normally in sandbox. Configure your webhook URL in the d When you're ready to go live: 1. Generate production API tokens in the dashboard and swap them for the sandbox credentials in your environment. -2. Remove sandbox magic values and unsigned sandbox OIDC tokens from your client and server code — production runs the real OTP, HPKE, WebAuthn, OIDC signature, and ECDSA flows. +2. Remove sandbox inbox calls, magic values, and unsigned sandbox OIDC test tokens from your client and server code — production runs the real OTP, HPKE, WebAuthn, OIDC signature, and ECDSA flows. 3. Configure production webhook endpoints. 4. Test with small amounts first. diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index d288fb73..5bd69dc8 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -2933,6 +2933,62 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /sandbox/email-otps/latest: + get: + summary: Get the latest sandbox email OTP + description: | + Return the active one-time password for a sandbox `EMAIL_OTP` auth method. Use this as a sandbox test inbox after creating an email OTP credential or after requesting a new email OTP challenge. + This endpoint is only for the sandbox environment and will fail for production platforms/keys. Production OTP codes are delivered by email and are never exposed by Grid. + operationId: getLatestSandboxEmailOtp + tags: + - Sandbox + security: + - BasicAuth: [] + parameters: + - name: authMethodId + in: query + required: true + description: Auth method ID returned by `POST /auth/credentials` or `GET /auth/credentials`. + schema: + type: string + example: AuthMethod:019e509f-3f4c-0dca-0000-a55f43ba887e + responses: + '200': + description: Sandbox email OTP found + content: + application/json: + schema: + $ref: '#/components/schemas/SandboxEmailOtpResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '403': + description: Forbidden - request was made with a production platform token + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: No active sandbox OTP exists for this auth method + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' /customers/bulk/csv: post: summary: Upload customers via CSV file @@ -15714,6 +15770,82 @@ components: response_body: type: string description: The raw body content returned by the webhook endpoint in response to the request + SandboxEmailOtpResponse: + type: object + required: + - authMethodId + - accountId + - email + - otpId + - otpCode + - expiresAt + - attemptsRemaining + properties: + authMethodId: + type: string + description: EMAIL_OTP auth method this sandbox code belongs to. + example: AuthMethod:019e509f-3f4c-0dca-0000-a55f43ba887e + accountId: + type: string + description: Internal account associated with the auth method's embedded wallet. + example: InternalAccount:019e509f-3a6b-0dca-0000-32c1a4a35df2 + email: + type: string + format: email + description: Email address or display name associated with the auth method. + example: alice@example.com + otpId: + type: string + description: Sandbox Turnkey OTP identifier. The code is scoped to this OTP id and rotates when a new challenge is requested. + example: 019e509f-4556-0dca-0000-f20dd8312a6f + otpCode: + type: string + pattern: ^[0-9]{6}$ + description: Six-digit sandbox OTP code to pass as `otp` to `POST /auth/credentials/{id}/verify`. + example: '381274' + expiresAt: + type: string + format: date-time + description: Time after which this sandbox OTP can no longer be verified. + example: '2026-05-21T20:15:00Z' + attemptsRemaining: + type: integer + minimum: 0 + description: Number of remaining wrong-code attempts before this sandbox OTP is locked out. + example: 5 + Error403: + type: object + required: + - message + - status + - code + properties: + status: + type: integer + enum: + - 403 + description: HTTP status code + code: + type: string + description: | + | Error Code | Description | + |------------|-------------| + | FORBIDDEN | Insufficient permissions | + | USER_NOT_READY | Customer exists but is not ready for operation | + | COUNTERPARTY_NOT_ALLOWED | Counterparty has not been enabled for your account | + | VELOCITY_LIMIT_EXCEEDED | Counterparty has exceeded velocity limits | + enum: + - FORBIDDEN + - USER_NOT_READY + - COUNTERPARTY_NOT_ALLOWED + - VELOCITY_LIMIT_EXCEEDED + message: + type: string + description: Error message + details: + type: object + description: Additional error details + additionalProperties: true BulkCustomerImportJobAccepted: type: object required: @@ -15904,39 +16036,6 @@ components: type: string description: The UMA address of the customer claiming the invitation example: $invitee@uma.domain - Error403: - type: object - required: - - message - - status - - code - properties: - status: - type: integer - enum: - - 403 - description: HTTP status code - code: - type: string - description: | - | Error Code | Description | - |------------|-------------| - | FORBIDDEN | Insufficient permissions | - | USER_NOT_READY | Customer exists but is not ready for operation | - | COUNTERPARTY_NOT_ALLOWED | Counterparty has not been enabled for your account | - | VELOCITY_LIMIT_EXCEEDED | Counterparty has exceeded velocity limits | - enum: - - FORBIDDEN - - USER_NOT_READY - - COUNTERPARTY_NOT_ALLOWED - - VELOCITY_LIMIT_EXCEEDED - message: - type: string - description: Error message - details: - type: object - description: Additional error details - additionalProperties: true SandboxSendRequest: type: object required: diff --git a/mintlify/snippets/sandbox-global-account-magic.mdx b/mintlify/snippets/sandbox-global-account-magic.mdx index cbd2aff1..b82368db 100644 --- a/mintlify/snippets/sandbox-global-account-magic.mdx +++ b/mintlify/snippets/sandbox-global-account-magic.mdx @@ -1,24 +1,28 @@ -The Grid sandbox accepts a small set of magic values for Global Account flows, so you can exercise the full request shape without standing up Turnkey, WebAuthn, or an OIDC provider. OTP, passkey, and wallet signatures use fixed sandbox-only values. OAuth uses JWT-shaped sandbox OIDC tokens: sandbox skips real IdP signature verification, but still validates the token claims, freshness, credential identity, and verify-time nonce binding. +The Grid sandbox accepts a small set of test helpers for Global Account flows, so you can exercise the full request shape without real OTP delivery, WebAuthn ceremony, or wallet signatures. Email OTP uses a sandbox test inbox. Passkey and wallet signatures use fixed sandbox-only values. OAuth accepts OIDC ID tokens from supported providers; for isolated sandbox tests, you can also pass a JWT-shaped test token. Sandbox skips real IdP signature verification, but still validates token claims, freshness, credential identity, and verify-time nonce binding. A wrong magic value or sandbox OIDC authentication failure returns `401 UNAUTHORIZED` with a `reason` field that names the specific check that failed. A malformed OIDC JWT can return `400 INVALID_INPUT` before authentication starts. ### Email OTP code -Pass `000000` as the body `otp` on `POST /auth/credentials/{id}/verify` when the credential type is `EMAIL_OTP`. The sandbox skips OTP delivery and accepts this value as a valid response to the issued challenge. +Sandbox does not send OTP emails. After creating an `EMAIL_OTP` credential or requesting a new email OTP challenge, call `GET /sandbox/email-otps/latest` with the auth method ID to retrieve the latest active code for that credential. Then pass the returned `otpCode` as the body `otp` on `POST /auth/credentials/{id}/verify`. ```bash -curl -X POST https://api.lightspark.com/grid/2025-10-13/auth/credentials/AuthMethod:abc123/verify \ +export PUBLIC_KEY="04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2" + +OTP_CODE=$(curl -sS "$GRID_BASE_URL/sandbox/email-otps/latest?authMethodId=AuthMethod:abc123" \ + -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" | jq -r '.otpCode') + +curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:abc123/verify" \ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ -H "Content-Type: application/json" \ - -H "Request-Id: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \ -d '{ "type": "EMAIL_OTP", - "otp": "000000", - "clientPublicKey": "04f45f2a..." + "otp": "'"$OTP_CODE"'", + "clientPublicKey": "'"$PUBLIC_KEY"'" }' ``` -Any other code returns `401 UNAUTHORIZED` with `reason: "Invalid OTP code"`. +The sandbox validates the code against the pending OTP state. A wrong, expired, consumed, or locked-out code returns `401 UNAUTHORIZED`. ### Passkey assertion signature @@ -55,7 +59,7 @@ Any other signature returns `401 UNAUTHORIZED` with `reason: "Invalid passkey si ### OAuth (OIDC) token -OAuth does not use a fixed magic token in sandbox. Pass a JWT-shaped OIDC token as `oidcToken`. The JWT signature segment can be a dummy value, but the payload must look like a real ID token. +OAuth does not use a fixed magic token in sandbox. Pass the OIDC ID token from your supported provider as `oidcToken`. For isolated sandbox tests, you can generate a JWT-shaped test token yourself. The JWT signature segment can be a dummy value, but the payload must look like a real ID token. For `POST /auth/credentials` with `type: "OAUTH"`, the sandbox token must include: @@ -80,7 +84,7 @@ const b64url = (value) => const payload = { iss: "https://accounts.google.com", sub: "sandbox-user-123", - aud: "grid-sandbox-oauth-client-id", + aud: "your-google-client-id.apps.googleusercontent.com", iat: now, exp: now + 300, nonce: crypto.createHash("sha256").update(publicKey).digest("hex"), diff --git a/openapi.yaml b/openapi.yaml index d288fb73..5bd69dc8 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2933,6 +2933,62 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /sandbox/email-otps/latest: + get: + summary: Get the latest sandbox email OTP + description: | + Return the active one-time password for a sandbox `EMAIL_OTP` auth method. Use this as a sandbox test inbox after creating an email OTP credential or after requesting a new email OTP challenge. + This endpoint is only for the sandbox environment and will fail for production platforms/keys. Production OTP codes are delivered by email and are never exposed by Grid. + operationId: getLatestSandboxEmailOtp + tags: + - Sandbox + security: + - BasicAuth: [] + parameters: + - name: authMethodId + in: query + required: true + description: Auth method ID returned by `POST /auth/credentials` or `GET /auth/credentials`. + schema: + type: string + example: AuthMethod:019e509f-3f4c-0dca-0000-a55f43ba887e + responses: + '200': + description: Sandbox email OTP found + content: + application/json: + schema: + $ref: '#/components/schemas/SandboxEmailOtpResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '403': + description: Forbidden - request was made with a production platform token + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: No active sandbox OTP exists for this auth method + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' /customers/bulk/csv: post: summary: Upload customers via CSV file @@ -15714,6 +15770,82 @@ components: response_body: type: string description: The raw body content returned by the webhook endpoint in response to the request + SandboxEmailOtpResponse: + type: object + required: + - authMethodId + - accountId + - email + - otpId + - otpCode + - expiresAt + - attemptsRemaining + properties: + authMethodId: + type: string + description: EMAIL_OTP auth method this sandbox code belongs to. + example: AuthMethod:019e509f-3f4c-0dca-0000-a55f43ba887e + accountId: + type: string + description: Internal account associated with the auth method's embedded wallet. + example: InternalAccount:019e509f-3a6b-0dca-0000-32c1a4a35df2 + email: + type: string + format: email + description: Email address or display name associated with the auth method. + example: alice@example.com + otpId: + type: string + description: Sandbox Turnkey OTP identifier. The code is scoped to this OTP id and rotates when a new challenge is requested. + example: 019e509f-4556-0dca-0000-f20dd8312a6f + otpCode: + type: string + pattern: ^[0-9]{6}$ + description: Six-digit sandbox OTP code to pass as `otp` to `POST /auth/credentials/{id}/verify`. + example: '381274' + expiresAt: + type: string + format: date-time + description: Time after which this sandbox OTP can no longer be verified. + example: '2026-05-21T20:15:00Z' + attemptsRemaining: + type: integer + minimum: 0 + description: Number of remaining wrong-code attempts before this sandbox OTP is locked out. + example: 5 + Error403: + type: object + required: + - message + - status + - code + properties: + status: + type: integer + enum: + - 403 + description: HTTP status code + code: + type: string + description: | + | Error Code | Description | + |------------|-------------| + | FORBIDDEN | Insufficient permissions | + | USER_NOT_READY | Customer exists but is not ready for operation | + | COUNTERPARTY_NOT_ALLOWED | Counterparty has not been enabled for your account | + | VELOCITY_LIMIT_EXCEEDED | Counterparty has exceeded velocity limits | + enum: + - FORBIDDEN + - USER_NOT_READY + - COUNTERPARTY_NOT_ALLOWED + - VELOCITY_LIMIT_EXCEEDED + message: + type: string + description: Error message + details: + type: object + description: Additional error details + additionalProperties: true BulkCustomerImportJobAccepted: type: object required: @@ -15904,39 +16036,6 @@ components: type: string description: The UMA address of the customer claiming the invitation example: $invitee@uma.domain - Error403: - type: object - required: - - message - - status - - code - properties: - status: - type: integer - enum: - - 403 - description: HTTP status code - code: - type: string - description: | - | Error Code | Description | - |------------|-------------| - | FORBIDDEN | Insufficient permissions | - | USER_NOT_READY | Customer exists but is not ready for operation | - | COUNTERPARTY_NOT_ALLOWED | Counterparty has not been enabled for your account | - | VELOCITY_LIMIT_EXCEEDED | Counterparty has exceeded velocity limits | - enum: - - FORBIDDEN - - USER_NOT_READY - - COUNTERPARTY_NOT_ALLOWED - - VELOCITY_LIMIT_EXCEEDED - message: - type: string - description: Error message - details: - type: object - description: Additional error details - additionalProperties: true SandboxSendRequest: type: object required: diff --git a/openapi/components/schemas/sandbox/SandboxEmailOtpResponse.yaml b/openapi/components/schemas/sandbox/SandboxEmailOtpResponse.yaml new file mode 100644 index 00000000..7e46af78 --- /dev/null +++ b/openapi/components/schemas/sandbox/SandboxEmailOtpResponse.yaml @@ -0,0 +1,42 @@ +type: object +required: + - authMethodId + - accountId + - email + - otpId + - otpCode + - expiresAt + - attemptsRemaining +properties: + authMethodId: + type: string + description: EMAIL_OTP auth method this sandbox code belongs to. + example: AuthMethod:019e509f-3f4c-0dca-0000-a55f43ba887e + accountId: + type: string + description: Internal account associated with the auth method's embedded wallet. + example: InternalAccount:019e509f-3a6b-0dca-0000-32c1a4a35df2 + email: + type: string + format: email + description: Email address or display name associated with the auth method. + example: alice@example.com + otpId: + type: string + description: Sandbox Turnkey OTP identifier. The code is scoped to this OTP id and rotates when a new challenge is requested. + example: 019e509f-4556-0dca-0000-f20dd8312a6f + otpCode: + type: string + pattern: ^[0-9]{6}$ + description: Six-digit sandbox OTP code to pass as `otp` to `POST /auth/credentials/{id}/verify`. + example: '381274' + expiresAt: + type: string + format: date-time + description: Time after which this sandbox OTP can no longer be verified. + example: '2026-05-21T20:15:00Z' + attemptsRemaining: + type: integer + minimum: 0 + description: Number of remaining wrong-code attempts before this sandbox OTP is locked out. + example: 5 diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 19b26f8d..476aded5 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -175,6 +175,8 @@ paths: $ref: paths/crypto/crypto_estimate-withdrawal-fee.yaml /sandbox/webhooks/test: $ref: paths/sandbox/sandbox_webhooks_test.yaml + /sandbox/email-otps/latest: + $ref: paths/sandbox/sandbox_email-otps_latest.yaml /customers/bulk/csv: $ref: paths/customers/customers_bulk_csv.yaml /customers/bulk/jobs/{jobId}: diff --git a/openapi/paths/sandbox/sandbox_email-otps_latest.yaml b/openapi/paths/sandbox/sandbox_email-otps_latest.yaml new file mode 100644 index 00000000..bf185395 --- /dev/null +++ b/openapi/paths/sandbox/sandbox_email-otps_latest.yaml @@ -0,0 +1,60 @@ +get: + summary: Get the latest sandbox email OTP + description: > + Return the active one-time password for a sandbox `EMAIL_OTP` auth method. + Use this as a sandbox test inbox after creating an email OTP credential or + after requesting a new email OTP challenge. + + This endpoint is only for the sandbox environment and will fail for + production platforms/keys. Production OTP codes are delivered by email and + are never exposed by Grid. + operationId: getLatestSandboxEmailOtp + tags: + - Sandbox + security: + - BasicAuth: [] + parameters: + - name: authMethodId + in: query + required: true + description: Auth method ID returned by `POST /auth/credentials` or `GET /auth/credentials`. + schema: + type: string + example: AuthMethod:019e509f-3f4c-0dca-0000-a55f43ba887e + responses: + '200': + description: Sandbox email OTP found + content: + application/json: + schema: + $ref: ../../components/schemas/sandbox/SandboxEmailOtpResponse.yaml + '400': + description: Bad request + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '403': + description: Forbidden - request was made with a production platform token + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error403.yaml + '404': + description: No active sandbox OTP exists for this auth method + 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