Idempotency and Retries
Related
"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_idis your own reference. PayMe stores and returns it but does not reject duplicates: eachgenerate-salecreates a new sale with its ownpayme_sale_id.generate-salewithbuyer_keycreates a new sale on every call before it charges the token, so a refused charge still leaves a sale behind.- Template sales: each
pay-saleon 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 accepts these filters:
sale_payme_id— PayMe's sale ID (one sale);transaction_id— your order ID. Becausetransaction_idis 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
- Retry only on network errors, timeouts, HTTP 429 code
998, or codes991,20002and999, whose outcome is unknown. Never retry a response withstatus_code1that names a problem with your input. - On
998, wait (honour theRetry-Afterheader), then check the sale before retrying. - Before retrying anything that moves money (
pay-sale,refund-sale,capture-sale,generate-salewithbuyer_key), check the sale's current state as above. - Store PayMe's
payme_sale_idas soon asgenerate-salereturns, so every later step refers to one sale. - Treat callbacks as confirmation, and process each one once — see Callbacks.
PayMe's own retries
- Callbacks: up to 3 attempts within about 90 seconds (30 s, then 60 s), then dropped. Details on 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 status5and sendsub-cancel, withoutsub-failure. Details on Recurring Payments.
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 andget-future-withdrawals. By default it allows 100 requests per 10 seconds.get-transactionshas the same limit; it is counted per partner when you sendpayme_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_code998,status_additional_info"Too Many Attempts.", and the headersX-RateLimit-Limit,X-RateLimit-Remaining,Retry-AfterandX-RateLimit-Reset. refund-saleandget-saleshave 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 partners@payme.io 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.