# Idempotency and Retries Building with an AI agent? Start at https://docs.payme.io/agents.md ## Related - [Errors](/payments/getting-started/errors) - [Callbacks](/payments/getting-started/callbacks) - [Refund a sale](/payments/post-sale-actions/post-refund-sale) - [List sales](/payments/query-apis/post-get-sales) - [Authorize now, capture later](/accepting-payments/capture-and-saved-cards/capture-sale) "Idempotent" means that sending the same request twice has the same effect as sending it once. **PayMe has no idempotency key**, so you must make retries safe yourself. ## What happens when a request runs twice | Request | Second call while the first is **still running** | Second call **after** the first finished | |---------|-----------------------------------------------|------------------------------------------| | `generate-sale` | A second, separate sale | A second, separate sale — `transaction_id` is **not** checked for uniqueness | | `generate-sale` with `buyer_key` / token (creates **and charges**) | A second charge is possible | A second charge is possible | | `pay-sale` | Rejected: HTTP 429, code `998` "Duplicated action detected, please retry later" | If the first succeeded: rejected with HTTP 500, code `305`, current status in `status_additional_info`. If the first failed: accepted as a **new charge attempt**. | | `refund-sale` — partial | Rejected: HTTP 429, code `998` | **Runs again** — a second refund, as long as refundable balance remains | | `refund-sale` — full | Rejected: HTTP 429, code `998` | Rejected with code `305` | | `capture-sale` | Rejected: HTTP 429, code `998` | An authorization can be captured only once. | Notes: - **`transaction_id` is your own reference.** PayMe stores and returns it but does not reject duplicates: each `generate-sale` creates a new sale with its own `payme_sale_id`. - **`generate-sale` with `buyer_key`** creates a new sale on every call before it charges the token, so a refused charge still leaves a sale behind. - **Template sales:** each `pay-sale` on a template creates a new sale, so the protections above do not apply to template sales. - PayMe does not promise that a retried payment can never charge twice. Check before you retry. - An authorization lasts up to 168 hours; a capture after that fails. A void can release the full authorized amount or part of it. ## Before retrying, check the sale If a request timed out and you do not know whether it ran, look the sale up instead of resending it. [`POST /get-sales`](/payments/query-apis/post-get-sales) accepts these filters: - `sale_payme_id` — PayMe's sale ID (one sale); - `transaction_id` — your order ID. Because `transaction_id` is not unique, this can return several sales. **By default `get-sales` leaves out unpaid sales** (status `initial`). To find an unpaid sale, pass `sale_status` including `initial`. `get-sales` works with `seller_payme_id` alone; partners can also use `payme_client_key`. **How to tell whether a timed-out operation ran:** read `sale_status` from `get-sales` — a capture shows `completed` (not `authorized` or `failed`), a full refund `refunded`, a void a voided status. After a partial refund the status is `partial-refund`, and it stays `partial-refund` even once partial refunds reach the full amount, so track the refunded total yourself. `sale_status` alone cannot show whether a **retried partial refund** already ran: compare `sale_refund_buffer` (the amount still refundable, returned by `refund-sale` and sent in each `refund` callback — `get-sales` does not return it) or count the `refund` callbacks, each of which has its own `payme_transaction_id`. ## Retrying safely 1. Retry only on network errors, timeouts, HTTP 429 code `998`, or codes `991`, `20002` and `999`, whose outcome is unknown. Never retry a response with `status_code` `1` that names a problem with your input. 2. On `998`, wait (honour the `Retry-After` header), then **check the sale** before retrying. 3. Before retrying anything that moves money (`pay-sale`, `refund-sale`, `capture-sale`, `generate-sale` with `buyer_key`), check the sale's current state as above. 4. Store PayMe's `payme_sale_id` as soon as `generate-sale` returns, so every later step refers to one sale. 5. Treat callbacks as confirmation, and process each one once — see [Callbacks](/payments/getting-started/callbacks). ## PayMe's own retries - **Callbacks:** up to 3 attempts within about 90 seconds (30 s, then 60 s), then dropped. Details on [Callbacks](/payments/getting-started/callbacks). - **Subscriptions:** a failed scheduled charge, including the first, puts the subscription in status `7` ("Failed - Pending automatic retry") only when another attempt is allowed. PayMe retries once a day, up to 7 attempts in total. A daily subscription and the seventh consecutive failure instead put it in status `5` and send `sub-cancel`, without `sub-failure`. Details on [Recurring Payments](/payments/saved-cards-recurring). ## Rate limits - One **per-seller** limit is shared by `generate-sale`, `pay-sale`, `capture-sale`, `POST /sales`, `capture-buyer-token`, `POST /tokens`, the 3D Secure calls and `get-future-withdrawals`. By default it allows **100 requests per 10 seconds**. `get-transactions` has the same limit; it is counted per partner when you send `payme_client_key`. - PayMe can set a different limit for a partner. It then applies to each of the partner's sellers separately. Partners can read theirs from `GET /partners` (`technical.rate_limit_max_attempts`, `technical.rate_limit_decay_seconds`). - Over the limit: **HTTP 429**, `status_error_code` `998`, `status_additional_info` `"Too Many Attempts."`, and the headers `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `Retry-After` and `X-RateLimit-Reset`. - `refund-sale` and `get-sales` have no rate limit. The default applies to both environments unless PayMe has set a different limit for your partner. To request a higher limit, write to with the limit you need, whether it is a burst or a permanent need, and your current plan; increases are reviewed by PayMe's technical team. Until then, handle `429` with exponential backoff and request spacing. Keep's rate limit is documented on [Keep → Partner API overview](/keep/partners-api/overview#errors-and-rate-limits).