The Grid sandbox environment simulates real payment flows without moving real money. You can control test outcomes using special account number patterns and test addresses.
KYC/KYB verification
In sandbox, you can trigger specific KYC/KYB verification outcomes using magic suffixes in customer and beneficial owner fields. These let you test different verification flows without waiting for real review.
Business customer verification
The last 3 digits of the registrationNumber in businessInfo determine the KYB status outcome when you call POST /verifications:
| Suffix | kybStatus | Behavior |
|---|
| 001 | PENDING | KYB verification remains pending |
| 002 | REJECTED | KYB verification is rejected |
| Any other | APPROVED | KYB verification is approved |
Beneficial owner KYC
The last 3 characters of the lastName in personalInfo determine the individual KYC status outcome:
| Suffix | kycStatus | Behavior |
|---|
| 001 | PENDING | KYC verification remains pending |
| 002 | REJECTED | KYC verification is rejected |
| Any other | APPROVED | KYC verification is approved |
Adding external accounts
The flows for creating external accounts in sandbox are the same as in production. The last 3 digits of an external account’s primary identifier (account number, IBAN, CLABE, Spark wallet address, etc.) determine the test scenario when that account is used in transfers or quotes. For identifiers with a domain part (e.g. PIX email keys), append the test digits to the username portion — for example, testuser.002@pix.com.br.
Beneficiary name verification
For account types that support beneficiary name verification, you can simulate different verification outcomes in sandbox. Use account identifiers with a 1xx suffix to trigger verification scenarios (this range is reserved for verification and does not conflict with transfer or quote test patterns):
| Suffix | beneficiaryVerificationStatus | Behavior |
|---|
| 102 | NOT_MATCHED | Account is valid but name does not match |
| 103 | PARTIAL_MATCH | Account is valid, name is a fuzzy match |
| 104 | PENDING | Verification still in progress |
| 105 | (error) | Returns 400 — invalid account |
| 106 | UNSUPPORTED | Payment rail does not support name verification |
| 107 | CHECKED_BY_RECEIVING_FI | Verification deferred to receiving financial institution (e.g., ACH) |
| 109 | (error) | Returns 500 — simulated API error |
| Any other | MATCHED | Account is valid, name matches exactly |
Transfer in
In production, internal accounts are funded by sending a bank transfer to the account’s payment instructions or by pulling from an external account. In sandbox, you have two options:
Transfer in from an external account
Use the /transfer-in endpoint to pull funds from an external account into an internal account. The external account’s number suffix determines the outcome:
| Suffix | Behavior |
|---|
| 002 | Insufficient funds — transfer fails immediately |
| 003 | Account closed/invalid — transfer fails immediately |
| 004 | Transfer rejected — bank rejects the transfer |
| 005 | Timeout/delayed failure — stays pending ~30s, then fails |
| Any other | Success — transfer completes normally |
Sandbox fund endpoint
Instantly add funds to any internal account using /sandbox/internal-accounts/{accountId}/fund:
curl -X POST https://api.lightspark.com/grid/2025-10-13/sandbox/internal-accounts/{accountId}/fund \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{ "amount": 100000 }'
Creating quotes (cross-currency transfers)
When creating a quote with an external account destination, the account number suffix determines the payment outcome after quote execution:
| Suffix | Behavior |
|---|
| 002 | Quote execution failed |
| 003 | Long payment — completes after approximately 6 minutes |
| 004 | Counterparty delivery failed |
| 005 | Receiving bank returned payment (completes then transitions to failed) |
| 006 | User cancellation |
| 007 | Payout and refund failed |
| Any other | Successful payment |
Executing a quote
After creating a quote, you need to fund it to trigger execution. There are two ways to do this in sandbox:
Prefunded internal account — If your quote’s source is an internal account, fund the account using one of the methods described in transfer in, then call the quote execute endpoint to trigger the transaction:
curl -X POST https://api.lightspark.com/grid/2025-10-13/quotes/{quoteId}/execute \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"
Real-time funding via sandbox send — If your quote uses real-time funding, the quote response includes payment instructions for you to transfer funds to. Use /sandbox/send to simulate this payment:
curl -X POST https://api.lightspark.com/grid/2025-10-13/sandbox/send \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"quoteId": "Quote:019542f5-b3e7-1d02-0000-000000000006",
"currencyCode": "USD"
}'
Transferring out funds
Use the /transfer-out endpoint to push funds from an internal account to an external account in the same currency. The external account’s number suffix controls the outcome using the same patterns as transfer in.
Sending to a UMA address
For UMA-based payments, use these sandbox addresses to simulate different scenarios:
| UMA Address | Behavior |
|---|
$success.usd@sandbox.uma.money | Payment succeeds (USD) |
$success.eur@sandbox.uma.money | Payment succeeds (EUR) |
$success.mxn@sandbox.uma.money | Payment succeeds (MXN) |
$pending.long.usd@sandbox.uma.money | Simulates a long-pending payment |
$fail.compliance.usd@sandbox.uma.money | Simulates a compliance check failure |
Simulating incoming UMA payments
Use the sandbox receive endpoint to simulate an incoming UMA payment to one of your platform’s users:
curl -X POST https://api.lightspark.com/grid/2025-10-13/sandbox/uma/receive \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"senderUmaAddress": "$success.usd@sandbox.uma.money",
"receiverUmaAddress": "$your.user@your.domain",
"receivingCurrencyCode": "USD",
"receivingCurrencyAmount": 5000
}'
Global Account magic values
The Grid sandbox accepts a small set of magic values that bypass real auth and credential checks for Global Account flows, so you can exercise the full request shape without standing up Turnkey, WebAuthn, or an OIDC provider. These values are sandbox-only — production enforces real signature verification, WebAuthn assertion, and OIDC nonce binding.
A wrong magic value (or any other value) returns 401 UNAUTHORIZED with a reason field that names the specific check that failed.
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.
curl -X POST https://api.lightspark.com/grid/2025-10-13/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..."
}'
Any other code returns 401 UNAUTHORIZED with reason: "Invalid OTP code".
Passkey assertion signature
Pass sandbox-valid-passkey-signature as assertion.signature on POST /auth/credentials/{id}/verify when the credential type is PASSKEY. The sandbox accepts the rest of the assertion as-is and skips the WebAuthn signature check.
curl -X POST https://api.lightspark.com/grid/2025-10-13/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": "PASSKEY",
"assertion": {
"credentialId": "...",
"clientDataJson": "...",
"authenticatorData": "...",
"signature": "sandbox-valid-passkey-signature"
},
"clientPublicKey": "04f45f2a..."
}'
Any other signature returns 401 UNAUTHORIZED with reason: "Invalid passkey signature". clientPublicKey is still required — the magic value bypasses the credential check, not the HPKE plumbing that seals the session signing key to the public key you supply.
OAuth (OIDC) token
Pass sandbox-valid-oidc-token as the body oidcToken on both POST /auth/credentials (OAUTH create) and POST /auth/credentials/{id}/verify (OAUTH).
curl -X POST https://api.lightspark.com/grid/2025-10-13/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": "OAUTH",
"oidcToken": "sandbox-valid-oidc-token",
"clientPublicKey": "04f45f2a..."
}'
Any other token returns 401 UNAUTHORIZED with reason: "Invalid OIDC token".
OAUTH create still requires a JWT-shaped token. On the initial POST /auth/credentials (OAUTH create), the oidcToken must be a structurally valid JWT (header.payload.signature) so Grid can decode the iss claim and resolve the provider name. The literal sandbox-valid-oidc-token works on verify but not on create — for create, sign your own dummy JWT with any payload that includes a recognized iss claim. The sandbox bypasses signature verification, not JWT structure parsing.
Pass sandbox-valid-signature as the Grid-Wallet-Signature HTTP header on any signed-retry flow:
POST /auth/credentials (add-additional-credential signed retry)
DELETE /auth/credentials/{id} (revoke credential)
DELETE /auth/sessions/{id} (revoke session)
POST /internal-accounts/{id}/export (export wallet)
POST /quotes/{quoteId}/execute (when source is an embedded wallet)
curl -X POST https://api.lightspark.com/grid/2025-10-13/quotes/Quote:abc123/execute \
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \
-H "Grid-Wallet-Signature: sandbox-valid-signature"
Any other header value returns 401 UNAUTHORIZED with reason: "Invalid Grid-Wallet-Signature".