Core Concepts

Idempotency

Retry any mutating request safely by attaching an Idempotency-Key header. LuniPay returns the cached response instead of processing the request a second time.

Why it matters

Network failures happen. A POST that creates a checkout session might time out, or your process might crash after LuniPay wrote the row but before your code saw the response. Without idempotency you have no safe way to retry — you either risk a duplicate charge or you skip the retry and lose the order.

An idempotency key lets you retry with confidence. LuniPay detects the duplicate and returns the response from the original request, unchanged.

How to send a key

Attach an Idempotency-Key header to any mutating request (POST, PATCH, DELETE). The value is yours to choose — a UUID v4 or v7 is the standard pick.

curl https://lunipay.io/api/v1/customers \
  -H "Authorization: Bearer sk_test_YOUR_KEY" \
  -H "Idempotency-Key: 17e8a4cd-3c52-4e15-9d29-5a7cfe6e1eaa" \
  -H "Content-Type: application/json" \
  -d '{ "email": "kim@example.com", "first_name": "Kim", "last_name": "Chee" }'

Replayed responses

The first successful request does the work and caches the response. On any subsequent request with the same key (within 24 hours), LuniPay returns the original response body and status code, and adds a Idempotent-Replayed: true header so you can tell that you got a cached result rather than a fresh one.

Body mismatch

If you retry with the same key but a different request body, LuniPay refuses the request with 409 idempotency_error. That protects you from accidentally overwriting a prior request with different data. The rule is: same key means same request, always.

{
  "error": {
    "type": "invalid_request_error",
    "code": "idempotency_error",
    "message": "Idempotency key already used with a different request body."
  }
}

Scope and expiry

  • Scope: keys are scoped to the API key that issued them. Two different API keys using the same key value do not collide.
  • Expiry: keys are retained for 24 hours. After that the record is purged and the key can be reused for a brand-new request.
  • Live vs test: keys are also scoped to livemode. A test request never matches a live one.

What about errors?

Errors below 500 are cached — retrying a bad request with the same key returns the same error. Errors 500 and above are not cached; they indicate a LuniPay-side failure and are safe to retry with the same key, which will re-process the request.

Generating keys

Use a fresh random UUID for each logical request and stash it in your database before you send. If the request times out, your next retry pulls the stored key and tries again — you never have to wonder whether the previous attempt landed.

import uuid
import lunipay

lunipay.api_key = "sk_test_YOUR_KEY"

key = str(uuid.uuid4())

# Persist `key` alongside the pending order so you can retry.
session = lunipay.CheckoutSession.create(
    amount=5000,
    currency="usd",
    success_url="https://example.com/thanks",
    idempotency_key=key,
)

A rule of thumb

If two network calls would logically be the same — same user, same cart, same moment in time — use the same idempotency key. If any of those change, generate a new one.