Seev Business API

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:

  1. Your server calls Seev.charge() to create a payment session.
  2. The request succeeds on Seev's side, but your server crashes before it receives the response.
  3. 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

ScenarioWithout idempotencyWith idempotency
Network timeout on chargeSecond attempt creates a duplicate chargeSecond attempt returns the original charge
Server restart mid-requestOrder is retried, customer charged twiceRetry is a no-op, original charge returned
User submits form twiceTwo sessions created, both potentially paidOne session, second submission ignored
Webhook retry from SeevYour handler processes the event twiceYour 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/json

Seev 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 });

On this page