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) and transaction_id (per-transaction lifecycle)
  • How event_id relates 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 uniqueevent_id is 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:

  1. Before every payment call, generate a fresh event_id (UUID4) and store it durably on the device.
  2. POST the payment with that event_id.
  3. Branch on the response:
    • 2xx — Parse the status field. You're done. Clear the stored event_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 stored event_id.
    • 5xx, timeout, or no response at all — The outcome is unknown. Proceed to step 4. Do not re-POST immediately.
  4. 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).
    • 200 with a terminal status — Surface that status. Clear the stored event_id.
    • 200 with pending or surcharge_pending — Keep polling; the transaction is still in flight.
    • 404 — The original POST never reached Koard. Re-POST once with the same event_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 OKstatus: 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_id once per attempt, on the client.
  • Persist the event_id durably until you've confirmed a terminal outcome — keychain on iOS, EncryptedSharedPreferences on Android, durable storage on backends.
  • Never reuse an event_id for 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_id in your receipts and merchant tooling so downstream lifecycle calls (capture, refund, etc.) have everything they need.