Base URL
The sandbox is a separate environment with its own base URL and its own credentials: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: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.
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.Link a bank account without Plaid Link
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:
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 itspreparationId 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:
Simulate refund and crypto-release states
Prepare a normal-priority deposit forrefunded, 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.authorized → achDebit.deliveryProgress → achDebit.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 publicopen_exposure result without exposing or hardcoding a tier threshold:
- Read
limitsByPriority.normaland assertavailable: true. - Prepare
normal.maxAmountUsdwithpriority: "normal". - Create it through
/v1/sandbox/deposits/directwithlifecycleSimulation.profile: "full_delivery". Assertstatus: "completed",debitStatus: "settled",releaseStatus: "completed", andconfirmedReleasedAmountUsdequal to the prepared principal. - Re-read limits and repeat steps 2–3 at the new
normal.maxAmountUsdwhile normal remains available. Use a fresh preparation and idempotency key each time. - The final normal block must return
available: false,maxAmountUsd: "0.00",reason: "open_exposure",suggestedAction: "auto_ramp", a non-nullclearsAt, andclearsAtIsEstimate: true.
Simulate program and instant capacity
Two sandbox-only endpoints let you force the publicaggregate_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.
Use this acceptance sequence:
- Read and save the original response.
- Read an active source’s baseline
limitsByPriority. - With high priority enabled, prepare an amount no greater than both
high.maxAmountUsdandhigh.maxEarlyReleaseAmountUsd, then create withriskSimulation.profile: "high_priority_available". Assertearly_fulland the full principal ininstantPortionUsd. This is the deterministic full-instant case. - To test normal program capacity, set
aggregateCapW2Usdto the currentopenExposureW2Usdplus 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 returnsaggregate_exposure. - Set
aggregateCapW2Usdto"0.00". Normal becomes unavailable withreason: "rail_halted"andsuggestedAction: "auto_ramp". - Restore the original W2 cap. Then set
aggregateCapW1UsdtocommittedExposureW1Usdplus a small desired instant portion. Keep W2 unchanged. Re-read limits and verifyhigh.maxEarlyReleaseAmountUsdis constrained while normal remains available. Prepare and create above that portion withhigh_priority_available; assertearly_partial, the constrainedinstantPortionUsd, and the remainder insettlementPortionUsd. - Prepare another high-priority amount above that portion, then set W1 to the current
committedExposureW1Usdbefore create. Create succeeds but changes to normal timing; render the create response without retrying. - 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: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.
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 forachDebitReturn.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 fiveachDebit.* 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:
- Process the same
achDebit.deliveryProgresspayload twice. Assert one webhook-inbox row and one push-outbox row for itseventId. - Process
achDebit.delivered, then process the earlierachDebit.deliveryProgresspayload. Assert the lowersequenceis recorded as stale and creates no push. - Process the same
achDebit.deliveredpayload again after a simulated timeout. Assert its stableeventIdstill creates no second push. - Fail one semantic delivery, read its exact
payloadfromGET /v1/integrator/webhooks/deliveries, and feed that payload into the handler. Assert it follows the same inbox and sequence rules.
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.
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 reachesconfirmed. The fiat leg is
simulated: the off-ramp is created and queues (awaiting_funding → queued), 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 opaqueagreementId:
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 tocompleted, and real onramp.*
webhooks fire:
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.