Idempotency
Retry a payment request without charging the customer twice.
An idempotency key lets you retry a request that may already have succeeded. When you send the same key twice, the API returns the original response instead of creating a second payment, so a timeout, a crashed process, or a double-clicked button costs the customer nothing.
Amounts in every example on this page are in the currency's smallest unit, so 5000 with currency GHS means GHS 50.00.
Retry a payment safely
Send an Idempotency-Key header on the POST request you want to protect. The header is optional on Checkout API payment initiation, and without it every call creates a new payment.
curl https://api.seevplus.com/api/v1/developer/payments \
-H "Authorization: Bearer $SEEV_CHECKOUT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: charge_order_10482" \
-d '{
"type": "checkout",
"amount": 5000,
"currency": "GHS",
"recipient": { "name": "Jane Doe", "email": "jane@example.com" },
"redirect_url": "https://yourapp.com/payment/callback"
}'Repeat that request with the same key and the same body, and you get the original payment back with 200. No second payment is created and no charge is attempted.
Choose a key
Derive the key from the operation it protects rather than generating one at call time. A key built from your order ID stays the same across every retry of that order, which is the property that makes retries safe. A fresh UUID per attempt makes each attempt look like new work and gives you no protection at all.
Prefix the key with the operation type, as in charge_order_10482, so the key is identifiable in logs and cannot collide with a payout that happens to share an ID.
Write the key to your own database before you call the API. If your process dies between the API responding and your code storing the result, the stored key is what lets you retry and recover the original payment.
// Reserve the key first, so a crash mid-request is recoverable.
await db.orders.update({
id: orderId,
idempotencyKey: `charge_order_${orderId}`,
});
const response = await fetch(
'https://api.seevplus.com/api/v1/developer/payments',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SEEV_CHECKOUT_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': `charge_order_${orderId}`,
},
body: JSON.stringify({
type: 'checkout',
amount: 5000, // smallest unit: GHS 50.00
currency: 'GHS',
recipient: { name: 'Jane Doe', email: 'jane@example.com' },
redirect_url: 'https://yourapp.com/payment/callback',
}),
},
);
const payload = await response.json();
await db.orders.update({
id: orderId,
checkoutUrl: payload.data.checkout_url,
});Handle a replayed or conflicting key
A key is scoped to the endpoint it was first used on, so the same value on a different endpoint starts a separate record rather than colliding. Keys are limited to 128 characters; a longer one is rejected before the payment is attempted.
A key is remembered for 24 hours from first use. After that window the same key is treated as new work and creates a new payment, so derive keys from the operation and retry within the day rather than resurrecting one later.
| Status | When you get it | What to do |
|---|---|---|
201 | The key was used before with the same body. The original response is replayed. | Treat it as success. Do not create a second order record. |
502 | The key was used before with a different body. | Do not retry as-is. Either send the original body with this key, or use a new key for the changed request. |
A conflicting key is rejected upstream, and SeevPlus returns that as 502 with the upstream text inside message:
{
"error": "Failed to initiate payment",
"message": "failed to initiate checkout: status code 409: {\"success\":false,\"error\":\"IDEMPOTENCY_CONFLICT\",\"message\":\"Duplicate idempotency key with different body\"}"
}Branch on the status and log the message, the same way you would for any other upstream failure. See Errors and codes for why the embedded code is not something to parse.
Treat idempotency as a safety net rather than a guarantee. It removes the common double-charge, but your own handler should still be safe to run twice: write the key before you call, and check your own records before you create a second order.
Deduplicate webhooks separately
An idempotency key protects requests you send. It does nothing for webhooks you receive, which Seev may deliver more than once for the same event. Deduplicate those on the event ID in your handler, and read Webhooks for the delivery and retry behaviour.