Skip to Content
API ReferenceSandbox simulation API

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:

Statuserror_codeTrigger
400sandbox_key_against_production_projectSandbox secret targeting production project. Fires only on the four explicit-projectId endpoints.
400production_key_against_sandbox_projectProduction secret targeting sandbox project. Fires only on the four explicit-projectId endpoints.
403sandbox_project_requiredEndpoint reached with non-sandbox project id (i.e. projects.kind is not 'sandbox').
404sandbox_invoice_not_foundInvoice id does not belong to the project.
422sandbox_invoice_transition_invalidCurrent invoice status does not permit the requested simulation.
422sandbox_invoice_terminalsimulate-duplicate-delivery invoked but no terminal event exists yet.
429sandbox_rate_limit_exceededPer-project simulate-* rate cap (per chain) exceeded.
500sandbox_reset_failedInternal error during reset() cascade.
500sandbox_delete_failedInternal 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:

FieldTypeNotes
seedstringDeterministic seed for the synthesized tx_hash. Same seed → same hash.

Response 200:

FieldType
event_idstring 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:

FieldType
event_idstring 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:

FieldTypeNotes
multipliernumber > 1Factor applied to the invoice’s amount_crypto_units.
extra_unitsstringAdditional 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:

FieldTypeNotes
fractionnumber ∈ (0, 1)Fraction of amount_crypto_units actually received.
amount_unitsstringExplicit 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:

FieldTypeRequiredNotes
event_typeenum of webhook event typesyesOne of invoice.detected, invoice.paid, invoice.overpaid, invoice.partial, invoice.expired, invoice.expired_paid_late, invoice.reverted.
amount_unitsstringyesPattern ^\d+$.
confirmationsintegeryesmin: 0.
block_heightintegeryesmin: 0.
chain_specificobjectnoChain-namespaced extra fields.
expected_current_statusinvoice status enumyesPrecondition; the simulate call rejects unless the invoice is currently in this status.
seedstringnoDeterministic 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:

FieldTypeRequiredNotes
chainenumyesOne of btc, eth, tron, ada, polygon, bsc, ton.
blocksintegeryesmin: 1; max: 64.

Response 200:

FieldType
advancedinteger (count of invoices whose status changed)
remaininginteger (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"