Idempotency
Every Koard payment request carries an event_id that uniquely identifies a single attempt. Use it to make your integration safe to retry on network failures, app crashes, or unclear outcomes — without ever charging a cardholder twice.
What You Learn
- The difference between
event_id(per-attempt) andtransaction_id(per-transaction lifecycle) - How
event_idrelates to traditional payment identifiers like the Retrieval Reference Number (RRN) - A safe retry protocol for SDK and REST integrations when a response is lost or delayed
- How to look up a transaction's outcome after a network failure
Before You Begin
- Read the Payment Lifecycle guide for an overview of how transactions move through their states.
- Have an authenticated API key or SDK session ready so you can call the lookup endpoint described below.
event_id and transaction_id
Koard uses two different identifiers, and they answer different questions.
| Identifier | Scope | Generated by | Purpose |
|---|---|---|---|
event_id |
A single API call (one attempt) | Your client (SDK or backend) | Idempotency key — guarantees that repeating the same call never produces a duplicate transaction |
transaction_id |
The entire lifecycle of a payment | Koard, on the first successful call | Groups all follow-up events (capture, tip adjust, incremental auth, reverse, refund) that act on the same original payment |
A single transaction can have multiple event_ids tied to it. For example, a hotel charge might look like:
| Operation | event_id (per attempt, unique) |
transaction_id (shared across the lifecycle) |
|---|---|---|
| Preauth | a1b2c3d4-…-1111 |
txn_2026_xyz |
| Incremental Auth | e5f6g7h8-…-2222 |
txn_2026_xyz |
| Tip Adjust | i9j0k1l2-…-3333 |
txn_2026_xyz |
| Capture | m3n4o5p6-…-4444 |
txn_2026_xyz |
Each line is a separate API call with a separate event_id. They all carry the same transaction_id so you can correlate them in reports, webhooks, and the dashboard.
If you've worked with card-network identifiers before, an event_id plays a similar role to the Retrieval Reference Number (RRN) attached to a single processor call — it identifies one specific attempt, not the broader transaction it's a part of.
Generating event_id
- Format: UUID4 (e.g.
b1f4d6a2-9c8e-4af7-b1d2-91a6e8c1f203). - Generated by your client, before the request leaves the device or your backend.
- Persisted durably by your client until you've confirmed the outcome (don't lose it to an app kill or process restart — it's the only way to look the attempt up later).
- Globally unique —
event_idis the primary key for the attempt, so re-using one will be rejected as a duplicate.
If you omit event_id on a request, Koard generates one for you. Don't rely on this — without a client-generated event_id saved before the call, you cannot safely retry or verify the outcome of a lost request.
How retries are protected
Every payment endpoint checks event_id against existing transactions before processing. The two possible outcomes:
| Server sees | Server returns | Meaning |
|---|---|---|
event_id not seen before |
Processes the payment, returns 2xx with the transaction details |
New attempt — handled normally |
event_id already used |
400 Bad Request — "This event ID already exists" |
Duplicate suppressed — the original attempt landed |
This is what makes the SDK retry protocol below safe.
Verifying a transaction's outcome
Whenever your client is unsure whether a previous request reached Koard (network drop, timeout, app killed mid-call), look the attempt up by event_id:
GET /v1/transactions/event/{event_id}
Authorization: Bearer <your_api_key>
Possible responses:
| Status | Meaning | What your client should do |
|---|---|---|
200 OK with a transaction body |
The request landed and was processed. The status field tells you the outcome. |
Surface the outcome to your code; do not retry the POST. |
404 Not Found |
Koard has no record of this event_id. |
Safe to retry the original POST with the same event_id. |
A transaction body returned from this endpoint includes status. Map it as follows:
status |
Terminal? | Notes |
|---|---|---|
captured |
✅ Yes | Funds taken |
settled |
✅ Yes | Funds finalized and batched |
authorized |
✅ Yes (for preauth flows) | Funds held; awaiting capture |
declined |
✅ Yes | Issuer rejected the card; surface to the user |
error |
✅ Yes | Processor or network error; treat as a failed attempt |
refunded |
✅ Yes | Refund completed |
reversed |
✅ Yes | Reversal completed |
canceled |
✅ Yes | Pre-auth or post-failure cancellation |
pending |
❌ No | Waiting on external input — poll again shortly |
surcharge_pending |
❌ No | Awaiting cardholder confirmation of a surcharge — poll again shortly |
Once you read a terminal status, the attempt is done — clear your local copy of the event_id and move on.
Alternative: reconcile from your own webhook events
Every transaction Koard processes also fires a webhook to any endpoint you've registered (see Webhooks). The webhook payload carries the same event_id and transaction_id that the API call returns, so you can use the webhook as an independent source of truth.
If your team already operates a backend with its own transaction store and APIs, you can layer a second recovery mechanism on top of the polling protocol above:
- Persist every webhook event into your own database, keyed by
event_id. - Expose a lookup in your own API (e.g.,
GET /your-backend/transactions?event_id=…). - When a client device can't reach Koard but can reach your backend, have it poll your API instead — your backend already knows the outcome from the webhook delivery.
This is optional. The GET /v1/transactions/event/{event_id} endpoint described above is the canonical source and is sufficient on its own. The webhook mirror is useful when your client only has connectivity to your own infrastructure, when you want a single reconciliation point that already aggregates other systems, or when you want to give your devices an alternate fallback path that doesn't depend on Koard reachability.
SDK retry protocol
Use this protocol whenever your client doesn't receive a clear 2xx or 4xx response from a payment call:
- Before every payment call, generate a fresh
event_id(UUID4) and store it durably on the device. - POST the payment with that
event_id. - Branch on the response:
2xx— Parse thestatusfield. You're done. Clear the storedevent_id.400 "This event ID already exists"— Your previous attempt already landed. Skip to step 4 to look up the outcome.- Other
4xx— A validation or authorization error. Surface to the caller. Clear the storedevent_id. 5xx, timeout, or no response at all — The outcome is unknown. Proceed to step 4. Do not re-POST immediately.
- Poll
GET /v1/transactions/event/{event_id}with backoff (suggested: 1 s, 2 s, 5 s, 10 s, 30 s, capped at ~2 minutes total).200with a terminal status — Surface that status. Clear the storedevent_id.200withpendingorsurcharge_pending— Keep polling; the transaction is still in flight.404— The original POST never reached Koard. Re-POST once with the sameevent_id, then resume polling.
Never re-POST a payment as the first response to a network failure. Always look it up by event_id first. The duplicate-event-id check protects you from double-charges, but only if you keep the same event_id across retries.
Worked example
A point-of-sale device taps a card. The SDK sends a Sale request with event_id=b1f4d6a2-…. Mid-flight, the device's Wi-Fi drops and the response never returns.
| Step | Client action | Outcome |
|---|---|---|
| 1 | Generated event_id=b1f4d6a2-… and stored it before tapping |
– |
| 2 | POSTed the Sale; connection lost waiting for response | Outcome unknown |
| 3 | After ~2 s of failed reachability, called GET /v1/transactions/event/b1f4d6a2-… |
200 OK — status: captured, transaction_id: txn_2026_xyz |
| 4 | Showed the cardholder a success screen; cleared the stored event_id |
Done — no double-charge risk |
Had step 3 returned 404, the client would have re-POSTed the Sale with the same event_id=b1f4d6a2-…. Koard would have either processed it as a new attempt (if the first POST never landed) or returned the duplicate-event-id error (if it had landed but the response was lost), at which point the client would resume polling.
Best practices
- Generate
event_idonce per attempt, on the client. - Persist the
event_iddurably until you've confirmed a terminal outcome — keychain on iOS, EncryptedSharedPreferences on Android, durable storage on backends. - Never reuse an
event_idfor a different attempt. If you want to start over (e.g., the cardholder taps "Cancel" and re-taps), generate a new one. - Cap your polling window (~2 minutes is a reasonable default) and surface "uncertain" to the merchant if it expires — they can verify in the dashboard.
- Surface the
transaction_idin your receipts and merchant tooling so downstream lifecycle calls (capture, refund, etc.) have everything they need.

