> ## Documentation Index
> Fetch the complete documentation index at: https://docs.droplinked.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotency and retries

> What is safe to repeat on the Droplinked API, how duplicate charges are prevented on the PSP side, and where you must supply your own dedupe key.

> [!WARNING]
>
> **Droplinked does not currently accept an `Idempotency-Key` request header.** Sending one
> has no effect — it is not read, not stored, and not honoured. Retry safety comes from the
> mechanisms below instead. This is stated plainly because assuming a header that does not
> exist is the most expensive wrong assumption a payments integration can make.

## Where duplicate-charge protection actually lives

Droplinked derives a **deterministic idempotency key for the outbound PSP call**, anchored
on the internal order id. A network blip, a deploy bounce, or an internal retry therefore
re-sends the same key to the provider and gets the provider's cached response back —
**the same payment intent, and no second authorisation on the buyer's card.**

| Provider | Key carried outbound |
| - | - |
| Stripe | `Idempotency-Key` — SHA-256 over `(orderId, attempt, environment)`, fixed length, no PII |
| PayPal | `PayPal-Request-Id` — the same hex prefix, trimmed to PayPal's 36-character limit |
| Paymob | a derived hash anchored on the order, not the internal order id |

The anchor is the **order id**, not the cart id and not the payment-intent id. A cart can be
forked into several checkout attempts; an order cannot. A new key is minted only when a
buyer explicitly re-attempts after a declined authorisation — which is the case where a
fresh intent is what you want.

## Per-endpoint retry safety

| Endpoint | Safe to retry? | Why |
| - | - | - |
| `POST /v2/orders` | **No — not idempotent.** | Creating an order snapshots and locks the cart. Repeating it creates a second order. Retry only after confirming the first call did not succeed, and carry your own request id so you can tell. |
| `POST /v2/orders/:orderId/payment-intent` | **Yes.** | The outbound PSP call carries the deterministic key, so a repeat returns the same intent rather than creating a second one. |
| `POST /v2/orders/:orderId/capture-paypal` | **Yes.** | Explicitly recovery-safe: an already-captured PayPal order confirms cleanly rather than re-capturing, and an already-confirmed order is a no-op. |
| `POST /v2/orders/:orderId/refund` | **Treat as not idempotent.** | Check order state before re-issuing. See the note below on refund scope. |
| Inbound webhooks | **Yes.** | At-least-once by design — see [Webhooks](/guides/payments/webhooks.md). |
| Cart mutations | **Effectively yes.** | Adding the same SKU to the same cart bumps quantity rather than creating a duplicate line. |

## Natural dedupe keys elsewhere on the platform

Several surfaces dedupe on a natural identifier rather than a supplied key. Where one
exists, it is documented on the endpoint's own page. Examples:

* Wallet registration dedupes on `merchantId : network : lowercased address`.
* Agentic intent recording dedupes on a caller-supplied `intentId`.
* Brand-attestation requests dedupe on `(shopSlug, PENDING)` — re-submitting returns the
  existing request id rather than a duplicate-conflict error.

## What you should do

1. **Generate your own correlation id per logical operation** and keep it in your logs. You
   will need it to reconcile a timeout, because the API will not dedupe for you.
2. **Never retry `POST /v2/orders` blind.** On a timeout, read back the order by cart before
   creating another.
3. **Retry `429` and `503`; do not retry `400`, `401`, `403`, `404`.** Back off using
   `Retry-After` — remembering it is in **milliseconds**, see
   [Rate limits](/api-reference/rate-limits.md).
4. **Treat the webhook as the source of truth for "paid".** Not the intent response, not
   the redirect return.

> [!NOTE]
>
> **Refund scope, stated precisely.** `POST /v2/orders/:orderId/refund` is merchant-only
> (a customer token is rejected) and today it services **Bonum-paid orders**: the use case
> verifies the order belongs to the caller's shop, is `CONFIRMED`, and was paid via Bonum
> before reversing it. It is not a universal cross-PSP refund route. Refunds for other
> providers are handled through that provider's own tooling — ask before you build a refund
> flow against this endpoint.

## Related

* [Order lifecycle](/guides/order-lifecycle.md)
* [Webhooks](/guides/payments/webhooks.md)
* [Rate limits](/api-reference/rate-limits.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.