Skip to main content
Spritz provides a sandbox so you can exercise the full money-movement lifecycle (onboarding, conversion, settlement, cards) against simulated rails, without moving real funds.

Base URL

The sandbox is a separate environment with its own base URL and its own credentials:
Use your sandbox integrator credentials here, exactly as you would in production. When you’re ready to go live, switch the base URL to https://platform.spritz.finance and swap in your production credentials.

Skipping identity verification

Real users must complete identity verification before they can move money. In the sandbox you can skip it to keep test flows fast:
Exactly one of country or failed: true is required. An empty body is a 400 — there is no default.

Capability groups

country selects a capability group: the set of offerings a verified user gets. It is not an ISO country code. EU is the EEA as a whole, because every member state sees the same offerings — passing a real country code like DE or ES is rejected with a 400. A 501 means the group is real but sandbox cannot simulate it yet; it is distinct from the 400 an unrecognised value gets, and it is not worth retrying. A temporary verification-service failure returns 503 with code SANDBOX_KYC_UNAVAILABLE and retryable: true. Do not treat this as a failed KYC decision. Read GET /v1/users/me; continue if the requested capability is already active, otherwise retry the same sandbox request with bounded backoff. The bypass changes verification state; it does not override product configuration. For an ACH-debit-enabled integrator, the US bypass makes the user’s fiat_to_crypto / ach_debit capability active with no separate terms requirement. Other products or regions can still have requirements, including EEA regional compliance. Read GET /v1/users/me after the bypass to see each capability’s status and outstanding requirements, and GET /v1/on-ramps/supported-pairs for the rail, network and token combinations the group can actually use. The bypass response is a legacy verification document with no public stable schema. Do not store or branch on it. Treat a 2xx as “simulation accepted,” then read the typed user profile and capabilities from GET /v1/users/me.
Sandbox-only endpoints return 403 in production. Don’t build production flows that depend on them.

Simulating the rails

The sandbox also lets you drive the parts of a flow that normally depend on banks and networks, so you can test the unhappy paths on purpose. Sandbox-plan ACH deposits are isolated from accumulated program-control halts and other test users. One simulated return cannot silently make later scenarios fail for an unrelated user. Use the named profiles below to request each observable outcome; do not try to reproduce production decision logic with Plaid test data. First bypass KYC with { "country": "US" } and confirm the user’s ACH debit capability is active. The sandbox bank-link endpoint rejects users in any other capability group. Use the sandbox link endpoint when an automated test cannot open Plaid Link:
This creates a Plaid Sandbox item and runs the normal token exchange, account sync, ownership check, funding-source creation, event, and cache-invalidation paths. Successful profiles accept simulation.account: "primary" | "secondary". It defaults to primary; use secondary to link a distinct matched bank for the same user. Link both before destructive tests such as repeated returns. Ownership mismatch and review profiles do not claim the bank account. A different user who owns that bank can still link it successfully. Link the same successful account slot a second time to test duplicate handling. The second request returns 409 with code: "PLAID_ACCOUNT_ALREADY_LINKED"; it does not create another funding source and does not change the active source. Close Link and re-list funding sources instead of retrying. To test unlinking, delete the bank with DELETE /v1/bank-accounts/{accountId}. Its funding source remains readable with status: "deleted". Linking the same healthy account slot again returns the original bank-account and funding-source IDs, restores the source to active, and leaves exactly one active source. Treat those IDs as stable; do not create a second local payment method.

Simulate deposit decision outcomes

Prepare a direct deposit, then send its preparationId to the sandbox create endpoint with one public outcome profile. These profiles select client-visible behavior; they do not identify the production provider input, policy rule, or threshold that caused it.
The first four profiles return 409 and create no deposit. A create attempt consumes its preparation. Reuse the same idempotency key and body only to recover after a timeout; prepare again for a new user attempt. Use both high-priority profiles with a high-priority preparation. high_priority_available returns the quoted instant split. high_priority_downgraded returns 200, preserves requestedPriority: "high", and sets:
The returned regular, instant, and total fee fields are also recalculated for the actual release timing. Treat the create response as authoritative.

Simulate refund and crypto-release states

Prepare a normal-priority deposit for refunded, release_failed, release_stalled, or full_delivery, then create it with one lifecycle profile:
These profiles create a real sandbox deposit record and make the resulting state available from both GET /v1/deposits/{depositId} and GET /v1/deposits/. They do not call a money-movement provider. The refunded profile also records the same durable achDebit.refunded semantic event used by the production refund path. Use the create response itself to assert authorized. Use release_stalled to assert the processing / queued UI. For deterministic partial and full delivery events, prepare a high-priority 500.00 deposit whose preparation contains positive instantPortionUsd and settlementPortionUsd, then create it with lifecycleSimulation.profile: "partial_then_full_delivery". The final create response is completed, while the semantic webhook snapshots preserve the intermediate partial state and exact amounts. The profile emits the sequence achDebit.authorizedachDebit.deliveryProgressachDebit.delivered without calling a crypto or bank provider. Separately run riskSimulation.profile: "high_priority_available" to exercise the provider-backed sandbox execution path. Poll the deposit every 2 seconds for up to 5 minutes and assert the final confirmedReleasedAmountUsd; do not require its transient partial state to last long enough for polling. The five-minute value is a test-harness observation bound, not a user delivery promise.

