CityPay Idempotency Guide for Integrators
Idempotency ensures that your integration can safely retry API requests without the risk of processing the same operation more than once. This is essential when dealing with payments, where duplicated requests may lead to duplicated authorisations or incorrect state transitions.
The CityPay API provides first-class support for idempotency on all unsafe operations (such as payment intent authorisation, capture, cancellation and refund).
This guide explains how to use it.
An idempotent request can be sent multiple times and will always result in the same outcome as if it were sent once.
For example: • If you request a cancellation twice, • And both requests contain the same Idempotency-Key, • Only one cancellation will be performed.
This protects you from: • network timeouts, • client retries, • browser resends, • parallel requests, • duplicated clicks.
CityPay enables idempotency through a single request header:
Idempotency-Key:
Choosing a key
A key should be: • unique per logical operation, • stable if you need to retry the same request, • a string (typically 16–64 characters).
You can generate keys using: • your order ID, • your payment intent ID, • a UUID, • or any other unique token.
Important: if you retry the same operation, you must use the same key each time.
Default behaviour
All official CityPay SDKs automatically generate a new idempotency key for each request. You do not need to do anything for normal, single-attempt requests.
Providing your own key
If you expect to retry a request, you should supply your own stable key:
await citypay.paymentIntents.cancel(
{ intentId },
{ idempotencyKey: `cancel_${intentId}` }
);
This ensures retries and network issues do not cause duplicate actions.
When CityPay receives an idempotent request:
First call • The operation is executed normally. • The result is stored temporarily. • The same response is returned to subsequent callers.
Retry with same key and same data • No new processing occurs. • The previously stored result is returned immediately.
Retry with same key but different data • The request is rejected with:
HTTP/1.1 409 Conflict
Idempotency-Status: conflict
Concurrent requests
If multiple requests using the same key arrive simultaneously: • Only one request will be executed. • Others will wait briefly for the result. • If the first request takes too long, they may receive:
HTTP/1.1 202 Accepted
Idempotency-Status: in-progress
Retry-After: 2
Excessive concurrency
If too many attempts for the same key arrive at once:
HTTP/1.1 429 Too Many Requests
Idempotency-Status: throttled
Retry-After: 2
CityPay returns clear headers to indicate idempotency behaviour:
|| Header || Meaning || | Idempotency-Key | Echo of the key you supplied | | Idempotency-Status | new, replayed, in-progress, throttled, or conflict | | Retry-After | Advice for retry timing (used for 202/429 responses) | | cp-request-id | Unique ID for tracing and support |
✔️ Always set an idempotency key on any request you may retry
This ensures safety under network or operational issues.
✔️ Use meaningful, structured keys
Example:
order_12345_cancel
pi_98765_auth
refund_12345_attempt1
✔️ For Payment Intent workflows
Use the payment intent ID as part of the key to ensure natural grouping.
✔️ Avoid using the same key for different operations
Reusing a key with different payloads will result in conflict (409).
✔️ Log your idempotency keys
This makes reconciliation and troubleshooting much easier.
⸻
- Common Examples
Cancelling a payment intent
await citypay.paymentIntents.cancel(
{ intentId: "pi_12345" },
{ idempotencyKey: "cancel_pi_12345" }
);
Retrying after a timeout
try {
return await citypay.paymentIntents.authorise(data, { idempotencyKey });
} catch (e) {
// Retry using the same key
return await citypay.paymentIntents.authorise(data, { idempotencyKey });
}
Node / Fetch example
fetch("https://api.citypay.com/v6/paymentintents/123/cancel", {
method: "POST",
headers: {
"Idempotency-Key": "my-unique-key",
"cp-api-key": "",
"Content-Type": "application/json"
}
});
409: conflict
You used the same idempotency key but changed the request data.
202: in-progress
Another request with the same key is still being processed. Use Retry-After header to retry safely.
429: throttled
Too many concurrent requests using the same key. Apply retry/backoff logic.
Replayed responses
This is normal—you are receiving the stored result from the original request.
CityPay’s idempotency system: • prevents duplicate processing, • simplifies retry logic, • handles concurrency safely, • ensures consistent and predictable outcomes.
Using the Idempotency-Key header on any operation that may be retried is the simplest way to ensure reliable payment flows.