Sandbox simulation API
Base URL: https://txnod.com/api/v1/sandbox. Every endpoint requires the same three HMAC headers as the rest of the API:
X-Project-Id— sandbox project ULID.X-Timestamp— current Unix timestamp (seconds), ±300-second window.X-Signature— HMAC-SHA256 hex digest of${timestamp}.${rawBody}using the sandbox API secret (sk_sandbox_...).
The SDK (client.sandbox.* per SDK reference) handles signing transparently. Prefer it over hand-rolled cURL except for smoke tests.
Authentication and cross-mode rules
Sandbox endpoints accept only sk_sandbox_... API secrets. A production secret targeting any sandbox endpoint is rejected at the gateway. The cross-mode check fires only on the four explicit-projectId endpoints ({projectId}/clock/advance, {projectId}/reset, DELETE {projectId}, {projectId}/wallets). The 10 invoice-scoped simulate endpoints have an implicit project_id (derived from the API key), so the cross-mode check is tautological there and does not fire.
Error taxonomy
Every sandbox endpoint can return any of the following sandbox-specific error codes:
| Status | error_code | Trigger |
|---|---|---|
| 400 | sandbox_key_against_production_project | Sandbox secret targeting production project. Fires only on the four explicit-projectId endpoints. |
| 400 | production_key_against_sandbox_project | Production secret targeting sandbox project. Fires only on the four explicit-projectId endpoints. |
| 403 | sandbox_project_required | Endpoint reached with non-sandbox project id (i.e. projects.kind is not 'sandbox'). |
| 404 | sandbox_invoice_not_found | Invoice id does not belong to the project. |
| 422 | sandbox_invoice_transition_invalid | Current invoice status does not permit the requested simulation. |
| 422 | sandbox_invoice_terminal | simulate-duplicate-delivery invoked but no terminal event exists yet. |
| 429 | sandbox_rate_limit_exceeded | Per-project simulate-* rate cap (per chain) exceeded. |
| 500 | sandbox_reset_failed | Internal error during reset() cascade. |
| 500 | sandbox_delete_failed | Internal error during DELETE cascade. |
Standard non-sandbox errors (validation_error, auth_invalid, signature_invalid, timestamp_out_of_window, internal_error) apply to every endpoint.
Invoice-scoped routes
Each route below acts on a single invoice belonging to the calling sandbox project. The project_id is implicit in the API key — cross-mode checks do not fire on these endpoints.
POST /sandbox/invoices/{invoiceId}/simulate-detect
Synthesizes an invoice.detected event (status pending → detected) and writes a synthetic invoice_transactions row whose confirmations=0 is the starting point for subsequent clock/advance calls. Idempotent on (invoice_id, 'detected').
Request body — all fields optional:
| Field | Type | Notes |
|---|---|---|
seed | string | Deterministic seed for the synthesized tx_hash. Same seed → same hash. |
Response 200:
| Field | Type |
|---|---|
event_id | string ULID |
status | 'detected' |
Errors: sandbox_invoice_not_found, sandbox_invoice_transition_invalid (invoice not in pending), sandbox_rate_limit_exceeded.
curl -X POST "https://txnod.com/api/v1/sandbox/invoices/$INVOICE_ID/simulate-detect" \
-H "X-Project-Id: $TXNOD_PROJECT_ID" \
-H "X-Timestamp: $(date +%s)" \
-H "X-Signature: $SIGNATURE" \
-H "Content-Type: application/json" \
-d '{"seed": "order-42"}'POST /sandbox/invoices/{invoiceId}/simulate-paid
Walks the invoice from detected → paid. Idempotent on event_id derived from (invoice_id, 'paid') so a re-confirm after a reorg replays with the same event_id.
Request body — empty {}.
Response 200:
| Field | Type |
|---|---|
event_id | string ULID |
status | 'paid' |
Errors: sandbox_invoice_not_found, sandbox_invoice_transition_invalid (invoice not in detected), sandbox_rate_limit_exceeded.
curl -X POST "https://txnod.com/api/v1/sandbox/invoices/$INVOICE_ID/simulate-paid" \
-H "X-Project-Id: $TXNOD_PROJECT_ID" \
-H "X-Timestamp: $(date +%s)" \
-H "X-Signature: $SIGNATURE" \
-H "Content-Type: application/json" -d '{}'POST /sandbox/invoices/{invoiceId}/simulate-overpaid
Walks the invoice from detected → overpaid. Pass either a multiplier (e.g. 1.5 for 150% of the invoice amount) or an explicit extra_units (chain-native smallest-unit string).
Request body — exactly one of:
| Field | Type | Notes |
|---|---|---|
multiplier | number > 1 | Factor applied to the invoice’s amount_crypto_units. |
extra_units | string | Additional smallest-units beyond the invoice amount. Pattern ^\d+$. |
Response 200: { event_id, status: 'overpaid' }. The webhook delivered to your handler is invoice.overpaid.
Errors: sandbox_invoice_not_found, sandbox_invoice_transition_invalid (invoice not in detected), validation_error, sandbox_rate_limit_exceeded.
curl -X POST "https://txnod.com/api/v1/sandbox/invoices/$INVOICE_ID/simulate-overpaid" \
-H "X-Project-Id: $TXNOD_PROJECT_ID" \
-H "X-Timestamp: $(date +%s)" \
-H "X-Signature: $SIGNATURE" \
-H "Content-Type: application/json" -d '{"multiplier": 1.5}'POST /sandbox/invoices/{invoiceId}/simulate-partial
Walks the invoice from detected → partial. Pass either a fraction (e.g. 0.5 for half the invoice amount) or an explicit amount_units.
Request body — exactly one of:
| Field | Type | Notes |
|---|---|---|
fraction | number ∈ (0, 1) | Fraction of amount_crypto_units actually received. |
amount_units | string | Explicit smallest-units received. Pattern ^\d+$. |
Response 200: { event_id, status: 'detected' } (invoice stays in detected; partial event fires).
Errors: sandbox_invoice_not_found, sandbox_invoice_transition_invalid, validation_error, sandbox_rate_limit_exceeded.
curl -X POST "https://txnod.com/api/v1/sandbox/invoices/$INVOICE_ID/simulate-partial" \
-H "X-Project-Id: $TXNOD_PROJECT_ID" \
-H "X-Timestamp: $(date +%s)" \
-H "X-Signature: $SIGNATURE" \
-H "Content-Type: application/json" -d '{"fraction": 0.5}'POST /sandbox/invoices/{invoiceId}/simulate-expire
Forces the invoice to expired.
Request body — empty {}.
Response 200: { event_id, status: 'expired' }.
Errors: sandbox_invoice_not_found, sandbox_invoice_transition_invalid (invoice already terminal), sandbox_rate_limit_exceeded.
curl -X POST "https://txnod.com/api/v1/sandbox/invoices/$INVOICE_ID/simulate-expire" \
-H "X-Project-Id: $TXNOD_PROJECT_ID" \
-H "X-Timestamp: $(date +%s)" \
-H "X-Signature: $SIGNATURE" \
-H "Content-Type: application/json" -d '{}'POST /sandbox/invoices/{invoiceId}/simulate-late-payment
Followup to simulate-expire. Walks the invoice from expired → expired_paid_late.
Request body — empty {}.
Response 200: { event_id, status: 'expired_paid_late' }.
Errors: sandbox_invoice_not_found, sandbox_invoice_transition_invalid (invoice not in expired), sandbox_rate_limit_exceeded.
curl -X POST "https://txnod.com/api/v1/sandbox/invoices/$INVOICE_ID/simulate-late-payment" \
-H "X-Project-Id: $TXNOD_PROJECT_ID" \
-H "X-Timestamp: $(date +%s)" \
-H "X-Signature: $SIGNATURE" \
-H "Content-Type: application/json" -d '{}'POST /sandbox/invoices/{invoiceId}/simulate-reorg
Walks the invoice from paid | overpaid | partial → reverted. Emits invoice.reverted.
Request body — empty {}.
Response 200: { event_id, status: 'reverted' }.
Errors: sandbox_invoice_not_found, sandbox_invoice_transition_invalid (invoice not in a terminal-paid state), sandbox_rate_limit_exceeded.
curl -X POST "https://txnod.com/api/v1/sandbox/invoices/$INVOICE_ID/simulate-reorg" \
-H "X-Project-Id: $TXNOD_PROJECT_ID" \
-H "X-Timestamp: $(date +%s)" \
-H "X-Signature: $SIGNATURE" \
-H "Content-Type: application/json" -d '{}'POST /sandbox/invoices/{invoiceId}/simulate-reconfirm
After a reorg, fresh paid event with a stable, deterministic event_id (not the original) — exercises the dispatcher’s idempotent re-emission contract.
Request body — empty {}.
Response 200: { event_id, status: 'paid' }.
Errors: sandbox_invoice_not_found, sandbox_invoice_transition_invalid (invoice not in reverted), sandbox_rate_limit_exceeded.
POST /sandbox/invoices/{invoiceId}/simulate-duplicate-delivery
Re-fires the most recent terminal webhook for this invoice with the SAME event_id — exercises the integrator’s idempotency layer.
Request body — empty {}.
Response 200: { event_id } (no status change).
Errors: sandbox_invoice_not_found, sandbox_invoice_terminal (no terminal event yet), sandbox_rate_limit_exceeded.
POST /sandbox/invoices/{invoiceId}/simulate-event
Low-level escape hatch. Synthesizes an arbitrary event with caller-supplied event_type, amount_units, confirmations, block_height, optional chain_specific payload, the expected_current_status precondition (rejects the call if the invoice is not in this status), and an optional seed.
Request body:
| Field | Type | Required | Notes |
|---|---|---|---|
event_type | enum of webhook event types | yes | One of invoice.detected, invoice.paid, invoice.overpaid, invoice.partial, invoice.expired, invoice.expired_paid_late, invoice.reverted. |
amount_units | string | yes | Pattern ^\d+$. |
confirmations | integer | yes | min: 0. |
block_height | integer | yes | min: 0. |
chain_specific | object | no | Chain-namespaced extra fields. |
expected_current_status | invoice status enum | yes | Precondition; the simulate call rejects unless the invoice is currently in this status. |
seed | string | no | Deterministic seed for synthesized hashes. |
Response 200: { event_id, status } reflecting the new invoice status.
Errors: sandbox_invoice_not_found, sandbox_invoice_transition_invalid (current status ≠ expected_current_status), validation_error, sandbox_rate_limit_exceeded.
Project-scoped routes
Each route below scopes by an explicit {projectId} in the path. Cross-mode checks fire here — a sandbox secret targeting a production project (or vice-versa) returns 400.
POST /sandbox/{projectId}/clock/advance
Increments the per-chain confirmation counter for every detected invoice in the project, walking detected invoices toward finalization. Per-chain rate-limited at 10 calls/min/project.
Request body:
| Field | Type | Required | Notes |
|---|---|---|---|
chain | enum | yes | One of btc, eth, tron, ada, polygon, bsc, ton. |
blocks | integer | yes | min: 1; max: 64. |
Response 200:
| Field | Type |
|---|---|
advanced | integer (count of invoices whose status changed) |
remaining | integer (count of detected invoices still below finalization) |
Errors: sandbox_project_required (project is not sandbox-kind), sandbox_key_against_production_project (cross-mode), production_key_against_sandbox_project (cross-mode), validation_error, sandbox_rate_limit_exceeded.
curl -X POST "https://txnod.com/api/v1/sandbox/$PROJECT_ID/clock/advance" \
-H "X-Project-Id: $TXNOD_PROJECT_ID" \
-H "X-Timestamp: $(date +%s)" \
-H "X-Signature: $SIGNATURE" \
-H "Content-Type: application/json" \
-d '{"chain": "btc", "blocks": 6}'POST /sandbox/{projectId}/reset
Soft-purges the project’s data tail (invoices, transactions, outbox events, address pool) while preserving the project shell, the seven xpubs, the chain bindings, the API key, and the sandbox PAT. Use as beforeAll/afterAll in test suites.
Request body — empty {}.
Response 200: { status: 'reset' }.
Errors: sandbox_project_required, sandbox_key_against_production_project, production_key_against_sandbox_project, sandbox_reset_failed.
curl -X POST "https://txnod.com/api/v1/sandbox/$PROJECT_ID/reset" \
-H "X-Project-Id: $TXNOD_PROJECT_ID" \
-H "X-Timestamp: $(date +%s)" \
-H "X-Signature: $SIGNATURE" \
-H "Content-Type: application/json" -d '{}'DELETE /sandbox/{projectId}
Cascade-deletes the entire sandbox project — shell, xpubs, bindings, API keys, sandbox PAT, all data tail. Irreversible. Per F-20.
Request body — none.
Response 200: { status: 'deleted' }.
Errors: sandbox_project_required, sandbox_key_against_production_project, production_key_against_sandbox_project, sandbox_delete_failed.
curl -X DELETE "https://txnod.com/api/v1/sandbox/$PROJECT_ID" \
-H "X-Project-Id: $TXNOD_PROJECT_ID" \
-H "X-Timestamp: $(date +%s)" \
-H "X-Signature: $SIGNATURE"GET /sandbox/{projectId}/wallets
Read-only listing of per-chain sandbox xpubs (auto-provisioned at project creation).
Request body — none.
Response 200: WalletsListResponse — array of per-chain entries with chain, xpub, bound_at_iso. Schema mirrors the production wallets-list response.
Errors: sandbox_project_required, sandbox_key_against_production_project, production_key_against_sandbox_project.
curl -X GET "https://txnod.com/api/v1/sandbox/$PROJECT_ID/wallets" \
-H "X-Project-Id: $TXNOD_PROJECT_ID" \
-H "X-Timestamp: $(date +%s)" \
-H "X-Signature: $SIGNATURE"Related
- SDK reference:
client.sandbox.*— the typed client surface. - Sandbox projects — project-creation flow + the agent loop.
- Sandbox safety — seven-layer defense + the recommended CI assertion.
- Webhook event types — every event carries
mode: 'production' | 'testnet' | 'sandbox'.