Idempotency
Use idempotency keys to safely retry API requests without creating duplicate transactions.
Network requests fail. Servers time out. Users double-click buttons. Idempotency keys let you retry any of these scenarios safely — the Seev API will recognise a repeated request and return the original response instead of creating a second transaction.
Why it matters
Consider a customer checking out on your platform:
- Your server calls
Seev.charge()to create a payment session. - The request succeeds on Seev's side, but your server crashes before it receives the response.
- Your retry logic fires and calls
Seev.charge()again.
Without an idempotency key, this creates two separate checkout sessions for the same order — and if both are completed, the customer is charged twice. With an idempotency key tied to the order, the second call returns the already-created session instead of making a new one.
This is especially critical for:
- Charge creation — duplicate charges are the worst possible outcome for customer trust
- Payouts — sending money twice is difficult to reverse
- Serverless and edge functions — these environments retry on timeout by design
What happens without it
| Scenario | Without idempotency | With idempotency |
|---|---|---|
| Network timeout on charge | Second attempt creates a duplicate charge | Second attempt returns the original charge |
| Server restart mid-request | Order is retried, customer charged twice | Retry is a no-op, original charge returned |
| User submits form twice | Two sessions created, both potentially paid | One session, second submission ignored |
| Webhook retry from Seev | Your handler processes the event twice | Your handler deduplicates on the event ID |
How idempotency keys work
Pass an Idempotency-Key header with any POST request. The value can be any string up to 255 characters — it just needs to be unique per logical operation.
POST /v1/payments/charge HTTP/1.1
Authorization: Bearer <your_api_key>
Idempotency-Key: order_9f4e1a2b-3c7d-4e8f-a1b2-c3d4e5f60001
Content-Type: application/jsonSeev stores the response for 24 hours. Any request with the same key within that window returns the cached response immediately — no new resource is created, no charge is attempted.
If you submit the same key with different parameters, the API returns a 409 DUPLICATE_CHARGE error rather than silently overwriting the original.
Generating idempotency keys
A good idempotency key is derived from the logical operation, not generated randomly at call time. Tie it to something stable — your internal order ID, a composite of user ID + cart ID, etc. This way retries naturally produce the same key.
import { randomUUID } from 'crypto';
// Option 1: deterministic — derive from your order ID (recommended)
const idempotencyKey = `charge_${orderId}`;
// Option 2: random UUID — generate once and store with your order record
const idempotencyKey = `charge_${randomUUID()}`;Pass it to Seev.charge():
const charge = await Seev.charge({
type: 'checkout',
amount: 5000,
currency: 'GHS',
recipient: { name: 'Jane Doe', email: 'jane@example.com' },
idempotencyKey: `charge_${orderId}`,
});import (
"fmt"
"github.com/google/uuid"
)
// Option 1: deterministic
idempotencyKey := fmt.Sprintf("charge_%s", orderID)
// Option 2: random UUID
idempotencyKey := fmt.Sprintf("charge_%s", uuid.New().String())charge, err := seevpay.Charge(seevpay.ChargeRequest{
Amount: seevpay.Int64Ptr(5000),
Currency: "GHS",
IdempotencyKey: idempotencyKey,
})import uuid
# Option 1: deterministic
idempotency_key = f"charge_{order_id}"
# Option 2: random UUID
idempotency_key = f"charge_{uuid.uuid4()}"charge = seev.charge(
amount=5000,
currency="GHS",
recipient={"name": "Jane Doe", "email": "jane@example.com"},
idempotency_key=idempotency_key,
)// Option 1: deterministic
$idempotencyKey = "charge_{$orderId}";
// Option 2: random UUID
$idempotencyKey = 'charge_' . \Ramsey\Uuid\Uuid::uuid4()->toString();
// or without a library:
$idempotencyKey = 'charge_' . sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);$charge = Seev::charge([
'amount' => 5000,
'currency' => 'GHS',
'recipient' => ['name' => 'Jane Doe', 'email' => 'jane@example.com'],
'idempotency_key' => $idempotencyKey,
]);Best practices
Derive keys from your data, don't generate them fresh on every attempt. If you generate a new UUID each time your retry loop fires, you defeat the purpose — each attempt looks like a new request. Generate the key once, persist it alongside your order record, and reuse it on every retry.
Use a meaningful prefix.
Prefix keys with the operation type (charge_, payout_, etc.). This makes them identifiable in logs and prevents accidental collisions across different operation types that happen to share an ID.
Don't reuse keys across different operations.
An idempotency key is scoped to the endpoint it was first used on. Using the same key on /v1/payments/charge and /v1/payouts/send is safe since they are separate namespaces — but using the same key for two different charges on the same endpoint will return a 409.
Store the key with your order before calling the API. Write the idempotency key to your database before making the API call. This way, if your process crashes after the API responds but before you save the result, you can recover by retrying with the same key.
// 1. Reserve the idempotency key in your database
await db.orders.update({ id: orderId, idempotencyKey: `charge_${orderId}` });
// 2. Call the API — safe to retry with the same key
const charge = await Seev.charge({
// ...
idempotencyKey: `charge_${orderId}`,
});
// 3. Store the result
await db.orders.update({ id: orderId, checkoutUrl: charge.redirect_url });