Simulate program pauses

Use the sandbox prepare endpoint with a normal deposit body plus one state:
No preparation or deposit is created in either blocked case.

Simulate per-user open exposure

Use one fresh user and one matched source. This sequence exercises the public open_exposure result without exposing or hardcoding a tier threshold:
  1. Read limitsByPriority.normal and assert available: true.
  2. Prepare normal.maxAmountUsd with priority: "normal".
  3. Create it through /v1/sandbox/deposits/direct with lifecycleSimulation.profile: "full_delivery". Assert status: "completed", debitStatus: "settled", releaseStatus: "completed", and confirmedReleasedAmountUsd equal to the prepared principal.
  4. Re-read limits and repeat steps 2–3 at the new normal.maxAmountUsd while normal remains available. Use a fresh preparation and idempotency key each time.
  5. The final normal block must return available: false, maxAmountUsd: "0.00", reason: "open_exposure", suggestedAction: "auto_ramp", a non-null clearsAt, and clearsAtIsEstimate: true.
Do not assert the number of deposits or infer a private exposure ceiling from their amounts. The current public limits decide each iteration. Use a fresh user for another run because completed deposits intentionally remain inside that user’s open-exposure window.

Simulate program and instant capacity

Two sandbox-only endpoints let you force the public aggregate_exposure, rail_halted, partial-instant, and high-to-normal outcomes: Instant scenarios require Spritz to enable high priority on the sandbox integrator. There is no public self-service switch. Ask Spritz for an enabled sandbox credential; if limitsByPriority.high.available remains false, record instant scenarios as blocked by configuration rather than failed. The W1/W2 names are API field names for two isolated sandbox capacity controls. The endpoint does not describe how production policy is calculated. Never hardcode the returned values into product behavior.
Cap changes affect every user under the authenticated sandbox integrator. Run these scenarios serially against a dedicated test integrator. Read both original values first and restore both in a finally block. If restoration fails, stop testing and restore them before another deposit test runs.
Use this acceptance sequence:
  1. Read and save the original response.
  2. Read an active source’s baseline limitsByPriority.
  3. With high priority enabled, prepare an amount no greater than both high.maxAmountUsd and high.maxEarlyReleaseAmountUsd, then create with riskSimulation.profile: "high_priority_available". Assert early_full and the full principal in instantPortionUsd. This is the deterministic full-instant case.
  4. To test normal program capacity, set aggregateCapW2Usd to the current openExposureW2Usd plus a small headroom below the source’s baseline maximum. Keep W1 unchanged. Re-read limits: the returned normal maximum should shrink. Preparing that maximum succeeds; one cent above returns aggregate_exposure.
  5. Set aggregateCapW2Usd to "0.00". Normal becomes unavailable with reason: "rail_halted" and suggestedAction: "auto_ramp".
  6. Restore the original W2 cap. Then set aggregateCapW1Usd to committedExposureW1Usd plus a small desired instant portion. Keep W2 unchanged. Re-read limits and verify high.maxEarlyReleaseAmountUsd is constrained while normal remains available. Prepare and create above that portion with high_priority_available; assert early_partial, the constrained instantPortionUsd, and the remainder in settlementPortionUsd.
  7. Prepare another high-priority amount above that portion, then set W1 to the current committedExposureW1Usd before create. Create succeeds but changes to normal timing; render the create response without retrying.
  8. Restore both original cap values, even if any assertion fails.

Simulate ACH returns

Prepare a normal direct deposit, then create it with a supported ACH return code:
The deposit is created normally. The simulated bank then returns it asynchronously. Poll the deposit until status: "returned" and reconcile the achDebitReturn.created webhook or GET /v1/integrator/ach-debit/returns. Sandbox accepts R01 through R39, plus R45 and R51. All 41 codes run through the same provider-backed return path and have been exercised end to end. Use a fresh sandbox user for each independent return so a prior source or user action cannot affect the next case. Start with R01, R05, and R10 as representative provider-path tracers. Assert that the return record contains a valid userAction, then route that value through the same handler as every other code. Test all four userAction branches with fixtures. Production return escalation policy is intentionally not public. Always branch on the returned userAction, never recreate a threshold from return history. Test userAction: "restricted" with a return-record fixture in your frontend contract suite.
A simulated return can report completed, or an aggregate failed/refunded crypto release outcome, before changing to returned. Keep processing later deposit and return updates. After returned, boundedly reconcile the funding source until it is disabled; a cached source read can lag briefly.
Use exactly one of riskSimulation, returnSimulation, or lifecycleSimulation. See ACH returns for the frontend contract.

ACH debit release checklist

Before production, record a pass for each row. Use fresh users except where a scenario explicitly tests repeated behavior. The sandbox does not currently provide a deterministic debit-submission failure, unknown return-code, ACH notice-of-change, or access-restored profile. Record these as contract-tested rather than sandbox-executed:

