StrimzStrimz
Concepts

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.

const session = await strimz.paymentSessions.create(
  { amount: '50000000', currency: 'USDC', description: 'Pro plan, August' },
  { idempotencyKey: `strimz_${crypto.randomUUID()}` },
)

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_progress with Retry-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.id is passed as bytes32 ref on the on-chain pay() call. The contract emits it in PaymentExecuted. The indexer keys the resulting Transaction row on (onchainTxHash, logIndex). Replays no-op.
  • chargeAttemptId is keccak256(abi.encodePacked(onchainSubId, currentPeriodEndAt, attempt)). The contract keeps a usedAttempts mapping. Charging with an id that is already used does not revert: the contract emits SubscriptionChargeSkipped with outcome Duplicate and moves no money.
  • Refund tx hashes are recorded in Refund.refundTxHash after 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.

On this page