Idempotency-Key
Pass a UUID (or anything ≤ 64 chars) on any POST. The API caches the
first response for 24h keyed on
(api_key_id, route, idempotency_key). A retry with the same key
returns the same body, the same status, and the same event_id.
409 idempotency_body_mismatch so you don’t accidentally
think a divergent call landed.
Event ids are the canonical handle
Even withoutIdempotency-Key, every prepare and submit returns an
event_id. That id is durable, scoped to your API key, and visible
across the entire surface:
GET /v1/events/{id}— current phase, signature, error.GET /v1/events?...&id=...— same row in the filterable feed.- Explorer link —
https://explorer.leash.market/event/<id>deep links to the timeline view.
event_id next to your domain object the moment prepare
returns. It’s the cheapest and safest way to recover from a crash
between prepare and submit.
Submit is idempotent on event_id
POST /v1/submit ignores duplicate signed transactions for the same
event_id. Concretely:
- First call lands the tx, returns
202 { signature }. - Second call with the same
event_idreturns200 { signature, replayed: true }with no extra RPC traffic and no double-spend risk. - A second call with a different signed transaction for the same
event_idreturns409 event_already_submitted.
event_id as the dedup key in your job
queue: workers can retry “submit this” forever and the chain only
sees one transaction.
Receipt ingest is idempotent on (network, receipt_hash)
POST /v1/receipts/{agent} returns { duplicate: true, event_id: null }
on the second hit. No new event is written. Buyers and sellers can
reposting the same receipt is therefore free.
Retry strategy that just works
isRetryable should match 429, 5xx, and network errors. Do not
retry 4xx other than 429 — those signal a contract violation, not
flakiness.
What about confirmation?
Confirmation is a background job, not the response. After submit:- Poll
GET /v1/events/{id}every ~5s untilphaseisconfirmedorfailed. Cheap, cached. - Or subscribe via webhooks — every phase transition
(
prepared→submitted→confirmed|failed) and everyreceipt.published/receipt.pulledis pushed to your endpoint with an HMAC-signedX-Leash-Signatureheader. Webhook deliveries are at-least-once and idempotent on(webhook_id, event_id), so the same event/phase combination only reaches you once even if our worker retries.