Test webhook failure and recovery

Use a disposable HTTPS receiver such as Webhook.site; you do not need to deploy test infrastructure. Register it for achDebitReturn.created, configure it to return 503, then run an R01 return simulation. Expect three signed POST attempts with the same payload. Then read GET /v1/integrator/webhooks/deliveries: the matching final record has success: false, responseStatus: 503, and no error. Reconcile the return through GET /v1/integrator/ach-debit/returns/{id} rather than waiting for another webhook. Repeat with a receiver that returns 2xx to verify the accepted path. See Webhooks for 4xx, timeout, and transport-error handling. To test an unknown outcome, configure the receiver to delay its response for 10 seconds. Expect three signed attempts. The delivery record has success: false, responseStatus: 504, and a non-empty error. Do not treat that as proof the event was unprocessed; reconcile the return and make the handler safe to repeat.

Prove push deduplication and ordering

Register the receiver for all five achDebit.* communication events. Capture the achDebit.authorized, achDebit.deliveryProgress, and achDebit.delivered payloads from one partial_then_full_delivery lifecycle simulation. Then run the captured payloads through the same durable handler used in production:
  1. Process the same achDebit.deliveryProgress payload twice. Assert one webhook-inbox row and one push-outbox row for its eventId.
  2. Process achDebit.delivered, then process the earlier achDebit.deliveryProgress payload. Assert the lower sequence is recorded as stale and creates no push.
  3. Process the same achDebit.delivered payload again after a simulated timeout. Assert its stable eventId still creates no second push.
  4. Fail one semantic delivery, read its exact payload from GET /v1/integrator/webhooks/deliveries, and feed that payload into the handler. Assert it follows the same inbox and sequence rules.
Do not implement this test by fetching the deposit and comparing its current status. The semantic payload, eventId, and sequence are the notification occurrence; API reads are only for current UI reconciliation.

Reset a funding source

DELETE /v1/sandbox/funding-sources/{fundingSourceId} removes a funding source so you can re-run a clean flow. A source disabled after a return cannot be linked again once removed. Use a fresh sandbox user for independent return scenarios, or link both deterministic account slots before testing repeated returns on one user. The /v1/sandbox/bills/* endpoints similarly simulate bill activation and verification challenges. See the API Reference for the full set of sandbox endpoints and their request shapes.

Testing the off-ramp

The sandbox covers the off-ramp API and the fiat side, with one deliberate exception worth planning around.

There is no testnet — the chain leg is real mainnet

We’ll be straight with you about this one, because it affects how you plan your test cycle: there is no test chain for the off-ramp. The way to exercise a real off-ramp end to end is a small mainnet transaction with real USDC — $1 is plenty. Yes, real money, even in sandbox. What you get for it is a full live flow with nothing stubbed, which is worth a great deal more than a mock right before you launch. Two things take the sting out:
  • The USDC lands in a Spritz wallet. Tell us what you sent and we’ll return it.
  • No real fiat is paid out in sandbox. You don’t need a real bank account — add a dummy one. The routing number must be genuinely valid (we validate it, so use a real bank’s), but the account number can be anything. Sandbox is the only place a made-up account number is the right answer.
One more small-transaction nuance: sandbox pricing is not the published production pricing — always read the fee from the quote’s fees/input fields rather than assuming the rate card applies to a test run.
In production, quote creation is gated on live-money approval: until your production integrator is approved for live testing, POST /v1/off-ramp-quotes fails with 400 and detail The integrator is not approved to move live money (sandbox_simulated_money_only). That gate does not apply to the sandbox environment.

Where a sandbox off-ramp stops

The crypto leg runs for real and the quote reaches confirmed. The fiat leg is simulated: the off-ramp is created and queues (awaiting_fundingqueued), and payment.created/payment.updated webhooks fire — but no payout is submitted, so the record never reaches in_flight or completed and there is no payment.completed event in sandbox. Test those states in production with a small live run.

Terms acceptance is headless

Where production asks the user to complete the provider’s hosted terms flow, the sandbox accepts any opaque agreementId:
The provider activates the customer asynchronously afterwards, so a small retry window applies before auto-ramp account creation succeeds — see On-ramp.

Testing the on-ramp

Simulate a fiat deposit arriving at an auto-ramp account — no real funds move, but the full conversion path runs, the on-ramp settles to completed, and real onramp.* webhooks fire:
The response carries the resulting onRampId, fees, and output; reconcile it through GET /v1/on-ramps/{id}.

Going to production

1

Swap the base URL

Point requests at https://platform.spritz.finance.
2

Use production credentials

Sandbox and production credentials are separate. Move your production integrator key and secret into your secrets manager.
3

Complete production approval

Live money movement is gated per integrator. If quote creation answers 400 with sandbox_simulated_money_only (or another live-money block reason), your production approval isn’t finished — talk to us.
4

Run real verification

Remove any sandbox KYC bypass; real users complete identity verification before moving money.