Test helpers
The /v1/test_helpers endpoints make things happen on the sandbox's simulated chains: a deposit to a UDA or a vault, or a payout with the outcome you choose. They answer only to sandbox keys.

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

MethodPathWhat it does
GET/v1/test_helpers/optionsWhat the helpers accept in your sandbox, and the budget left
POST/v1/test_helpers/uda/depositsSend a deposit to one of your UDAs
POST/v1/test_helpers/vault/depositsSend a deposit to one of your vaults
POST/v1/test_helpers/payoutsCreate a payout whose outcome you choose
POST/v1/test_helpers/payouts/{payout_id}/releaseLet a held payout through
POST/v1/test_helpers/payouts/{payout_id}/failMake a held payout fail
GET/v1/test_helpers/simulationsList 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:

HeaderMeaning
X-RateLimit-LimitSimulations allowed per window
X-RateLimit-RemainingSimulations left in this window
X-RateLimit-ResetUnix 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
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.

Response (data, shortened)
{
  "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.

FieldRequiredDescription
uda_idYesA UDA in your sandbox
network_idYesA simulated network the UDA has an address on
assetYes{"native": true} or {"token": "<contract address>"}
amountYesA positive whole number of base units, digits only ("25000000" is 25 USDC)
settlementNosuccess (default), fail, or retry_then_success
presetNounderpaid, overpaid, wrong_network, wrong_asset, or dust. Labels the simulation; send the preset's facts from options as the other fields
cURL
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):

Response
{
  "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.

FieldRequiredDescription
vault_idYesA vault in your sandbox (GET /v1/vault lists them)
network_idYesA simulated network the vault has an address on
assetYes{"native": true} or {"token": "<contract address>"}
amountYesA positive whole number of base units
presetNoOnly dust applies
cURL
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.

FieldRequiredDescription
vault_idYesThe vault to send from
reference_idYesYour unique reference. A repeat is refused with 409
to_addressYesThe recipient address
network_symbolYesThe network, for example ethereum
assetYesThe asset symbol, for example USDT
amount / usd_amountOne of themA crypto amount or a USD amount, as in the payout API
typeNonormal (default) or forceful
expired_atNoWhen the payout expires, RFC 3339
remarkNoA free-text note
outcomeYessuccess, fail, or hold
cURL
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 receive payout.confirmed.
  • fail — the transaction fails on chain and you receive payout.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
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
curl -g 'https://api.stridge.com/v1/test_helpers/simulations?limit=20&filter[]=state:eq:failed' \
  -H 'X-API-Key: sk_sbx_...'
Response
{
  "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:

ParameterValuesDescription
waitobserved or completedobserved: until Stridge records the first object. completed: until the flow reaches its end
timeout_ms0–30000How 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" }
}
StatuserrorWhen
400idempotency key requiredA POST without Idempotency-Key
400idempotency key too longA key over 255 characters
400invalid request bodyThe body is not valid JSON
400unknown waitwait is not observed or completed
400malformed timeout_mstimeout_ms is not a non-negative integer
403Insufficient permissionsA payout helper called with a key without the payout scope
404not a sandbox environmentAny helper called with a live key
404uda not found / vault not found / payout not found / simulation not foundThe id is not in your sandbox
404network not simulatedThe network is not one of the simulated networks
404uda has no address on this networkThe UDA is not payable on that network
404vault has no wallet on this networkThe vault has no address on that network
409environment provisioningYour sandbox is still being set up
409environment resettingYour sandbox is being reset
409payout not heldRelease or fail on a payout that is not held; details.state has its status
409a request with this idempotency key is still in flightThe same key is already being processed
409duplicate reference_id for this vaultA payout reference_id you already used on this vault; details.payout_id and details.status name the payout that holds it
422uda_id, network_id, asset and amount are requiredA UDA deposit missing a field
422vault_id, network_id, asset and amount are requiredA vault deposit missing a field
422vault_id, reference_id, to_address, network_symbol and asset are requiredA payout missing a field
422state exactly one of amount and usd_amountA payout with both or neither
422invalid type, must be 'normal' or 'forceful'An unknown payout type
422unknown assetThe asset is not carried by the network
422malformed amountamount is not a positive whole number of base units
422unknown settlement outcomesettlement is not one of the listed values
422asset not accepted by the udaA settlement stated for an asset the UDA does not accept; details.accepted_assets lists the ones it does
422unknown presetThe preset does not exist, or does not apply to this helper
422unknown payout outcomeoutcome is not success, fail, or hold
422idempotency key replayed with a different bodyA key reused for another request
429simulation budget exhaustedThe hourly budget is spent; see the X-RateLimit-* headers
Was this page helpful?