A test helper never writes Stridge records directly. It puts a transfer on a simulated chain, and Stridge picks it up exactly as it would in live: the deposit, settlement, payout, and webhooks all come from the normal pipeline. That is why a simulation tests your integration, not just your test setup.
Each call creates a simulation: a record of what you asked for and what came of it. See Simulations for how each one plays out.
Endpoints
| Method | Path | What it does |
|---|---|---|
GET | /v1/test_helpers/options | What the helpers accept in your sandbox, and the budget left |
POST | /v1/test_helpers/uda/deposits | Send a deposit to one of your UDAs |
POST | /v1/test_helpers/vault/deposits | Send a deposit to one of your vaults |
POST | /v1/test_helpers/payouts | Create a payout whose outcome you choose |
POST | /v1/test_helpers/payouts/{payout_id}/release | Let a held payout through |
POST | /v1/test_helpers/payouts/{payout_id}/fail | Make a held payout fail |
GET | /v1/test_helpers/simulations | List your simulations |
GET | /v1/test_helpers/simulations/{simulation_id} | Read one simulation, optionally waiting for it |
All of them take a sandbox key in X-API-Key on https://api.stridge.com. A live key gets 404 (not a sandbox environment) on every one of them.
Any sandbox key can read options and simulations and simulate deposits, whatever its scopes. The payout helpers need a key with the payout scope; without it they answer 403.
Idempotency
Every POST helper requires an Idempotency-Key header. A helper call pulls a lever on a chain, so a retry must never pull it twice.
- Same key, same body: you get the first answer again, with
Idempotency-Replay: true. No new simulation is created and no budget is spent. - Same key, different body:
422(idempotency key replayed with a different body). One key names one request across every helper in your sandbox. - Same key while the first request is still running:
409(a request with this idempotency key is still in flight). Retry shortly. - No key:
400(idempotency key required). A key longer than 255 characters:400(idempotency key too long).
Keys are remembered for 24 hours. A refused request is not remembered, so you can fix the body and retry with the same key.
Budget and rate limits
Each sandbox can run 600 simulations per hour, in a fixed one-hour window. Every deposit, payout, release, and fail spends one. Reads (options, simulations) and replays spend nothing.
When the budget runs out, the helpers answer 429 (simulation budget exhausted) with these headers:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Simulations allowed per window |
X-RateLimit-Remaining | Simulations left in this window |
X-RateLimit-Reset | Unix time when the window resets |
GET /v1/test_helpers/options reports the same numbers in budget. The helper endpoints also share the sandbox's per-minute request rate limit.
Read the options
GET /v1/test_helpers/options lists what you can name in a helper call: your UDAs and vaults, the simulated networks with their assets, the presets, every lever value, the webhook event types a simulation can cause, and how fast each network moves.
curl 'https://api.stridge.com/v1/test_helpers/options?uda_id=31d32b68-e455-42b7-9d39-d2904adbe1b8' \
-H 'X-API-Key: sk_sbx_...'With uda_id, the udas list holds just that UDA and every preset carries its facts for it: the exact network_id, asset, and amount to send.
{
"budget": { "limit": 600, "window_s": 3600, "remaining": 594 },
"udas": [
{
"id": "31d32b68-e455-42b7-9d39-d2904adbe1b8",
"network_ids": ["60", "9001", "8453", "614", "9006", "966"],
"cross_chain": true,
"cross_chain_routes": [
{ "network_id": "60", "asset": { "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } }
],
"accepted_assets": [
{ "network_id": "60", "asset": { "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } }
]
}
],
"vaults": [
{ "id": "550e8400-e29b-41d4-a716-446655440000", "label": "hot", "network_ids": ["60", "195"] }
],
"networks": [
{
"network_id": "60",
"name": "Ethereum",
"family": "evm",
"assets": [
{ "symbol": "ETH", "token": null, "decimals": 18 },
{ "symbol": "USDC", "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "decimals": 6 }
],
"levers": { "deposit": true, "payout_outcomes": ["success", "fail", "hold"], "bridge": false }
}
],
"presets": [
{
"id": "underpaid",
"label": "Underpaid",
"description": "Less than the address usually receives, in an asset it accepts.",
"applies_to": ["uda_deposit"],
"facts": {
"network_id": "60",
"asset": { "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" },
"amount": "1000000"
}
}
],
"settlement_outcomes": ["success", "fail", "retry_then_success"],
"payout_outcomes": ["success", "fail", "hold"],
"event_types": [
"deposit.confirmed",
"vault.deposit.detected",
"uda.settlement.created",
"uda.settlement.retrying",
"uda.settlement.completed",
"uda.settlement.failed",
"payout.confirmed",
"payout.failed",
"payout.expired"
],
"timing": {
"networks": [
{ "network_id": "60", "block_time_ms": 1000, "observable_blocks": 1, "confirmation_blocks": 12, "confirmation_delay_ms": 0 }
]
}
}Use networks[].assets to name an asset: a native coin is {"native": true}, a token is {"token": "<contract address>"}. Read timing to size your test timeouts instead of hard-coding them; the ids and numbers above are only an example. The example also leaves out bridge_outcomes, which you can ignore because the bridge lever is not available, and the "livemode": false that every object in a sandbox response carries.
Simulate a UDA deposit
POST /v1/test_helpers/uda/deposits sends money to one of your UDAs on a simulated network.
| Field | Required | Description |
|---|---|---|
uda_id | Yes | A UDA in your sandbox |
network_id | Yes | A simulated network the UDA has an address on |
asset | Yes | {"native": true} or {"token": "<contract address>"} |
amount | Yes | A positive whole number of base units, digits only ("25000000" is 25 USDC) |
settlement | No | success (default), fail, or retry_then_success |
preset | No | underpaid, overpaid, wrong_network, wrong_asset, or dust. Labels the simulation; send the preset's facts from options as the other fields |
curl -X POST 'https://api.stridge.com/v1/test_helpers/uda/deposits' \
-H 'X-API-Key: sk_sbx_...' \
-H 'Idempotency-Key: deposit-retry-1' \
-H 'Content-Type: application/json' \
-d '{
"uda_id": "31d32b68-e455-42b7-9d39-d2904adbe1b8",
"network_id": "60",
"asset": { "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" },
"amount": "25000000",
"settlement": "retry_then_success"
}'It answers 201 with the simulation and a Location header pointing at it (/test_helpers/simulations/{id}, relative to /v1):
{
"success": true,
"message": "success",
"data": {
"id": "019a1b2c-3d4e-7f60-8a9b-0c1d2e3f4a5b",
"object": "simulation",
"environment_id": "3f2c1d9e-8a47-4b6e-9c11-2d5e7f8a9b04",
"livemode": false,
"kind": "uda_deposit",
"preset": null,
"request": {
"uda_id": "31d32b68-e455-42b7-9d39-d2904adbe1b8",
"network_id": "60",
"asset": { "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" },
"amount": "25000000",
"settlement": "retry_then_success"
},
"state": "running",
"chain": { "network_id": "60", "hash": "0x9c1e…4b7a" },
"caused": [],
"epoch": 2,
"error": null,
"outcome": null,
"created_at": "2026-10-25T09:46:50Z",
"updated_at": "2026-10-25T09:46:50Z"
}
}A deposit in an asset the UDA does not accept is still sent: Stridge records it and settles none of it. Stating a settlement for such a deposit is refused with 422 (asset not accepted by the uda), and details lists the assets the UDA does accept on that network. The wrong_asset preset with settlement: "success" is allowed.
Simulate a vault deposit
POST /v1/test_helpers/vault/deposits sends money to one of your vaults, for example to fund it before testing payouts.
| Field | Required | Description |
|---|---|---|
vault_id | Yes | A vault in your sandbox (GET /v1/vault lists them) |
network_id | Yes | A simulated network the vault has an address on |
asset | Yes | {"native": true} or {"token": "<contract address>"} |
amount | Yes | A positive whole number of base units |
preset | No | Only dust applies |
curl -X POST 'https://api.stridge.com/v1/test_helpers/vault/deposits' \
-H 'X-API-Key: sk_sbx_...' \
-H 'Idempotency-Key: fund-vault-1' \
-H 'Content-Type: application/json' \
-d '{
"vault_id": "550e8400-e29b-41d4-a716-446655440000",
"network_id": "60",
"asset": { "token": "0xdAC17F958D2ee523a2206206994597C13D831ec7" },
"amount": "500000000"
}'It answers 201 with a simulation of kind vault_deposit. The vault deposit sends vault.deposit.detected, and the simulation completes once the vault is credited.
Simulate a payout
POST /v1/test_helpers/payouts creates an ordinary payout, as POST /v1/transfer/payout would, and decides first what happens to its transaction. Fund the vault with a vault deposit before you send from it.
| Field | Required | Description |
|---|---|---|
vault_id | Yes | The vault to send from |
reference_id | Yes | Your unique reference. A repeat is refused with 409 |
to_address | Yes | The recipient address |
network_symbol | Yes | The network, for example ethereum |
asset | Yes | The asset symbol, for example USDT |
amount / usd_amount | One of them | A crypto amount or a USD amount, as in the payout API |
type | No | normal (default) or forceful |
expired_at | No | When the payout expires, RFC 3339 |
remark | No | A free-text note |
outcome | Yes | success, fail, or hold |
curl -X POST 'https://api.stridge.com/v1/test_helpers/payouts' \
-H 'X-API-Key: sk_sbx_...' \
-H 'Idempotency-Key: payout-hold-1' \
-H 'Content-Type: application/json' \
-d '{
"vault_id": "550e8400-e29b-41d4-a716-446655440000",
"reference_id": "order-12345",
"to_address": "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B",
"network_symbol": "ethereum",
"asset": "USDT",
"amount": "100.50",
"outcome": "hold"
}'It answers 201 with a simulation of kind payout. Its caused list names the payout, so you have the payout_id for the next step.
success— the transaction confirms and you receivepayout.confirmed.fail— the transaction fails on chain and you receivepayout.failed.hold— the transaction is broadcast and held outside the chain until you release it or fail it. Use it to test what your system shows while a payout is in flight.
Release or fail a held payout
POST /v1/test_helpers/payouts/{payout_id}/release lets a held transaction into a block, and the payout confirms. POST /v1/test_helpers/payouts/{payout_id}/fail gives it a failure instead. Neither takes a body; both need an Idempotency-Key.
curl -X POST 'https://api.stridge.com/v1/test_helpers/payouts/7c9e6679-7425-40de-944b-e07fc1f90ae7/release' \
-H 'X-API-Key: sk_sbx_...' \
-H 'Idempotency-Key: release-order-12345'Each answers 201 with a simulation of kind payout_release or payout_fail. Only a payout created with outcome: "hold" can be decided, and only once. Anything else is refused with 409 (payout not held), and details.state gives the payout's current status. A hold does not last forever: decide it promptly after the hold simulation completes, or the held transaction is dropped and the decision is refused.
List and read simulations
GET /v1/test_helpers/simulations lists your sandbox's simulations, newest first. It takes page and limit, and filters in the form filter[]=field:operator:value on kind, state, preset, created_at, and updated_at.
curl -g 'https://api.stridge.com/v1/test_helpers/simulations?limit=20&filter[]=state:eq:failed' \
-H 'X-API-Key: sk_sbx_...'{
"success": true,
"message": "success",
"data": [ { "id": "019a1b2c-3d4e-7f60-8a9b-0c1d2e3f4a5b", "object": "simulation", "state": "failed", "...": "..." } ],
"pagination": { "page": 1, "limit": 20, "total_pages": 1, "total_items": 1, "has_next": false, "has_prev": false }
}GET /v1/test_helpers/simulations/{simulation_id} reads one. Add wait to hold the request until the simulation gets somewhere:
| Parameter | Values | Description |
|---|---|---|
wait | observed or completed | observed: until Stridge records the first object. completed: until the flow reaches its end |
timeout_ms | 0–30000 | How long to wait. Default 10000; larger values are capped at 30000 |
A waiting read always answers 200 with the state the simulation reached, even if the wait timed out. Check state rather than assuming it completed. A simulation that stops (fails or expires) ends the wait early.
Errors
Errors use the standard error body:
{
"success": false,
"code": 409,
"error": "payout not held",
"details": { "state": "confirmed" }
}| Status | error | When |
|---|---|---|
400 | idempotency key required | A POST without Idempotency-Key |
400 | idempotency key too long | A key over 255 characters |
400 | invalid request body | The body is not valid JSON |
400 | unknown wait | wait is not observed or completed |
400 | malformed timeout_ms | timeout_ms is not a non-negative integer |
403 | Insufficient permissions | A payout helper called with a key without the payout scope |
404 | not a sandbox environment | Any helper called with a live key |
404 | uda not found / vault not found / payout not found / simulation not found | The id is not in your sandbox |
404 | network not simulated | The network is not one of the simulated networks |
404 | uda has no address on this network | The UDA is not payable on that network |
404 | vault has no wallet on this network | The vault has no address on that network |
409 | environment provisioning | Your sandbox is still being set up |
409 | environment resetting | Your sandbox is being reset |
409 | payout not held | Release or fail on a payout that is not held; details.state has its status |
409 | a request with this idempotency key is still in flight | The same key is already being processed |
409 | duplicate reference_id for this vault | A payout reference_id you already used on this vault; details.payout_id and details.status name the payout that holds it |
422 | uda_id, network_id, asset and amount are required | A UDA deposit missing a field |
422 | vault_id, network_id, asset and amount are required | A vault deposit missing a field |
422 | vault_id, reference_id, to_address, network_symbol and asset are required | A payout missing a field |
422 | state exactly one of amount and usd_amount | A payout with both or neither |
422 | invalid type, must be 'normal' or 'forceful' | An unknown payout type |
422 | unknown asset | The asset is not carried by the network |
422 | malformed amount | amount is not a positive whole number of base units |
422 | unknown settlement outcome | settlement is not one of the listed values |
422 | asset not accepted by the uda | A settlement stated for an asset the UDA does not accept; details.accepted_assets lists the ones it does |
422 | unknown preset | The preset does not exist, or does not apply to this helper |
422 | unknown payout outcome | outcome is not success, fail, or hold |
422 | idempotency key replayed with a different body | A key reused for another request |
429 | simulation budget exhausted | The hourly budget is spent; see the X-RateLimit-* headers |