Idempotency
Replay-safe mutations through the Idempotency-Key header, claimed by the record itself.
Retries are a fact of distributed systems: clients time out, connections drop, workers die mid-run. SDP lets you replay a mutation safely by sending an Idempotency-Key header — a unique value (a UUID works well) that you mint once per logical operation and reuse on every retry of that operation.
curl -X POST https://api.solana.com/v1/payments/transfers \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: 8a2f6c1e-9d4b-4f3a-b7e5-2c8d9f0a1b3c" \
-H "Content-Type: application/json" \
-d '{ ... }'How replay resolution works
SDP's model differs from response-cache designs: the idempotency key is claimed by the business record itself, not by a snapshot of the original response.
- The first request creates the record — a transfer, a batch, an issuance transaction — carrying your key and a fingerprint of the normalized request payload.
- A retry with the same key and the same payload returns HTTP 200 with the stored record, in the normal success envelope. Because it is the live record and not a cached response, a replay reflects current state — a transfer that has since finalized comes back
finalized, not theprocessingyou would have seen on the first attempt. - A request with the same key but a different payload is rejected with 409
CONFLICT("Idempotency key already used with different request payload"). The fingerprint comparison is field-normalized, so formatting differences don't cause false conflicts.
Keys are retained for the lifetime of the record they claimed — there is no TTL, and a replay works for as long as the record exists.
The header itself is validated on every /v1 route (1–255 printable ASCII characters), but only the endpoints below consume it. Elsewhere it is accepted and ignored.
Where it applies
Each domain claims keys in its own namespace, and the namespaces are scoped differently:
| Domain | Operations | Key namespace |
|---|---|---|
| Payments — transfers | POST /v1/payments/transfers | Per project |
| Payments — batches | POST /v1/payments/transfer-batches | Per project |
| Issuance | Execute-mode deploy, mint, burn, force-burn, freeze, thaw, pause, unpause, seize, and authority transfer | Per organization, shared across all issuance operations and tokens |
| Custody | Provider credential submission (dashboard flow) | Required on every submission |
Namespaces are independent across domains — the same key value used on a transfer and on a mint are two separate claims that never collide. Within issuance, however, the namespace is organization-wide: reusing a key for a different issuance operation, even on a different token, returns 409.
This model is open to change
The per-domain differences above reflect the order in which these surfaces shipped, not a finished design, and we expect them to converge. Treat the core contract as stable — one key per logical operation, replay returns the record, payload mismatch returns 409 — and treat the scoping details (per-project vs per-organization namespaces, which endpoints consume the header) as subject to change. We are considering extending consumption to more mutation endpoints and unifying namespace scoping across domains.