Idempotency
Which retries Strimz makes safe today, at the API, in the queues, and on chain.
Idempotency means an operation can be run more than once but only takes effect once. Without it, a network blip during a refund could turn into a double refund.
Strimz has idempotency at the API, in the queues and on chain.
At the API
The SDK sends an X-Strimz-Idempotency-Key header on every POST and
PATCH: a fresh strimz_<uuid> by default, or the value you pass.
The API remembers each key for 24 hours, per merchant and mode:
- Same key, same request: you get the first response again, with
the header
X-Strimz-Idempotent-Replayed: true. Nothing runs twice and no second webhook is sent. - Same key, different request (other path or body):
409 idempotency_error. - Same key while the first request is still running:
409 idempotency_in_progresswithRetry-After: 1.
Every response is stored, errors included. If a request failed and you want to try it again as a new attempt, use a new key.
A key is 1 to 255 visible ASCII characters, so a value from your own
system, like order-1234, works. Anything else is 400 invalid_request.
Keys are ignored on routes that return a one-time secret: creating or rotating an API key, and creating a webhook endpoint or rotating its secret. Strimz never stores those responses. A repeat makes a spare key that you can revoke.
If Strimz can't reach its idempotency store, the write fails with 503 service_unavailable rather than running without the check.
SDK retries
The SDK retries network failures, 429 responses (honouring
Retry-After), 409 idempotency_in_progress, and 5xx responses for
GET and for calls that carry an idempotency key. A retry reuses the
same key, so it gets the first request's response instead of creating
a second resource.
In the queues
Payer-signed submissions to /v1/relay/payments and
/v1/relay/subscriptions do not use a key you choose. Strimz derives
the job ID from the signed calldata and returns it as idempotencyKey.
Resending the same signed payload returns the same submission, and a
newly signed retry is a new submission. A retry for a payment session
cannot become a second charge: every authorization for the session uses
the same EIP-3009 nonce, which the token accepts once per payer wallet,
and Strimz refuses a new attempt while an earlier one can still land.
See the relay section of the endpoint list.
Subscription charges are guarded by chargeLock: the sweeper locks
a row before it enqueues the charge, so two scheduler replicas cannot
queue the same subscription at once.
Webhook deliveries carry an X-Strimz-Delivery-Id that stays the same
across retries. A delivery job that runs again after the delivery is
already delivered or permanently_failed does nothing.
On-chain
The contracts use deterministic identifiers for state-changing actions:
PaymentSession.idis passed asbytes32 refon the on-chainpay()call. The contract emits it inPaymentExecuted. The indexer keys the resultingTransactionrow on(onchainTxHash, logIndex). Replays no-op.chargeAttemptIdiskeccak256(abi.encodePacked(onchainSubId, currentPeriodEndAt, attempt)). The contract keeps ausedAttemptsmapping. Charging with an id that is already used does not revert: the contract emitsSubscriptionChargeSkippedwith outcomeDuplicateand moves no money.- Refund tx hashes are recorded in
Refund.refundTxHashafter the merchant signs the transfer. The indexer matches incoming ERC-20 Transfer events by hash. Replays of the same hash flip nothing.
Because the chain enforces these, a replayed signed transaction can't cause a duplicate charge.

