# Boarding a Merchant with Fiserv You can board a Fiserv merchant either through the Koard Merchant Management System (MMS) UI or programmatically via the API. Koard targets Fiserv's **UMF Rapid Connect** interface (spec `UMF_RMY019_2026.02.16`, v15.04.4) over the Datawire transport. Per the UMF spec, a Fiserv merchant is identified at the integrator boundary by three merchant-supplied values — **MID**, **TID**, **MCC** — plus the **TPPID** that Koard owns and sets once per environment. Everything else needed to run an authorization is configured by Koard (TPPID, terminal-capability constants); a Datawire ID (DID) is used **only when Datawire is enabled** on the terminal. ## Fiserv Flavors Fiserv Rapid Connect fronts several acquiring platforms. Koard selects the right one per merchant via the **processor config** — you board with the same VAR-sheet shape shown on this page regardless of flavor; only the `processor_config_id` and the Fiserv-assigned `GroupID` differ. | Flavor | Front-end | Group ID | Capture / settlement | |---|---|---|---| | **Nashville** (Classic) | Nashville (Envoy) | `10001` | Host capture | | **Nashville North** | Nashville front-end → North back-end | `10001` | Terminal capture (North PTS) — pass `settlement_mid` | | **Cardnet North** | Cardnet / North | `30001` | Terminal capture (North PTS) — pass `settlement_mid` | | **Omaha** | Omaha (FDR) | `40001` | Hybrid-host capture | The `GroupID` in the VAR packet tells you which front-end the merchant sits on. Use the `processor_config_id` for that flavor; everything else on this page is identical across flavors. ## Before You Start Fiserv provisions the merchant on their side; Koard never makes a "create merchant" call. Once a merchant is set up on Fiserv's platform, your VAR sheet packet contains: | Provided by Fiserv | UMF tag | Format | What it is | |---|---|---|---| | **Merchant ID** (MID) | `MerchID` | `an` ..16 | Fiserv-assigned merchant identifier (UMF §3.1.11 — "A unique ID used to identify the Merchant. The merchant must use the value assigned by Fiserv.") | | **Terminal ID** (TID) | `TermID` | `an` ..8 | Per-terminal identifier (UMF §3.1.10 — "A unique ID assigned by Fiserv to identify a terminal."). In certification, all transactions must run on TID `00000001`. | | **Merchant Category Code** | `MerchCatCode` | `N` 4 | ISO 18245 4-digit MCC (UMF §3.1.13) | | **Group ID** (GID) | `GroupID` | `an` 5..13 | Assigned by Fiserv to identify the individual merchant or group of merchants (UMF §3.1.23). **Spec defines no default**. | | **Datawire ID** (DID) | *(Datawire transport, not a UMF body field)* | — | Used **only when the terminal has Datawire enabled** (`var_sheet.datawire_enabled: true`, the default). With Datawire enabled: paste the Fiserv-issued DID, or omit it and Koard provisions one at boarding. With `datawire_enabled: false`, no DID is used — sending one is rejected. | The UMF `TPPID` field (Rapid Connect ID assigned by Fiserv for a specific version of vendor/merchant software, UMF §3.1.9) is **not** on the merchant VAR sheet — it identifies the **integrator's certified SDK build**, not the merchant. Koard owns it and sends a fixed value (`RMY019` for the current Koard build) on every transaction. ## Via the MMS After creating the merchant account, click **New Terminal** and select Fiserv as the processor. You'll be presented with the **Fiserv VAR Sheet Information** form. Fill in all required fields (marked with `*`) using the values from the merchant's Fiserv VAR sheet: | MMS Label | Required | Notes | |---|---|---| | Merchant ID | Yes | Fiserv-assigned MID, up to 16 alphanumeric | | Terminal ID | Yes | Up to 8 alphanumeric. Zero-padded to 8 chars when used as Datawire `AuthKey2`. | | Merchant Category Code | Yes | 4-digit MCC. UMF treats it as `O\|C` (optional in request — the boarded MID record carries the default), but Koard requires it on the API for surcharge and reporting. | | Group ID | Yes | 5-13 alphanumeric. No spec default; supply the value Fiserv assigned in your VAR packet. | | Datawire Enabled | No | Whether the terminal routes over Datawire. Default on. When off, no Datawire ID is used. | | Datawire ID | No | Only when Datawire is enabled. Paste from the VAR packet if provided; if left blank, Koard provisions one at boarding. | Once saved, assign the terminal to a location and generate merchant credentials as usual. ## Via the API Send `X-Koard-apikey: {API_KEY}` with every request. Use `https://api.uat.koard.com` for sandbox and `https://api.koard.com` for production. ### Create Terminal — `POST /v2/terminals` **Request body** | Field | Required | Description | |---|---|---| | `account_id` | Yes | Merchant account ID | | `processor_config_id` | Yes | Fiserv processor configuration ID | | `terminal_name` | Yes | Display name for the terminal | | `terminal_description` | No | Optional description | | `mid` | Yes | Fiserv `MerchID` (top-level on the request — not in `var_sheet`) | | `tid` | Yes | Fiserv `TermID` (top-level) | | `mcc` | Yes | 4-digit MCC (top-level) | | `var_sheet.group_id` | Yes | Fiserv `GroupID` — assigned by Fiserv, no spec default | | `var_sheet.datawire_enabled` | No | Bool, default `true`. `true` = route over Datawire (a DID is used). `false` = no DID (sending `did` alongside is rejected as contradictory). | | `var_sheet.did` | No | Datawire ID. Only when `datawire_enabled` is `true`: send an already-issued DID, or omit it and Koard provisions one at boarding. | **Required vs conditional fields** Every Fiserv board — regardless of flavor — **requires** the top-level `account_id`, `processor_config_id`, `terminal_name`, `mid`, `tid`, `mcc`, plus the `var_sheet` **address** (`merchant_street_address`, `merchant_city`, `merchant_state`, `merchant_postal_code`), `country_code`, and `industry`. The rest is **conditional on the flavor**: | `var_sheet` field | Conditional? | When | |---|---|---| | `group_id` | Resolved from the processor config | Don't send it unless you're overriding the config's Group ID. | | `settlement_mid` | Conditional | North-settling flavors (**Nashville North**, **Cardnet North**). Optional — defaults to a copy of `mid` when omitted. | | `equipment` | Optional | POS Solution Name (enum) — boarding metadata only; the wire identifies the build via `TPPID`. | | `datawire_enabled` | Optional | Default `true`. `false` ⇒ no Datawire ID is used (and sending `did` is rejected as contradictory). | | `did` | Optional | Only when `datawire_enabled` is `true`: supply an issued DID, or omit it to have Koard provision one at boarding. | **Example — DID already issued** curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_live_fiserv_us", "terminal_name": "Front Counter iPhone", "mid": "RCTST1000118756", "tid": "00000003", "mcc": "5812", "var_sheet": { "group_id": "40001", "did": "00067045767186571068" } }' **Example — DID provisioned at boarding (Datawire enabled)** With `datawire_enabled` true (the default), omit `did` and Koard provisions a Datawire ID at boarding and persists it on the terminal. curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_live_fiserv_us", "terminal_name": "Front Counter iPhone", "mid": "RCTST1000118756", "tid": "00000099", "mcc": "5812", "var_sheet": { "group_id": "40001" } }' The response includes the persisted `var_sheet.did` so you can confirm registration succeeded. ### Update Terminal — `PUT /v2/terminals/{terminal_id}` To correct or update VAR sheet fields after creation, send a `PUT` with a `var_sheet` object containing only the fields you want to change. curl https://api.uat.koard.com/v2/terminals/500600700001 \ -X PUT \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "var_sheet": { "did": "00067045767186571068" } }' ## Batch Management Fiserv is **host capture** today — the host manages the batch and settles on its cutoff. There is no merchant-driven batch open/close model, and the MMS batch panel is read-only for Fiserv terminals. Merchant-driven batch management (see [Automated Batch Scheduling](/docs/guides/batch-settlements/details/automated-batch-scheduling)) is on the roadmap. Until then, leave batch management disabled in the MMS. ## Gotchas - **Omaha settlement** — for merchants on Fiserv DD / sponsor-bank funding via FDC, confirm the Omaha flavor is provisioned before boarding. - **`tid` is zero-padded to 8 characters as `AuthKey2`** in the Datawire envelope. Don't pad it yourself in the VAR packet — supply the raw value (Koard pads). - **`TranFee` (Merchant Surcharge) triggers full declines without enrollment.** The UMF spec is explicit: "Failure to do this will cause all credit card transactions with the surcharge amount field populated to be declined." Don't enable surcharge until Fiserv confirms enrollment. ## Troubleshooting **`400 Bad Request` on create** - Confirm `mid`, `tid`, and `mcc` are at the top level of the request, NOT inside `var_sheet`. - Confirm `group_id` is inside `var_sheet` and is 5-13 alphanumeric chars. - Verify `processor_config_id` is a valid Fiserv config ID for your environment. **Transactions erroring with `INVALID MERCHANT`** - The acquirer MID is platform-level. If your merchant requires a different acquirer relationship, contact Koard support — per-merchant acquirer routing is a future enhancement, not configurable on the VAR sheet today. **Need Omaha settlement** - Confirm the Omaha flavor is provisioned for the merchant before boarding. # Getting Started with Koard Welcome to Koard's Tap to Pay on iPhone integration guide. This comprehensive documentation will help you integrate Koard's payment solutions into your iOS applications, enabling secure tap-to-pay transactions for your merchants. [](https://www.koard.com/videos/apple-tap-to-pay.mp4) ## What is Koard? Koard is a payment platform that specializes in Tap to Pay on iPhone solutions, helping Payment Service Providers (PSPs) and Independent Software Vendors (ISVs) integrate Apple's tap-to-pay technology into their existing applications. We streamline the complex process of Apple certification and merchant onboarding, allowing you to go live with tap-to-pay payments in under 6 weeks instead of a multi-year process. ## Key Benefits * **Fast Time to Market**: Launch tap-to-pay payments in under 6 weeks * **Apple Partnership**: Bypass L3 certification requirements through our Apple partnership * **Comprehensive Support**: End-to-end guidance from Apple setup to merchant onboarding * **Precompiled SDK**: Easy integration with our .xcframework distribution * **Merchant Management**: Complete portal for merchant configuration and credential management * **Payment Routing**: Flexible integration with multiple payment processors ## Who Can Use Koard? Koard is designed for: * **Payment Service Providers (PSPs)** serving multiple merchants * **Independent Software Vendors (ISVs)** building payment applications * **Large merchants** processing significant transaction volumes * **US and Europe-based enterprises** with existing POS infrastructure **Prerequisites** **Business Requirements** * US or Europe-based enterprise PSP or ISV * Serve multiple merchants or process large transaction volumes * Existing experience with physical POS terminals or similar technology * Ability to track items and orders in your own database * Capacity to update your app for Apple compliance requirements **Technical Requirements** * **iPhone Model**: iPhone XS or later * **iOS Version**: iOS 17.4 or later * **Apple Developer Account**: Organization-level account required * **GitHub Account**: Required for SDK dependency management * **Supported PSP**: Integration with a Koard-supported Payment Service Provider * **Sandbox Apple Account**: Dedicated Sandbox tester signed in on a test iPhone that will be used for certification and QA **Test Device Required**: Plan for a dedicated test iPhone running iOS 17.4 or later with your Sandbox Apple Account signed in. Production Apple IDs cannot be used for Sandbox testing. ## Integration Phases ### Phase 1: Apple Partnership Setup * Establish relationship with Apple through Koard * Bypass L3 certification requirements * Set up Apple Business Register Account * Configure environment and KEK exchange processes ### Phase 2: Merchant Onboarding * Create merchant configurations via Koard's portal * Set up merchant credentials and processor integrations * Upload VAR sheets for supported processors and gateways * Configure payment routing with chosen processors ### Phase 3: SDK Integration * Integrate Koard's precompiled .xcframework * Implement payment flows in your iOS application * Test transactions in certification environment * Prepare for production deployment ### Phase 4: Launch and Scale * Deploy to production environment * Onboard merchants and provision iPhone terminals * Monitor transactions and optimize performance * Scale your tap-to-pay business ## Quick Start Path 1. **Verify Prerequisites**: Confirm you meet all business and technical requirements 2. **Contact Koard**: Reach out to our team to discuss your integration needs 3. **Set Up Apple Partnership**: Work with Koard to establish your Apple relationship 4. **Configure Merchants**: Use our portal to set up your merchant configurations 5. **Integrate SDK**: Follow our technical integration guide 6. **Test and Launch**: Complete testing and go live with tap-to-pay payments ## Next Steps * [Setting up the Merchant](/docs/getting-started-with-koard/setting-up-the-merchant) - Learn how to configure merchant accounts and credentials * [Creating a Sandbox Apple Account](/docs/setting-up-the-ios-sdk/creating-a-sandbox-apple-account) - Configure dedicated testers and devices * [Installing the SDK](/docs/setting-up-the-ios-sdk/installing-the-sdk) - Technical integration guide * [Payment Lifecycle](/docs/payments/payment-lifecycle) - Understand the complete payment flow * [Getting Ready for Production](/docs/setting-up-the-ios-sdk/getting-ready-for-production) - Prepare schemes and API keys for launch ## Support For questions or assistance with your integration: * **Technical Support**: Contact our development team * **Business Inquiries**: Reach out to Behailu at * **PSP Partnerships**: Ask about our updated list of supported Payment Service Providers # Setting up the Merchant via API Automate merchant onboarding with Koard's REST API. This guide walks through creating the merchant account, provisioning a terminal, associating it with a location, and issuing SDK credentials without using the Merchant Management System UI. - **Koard API key** with permission to manage merchant accounts - **Processor configuration IDs** that the merchant should use - **Location identifiers** (either newly created via API or existing records) linked to the merchant - **Dedicated test device** enrolled in the appropriate Apple sandbox for Tap to Pay validation For each request, send `X-Koard-apikey: {API_KEY}` and specify the target environment (`https://api.uat.koard.com` for sandbox or `https://api.koard.com` for production). Use `POST /v2/accounts` to create the merchant record tied to your processor configuration. The payload must include the account `type`, `name`, `description`, and an `address` object. **Request** curl https://api.uat.koard.com/v2/accounts \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "merchant", "name": "Bluebird Coffee Roasters", "description": "Retail coffee shop using Tap to Pay on iPhone", "address": { "street_line1": "123 Market Street", "city": "San Francisco", "state": "CA", "zip": "94105" }, "tax_id": "12-3456789", "mcc": "5812", "available_processor_configs": ["prc_live_payroc_us"] }' **200 Response** { "id": "100200300001", "type": "merchant", "name": "Bluebird Coffee Roasters", "description": "Retail coffee shop using Tap to Pay on iPhone", "status": "active", "tax_id": "12-3456789", "mcc": "5812", "address": { "street_line1": "123 Market Street", "city": "San Francisco", "state": "CA", "zip": "94105" }, "available_processor_configs": ["prc_live_payroc_us"], "created_at": "2024-10-15T18:21:04.123Z" } Store the returned `id`—you will use it when you create the terminal and credentials. In the examples that follow we will reference the ID `100200300001`. Provision a terminal with `POST /v2/terminals`. Provide the merchant's processor details using the processor-specific VAR sheet format. **Request** curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_live_payroc_us", "terminal_name": "Front Counter iPhone", "var_sheet": { "acquirerBin": "123456", "merchantNumber": "123456789012", "storeNumber": "0001", "terminalNumber": "0001", "merchantCategoryCode": "5812", "merchantName": "Bluebird Coffee Roasters", "merchantLocation": "San Francisco", "merchantState": "CA", "cityCode": "94105", "acceptorStreetAddress": "123 Market Street", "industryCode": "R", "acceptorPhone": "4155551234", "acceptorCustomerServicePhone": "4155551234" } }' **201 Response** { "terminal_id": "500600700001", "name": "Front Counter iPhone", "account_id": "100200300001", "mid": "123456789012", "tid": "0001", "processor_config_id": "prc_live_payroc_us", "status": "active", "created_at": "2024-10-15T18:21:05.015Z", "var_sheet": { "acquirerBin": "123456", "merchantNumber": "123456789012", "storeNumber": "0001", "terminalNumber": "0001", "merchantCategoryCode": "5812", "merchantName": "Bluebird Coffee Roasters", "merchantLocation": "San Francisco", "merchantState": "CA", "cityCode": "94105", "acceptorStreetAddress": "123 Market Street", "industryCode": "R", "acceptorPhone": "4155551234", "acceptorCustomerServicePhone": "4155551234" } } The response contains the terminal configuration with an auto-generated `terminal_id`. See the API reference for the full `TSYSVarSheet` schema with all required and optional fields. Update an existing location with `PUT /v1/locations/{location_id}` so that the location references the new terminal ID. Include any additional fields you need to change (for example, contact info or status). **Request** curl https://api.uat.koard.com/v1/locations/300400500001 \ -X PUT \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Bluebird Coffee HQ", "terminal_id": "500600700001", "processor_config_id": "prc_live_payroc_us", "status": "active" }' **200 Response** { "id": "300400500001", "name": "Bluebird Coffee HQ", "account_id": "100200300001", "terminal_id": "500600700001", "processor_config_id": "prc_live_payroc_us", "status": "active", "updated_at": "2024-10-15T18:21:06.287Z" } If you do not yet have a location record, create one first with `POST /v1/locations`, then repeat this update call to attach the terminal. Generate the merchant's SDK login credentials using `POST /v1/accounts/credentials`. You may supply a custom `code` and `pin`, or let Koard create randomized values by omitting them. **Request** curl https://api.uat.koard.com/v1/accounts/credentials \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001" }' **200 Response** { "id": "900100200003", "account_id": "100200300001", "code": "483920123456", "pin": "739051", "is_active": true, "created_at": "2024-10-15T18:21:07.431Z" } The response returns the `code` and `pin` only once. Store them securely and deliver them to your merchant through a trusted channel so they can authenticate with the Koard SDK. ## Next steps - Verify the credentials by logging into the Koard iOS SDK test harness. - Run a test payment in the UAT environment to confirm the terminal and location configuration. - When ready for production, repeat the flow against `https://api.koard.com` with live processor credentials. # Fiserv Cardnet North Cardnet is the **North (CES) front-end** settling to the North (PTS) back-end — terminal capture. Board it like any Fiserv terminal (see the [Fiserv Overview](/boarding-a-merchant/fiserv/fiserv-overview) for the shared shape, SRS, and UMF coverage) using the **Cardnet North** processor config. Note the distinct MID/TID formats. ## Boarding spec | Field | Value / format | Notes | |---|---|---| | **Group ID** | `30001` | Cardnet / North front-end — a **different** front-end from Nashville (`10001`). | | **Merchant ID** (MID) | **12 digits** (`MerchID`) | Cardnet front-end MID, usually the same as the 12-digit Settlement MID. Top-level `mid`. | | **Terminal ID** (TID) | **6 alphanumeric** (`TermID`) | The "Bank TID". Top-level `tid`. | | **Settlement MID** | 12 digits | North Settlement MID. VAR-sheet `settlement_mid` — optional; defaults to a copy of `mid` (which usually equals it). | | **MCC** | 4-digit | Top-level `mcc`. | | **Equipment** (POS Solution) | `CreditCallLTDGTWRC` or `CRDCallResellerRCSS` | VAR-sheet `equipment`. | ## Request shape curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_fiserv_cardnet_north", "terminal_name": "Lane 1", "mid": "445197000368", "tid": "A1B2C3", "mcc": "5045", "var_sheet": { "group_id": "30001", "settlement_mid": "445197000368", "industry": "retail_qsr_grocery", "equipment": "CRDCallResellerRCSS", "merchant_street_address": "102 Woodmont Blvd Ste 125", "merchant_city": "Nashville", "merchant_state": "TN", "merchant_postal_code": "37205", "country_code": "840" } }' ## Capture & settlement **Terminal capture (North PTS).** Same as Nashville North — the gateway holds the batch and submits at cutoff. Boarding must match the host's configured capture mode. ## Gotchas - **Different front-end, different Group ID (`30001`).** Cardnet is *not* the Nashville front-end — it has its own Group ID and its own MID/TID formats. - **12-digit MID, 6-char alphanumeric TID.** These formats differ from the 7-digit Nashville/Omaha values — copy them exactly from the VAR packet. - **⚠️ MID/TID reversal risk.** If the MID is boarded as the TID (or vice-versa) and that reversed combo is live for another merchant, funds route to the wrong account. Confirm deposits before going live. - **`settlement_mid` usually equals the MID** for Cardnet; omit it and Koard copies `mid`. # Boarding a Merchant with Elavon You can board an Elavon merchant either through the Koard Merchant Management System (MMS) UI or programmatically via the API. Elavon assigns the merchant a **Bank Number** and **Terminal Number** out-of-band. Those two values plus the standard top-level fields (`mid`, `tid`, `mcc`) are everything Koard needs from the merchant to board a terminal. ## Before You Start Elavon provisions merchants and terminals on their side; Koard never makes a "create merchant" call. Once provisioning is complete, the merchant's packet contains: | Provided by Elavon | What it is | |---|---| | **Bank Number** | 6 digits — assigned by Elavon per merchant (viaConex v4.090 §11.9, p.144) | | **Terminal Number** | 16 digits — assigned by Elavon per POS device (viaConex v4.090 §11.9, p.144) | That's it. Everything else (Application ID, Vendor ID, Registration Key) is configured by Koard once per environment and never appears on a merchant VAR sheet. ## How `terminal_id` is built Elavon's wire format uses a single 22-digit `Terminal_ID` on every request. The spec is explicit (viaConex v4.090 §11.9 p.144): > Digits 1–6 = Bank Number (6 digits, fixed length, assigned by Elavon) > Digits 7–22 = Terminal Number (16 digits, fixed length, assigned by Elavon) The 22-digit `Terminal_ID` is **pure concatenation** — no padding, no spacing. You can supply either form: | Format | Example | Koard does | |---|---|---| | `bank_number` + `terminal_number` separately | `bank_number="001734"`, `terminal_number="0008025708085490"` | Concatenates to `0017340008025708085490` | | Pre-built 22-digit `tid` | `tid="0017340008025708085490"` | Uses as-is, splits into bank + terminal internally | Pick whichever your merchant's paperwork makes easier. Both end up with identical persisted state. ## Via the MMS After creating the merchant account, click **New Terminal** and select Elavon as the processor. You'll be presented with the **Elavon VAR Sheet Information** form. | MMS Label | Required | Notes | |---|---|---| | Merchant ID | Yes | 12-digit MID from Elavon | | Bank Number | Either this **or** the 22-digit Terminal ID | 6 digits | | Terminal Number | Either this **or** the 22-digit Terminal ID | 16 digits | | 22-digit Terminal ID | Either this **or** Bank Number + Terminal Number | Pre-concatenated single value | | Merchant Category Code | Yes | 4-digit MCC | Optional fields are accepted (see [VAR Sheet Fields → Optional](#optional)) but **not required to onboard**. Address, phone, DBA name and similar fields are only used when the merchant joins Elavon's Dynamic Merchant Data program, which is restricted-access and explicitly approved per merchant (viaConex §3 Block 03, p.22552–22557). ## Via the API Send `X-Koard-apikey: {API_KEY}` with every request. Use `https://api.uat.koard.com` for sandbox and `https://api.koard.com` for production. ### Create Terminal — `POST /v2/terminals` **Request body** | Field | Required | Description | |---|---|---| | `account_id` | Yes | Merchant account ID | | `processor_config_id` | Yes | Elavon processor configuration ID | | `terminal_name` | Yes | Display name for the terminal | | `terminal_description` | No | Optional description | | `mid` | Yes | Elavon-assigned 12-digit Merchant ID | | `tid` | Yes — either the 22-digit form OR omit and use `var_sheet.bank_number` + `var_sheet.terminal_number` | If you supply the 22-digit form, Koard splits it; if you supply Bank + Terminal in `var_sheet`, Koard concatenates them. | | `mcc` | Yes | 4-digit MCC | | `var_sheet` | Conditional | Required only if you split Bank Number from Terminal Number, or want to set any optional fields. | **Example — pre-built 22-digit Terminal ID** curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_live_elavon_us", "terminal_name": "Front Counter iPhone", "mid": "123456789012", "tid": "0017340008025708085490", "mcc": "5812" }' **Example — split Bank + Terminal Number** curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_live_elavon_us", "terminal_name": "Front Counter iPhone", "mid": "123456789012", "mcc": "5812", "var_sheet": { "bank_number": "001734", "terminal_number": "0008025708085490" } }' ### Update Terminal — `PUT /v2/terminals/{terminal_id}` To correct or update VAR sheet fields after creation, send a `PUT` with a `var_sheet` object containing only the fields you want to change. ## VAR Sheet Fields ### Required | Field | Format | Description | |---|---|---| | `bank_number` | 6 digits | First 6 digits of the Elavon `Terminal_ID`. Assigned by Elavon per merchant. Required only if `tid` was not supplied as the full 22-digit form. | | `terminal_number` | 16 digits | Last 16 digits of the Elavon `Terminal_ID`. Assigned by Elavon per POS device. Required only if `tid` was not supplied as the full 22-digit form. | ### Optional | Field | Default | Description | |---|---|---| | `merchant_dba_name` | — | Restricted-access. Only honored if your merchant is enrolled in Elavon's Dynamic Merchant Data program (viaConex v4.090 §3 Block 03). Otherwise ignored at auth; supply at clearing time instead. | | `merchant_city` | — | Same restriction as `merchant_dba_name`. | | `merchant_state` | — | Same restriction. | | `merchant_zip` | — | Same restriction. | | `merchant_country` | `USA` | 3-letter alpha country code per ISO 3166-1 alpha-3. Allowed values include `USA`, `CAN`, `MEX`, `GBR`, etc. — see Elavon's clearing currency/country table. Same restriction at auth; required (mandatory) on the clearing BHR per Elavon Clearing Format §3. | | `acceptor_phone` | — | Restricted-access. Only carried via the optional Merchant Address Addendum (MAA) record on the clearing file. | | `currency_code` | `USD` | 3-letter alpha currency code per ISO 4217 (e.g. `USD`, `CAD`, `EUR`, `GBP`). Auth and clearing paths both expect alpha; only the EMV TLV tag `5F2A` uses ISO 4217 numeric (e.g. `840`). | | `surcharge_rate` | — | Surcharge percentage. Set to `null` to disable, `0` to never surcharge. Configured via `PUT /v2/terminals/{terminal_id}`. | ## Batch Management Elavon merchants in production use **clearing files** (the `.txt` flat-file format defined by Elavon Clearing Format v4.69) for daily settlement. Koard handles this on the merchant's behalf — there is no merchant-driven batch open/close model in the live auth flow for Elavon. If your merchant requires manual or automated batch scheduling via `PUT /v2/terminals/{terminal_id}` with a `batch_schedule`, see [Automated Batch Scheduling](/docs/guides/batch-settlements/details/automated-batch-scheduling). ## Country & Currency — Cheat Sheet | Where used | Format | Examples | |---|---|---| | Auth (viaConex Block 03 Dynamic_Country_Code) | 3-letter alpha | `USA`, `CAN`, `MEX` | | Auth EMV TLV tag 9F1A | 3-digit numeric | `840` (US), `124` (CA), `826` (GB) | | Clearing BHR Merchant Country | 3-letter alpha | `USA`, `CAN` | | Auth currency | 3-letter alpha | `USD`, `CAD`, `EUR`, `GBP` | | EMV TLV tag 5F2A | 3-digit numeric | `840` (USD), `124` (CAD) | The Koard API exposes the alpha forms (`USD`, `USA`) on the VAR sheet — the numeric EMV tags are constructed internally during the auth message build. ## Gotchas - **`mid` and `tid` are top-level fields** on the request, NOT inside `var_sheet`. This matches every other processor on Koard. - **`tid` is the 22-digit Elavon `Terminal_ID`** — not a 3- or 4-digit lane number like TSYS or Worldpay. Build it from Bank Number + Terminal Number per the [How `terminal_id` is built](#how-terminal_id-is-built) section, or paste the pre-concatenated form. - **Merchant DBA name / city / state / zip / phone are optional at auth** and restricted to Elavon's Dynamic Merchant Data program. Don't add them unless your merchant is explicitly enrolled — Elavon silently ignores them otherwise (viaConex §3 Block 03). - **Country code is alpha-3 at the API level** (`USA`, not `840`). The numeric form only appears inside EMV TLV tags, which Koard builds internally. ## Troubleshooting **`400 Bad Request` on create with `bank_number` / `terminal_number` errors** - Verify `bank_number` is exactly 6 digits and `terminal_number` is exactly 16 digits. - Or supply the full 22-digit `tid` directly and omit the `var_sheet` split. **Transactions erroring with `INVALID TERMINAL`** - `bank_number + terminal_number` must match what Elavon has on file character-for-character, including leading zeros. **Wrong merchant name on statements** - Auth-path dynamic merchant fields are restricted. The DBA name on statements is set in the **clearing file** (BHR record), not on the auth. Update the merchant's DBA at Elavon directly or via the clearing pipeline. # Fiserv Nashville (Classic) The Nashville (Envoy) front-end is Fiserv's classic host-capture platform. Board it exactly like any Fiserv terminal — see the [Fiserv Overview](/boarding-a-merchant/fiserv/fiserv-overview) for the shared VAR-sheet shape, SRS onboarding, and UMF field coverage — using the **Nashville** processor config. ## Boarding spec | Field | Value / format | Notes | |---|---|---| | **Group ID** | `10001` | Nashville front-end. Supply the value on your VAR packet. | | **Merchant ID** (MID) | 7 digits (`MerchID`, `an` ..16) | Fiserv-assigned Nashville MID. Sent top-level as `mid`. | | **Terminal ID** (TID) | 7 digits (`TermID`, `an` ..8) | Sent top-level as `tid`. Zero-padded to 8 as Datawire `AuthKey2`. | | **MCC** | 4-digit | Top-level `mcc`. | | **Equipment** (POS Solution) | `CreditCallHCRC` (Retail) or `CreditCallHCECRC` (eCommerce) | VAR-sheet `equipment`. Boarding metadata — the wire identifies the build via `TPPID`. | | **Settlement MID** | — | Not used — Nashville Classic is host capture. | ## Request shape curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_fiserv_nashville", "terminal_name": "Front Counter iPhone", "mid": "9446055", "tid": "9259755", "mcc": "5045", "var_sheet": { "group_id": "10001", "industry": "retail_qsr_grocery", "equipment": "CreditCallHCRC", "merchant_street_address": "102 Woodmont Blvd Ste 125", "merchant_city": "Nashville", "merchant_state": "TN", "merchant_postal_code": "37205", "country_code": "840" } }' Omit `did` to have Koard SRS-register a fresh Datawire ID at boarding time (returned on the created terminal). ## Capture & settlement Nashville Classic is **host capture** — the host holds the open batch and settles on its cutoff. There is no merchant-driven batch open/close; the MMS batch panel is read-only. ## Gotchas - **Group ID `10001`.** Do not reuse a sandbox value like `40001` — that's a different platform. Always use the Group ID on the merchant's VAR packet. - **`tid` is zero-padded to 8** as the Datawire `AuthKey2`; supply the raw 7-digit value — Koard pads it. - **`did` blank ⇒ auto-mint.** SRS registration + activation can take a few seconds; if you see Datawire `Retry` on the first transactions, wait ~30–60s and retry. - **Equipment / TPPID are not the same thing.** `equipment` (e.g. `CreditCallHCRC`) is boarding metadata; the transmitted `TPPID` (`RMY019` today) identifies the certified build and is set by Koard, not the merchant. # Boarding a Merchant with TSYS You can board a TSYS merchant either through the Koard Merchant Management System (MMS) UI or programmatically via the API. Both paths require the same VAR sheet information provided by TSYS. ## Via the MMS After creating the merchant account, click **New Terminal** and select TSYS as the processor. You'll be presented with the **TSYS VAR Sheet Information** form. ![TSYS VAR Sheet Information form](/tsys-var-sheet-form.png) Fill in all required fields (marked with `*`) using the values from the merchant's TSYS VAR sheet: | MMS Label | Required | Notes | | ------------------------------- | -------- | ------------------------------------------------------------------------------------ | | Acquirer BIN | Yes | 6-digit BIN from TSYS | | Merchant Number / MID | Yes | 12-digit TSYS merchant number | | Store Number | Yes | 4 digits — use `0001` for single-location merchants | | Terminal Number | Yes | 4 digits — use `0001` for the first terminal | | Merchant Name | Yes | DBA name as it should appear on cardholder statements | | Merchant Location | Yes | Merchant city | | Merchant State | Yes | Select from dropdown | | Merchant Category Code | Yes | 4-digit MCC | | Industry Code | Yes | See [Industry Codes](#industry-codes) below | | City Code / ZIP | Yes | 5-digit ZIP code | | Language Indicator | Yes | See [Language Indicators](#language-indicators) below | | Time Zone Diff | Yes | See [Time Zone Codes](#time-zone-codes) below | | Acceptor Street Address | Yes | Physical street address | | Acceptor Customer Service Phone | Yes | 10 digits, no formatting | | Acceptor Phone | Yes | 10 digits, no formatting | | Authentication Code | No | UAT only — submit to TSYS to authenticate a terminal and receive a `gen_key` | | Gen Key | No | Pass this if you have previously authenticated a terminal with `authentication_code` | | Currency Code | No | Defaults to `840` (USD) | | Country Code | No | Defaults to `840` (US) | Once saved, assign the terminal to a location and generate merchant credentials as usual. ## Via the API Send `X-Koard-apikey: {API_KEY}` with every request. Use `https://api.uat.koard.com` for sandbox and `https://api.koard.com` for production. ### Create Terminal — `POST /v2/terminals` **Request body** | Field | Required | Description | | ---------------------- | -------- | ---------------------------------------- | | `account_id` | Yes | Merchant account ID | | `processor_config_id` | Yes | TSYS processor configuration ID | | `terminal_name` | Yes | Display name for the terminal | | `terminal_description` | No | Optional description | | `var_sheet` | Yes | TSYS VAR sheet object — see fields below | **Example** ```bash curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_live_tsys_us", "terminal_name": "Front Counter iPhone", "var_sheet": { "acquirer_bin": "123456", "merchant_number": "123456789012", "store_number": "0001", "terminal_number": "0001", "mcc": "5812", "merchant_name": "Bluebird Coffee Roasters", "merchant_location": "San Francisco", "merchant_state": "CA", "city_code": "94105", "acceptor_street_address": "123 Market Street", "industry_code": "R", "acceptor_phone": "4155551234", "acceptor_customer_service_phone": "4155551234" } }' ``` ### Update Terminal — `PUT /v2/terminals/{terminal_id}` To correct or update VAR sheet fields after creation, send a `PUT` with a `var_sheet` object containing only the fields you want to change. All VAR sheet fields are optional on update. **Example** ```bash curl https://api.uat.koard.com/v2/terminals/500600700001 \ -X PUT \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "var_sheet": { "merchant_name": "Bluebird Coffee — Mission", "acceptor_street_address": "456 Valencia Street" } }' ``` ## VAR Sheet Fields ### Required | Field | Format | Description | | --------------------------------- | --------- | ------------------------------------------------------------------------- | | `acquirer_bin` | 6 digits | TSYS acquirer BIN — provided by TSYS for your VAR sheet | | `merchant_number` | 12 digits | TSYS merchant number — unique identifier for the merchant at the acquirer | | `store_number` | 4 digits | Store number — typically `0001` if the merchant has a single location | | `terminal_number` | 4 digits | Terminal number — typically `0001` for the first terminal at a store | | `mcc` | 4 digits | MCC for the merchant's business type (e.g. `5812` for restaurants) | | `merchant_name` | string | Merchant DBA name as it should appear on cardholder statements | | `merchant_location` | string | Merchant city | | `merchant_state` | 2 letters | US state abbreviation (e.g. `CA`) | | `city_code` | 5 digits | ZIP code (e.g. `94105`) | | `acceptor_street_address` | string | Physical street address of the merchant location | | `industry_code` | string | See [Industry Codes](#industry-codes) below | | `acceptor_phone` | 10 digits | Merchant phone number — digits only, no formatting | | `acceptor_customer_service_phone` | 10 digits | Customer-facing service phone number — digits only, no formatting | | `time_zone_diff` | 3 digits | TSYS time zone code — see [Time Zone Codes](#time-zone-codes) below | ### Optional | Field | Default | Description | | --------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `currency_code` | `840` | ISO 4217 numeric currency code — `840` for USD | | `country_code` | `840` | ISO 3166 numeric country code — `840` for US | | `language_indicator` | `00` | 2-digit language indicator — see [Language Indicators](#language-indicators) below | | `authentication_code` | — | Optional. If supplied and the terminal has no `gen_key` on file, Koard sends Transaction Code `TA` (Terminal Authenticate, EIS 1080 §6.223) and TSYS returns response `A1 — ACTIVATED` with the 24-character `gen_key`. If the code is invalid, TSYS returns `A2 — NOT ACTIVATED`. If the terminal already has a `gen_key` and you want to replace it, send `authentication_code` along with `override_gen_key=true` (see below). | | `gen_key` | — | 24-character key returned after a successful `TA` authentication. If your partner already has the `gen_key`, supply it directly and Koard will use it on every subsequent request — `authentication_code` is not needed in that case. | | `override_gen_key` | `false` | Set to `true` only when you also supply a valid `authentication_code` AND want to replace an existing `gen_key`. Koard performs a `TD` (Terminal Deactivate, EIS 1080 §6.223) followed by `TA` so the old key is invalidated before the new one is issued. Rejected if `authentication_code` is absent. | | `surcharge_rate` | — | Surcharge percentage to apply automatically (e.g. `3.5` for 3.5%). Set to `null` to disable automatic surcharge logic. Set to `0` to never surcharge. Configured via `PUT /v2/terminals/{terminal_id}` | ## Industry Codes | Code | Industry Type | | ---- | -------------------------------------- | | `A` | Auto Rental | | `B` | Bank / Financial Institution | | `D` | Direct Marketing | | `H` | Hotel | | `L` | Limited Amount Terminal | | `O` | Oil Company / Automated Fueling System | | `P` | Passenger Transport | | `R` | Retail / Restaurant / Grocery | Use `R` for most mPOS use cases. ## Language Indicators | Indicator | Language | | --------- | -------------------- | | `00` | English | | `01` | Spanish | | `02` | Portuguese | | `03` | Reserved for Irish | | `04` | Reserved for French | | `05` | Reserved for German | | `06` | Reserved for Italian | | `07` | Reserved for Dutch | ## Time Zone Codes | Code | Time Zone | | ----- | -------------- | | `705` | Eastern (EST) | | `706` | Central (CST) | | `707` | Mountain (MST) | | `708` | Pacific (PST) | ## Batch Management Options When creating a TSYS terminal, you choose how batches are managed: ### Option 1: Manual Batch Management (Default) Create the terminal without a `batch_schedule`. You (or your merchant) are responsible for opening and closing batches: ```json POST /v2/terminals { "account_id": "100200300001", "processor_config_id": "prc_live_tsys_us", "terminal_name": "Front Counter iPhone", "var_sheet": { "acquirer_bin": "123456", "merchant_number": "123456789012", "store_number": "0001", "terminal_number": "0001", "mcc": "5812", "merchant_name": "Bluebird Coffee", "merchant_location": "San Francisco", "merchant_state": "CA", "city_code": "94105", "acceptor_street_address": "123 Market Street", "industry_code": "R", "acceptor_phone": "4155551234", "acceptor_customer_service_phone": "4155551234" } } ``` With manual management, you must: * Open a batch before processing transactions (`POST /v1/batches/open`) * Close the batch at the end of the day or period (`POST /v1/batches/{batch_id}/close`) * Open a new batch for the next period ### Option 2: Automated Batch Scheduling After creating the terminal, enable automated scheduling via `PUT /v2/terminals/{terminal_id}`: ```json PUT /v2/terminals/{terminal_id} { "batch_schedule": { "timezone": "US/Eastern", "is_active": true, "schedule": [ { "day": "MON", "times": ["23:00"] }, { "day": "TUE", "times": ["23:00"] }, { "day": "WED", "times": ["23:00"] }, { "day": "THU", "times": ["23:00"] }, { "day": "FRI", "times": ["23:00"] }, { "day": "SAT", "times": ["23:00"] } ] } } ``` With automated scheduling: * Koard automatically closes and reopens batches on your schedule * TSYS batch numbers are auto-managed (001-999, wrapping, no reuse within 5 days) * Failed closes trigger retries and webhook error notifications See [Automated Batch Scheduling](/docs/guides/batch-settlements/details/automated-batch-scheduling) for full configuration details, timezone options, and edge cases. Automated scheduling is available for **TSYS**, **Elavon**, and **Worldpay** terminals only. Fiserv and Payroc terminals must use manual batch management. ## Gotchas * **`merchant_number` must be exactly 12 digits.** TSYS will reject shorter values. Left-pad with zeros if your MID is fewer than 12 digits. * **`store_number` and `terminal_number` must be exactly 4 digits.** Use `0001`, not `1`. * **`city_code` must be exactly 5 digits.** Left-pad with a zero for ZIP codes starting with `0` (e.g. `02101` for Boston). * **Phone numbers must be exactly 10 digits.** No dashes, spaces, or country codes — strip all formatting before submitting. * **`merchant_name` appears on cardholder statements.** Make sure it matches the merchant's registered DBA name — discrepancies can trigger disputes. * **`industry_code` affects transaction routing.** Using the wrong code can cause authorization failures or incorrect interchange rates. * **`acquirer_bin` is VAR sheet-level, not per-merchant.** All merchants under the same TSYS VAR sheet share the same BIN. Do not confuse this with the merchant number. ## Troubleshooting **`400 Bad Request` on create** * Check that all required VAR sheet fields are present. * Verify `merchant_number` is 12 digits, `store_number` / `terminal_number` are 4 digits, `city_code` is 5 digits, and phone numbers are 10 digits. * Confirm `processor_config_id` is a valid TSYS config ID for your environment. **Transactions erroring after boarding** * A VAR sheet field is likely incorrect. The most common culprits are `acquirer_bin`, `merchant_number`, `store_number`, and `terminal_number` — verify each matches exactly what TSYS has on file, including leading zeros. * Check `industry_code` is appropriate for the merchant's transaction type — an incorrect code can cause authorization failures. * If there is no open batch in our system for the terminal, transactions will error. Ensure a batch has been opened before processing payments. **Wrong merchant name on statements** * Use `PUT /v2/terminals/{terminal_id}` to update `merchant_name` in the `var_sheet`. Changes take effect on the next transaction. # Boarding a Merchant with Payroc Payroc merchants require two values from Payroc to board a terminal: a **Processing Terminal ID** and a **Processing MID**. These are provided by Payroc and entered when configuring the terminal in the Koard MMS. ## Via the MMS After creating the merchant account, click **New Terminal** and select Payroc as the processor. Enter the **Processing Terminal ID** and **Processing MID** provided by Payroc. ![Payroc terminal boarding form](/payroc-terminal-board.png) Once saved, assign the terminal to a location and generate merchant credentials. The merchant can then use those credentials to log into the SDK and run payments. Surcharging is not supported for Payroc processor configurations. ## Via the API Send `X-Koard-apikey: {API_KEY}` with every request. Use `https://api.uat.koard.com` for sandbox and `https://api.koard.com` for production. ### Create Terminal — `POST /v2/terminals` | Field | Required | Description | |-------|----------|-------------| | `account_id` | Yes | Merchant account ID | | `processor_config_id` | Yes | Payroc processor configuration ID | | `terminal_name` | Yes | Display name for the terminal | | `terminal_description` | No | Optional description | | `var_sheet.processing_terminal_id` | Yes | Processing Terminal ID provided by Payroc | | `var_sheet.processing_merchant_id` | Yes | Processing MID provided by Payroc | **Example** curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_live_payroc_us", "terminal_name": "Front Counter iPhone", "var_sheet": { "processing_terminal_id": "YOUR_PROCESSING_TERMINAL_ID", "processing_merchant_id": "YOUR_PROCESSING_MID" } }' # Setting Up the Entitlement for Tap to Pay on iPhone Learn how to request and configure the Tap to Pay on iPhone entitlement from Apple, which is required to enable contactless payment processing in your iOS application. [Get started with Koard](/docs/getting-started-with-koard/introduction) ## Overview The Tap to Pay on iPhone entitlement is a managed capability that Apple provides to authorized Payment Service Providers (PSPs) and their partners. This entitlement allows your app to use Apple's ProximityReader framework to accept contactless payments directly on iPhone. This guide covers the entitlement request process, configuration steps, and verification procedures based on [Apple's official documentation](https://developer.apple.com/documentation/proximityreader/setting-up-the-entitlement-for-tap-to-pay-on-iphone). **What you learn** In this guide, you'll learn: * How to request the Tap to Pay entitlement from Apple * How to configure the entitlement in your Apple Developer account * How to add the entitlement to your Xcode project * How to verify the entitlement is properly configured * How to handle entitlement requirements for development and distribution **Prerequisites** Before you begin, ensure you have: * **Apple Developer Account** (organization-level account required) * **Account Holder Access** (entitlement requests must be made by the account holder) * **PSP Partnership** (your organization must be an authorized Payment Service Provider or partner) * **Xcode 16.3 or later** (for project configuration) * **App ID Created** (your app identifier must be registered in Apple Developer) * **Sandbox Apple Account signed in on test device** (dedicated iPhone running Developer Mode for entitlement validation) **Keep a Test iPhone Ready**: Entitlement validation requires running builds on a physical test device signed in with your Sandbox Apple Account. Do not rely on production Apple IDs for these flows. ## Requesting the Entitlement The Tap to Pay on iPhone entitlement must be requested through Apple's developer portal. This process is handled by Apple and requires approval. **1. Access Apple Developer Portal** 1. **Log in to your Apple Developer account** as the account holder 2. **Navigate to Certificates, Identifiers & Profiles** 3. **Select your organization** if you have multiple accounts **Important**: Only the account holder can request the Tap to Pay entitlement. Team members or admins cannot submit this request. **2. Request Tap to Pay Entitlement** 1. **Go to Identifiers** section 2. **Select your App ID** (or create one if needed) 3. **Navigate to Additional Capabilities** 4. **Find "Tap to Pay on iPhone"** in the list of capabilities 5. **Click "Request"** or "Enable" to submit your request **3. Wait for Approval** Apple will review your request and typically respond within **one to two business days**. You'll receive an email notification when the entitlement is approved or if additional information is needed. **Processing Time**: The approval process typically takes 1-2 business days. You must start with the development certificate to access Apple's CERT environment. **4. Verify Entitlement Status** Once approved: 1. **Return to Certificates, Identifiers & Profiles** 2. **Select your App ID** 3. **Check Additional Capabilities** 4. **Verify "Tap to Pay on iPhone"** appears under Managed Capabilities The entitlement will now be available for use in your provisioning profiles. ## Configure Your App ID After receiving approval, configure your App ID to include the entitlement: ### Enable the Capability 1. **Navigate to Certificates, Identifiers & Profiles > Identifiers** 2. **Select your App ID** 3. **Scroll to Additional Capabilities** 4. **Enable "Tap to Pay on iPhone"** 5. **Save your changes** The capability will now be available for your App ID and can be included in provisioning profiles. ## Add Entitlement to Your Xcode Project Once the entitlement is approved and configured in your Apple Developer account, add it to your Xcode project: **1. Create Entitlements File** 1. **Open your project in Xcode** 2. **Select your project** in the Project Navigator 3. **Choose File > New > File** 4. **Select Property List** under Resource 5. **Name the file** `[YourProjectName].entitlements` 6. **Add it to your app target** **2. Configure Build Settings** 1. **Select your project** in the Project Navigator 2. **Select your app target** 3. **Go to Build Settings tab** 4. **Search for "Code Signing Entitlements"** 5. **Set the path** to `[YourProjectName].entitlements` **3. Add Entitlement Key** 1. **Open the `.entitlements` file** in Xcode 2. **Add the following key-value pair**: ```xml com.apple.developer.proximity-reader.payment.acceptance ``` The file should look like this: ```xml com.apple.developer.proximity-reader.payment.acceptance ``` **4. Update Provisioning Profile** 1. **In Xcode, go to Signing & Capabilities** 2. **Select your development team** 3. **Xcode will automatically download** a new provisioning profile that includes the entitlement 4. **Verify the entitlement** appears in the Capabilities section **Note**: If Xcode doesn't automatically update the provisioning profile, you may need to manually regenerate it in the Apple Developer portal. ## Verify Entitlement Configuration After configuring the entitlement, verify it's properly set up: ### Check in Xcode 1. **Select your project** in Xcode 2. **Select your app target** 3. **Go to Signing & Capabilities tab** 4. **Verify "Tap to Pay on iPhone"** appears in the Capabilities section ### Verify in Code You can programmatically verify the entitlement is present by checking the `readerIdentifier` property: ```swift import ProximityReader do { let readerIdentifier = try await PaymentCardReader().readerIdentifier print("Entitlement verified: (readerIdentifier)") // Entitlement is present and valid } catch { print("Entitlement error: (error)") // Handle notAllowed error if entitlement is missing } ``` If the entitlement is missing, `readerIdentifier` will throw a `notAllowed` error. ## Development vs Distribution Entitlements The Tap to Pay entitlement has different requirements for development and distribution: ### Development Entitlement * **Purpose**: Internal testing and development * **Provisioning**: Development provisioning profiles * **Distribution**: Ad-hoc distribution via .ipa files * **Testing**: Limited to registered test devices ### Distribution Entitlement * **Purpose**: TestFlight and App Store distribution * **Provisioning**: Distribution provisioning profiles * **Distribution**: TestFlight beta testing and App Store submissions * **Testing**: Available to all TestFlight testers and App Store users **Important**: If you've already received the development entitlement and need to distribute via TestFlight or the App Store, you must request the distribution entitlement separately. Respond to the original approval email from Apple to request the distribution entitlement. ## Troubleshooting ### Entitlement Not Appearing If the entitlement doesn't appear in your Apple Developer account: * **Verify account holder status**: Only the account holder can request entitlements * **Check PSP partnership**: Ensure your organization is authorized as a PSP or partner * **Contact Apple**: Reach out to Apple Developer Support if the entitlement is not available ### Entitlement Not Working in Xcode If the entitlement doesn't work in Xcode: * **Verify provisioning profile**: Ensure your provisioning profile includes the entitlement * **Check entitlements file**: Verify the `.entitlements` file contains the correct key * **Update provisioning profile**: Regenerate your provisioning profile in Apple Developer portal * **Clean build folder**: In Xcode, go to Product > Clean Build Folder ### Reader Identifier Returns Error If `readerIdentifier` throws a `notAllowed` error: * **Verify entitlement in Apple Developer**: Check that the entitlement is approved and enabled * **Check provisioning profile**: Ensure the profile includes the Tap to Pay capability * **Verify device registration**: For development, ensure test devices are registered * **Check code signing**: Verify your app is signed with the correct provisioning profile ## Additional Resources * [Apple's Tap to Pay Documentation](https://developer.apple.com/documentation/proximityreader/setting-up-the-entitlement-for-tap-to-pay-on-iphone) * [ProximityReader Framework Reference](https://developer.apple.com/documentation/proximityreader) * [Apple Developer Portal](https://developer.apple.com/account) ## See also * [Adding Support for Tap to Pay on iPhone](/docs/setting-up-the-ios-sdk/adding-support-for-tap-to-pay-on-iphone) - Complete implementation guide * [Installing the SDK](/docs/setting-up-the-ios-sdk/installing-the-sdk) - Set up the Koard iOS SDK * [Creating a Sandbox Apple Account](/docs/setting-up-the-ios-sdk/creating-a-sandbox-apple-account) - Prepare dedicated testers * [Getting Ready for Production](/docs/setting-up-the-ios-sdk/getting-ready-for-production) - Configure schemes and API keys * [Developing with Apple](/docs/appendix/developing-with-apple) - Security and environment guidelines # Launching on the App Store Everything you need to do with Apple — separate from your Koard integration — before your app can go live to merchants. **What you'll learn** In this guide, you'll learn: * The 7-step launch journey and where most teams get stuck * How to pick your distribution path before you write a line of code * The two entitlements you need (and why TestFlight requires the second one) * In-app experience requirements Apple inspects during review * The `prepare()` call and why it must happen at launch, not at checkout * The three video walkthroughs Apple requires — and how to record them * What to include in your App Store Connect submission notes * The most common rejection reasons and how to avoid every one **This is separate from your Koard integration.** Even after your Koard integration is technically complete, you cannot launch your app to merchants without Apple's approval. That approval is a separate, multi-step process you run directly with Apple. Most customers who reach this stage without a plan lose 2–6 weeks to back-and-forth with Apple Review. Track your progress in **Launch Readiness** inside the Koard MMS — it mirrors this guide step by step. ## Why this matters Four of the most common rejection reasons we see from Apple Review: 1. Submission videos don't show how a merchant accepts Apple's Terms & Conditions or links the merchant account. 2. No in-app merchant education — the developer planned to train merchants 1:1 in person. Apple requires _in-app_ education on top of whatever else you do. 3. The `prepare()` call isn't implemented, which means the first tap-to-pay attempt in front of a real customer takes 30–40 seconds. 4. Button copy mixes "Tap to Pay" with "Tap to Pay on iPhone" — Apple is strict about the full product name. None of that is a Koard problem. All of it is an Apple-side gate, and it routinely costs developers weeks they didn't plan to spend. This guide and the in-MMS checklist exist so you find these things in week one of design — not in week one of submission. ## The launch journey Each step is a distinct gate. You cannot skip any of them. **1. Request the Development Entitlement** Free and near-instant. Lets you build and test on developer-registered devices against the PSP sandbox. [Request at Apple's developer portal](https://developer.apple.com/contact/request/tap-to-pay-on-iphone) **2. Build to spec** Use this guide and the Koard MMS checklist to build the required UX — awareness moment, merchant education, T\&C flow, and checkout experience. Don't skip any section. **3. Record the three walkthrough videos** Apple requires a New User, Existing User, and Checkout walkthrough. These cannot be screen-recorded — use a second device to film the screen. **4. Request the Publishing Entitlement** Reply to the email Apple sent you when they granted the Dev entitlement. Apple reviews your app against the v1.5.1 checklist. Allow approximately 5 business days. **TestFlight requires this entitlement.** You cannot distribute via TestFlight until Publishing is granted. There is no shortcut. **5. Submit to App Store Connect** Separate review by the App Store team on top of the entitlement review. Include your test account credentials, video links, wireframes, and entitlement declaration in the submission notes. **6. Pilot** Apple recommends testing with a small group of representative merchants before GA. Use TestFlight or ship behind a feature flag you control. **7. General Availability** Flip your backend flag or publish your App Store listing with Tap to Pay enabled. Update your product page messaging only after the flag is live. ## Pick your distribution path first Apple has different requirements depending on how your app reaches merchants. Decide before you design any screens — the distribution path determines which checklist items are required for you. | Path | Who it's for | Onboarding | Awareness in app | Merchant education | | --------------------- | ---------------------------------------------- | -------------------------------- | ----------------------------------------- | ------------------ | | **Public App Store** | SMBs, anyone discovering your app via search | Required to be in-app, < 15 min | Required (full-screen modal, push, email) | Required | | **Unlisted App** | Specific BYO-device audience reached via URL | External OK | Recommended | Highly recommended | | **Custom App** | One named enterprise customer, branded version | External OK | Recommended | Highly recommended | | **Enterprise (ADEP)** | MDM-deployed to managed devices | External (no Apple ID on device) | Optional | Highly recommended | **Public App Store triggers the full in-app onboarding requirement.** Customers who pick this path because "we want it discoverable" often don't realize it requires a complete in-app merchant sign-up flow that works in under 15 minutes. **Most underused:** Custom App and Unlisted App, which carry lighter requirements for enterprise deployments. Apps that ship via MDM to managed devices are typically a fit for Unlisted. If you're not sure, the Koard solutions team can talk it through — but pick before you start designing screens. ## Two entitlements, not one | Entitlement | What it lets you do | How to get it | When | | --------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------- | | **Development** | Build, run on developer-registered devices, test against the PSP sandbox | [Request at Apple's developer portal](https://developer.apple.com/contact/request/tap-to-pay-on-iphone) | Day one | | **Publishing** | TestFlight, App Store, internal enterprise distribution | Reply to the email Apple sent you with the Dev entitlement. Apple reviews your app against the v1.5.1 checklist. | Once your build meets requirements | **TestFlight requires the Publishing Entitlement.** If you planned to ship a small pilot via TestFlight first, that already requires you to pass Apple's full review. There is no shortcut. ## The in-app experience Apple requires Apple inspects four moments in your user flow. Each has its own checklist covered in the sections below. Together they form the bulk of the v1.5.1 review. **Digital onboarding (Public distribution only)** A new user who just downloaded your app must be able to apply to become an authorised merchant _inside the app_, on an iPhone, and — for the majority of approvable users — accept their first payment within 15 minutes. Practical interpretation: * An external "contact sales" form does not count as digital onboarding. * A `WKWebView` wrapper around your existing onboarding portal _does_ count. * If your KYC requires manual review, the path must still start in-app and use push/email/SMS to bring the user back. **Awareness & enrollment** At least one **awareness moment** — Apple's strong preference is a full-screen modal — must communicate to eligible users that Tap to Pay on iPhone exists in your app. New merchants see it during onboarding. Existing merchants see it on first login after the feature ships. Required elements: * A launch email (Apple has a template). * A push notification (Apple has a template). * A way to enroll _outside_ the checkout flow (in Settings or similar). * A way to enroll _from_ the checkout flow if the user taps "Tap to Pay" without being enrolled. * Only admin-class users can accept the T\&C. Non-admins must see a "contact your admin" message. **Don't forget existing users.** The awareness moment is required for both new merchants during onboarding and existing merchants on their first login after the feature ships. Apple rejects apps that only show it to new users. **Merchant education** This is the most frequently missed requirement. You must provide in-app education demonstrating how to accept payment with Tap to Pay on iPhone. _External training — 1:1 sessions, newsletters, training videos sent by email — does not satisfy this requirement on its own._ **The Koard SDK handles this for you.** The Koard SDK wraps Apple's [`ProximityReaderDiscovery`](https://developer.apple.com/documentation/ProximityReader/ProximityReaderDiscovery) API and surfaces Apple-designed education screens directly in your app. Wiring up the Koard SDK's merchant-education entry point satisfies Apple's checklist item 4.1\* in full — Apple's own language: _"If you use ProximityReaderDiscovery this will fulfill all of the merchant education requirements."_ No need to design your own screens. Two things you still need to handle yourself: 1. **Make education reachable later.** The post-enrollment moment is covered by the SDK, but Apple also requires that users can find the education screens from Settings or Help (checklist item 4.2). Add a "Tap to Pay on iPhone" row in Settings that re-invokes the SDK's education flow. 2. **Cover region-specific requirements if applicable.** If you deploy in a PIN-required region, your education must demonstrate PIN entry and its accessibility features (4.6). If you deploy in a Fallback-required region, demonstrate the fallback payment method (4.7). If you're targeting iOS earlier than 18, you must build your own education screens using Apple's Marketing Guide and Toolkit assets covering, at minimum: * How to accept a contactless card (landscape position, top of iPhone) * How to accept Apple Pay and other digital wallets * PIN entry + accessibility (region-dependent) * Fallback payment method (region-dependent) **Transaction experience** The "Tap to Pay on iPhone" button at checkout must: * Be in a prominent, no-scroll location. * Use exact copy: `"Tap to Pay on iPhone"` (or `"Tap to Pay"` only if the button is too small; `"Charge"` only if Tap to Pay is your sole acceptance method). * **Never** be greyed out or hidden based on enrollment status — if the user isn't enrolled, tapping starts enrollment. * Use the `wave.3.right.circle` or `wave.3.right.circle.fill` SF Symbol if using an icon. After a successful tap: show a processing screen, then a clear approved/declined/timed-out result, and a digital receipt option (SMS, email, QR, or iOS Share). **Button copy is strictly enforced.** Apple is literal — "Tap to Pay" and "Tap to Pay on iPhone" are not interchangeable. Use the full name unless space genuinely does not allow it. ## The `prepare()` call — don't skip this Koard's SDK exposes this as `KoardMerchantSDK.shared.prepare()`. It warms up the reader. The first call after install takes **30–40 seconds**. Subsequent calls take **5–6 seconds, once every 24 hours**. **Where to call it:** at app launch _and_ every time the app comes to the foreground. **Where not to call it:** at checkout. If you do, the first time your merchant tries to take a payment in front of a real customer, they will stand there for 40 seconds. Apple flags this in reviews. It's checklist item 1.4. Implement it on day one. Apple's reviewers flag this often. ```swift // AppDelegate.swift func applicationDidBecomeActive(_ application: UIApplication) { Task { try? await KoardMerchantSDK.shared.prepare() } } ``` ## Terms & Conditions: two paths There are exactly two ways a merchant can accept Apple's Tap to Pay T\&C. Pick one based on your distribution path. **User-led (default)** In-app, the merchant signs into their Apple Account, taps "Accept", and the device kicks off terminal-profile building. This is the path for Public, Unlisted, and Custom App distribution where merchants have their own Apple ID on the device. **Never cache the T\&C status locally.** Always read T\&C acceptance state from Apple via the Koard SDK. A local flag can fall out of sync with Apple's records and cause unexpected T\&C prompts at the worst possible moment. **Enterprise (Apple Business Connect)** For MDM-managed devices without an Apple ID, an organisation admin accepts terms on behalf of the merchant via **Apple Business Connect**. Koard provides the token your admin uses to do this — talk to your Koard solutions contact to set it up _before_ you deploy devices. If the merchant hasn't linked in ABC, the user will see the T\&C prompt on the device anyway. This is the path most MDM-deployed enterprise apps take. It's documented in Apple's v1.5.1 checklist item 3.8.2. ## The three required videos Apple requires three video walkthroughs as part of the Publishing Entitlement review. **You must record with a second device.** The Tap to Pay on iPhone reader screen cannot be screen-recorded. Use a separate iPhone to film the device under test. **New User Flow** Show each of the following in order — Apple rejects videos that skip or cut any step: 1. Account creation 2. KYC (if applicable) 3. Merchant approval 4. Tap to Pay awareness moment 5. T\&C acceptance 6. Merchant education 7. Terminal profile configuration progress indicator 8. Completed indicator 9. One full transaction **Existing User Flow** Show each of the following in order: 1. Sign in to existing account 2. Tap to Pay button visible before T\&C is accepted 3. Awareness moment for existing users 4. T\&C acceptance 5. Merchant education 6. Progress indicator 7. One full transaction 8. PIN entry (if applicable for your region) 9. Fallback payment method (if applicable for your region) **Checkout Flow** Show each of the following in order: 1. Add items to cart (or enter amount) 2. Payment options screen 3. Tap to Pay button 4. Initiate and complete a Tap to Pay transaction 5. PIN entry (if applicable) 6. Fallback (if applicable) **Common rejection:** the videos exist but skip a step ("we cut to after T\&C acceptance"). Re-record showing every step in sequence. If you need to unlink your Apple Account to re-record T\&C acceptance, Apple has documentation on resetting the T\&C state. ## Submitting to App Store Connect For Public, Unlisted, or Custom App distribution, passing the Publishing Entitlement review is not the end. Your app then goes through standard App Store Review _plus_ a Tap to Pay-specific review. In your App Store Connect submission notes, include: * A declaration that you are using the Tap to Pay on iPhone entitlement. * A description of your use case (e.g. "a point-of-sale app for SMB merchants"). * **A test account that works.** Apple says "a vast majority of rejections are due to test accounts not working properly." Validate on a fresh device before submitting. * A link to your video walkthrough _and_ high-fidelity wireframes of your checkout experience. * If your app is geo-fenced or uses a feature flag for Tap to Pay — declare that explicitly. * If Tap to Pay is your _only_ acceptance method, set `UIRequiredDeviceCapabilities` to include `iphone-ipad-minimum-performance-a12` so incompatible devices cannot download the app. **Enterprise apps: do not mention MDM.** If you're MDM-distributing in the enterprise, do not mention MDM in your App Store Connect app details. It flags a different (incorrect) entitlement review. Route enterprise distribution via ADEP. **Respond on the same thread.** Apple will often respond to a missing piece as a "rejection" with a note in the body saying it's actually a request for information. Reply on the same message thread — don't open a new one. ## Common pitfalls | # | Pitfall | Catch it before submission by… | | -- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | 1 | Submission videos skip T\&C / merchant-linking | Using the Videos checklist in MMS Launch Readiness — it lists every Apple-required step per video | | 2 | No in-app merchant education ("we train in person") | Wiring up the Koard SDK's merchant-education entry point (wraps `ProximityReaderDiscovery`, iOS 18+) — satisfies 4.x in one call | | 3 | `prepare()` not implemented or called at checkout | Implementing on day one at app launch and `applicationWillEnterForeground` | | 4 | Test account credentials don't work on Apple's review device | Validating on a fresh iPhone _before_ submitting | | 5 | Tap to Pay button uses wrong copy or greys out when not enrolled | Apple is literal — use "Tap to Pay on iPhone"; never grey the button | | 6 | Mentioned MDM in App Store Connect notes for an enterprise app | Strip the MDM mention; route enterprise via ADEP, not the App Store | | 7 | Awareness moment present but only for new users | Add an existing-user awareness moment too (full-screen modal on first login post-launch) | | 8 | T\&C local cache out of sync with Apple | Always read T\&C status from Apple via the Koard SDK — never trust a local flag | | 9 | Geo-fencing not declared in App Store Connect notes | Add a line: "App is geo-fenced to US, CA. Test account works in these regions." | | 10 | Marketing channels skipped (launch email, push, hero banner) | Items 6.1, 6.2, 6.3 — required for Public distribution | ## After approval: pilot, then GA Apple strongly recommends piloting with a small group of representative merchants before flipping to general availability. Two options: * **TestFlight** — invite-only, requires the Publishing Entitlement. * **Feature flag** — ship the binary to GA with Tap to Pay hidden behind a backend flag you control. This is the path Apple recommends for existing apps, because most users will already have the updated version when you flip the flag. If you go this route, declare it in your App Store Connect submission, and do not update your App Store product page with Tap to Pay messaging until you flip the flag. Apple ships a pilot questionnaire in the Getting Started PDF (Appendix). Use it. ## Quick reference links * [Apple Tap to Pay on iPhone developer site](https://developer.apple.com/tap-to-pay-on-iphone/) * [Request the entitlement](https://developer.apple.com/contact/request/tap-to-pay-on-iphone/) * [Human Interface Guidelines — Tap to Pay on iPhone](https://developer.apple.com/design/human-interface-guidelines/tap-to-pay-on-iphone) * [Apple Marketing Guidelines](https://developer.apple.com/tap-to-pay/marketing-guidelines/) * [ProximityReaderDiscovery API (iOS 18+)](https://developer.apple.com/documentation/ProximityReader/ProximityReaderDiscovery) * [App Store Review Guidelines](https://developer.apple.com/app-store/review/guidelines/) ## Where to get help * **In-MMS checklist** — `Launch Readiness` in your Koard dashboard mirrors this guide section by section. * **Apple entitlement questions** — reply on your existing `applepayentitlements@apple.com` thread, citing your Case ID. * **PSP-side questions** (T\&C from Apple vs local, ABC tokens for enterprise, `prepare()` semantics) — your Koard solutions contact. ## See also * [Setting Up the Entitlement for Tap to Pay on iPhone](/docs/setting-up-the-ios-sdk/setting-up-the-entitlement-for-tap-to-pay-on-iphone) - Request and configure the Apple development entitlement * [Adding Support for Tap to Pay on iPhone](/docs/setting-up-the-ios-sdk/adding-support-for-tap-to-pay-on-iphone) - Configure Xcode and run your first test transaction * [Getting Ready for Production](/docs/setting-up-the-ios-sdk/getting-ready-for-production) - Set up Xcode schemes and API keys for your production build * [Apple Best Practices and Guidelines](/docs/appendix/apple-best-practices-and-guidelines) - UX patterns, ProximityReader usage, and security practices * [Running Payments](/docs/setting-up-the-ios-sdk/running-payments) - SDK reference for payment flows _This guide reflects Apple's published requirements as of March 2025 (App Review Checklist v1.5.1) and September 2023 (Getting Started v1.2). Apple updates these documents periodically. If you see a discrepancy, the Apple source wins — please flag it to us._ # Installing the SDK Install the Koard Merchant SDK to enable tap-to-pay functionality in your iOS application. If you're ready to start developing, see our [Tap to Pay configuration guide](/docs/setting-up-the-ios-sdk/adding-support-for-tap-to-pay-on-iphone). [Get started with Koard](/docs/getting-started-with-koard/introduction) **What you learn** In this guide, you'll learn: * How to add the Koard SDK with Swift Package Manager * How to install the SDK manually as an XCFramework * How to configure embed settings for proper functionality * How to verify your installation is working correctly **Prerequisites** Before you begin, ensure you have: * **Xcode 16.3 or later** (required for building and distribution) * **iOS 17.4+ deployment target** (minimum supported version) * **Swift 5.9+** (minimum Swift toolchain) * **iPhone XS or greater** (supported hardware for tap-to-pay) * **Valid Koard merchant account** (configured in Koard MMS) * **Sandbox Apple Account signed in on test device** (dedicated iPhone with Developer Mode enabled) **Current version**: The latest published release is **1.0.20**. See the SDK [CHANGELOG](https://github.com/koardlabs/koard-ios/blob/main/CHANGELOG.md) for behavior changes — 1.0.20 changed how `prepare()` and `linkAccountAsync()` report failures, which affects existing integrations. **Test Device**: Use a dedicated test iPhone with your Sandbox Apple Account signed in. Avoid mixing production Apple IDs on the same hardware to prevent authentication conflicts. ## Step 1: Add the SDK We recommend **Swift Package Manager** for most projects. If you cannot use SPM, install the SDK manually as an XCFramework instead. ### Option A: Swift Package Manager (Recommended) 1. **In Xcode, choose File → Add Package Dependencies…** 2. **Enter the package URL**: `https://github.com/koardlabs/koard-ios.git` 3. **Set the dependency rule** to "Up to Next Major Version" starting from `1.0.20` 4. **Add the `KoardSDK` library product** to your app target You can also add it directly to a `Package.swift`: ```swift .package(url: "https://github.com/koardlabs/koard-ios.git", from: "1.0.20") ``` Then list `KoardSDK` as a dependency of your target. ### Option B: Manual XCFramework 1. **Download the latest `KoardSDK.xcframework.zip`** from the [Releases page](https://github.com/koardlabs/koard-ios/releases) or get it directly from the team 2. **Extract the ZIP file** to reveal the `KoardSDK.xcframework` bundle 3. **Drag `KoardSDK.xcframework` into your Xcode project** (or use your target's **General → Frameworks, Libraries, and Embedded Content → "+" → Add Other… → Add Files…**) 4. **Ensure "Copy items if needed" is checked** and the framework is added to your app target **Note**: The XCFramework format ensures compatibility across different architectures (iOS Simulator, iOS Device) and simplifies distribution compared to traditional frameworks. ### Option C: CocoaPods The SDK ships a podspec (`KoardSDK`), which vendors the same `KoardSDK.xcframework`. Add it to your `Podfile`: ```ruby pod 'KoardSDK', '~> 1.0.20' ``` Then run `pod install` and open the generated `.xcworkspace`. ## Step 2: Configure Embed Settings When installing the XCFramework manually, embedding is crucial for the SDK to work properly in your app. (Swift Package Manager handles embedding automatically.) 1. **Open your target's "General" tab under "Frameworks, Libraries, and Embedded Content"** 2. **Find `KoardSDK.xcframework` in the list** 3. **Change the "Embed" setting from "Do Not Embed" to "Embed & Sign"** **Important**: If you skip this step for a manual install, you'll get runtime crashes when trying to use the SDK. The framework must be embedded and signed to function properly. ## Step 3: Verify Installation ### Check Framework Integration 1. **Build your project** (⌘+B) to ensure there are no compilation errors 2. **Verify the package or framework appears** in your project navigator 3. **For a manual install, check the framework is listed** under "Frameworks, Libraries, and Embedded Content" ### Test Basic Import Add this import statement to one of your Swift files to verify the SDK is accessible: ```swift import KoardSDK ``` If the import succeeds without errors, your installation is working correctly. ## Step 4: Initialize the SDK Once the SDK is installed, initialize it early in your app lifecycle with your `KoardOptions` and API key: ```swift import KoardSDK // Configure SDK options let options = KoardOptions( environment: .uat, // .uat | .production | .custom(String) loggingLevel: .debug // .none | .error | .warning | .debug | .verbose ) // Initialize with your API key KoardMerchantSDK.shared.initialize(options: options, apiKey: "your-koard-api-key") ``` For details on retrieving your key, see [Retrieving Your API Key](/docs/setting-up-the-ios-sdk/retrieving-your-api-key). For authentication and payments, see [Running Payments](/docs/setting-up-the-ios-sdk/running-payments). ## Troubleshooting ### Common Installation Issues * **Build Errors**: Ensure you're using Xcode 16.3 or later * **Runtime Crashes**: For a manual XCFramework install, verify the framework is set to "Embed & Sign" * **Import Errors**: Check that the package product (or XCFramework) was added to the correct target, and that you `import KoardSDK` * **Architecture Issues**: The XCFramework automatically handles different architectures ### Verification Checklist * [ ] Xcode 16.3 or later installed * [ ] iOS 17.4+ deployment target set * [ ] `KoardSDK` added via Swift Package Manager, or `KoardSDK.xcframework` added and set to "Embed & Sign" * [ ] Project builds without errors * [ ] `import KoardSDK` works correctly ## Next Steps Once the SDK is installed and configured: * [Create a Sandbox Apple Account](/docs/setting-up-the-ios-sdk/creating-a-sandbox-apple-account) - Ensure your testers are ready * [Configure Tap to Pay](/docs/setting-up-the-ios-sdk/adding-support-for-tap-to-pay-on-iphone) - Enable contactless payment functionality * [Implement Payments](/docs/setting-up-the-ios-sdk/running-payments) - Add payment processing to your app * [Understand Payment Lifecycle](/docs/payments/payment-lifecycle) - Learn about the complete payment flow * [Get Ready for Production](/docs/setting-up-the-ios-sdk/getting-ready-for-production) - Set up schemes and API keys for launch ## See also This wraps up the SDK installation. See the links below for next steps in your integration: * [Creating a Sandbox Apple Account](/docs/setting-up-the-ios-sdk/creating-a-sandbox-apple-account) - Set up dedicated testers * [Adding Tap to Pay](/docs/setting-up-the-ios-sdk/adding-support-for-tap-to-pay-on-iphone) - Enable contactless payments * [Running Payments](/docs/setting-up-the-ios-sdk/running-payments) - Implement payment processing * [Payment Lifecycle](/docs/payments/payment-lifecycle) - Complete payment flow guide * [Apple Best Practices](/docs/appendix/apple-best-practices-and-guidelines) - iOS development guidelines # Running Payments Process payments with the Koard Merchant SDK to enable tap-to-pay functionality in your iOS application. If you're ready to start developing, see our [SDK installation guide](/docs/setting-up-the-ios-sdk/installing-the-sdk). [Get started with Koard](/docs/getting-started-with-koard/introduction) **What you learn** In this guide, you'll learn: * How to initialize and authenticate with the Koard Merchant SDK * How to set up location management for multi-location merchants * How to prepare card reader sessions for tap-to-pay * How to process different types of transactions (sale, preauth, refund) * How to handle transaction responses and error scenarios ## Before you begin This comprehensive guide covers everything you need to know about integrating and using the KoardMerchantSDK in your iOS application. For payment concepts and API payloads, explore the [Payments guides](/docs/guides/payments/overview.md)—including [Sale](/docs/payments/methods/sale) and [Preauth](/docs/payments/methods/preauth). To understand the complete flow, see the [Payment Lifecycle guide](/docs/guides/payments/details/payment-lifecycle.md). **Test on Real Hardware**: Keep a dedicated test iPhone with your Sandbox Apple Account signed in. Simulator builds cannot exercise Tap to Pay, and production Apple IDs won't work in the Sandbox environment. ## Key Concepts ### 1. Authentication Tokens The SDK manages several types of tokens automatically: * **API Key**: Your API key for Koard services * **Card Reader Token**: Apple's ProximityReader token for Tap to Pay functionality ### 2. Card Reader Sessions The SDK handles Apple's ProximityReader lifecycle: * **Preparation**: Refreshes tokens and prepares the reader for transactions * **Transaction Processing**: Manages card reading and data collection * **Session Management**: Handles background/foreground transitions automatically ### 3. Location Management Multi-location merchants must set an active location before processing payments: * **Retrieve available locations** after login * **Set the active location** for all subsequent transactions * **Location data is persisted** across app sessions ## Initialize the SDK Initialize the SDK early in your app lifecycle (typically in `AppDelegate` or `SceneDelegate`): ```swift import KoardSDK class AppDelegate: UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Configure SDK options let options = KoardOptions( environment: .uat, // .uat | .production | .custom(String) loggingLevel: .debug // .none | .error | .warning | .debug | .verbose ) // Initialize with your API key KoardMerchantSDK.shared.initialize( options: options, apiKey: "your-koard-api-key" ) return true } } ``` ## Authenticate the Merchant Before processing any payments, authenticate the merchant. The login function returns a JWT token that is then passed in the Bearer token of all successive requests. ```swift import KoardSDK private func authenticateMerchant() async throws { do { // Login with merchant credentials try await KoardMerchantSDK.shared.login( code: "your-merchant-code", pin: "your-merchant-pin" ) print("Merchant authenticated successfully") // After login, set up location try await setupLocation() } catch { print("Authentication failed: \(error)") throw error } } ``` If you have already resolved the merchant identity into a single opaque string (for example via a QR scan, SSO callback, or server-issued provisioning token), you can log in with an alias instead of a code and PIN. It produces the same session token: ```swift try await KoardMerchantSDK.shared.login(alias: "your-merchant-alias") ``` **Session-token auth**: Login is session-token only. The SDK persists the session token and never stores the merchant code, PIN, or alias. When the session expires, call `login(...)` again to re-authenticate. ## Set Location Retrieve and set the active location. Locations are attached to terminals which determines the MID, TID and Processor Configuration to be used for the payments API. This determines whether the merchant is leveraging TSYS, Payroc, Fiserv, or Elavon payment processing rails. ```swift private func setupLocation() async throws { do { // Get available locations let locations = try await KoardMerchantSDK.shared.locations() guard !locations.isEmpty else { throw PaymentError.noLocationsAvailable } // For single location merchants, use the first location let activeLocation = locations.first! // For multi-location merchants, let user select // let activeLocation = userSelectedLocation // Set the active location KoardMerchantSDK.shared.setActiveLocationID(activeLocation.id) print("Active location set: \(activeLocation.name)") } catch { print("Location setup failed: \(error)") throw error } } ``` ## Prepare a Card Reader Session Before accepting payments, prepare the card reader. The reader preparation leverages a PaymentCardReader.Token associated with the merchant session. ```swift import KoardSDK private func prepareCardReader() async throws { do { // Check if account is linked (required for Tap to Pay) let isLinked = try await KoardMerchantSDK.shared.isAccountLinked() if !isLinked { // Link the merchant account to Apple Pay try KoardMerchantSDK.shared.linkAccount() // Wait for linking to complete // This typically requires user interaction return } // Prepare the card reader session try await KoardMerchantSDK.shared.prepare() print("Card reader prepared and ready") // Optional: Monitor reader status monitorReaderStatus() } catch { print("Card reader preparation failed: \(error)") throw error } } private func monitorReaderStatus() { Task { // Monitor reader events for await event in KoardMerchantSDK.shared.readerEvents { DispatchQueue.main.async { self.handleReaderEvent(event) } } } } private func handleReaderEvent(_ event: Event) { switch event { case .readyForTap: print("Ready for tap") case .cardDetected: print("Card detected") case .readCompleted: print("Card read completed") case .readCancelled: print("Card read cancelled") default: print("Reader event: \(event.description)") } } ``` ## Process Sale Transactions Sale transactions are single-step auth + capture that immediately capture funds. Use sales when the final amount is known at payment time. For more details on when to use Sale vs Preauth, compare the [Sale](/docs/payments/methods/sale) and [Preauth](/docs/payments/methods/preauth) guides. ```swift private func processSale() async throws { // Create payment breakdown (optional) let breakdown = PaymentBreakdown( subtotal: 1000, // $10.00 in cents taxRate: 8.75, // 8.75% as a percent value (not a 0-1 decimal) taxAmount: 88, // $0.88 in cents tipAmount: 200, // $2.00 in cents tipType: .fixed // or .percentage ) // Create currency let currency = CurrencyCode(currencyCode: "USD", displayName: "US Dollar") do { // Process the sale let response = try await KoardMerchantSDK.shared.sale( amount: 1288, // Total amount in cents breakdown: breakdown, // Optional breakdown currency: currency, eventId: UUID().uuidString, // Optional idempotency/tracking key (UUID4) type: .sale // Defaults to .sale ) // Handle the response try await handleTransactionResponse(response) } catch { print("Sale failed: \(error)") throw error } } ``` **Tracking and idempotency**: `sale` and `preauth` take an optional `eventId` (UUID4), not a `transactionId`. Koard generates the transaction ID and returns it on `response.transactionId`. The reader is driven internally, so a Tap to Pay sheet is presented during these calls. ## Process Preauthorization Transactions Preauthorization transactions authorize funds without capturing them. They can be incrementally authorized, captured, or reversed. Use preauth when the final amount is uncertain (e.g., restaurant with tip) or when you need to verify funds availability. To complete a preauth, capture the payment using the transaction ID. For the complete flow, see [Preauth](/docs/payments/methods/preauth) and [Capture](/docs/payments/methods/capture). ```swift private func processPreauth() async throws { let currency = CurrencyCode(currencyCode: "USD", displayName: "US Dollar") do { // Process preauthorization (breakdown is optional — pass nil) let response = try await KoardMerchantSDK.shared.preauth( amount: 1000, // Amount to preauthorize in cents breakdown: nil, // Optional breakdown currency: currency, eventId: UUID().uuidString // Optional idempotency/tracking key (UUID4) ) print("Preauth successful: \(response.transactionId ?? "Unknown")") // Store transaction ID for later capture/reverse UserDefaults.standard.set(response.transactionId, forKey: "lastPreauthId") } catch { print("Preauth failed: \(error)") throw error } } ``` ## Handle Transaction Responses Payment methods return a `TransactionResponse`. Read the rich domain object from `response.transaction` (a `KoardTransaction?`). Its `status` is the public `KoardTransaction.Status` enum, which includes `pending`, `authorized`, `captured`, `surchargePending`, `surchargeApplied`, `approved`, `declined`, `refunded`, `reversed`, `pickupCard`, `timedOut`, `canceled`, `cancelled`, `error`, `settled`, and `unknown`. ```swift private func handleTransactionResponse(_ response: TransactionResponse) async throws { guard let transaction = response.transaction else { throw PaymentError.invalidResponse } switch transaction.status { case .approved: print("Transaction approved!") print("Transaction ID: \(transaction.transactionId)") print("Amount: $\(Double(transaction.totalAmount) / 100.0)") case .surchargePending: print("Surcharge pending - customer approval required") // Show surcharge disclosure to customer if let disclosure = transaction.surchargeDisclosure { let approved = try await showSurchargeDisclosure(disclosure) // Confirm or deny the surcharge let confirmedTransaction = try await KoardMerchantSDK.shared.confirm( transaction: transaction.transactionId, confirm: approved ) print("Final transaction status: \(confirmedTransaction.status)") } case .declined: print("Transaction declined: \(transaction.statusReason ?? "Unknown reason")") case .error: print("Transaction error: \(transaction.statusReason ?? "Unknown error")") default: print("Transaction status: \(transaction.status.string)") } } private func showSurchargeDisclosure(_ disclosure: String) async throws -> Bool { // Show disclosure to customer and get their approval // This should be implemented based on your UI requirements return await withCheckedContinuation { continuation in DispatchQueue.main.async { let alert = UIAlertController( title: "Surcharge Notice", message: disclosure, preferredStyle: .alert ) alert.addAction(UIAlertAction(title: "Accept", style: .default) { _ in continuation.resume(returning: true) }) alert.addAction(UIAlertAction(title: "Decline", style: .cancel) { _ in continuation.resume(returning: false) }) // Present alert (you'll need to implement this based on your view hierarchy) // self.present(alert, animated: true) } } } ``` ## Handle Partial Approvals When the issuer authorizes less than the requested amount, the transaction comes back with a `statusReason` of `partial_approval` (the `StatusReason.partialApproval` case). The top-level `status` still reads as `approved`/`captured`. Use the `partialAuthApproval` method to accept the partial amount as final, or reject it to release the hold: ```swift private func handlePartialApproval(_ transaction: KoardTransaction) async throws { guard transaction.isPartialApproval else { return } // remainingAmount is the amount still owed after the partial authorization if let remaining = transaction.remainingAmount { print("Partial approval — remaining balance: \(remaining) cents") } // Accept the partial amount as final (or pass approve: false to release the hold) let settled = try await KoardMerchantSDK.shared.partialAuthApproval( transactionId: transaction.transactionId, approve: true, eventId: UUID().uuidString // Optional UUID4 for idempotency ) print("Partial-auth settled status: \(settled.status.string)") // Optionally run a fresh sale for the remaining balance on another card. } ``` To collect the remaining balance on a second card, pass the original transaction's id as `partialAuthTransactionId` on the follow-up `sale` (or `preauth`). This links the two authorizations so they settle together: ```swift let remainder = try await KoardMerchantSDK.shared.sale( amount: remainingAmount, breakdown: nil, currency: CurrencyCode(currencyCode: "USD", displayName: "US Dollar"), eventId: UUID().uuidString, partialAuthTransactionId: transaction.transactionId ) ``` ## Transaction Management ### Refund a Transaction ```swift import KoardSDK private func processRefund(transactionId: String, amount: Int? = nil) async throws { do { let response = try await KoardMerchantSDK.shared.refund( transactionId: transactionId, amount: amount, // nil for full refund eventId: UUID().uuidString // Optional UUID4 for idempotency ) print("Refund successful: \(response.transactionId ?? "Unknown")") } catch { print("Refund failed: \(error)") throw error } } ``` To run a card-present refund (presenting the Tap to Pay sheet and capturing card data with the refund), pass `withTap: true`. When `withTap` is set, an `amount` is required: ```swift let response = try await KoardMerchantSDK.shared.refund( transactionId: transactionId, amount: 1288, // Required when withTap is true withTap: true ) ``` ### Reverse a Preauthorization ```swift private func reversePreauth(transactionId: String, amount: Int? = nil) async throws { do { let response = try await KoardMerchantSDK.shared.reverse( transactionId: transactionId, amount: amount // nil for full reversal ) print("Reversal successful: \(response.transactionId ?? "Unknown")") } catch { print("Reversal failed: \(error)") throw error } } ``` **Note**: Transactions can be partially reversed and refunded. When an authorization is reversed to 0 or a capture is refunded to 0, the transaction status becomes "cancelled". For more details, see our [Payment Lifecycle guide](/docs/guides/payments/details/payment-lifecycle.md). ### Incremental Authorization Authorize additional amounts on an existing preauth transaction. This is useful for adding incidental charges (e.g., hotel mini bar, additional restaurant items): ```swift private func incrementalAuth(transactionId: String, additionalAmount: Int) async throws { // Optional: Add breakdown for the additional amount let breakdown = PaymentBreakdown( subtotal: additionalAmount, taxRate: 8.75, // 8.75% as a percent value (not a 0-1 decimal) taxAmount: Int(Double(additionalAmount) * 0.0875), tipAmount: 0, tipType: .fixed ) do { let response = try await KoardMerchantSDK.shared.auth( transactionId: transactionId, amount: additionalAmount, breakdown: breakdown // Optional ) print("Incremental auth successful: \(response.transactionId ?? "Unknown")") } catch { print("Incremental auth failed: \(error)") throw error } } ``` ### Capture a Transaction Capture a previously authorized preauth transaction. You can capture the full authorized amount or a partial amount (e.g., adjust for final tip): ```swift private func captureTransaction(transactionId: String, finalAmount: Int? = nil) async throws { // Optional: Update breakdown with final tip amount let finalBreakdown = PaymentBreakdown( subtotal: 1000, // $10.00 taxRate: 8.75, // 8.75% as a percent value (not a 0-1 decimal) taxAmount: 88, // $0.88 tipAmount: 300, // $3.00 final tip tipType: .fixed ) do { let response = try await KoardMerchantSDK.shared.capture( transactionId: transactionId, amount: finalAmount, // nil to capture full authorized amount breakdown: finalBreakdown // Optional: updated breakdown with final tip ) print("Capture successful: \(response.transactionId ?? "Unknown")") } catch { print("Capture failed: \(error)") throw error } } ``` ### Adjust the Tip Adjust the tip on a completed transaction (for example, after a customer finalizes a tip in table service): ```swift private func adjustTip(transactionId: String, newTipTotal: Int) async throws { let response = try await KoardMerchantSDK.shared.tipAdjust( transactionId: transactionId, amount: newTipTotal, // New tip amount in cents tipType: .fixed, // .fixed or .percentage (PaymentBreakdown.TipType) eventId: UUID().uuidString // Optional UUID4 ) print("Tip adjusted: \(response.transactionId ?? "Unknown")") } ``` ### Send a Receipt Deliver a receipt for a completed transaction by email, SMS, or both. Pass at least one of `email` / `phoneNumber`: ```swift private func sendReceipt(transactionId: String) async throws { let response = try await KoardMerchantSDK.shared.sendReceipts( transactionId: transactionId, email: "customer@example.com", phoneNumber: "+15551234567" ) print("Receipt delivery: \(response)") } ``` ### Create a Fallback Payment Link When a tap cannot complete — an unsupported card, a reader problem, or a customer who would rather pay on their own device — generate a hosted payment link for the same amount: ```swift private func createFallback(amount: Int, breakdown: PaymentBreakdown?) async throws { let fallback = try await KoardMerchantSDK.shared.createFallbackLink( amount: amount, breakdown: breakdown ) print("Fallback link: \(fallback)") // Share the link with the customer (SMS, email, or QR code) } ``` ## Transaction History The SDK provides methods to retrieve and filter transaction history: ```swift import KoardSDK private func getTransactionHistory() async throws { do { // Get recent transactions let history = try await KoardMerchantSDK.shared.transactionHistory() print("Found \(history.transactions.count) transactions") // Filter by status let approvedTransactions = try await KoardMerchantSDK.shared.transactionsByStatus("approved") // Search transactions let searchResults = try await KoardMerchantSDK.shared.searchTransactions("card_number_here") // Advanced filtering let filteredTransactions = try await KoardMerchantSDK.shared.searchTransactionsAdvanced( startDate: Date().addingTimeInterval(-86400 * 7), // Last 7 days endDate: Date(), statuses: ["approved", "declined"], types: ["sale", "refund"], minAmount: 100, // $1.00 maxAmount: 10000, // $100.00 limit: 50 ) } catch { print("Transaction history failed: \(error)") throw error } } ``` **Note**: For webhook-based transaction monitoring, see our [Available Events guide](/docs/webhooks/available-events). ## Error Handling SDK calls throw `KoardMerchantSDKError`. Use its `errorDescription` property for a user-facing message. Match the cases you care about: ```swift private func handleSDKError(_ error: Error) { if let koardError = error as? KoardMerchantSDKError { switch koardError { case .missingLocationID: print("No active location set") // Prompt user to select location case .accountNotLinked: print("Tap to Pay account is not linked") // As of 1.0.20, prepare() no longer links the account for you. // Drive linking explicitly, then call prepare() again. try KoardMerchantSDK.shared.linkAccount() case .readerTokenInvalid(let message): print("Reader token invalid: \(message ?? "retry preparation")") // Usually transient — retrying prepare() recovers case .unauthorized: print("Not authenticated or session expired") // Redirect to login case .blockedAccount: print("This merchant account is blocked") case .rateLimited(let message): print("Rate limited: \(message ?? "slow down and retry")") // Back off and retry later case .network(let description, _): print("Network error: \(description)") case .server(let message): print("Server error: \(message ?? "try again later")") case .TTPPaymentFailed(.canceled): print("Customer canceled at the Tap to Pay sheet") // Benign outcome — treat as canceled, not a failure case .TTPPaymentFailed(let ttpError): print("Tap to Pay error: \(ttpError)") // Handle other reader errors case .invalidParameters(let message): print("Invalid parameters: \(message)") default: print("Koard SDK error: \(koardError.errorDescription)") } } else { print("General error: \(error)") } } ``` **Error type**: The SDK's error type is `KoardMerchantSDKError`. Earlier releases added `.rateLimited(message:)` for HTTP 429 and `TTPPaymentError.canceled` for Tap to Pay sheet cancellations. Network/transport failures are thrown as `.network(description:underlying:)` rather than raw `URLError`, and a missing session on a payment/refund/pre-auth throws `.unauthorized`. **Upgrading to 1.0.20**: Two behavior changes require code updates.\ \ **1. `prepare()` no longer auto-links the Tap to Pay account.** It now throws `KoardMerchantSDKError.accountNotLinked` instead. Guard for that case and drive linking explicitly with `linkAccount()` (or `linkAccountAsync()`), then call `prepare()` again.\ \ **2. `linkAccountAsync()` now throws** when linking fails or is declined — previously it never threw. Wrap it in `try`/`catch` and keep showing your "link account" prompt on failure.\ \ `prepare()` can also throw the new `.readerTokenInvalid`; a retry typically recovers. ## Session Management ```swift private func handleAppLifecycle() { // The SDK automatically handles background/foreground transitions // But you can monitor the status if needed NotificationCenter.default.addObserver( forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main ) { _ in Task { // Check if card reader needs re-preparation if !KoardMerchantSDK.shared.status.isReady { try? await self.prepareCardReader() } } } } ``` ## Logout and Cleanup ```swift private func logout() { // Clear all session data KoardMerchantSDK.shared.logout() print("Logged out successfully") // Redirect to login screen } ``` ## Best Practices ### SDK Management * **Token Management**: The SDK handles all token refresh automatically * **Error Handling**: Always wrap SDK calls in try-catch blocks * **Background Handling**: The SDK manages background transitions automatically * **Session Preparation**: Call `prepare()` before each payment session ### Payment Processing * **Amount Formatting**: Always use base currency units (e.g., 1050 cents for $10.50) * **Include Breakdowns**: Provide detailed breakdowns for accurate tax and tip reporting * **Location Setting**: Set active location before any payment operations * **Store Transaction IDs**: Save transaction IDs for all follow-up operations ### User Experience * **Monitor Reader Events**: Track reader events for better UX feedback * **Handle All States**: Implement handlers for all transaction states * **Provide Clear Feedback**: Show clear messages for declined or failed transactions ### Gateway Considerations * **Know Your Gateway**: Different gateways (TSYS, Payroc) have different features * **Batch Management**: Understand your gateway's batch requirements * **Response Codes**: Response codes vary by gateway For more best practices, see: * [Payment Lifecycle guide](/docs/guides/payments/details/payment-lifecycle.md) * [Sale](/docs/payments/methods/sale) and [Preauth](/docs/payments/methods/preauth) * [Capture](/docs/payments/methods/capture) and [Refund](/docs/payments/methods/refund) ## Troubleshooting ### Common Issues **Account Linking Issues** * Ensure device has iCloud account configured * Verify device has passcode enabled * Check that device supports Apple Tap to Pay on iPhone **Token Expiration** * SDK automatically refreshes tokens * Check network connectivity * Verify API key is valid **Card Reader Not Ready** * Call `prepare()` before processing payments * Ensure merchant is authenticated with `login()` * Check that account is linked with `isAccountLinked()` **Missing Location** * Verify location is set with `setActiveLocationID()` * Ensure location has valid terminal configuration * Check that location belongs to authenticated merchant **Transaction Errors** * Check transaction state before performing operations * Verify amounts are within valid ranges * Review gateway response for detailed error information For more troubleshooting help, see: * [Payment Lifecycle guide](/docs/guides/payments/details/payment-lifecycle.md) * [Incremental Auth](/docs/payments/methods/incremental-auth) and [Tip Adjust](/docs/payments/methods/tip-adjust) * [Reverse](/docs/payments/methods/reverse) and [Refund](/docs/payments/methods/refund) ## Requirements * **iOS 17.4+** * **Xcode 16.3+** * **Swift 5.9+** ## See also This wraps up payment processing with the iOS SDK. See the links below for next steps in your integration: * [Payment Lifecycle](/docs/payments/payment-lifecycle) - Complete payment flow guide * [Adding Tap to Pay](/docs/setting-up-the-ios-sdk/adding-support-for-tap-to-pay-on-iphone) - Enable contactless payments * [Installing the SDK](/docs/setting-up-the-ios-sdk/installing-the-sdk) - SDK installation guide * [Creating a Sandbox Apple Account](/docs/setting-up-the-ios-sdk/creating-a-sandbox-apple-account) - Maintain test identities * [Getting Ready for Production](/docs/setting-up-the-ios-sdk/getting-ready-for-production) - Switch schemes and API keys * [Apple Best Practices](/docs/appendix/apple-best-practices-and-guidelines) - iOS development guidelines # Installing the SDK Install the Koard Merchant SDK to enable tap-to-pay functionality in your Android application. If you're ready to start developing, see our Android SDK implementation guide. [Get started with Koard](/docs/getting-started-with-koard/introduction) **What you learn** In this guide, you'll learn: * How to add the Koard Merchant SDK to your Android project * How to configure Gradle dependencies * How to initialize the SDK in your application * How to verify your installation is working correctly **Prerequisites** Before you begin, ensure you have: * **Android Studio Hedgehog or newer** (required for modern Gradle support) * **JDK 21** (required for compilation) * **Android SDK 36** (minimum SDK 31 - Android 12) * **Physical Android device with NFC support** (Android 12+) — see [Supported Devices & NFC Tap Location](/docs/guides/android-sdk/details/supported-devices) * **Valid Koard merchant account** (configured in Koard MMS) * **Install the Visa Kernel app** on every NFC-enabled test device before running tap-to-pay flows (download from the Google Play Store) **Physical Device Required**: Tap to Pay on Android requires a physical device with NFC hardware. The Android emulator does not support NFC contactless payments. **Developer Mode Workflow**: When working with Tap to Pay on Android, follow this important workflow: 1. **Enable Developer Mode** - Turn on developer mode to install and test app revisions 2. **Install/Update Your App** - Deploy your application updates 3. **Disable Developer Mode** - You MUST turn off developer mode before running tap to pay transactions 4. **Run Transactions** - Process payments with developer mode disabled Tap to Pay transactions will NOT work if developer mode is enabled. This is a security requirement from the payment processor. ## Step 1: Add the SDK Dependency The Koard Android SDK is published to **Maven Central**. The current release is `com.koard:koard-android-sdk:1.0.6` (packaging `aar`). No credentials and no custom repository are required — you only need `mavenCentral()` in your repositories, which most Android projects already have. ### 1a. Ensure Maven Central is a repository Most Android projects already resolve dependencies from Maven Central. If yours doesn't, add `mavenCentral()` to `dependencyResolutionManagement` in your **`settings.gradle.kts`**: ```kotlin dependencyResolutionManagement { repositories { google() mavenCentral() } } ``` ### 1b. Add the dependency Add the Koard SDK to your **app module's `build.gradle.kts`**: ```kotlin dependencies { implementation("com.koard:koard-android-sdk:1.0.6") } ``` ### 1c. Declare the libraries the SDK does _not_ bring in The 1.0.6 artifact is a **shaded fat AAR**. OkHttp, Okio, Retrofit, kotlinx.serialization, and Ktor are packaged _inside_ the AAR (relocated under `com.koardlabs.*`) and are **deliberately dropped from the published POM** — this isolates the SDK from whatever versions your app uses. The published POM declares only: | Dependency | Version | | ------------------------------------------------ | ------- | | `org.jetbrains.kotlin:kotlin-stdlib` | 2.2.21 | | `org.jetbrains.kotlin:kotlin-parcelize-runtime` | 2.2.21 | | `androidx.security:security-crypto` | 1.1.0 | | `com.google.android.gms:play-services-safetynet` | 18.0.1 | | `com.nimbusds:nimbus-jose-jwt` | 10.0.2 | **Nothing else resolves transitively.** AndroidX, Kotlin coroutines, and any networking or serialization library your own code uses must be declared in your app module. If you previously relied on the SDK to pull OkHttp/Retrofit/kotlinx.serialization in for you (1.0.5 and earlier shipped them as ordinary transitive dependencies), declare them directly now. ```kotlin dependencies { implementation("com.koard:koard-android-sdk:1.0.6") // Declare what YOUR code needs — the SDK no longer supplies these transitively. implementation("androidx.core:core-ktx:1.12.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") } ``` ### Fallback: Manual AAR Installation If you have been supplied the SDK as a raw `.aar` file (for example, an offline or pre-release build), you can side-load it instead of resolving from Maven Central: 1. **Create a `libs` directory** in your app module if it doesn't exist 2. **Copy the AAR file** (e.g., `koard-android-release.aar`) to the `libs/` directory 3. **Add the dependency** in your `build.gradle.kts`: ```kotlin dependencies { implementation(files("libs/koard-android-release.aar")) // A raw file dependency carries no POM, so declare everything the SDK // would otherwise pull in — plus whatever your own code needs. // The five dependencies the published POM declares: implementation("org.jetbrains.kotlin:kotlin-stdlib:2.2.21") implementation("org.jetbrains.kotlin:kotlin-parcelize-runtime:2.2.21") implementation("androidx.security:security-crypto:1.1.0") implementation("com.google.android.gms:play-services-safetynet:18.0.1") implementation("com.nimbusds:nimbus-jose-jwt:10.0.2") // Plus whatever your own code needs: implementation("androidx.core:core-ktx:1.12.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") } ``` Do **not** add OkHttp, Retrofit, Okio, kotlinx.serialization, or Ktor "for the SDK" — they are already shaded inside the AAR under `com.koardlabs.*`. Add them only if _your own_ code uses them; the shaded copies will not conflict with yours. ## Step 2: Configure AndroidManifest.xml Permissions Add the required permissions, the `` tag, and the NFC intent filter to your `AndroidManifest.xml`: ```xml ``` The `` tag declares visibility to the Tap to Pay kernel app (`com.visa.kic.app.kernel`), which is required for the SDK to detect and communicate with the Visa kernel during tap-to-pay flows. **Where each permission comes from.** `INTERNET`, `ACCESS_NETWORK_STATE`, `NFC`, and `WAKE_LOCK` are declared in the SDK's own manifest and merged into your app, but declaring them explicitly keeps your permission surface auditable. `ACCESS_WIFI_STATE` and `REBOOT` are **required by Visa's Kernel-in-Cloud (KiC) integration spec** (§3.3.3, "mandatory minimum permissions") for apps that integrate the Tap to Pay Ready kernel — they are **not** contributed by the Koard SDK, so you must declare them yourself. The SDK also contributes `` and `android.hardware.nfc.hce`, both with `required="false"`. The `` tag and the NFC intent filter are likewise not contributed by the SDK. If your activity sets `android="portrait"`, also declare `` so Google Play does not filter out POS devices (Sunmi D3, iMin Swan 1 Pro, and similar) that do not report that feature. ### Add the NFC Tech Filter Resource Create `res/xml/nfc_tech_filter.xml` referenced by the `meta-data` above: ```xml android.nfc.tech.IsoDep ``` ## Step 3: Configure Build Settings ### Set Minimum SDK Version Ensure your app's minimum SDK is set to Android 12 (API level 31): ```kotlin android { compileSdk = 36 // belongs on `android { }`, NOT inside defaultConfig defaultConfig { minSdk = 31 targetSdk = 36 } } ``` ### Configure Java Compatibility Set Java version to 21: ```kotlin android { compileOptions { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } kotlinOptions { jvmTarget = "21" } } ``` ### Add Build Flavors (Optional) The SDK ships exactly two built-in environments — `KoardEnvironment.UAT` and `KoardEnvironment.PROD` — so mirror the demo app and define one flavor per environment: ```kotlin android { flavorDimensions += "environment" productFlavors { create("uat") { dimension = "environment" isDefault = true buildConfigField("String", "ENVIRONMENT", "\"UAT\"") applicationIdSuffix = ".uat" // UAT: https://api.uat.koard.com } create("prod") { dimension = "environment" buildConfigField("String", "ENVIRONMENT", "\"PROD\"") // Production: https://api.koard.com } } } ``` There is no `dev` environment constant. To target a non-standard backend, use `KoardEnvironment.Custom(koardApiUrl, enrollmentUrl, visaCloudPosUrl, name)` rather than adding a `dev` flavor that has nothing to map onto. ## Step 4: Initialize the SDK ### Create Application Class Initialize the SDK in your `Application.onCreate()` method on a worker thread: ```kotlin import android.app.Application import com.koardlabs.merchant.sdk.KoardMerchantSdk import com.koardlabs.merchant.sdk.domain.KoardEnvironment import com.koardlabs.merchant.sdk.domain.KoardLogLevel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext class MyApplication : Application() { override fun onCreate() { super.onCreate() // Initialize SDK synchronously during app startup runBlocking { withContext(Dispatchers.IO) { try { // The only environments are UAT, PROD, and Custom(...) val environment = KoardEnvironment.PROD KoardMerchantSdk.initialize( application = this@MyApplication, apiKey = "your-api-key", environment = environment, timeoutSeconds = 30L, logLevel = KoardLogLevel.DEBUG ) println("Koard SDK initialized successfully") } catch (e: Exception) { // Handle initialization error println("Failed to initialize SDK: ${e.message}") } } } } } ``` **Important**: * SDK initialization must be performed on a worker thread (not the main UI thread). Always use `Dispatchers.IO` with coroutines. * The `initialize()` method takes `application`, `apiKey`, `environment`, and the optional `timeoutSeconds` (default `30L`) and `logLevel` - NOT merchantCode/merchantPin. * Merchant authentication is done separately using `login(merchantCode, merchantPin)` after initialization. * `logLevel` is the **only** place SDK logging verbosity can be set — there is no runtime setter. It defaults to `KoardLogLevel.DEBUG` in debug builds and `KoardLogLevel.NONE` in release. ### Register Application in Manifest Add your custom Application class to `AndroidManifest.xml`: ```xml ``` ## Step 5: Register Your Activity for NFC Android dispatches `TECH_DISCOVERED` to whatever app claims it, so an unrelated NFC app can jump into the foreground the moment the customer presents a card. `registerActivityForNfc(this)` claims NFC foreground dispatch for your activity so it stays in front for the duration of the tap — including the receipt or animation screens that follow, while the card may still be in the field (KiC integration guide §3.4.14). Call it from `onResume()` and `unregisterActivityForNfc(this)` from `onPause()`. Both are suspend functions that must run on a worker thread. Visa marks both APIs **Implementation: Optional** (§3.3.15.1, §3.3.15.2) — a tap can complete without them — but skipping them leaves the tap exposed to being interrupted by another app, so Koard recommends wiring them up on every activity that can host a tap. ```kotlin class MainActivity : ComponentActivity() { private val nfcMutex = Mutex() override fun onResume() { super.onResume() lifecycleScope.launch(Dispatchers.IO) { nfcMutex.withLock { KoardMerchantSdk.getInstance().registerActivityForNfc(this@MainActivity) } } } override fun onPause() { lifecycleScope.launch(Dispatchers.IO) { nfcMutex.withLock { KoardMerchantSdk.getInstance().unregisterActivityForNfc(this@MainActivity) } } super.onPause() } } ``` **Guard the pair with a mutex** (as the demo app does) so a rapid resume/pause cycle cannot interleave the two calls. Also add the `TECH_DISCOVERED` intent filter and the `@xml/nfc_tech_filter` tech-list to the same activity in your manifest — per KiC §3.4.14 the registration APIs are meant to be used alongside that manifest declaration, and it is not contributed by the Koard SDK. `unregisterActivityForNfc()` is safe to call from `onPause()` even during a tap: the SDK detects an in-flight tap and skips the unregister. This matters because the Visa kernel foregrounds its own secure Online-PIN keypad, which pauses your activity — tearing down the registration there would abort PIN entry and decline the sale. ## Step 6: Authenticate Merchant After SDK initialization, authenticate the merchant using their credentials: ```kotlin import androidx.lifecycle.lifecycleScope import com.koardlabs.merchant.sdk.KoardMerchantSdk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launch(Dispatchers.IO) { try { val sdk = KoardMerchantSdk.getInstance() // Login with merchant credentials val success = sdk.login( merchantCode = "your-merchant-code", merchantPin = "your-merchant-pin" ) if (success) { println("Merchant authenticated successfully") } else { println("Authentication failed") } } catch (e: Exception) { println("Error: ${e.message}") } } } } ``` The `login()` method authenticates the merchant and retrieves an access token. This must be done before processing transactions. `login()` returns `false` rather than throwing when the credentials are rejected. If your integration has already resolved the merchant identity into a single opaque string (a QR scan, an SSO callback, or a server-issued provisioning token), use the `login(alias: String): Boolean` overload instead. It hits the same login route and yields the same session token; the alias itself is never stored. Call `logout()` to end the session. Logout clears the session token and the active location but **preserves enrollment**, so the same merchant can log back in without re-enrolling. Switching the device to a _different_ merchant requires an explicit `unenrollDevice()` first. ### Test Basic Import Add this import statement to verify the SDK is accessible: ```kotlin import com.koardlabs.merchant.sdk.KoardMerchantSdk import com.koardlabs.merchant.sdk.domain.KoardEnvironment import com.koardlabs.merchant.sdk.domain.KoardLocation import com.koardlabs.merchant.sdk.domain.exception.KoardException ``` If the imports succeed without errors, your installation is working correctly. ## Step 7: Select a Location and Enroll the Device **Both of these steps are mandatory.** Enrollment is **not** automatic — nothing happens as a side effect of `login()`. Until the device has an active location _and_ is enrolled, `sale()`, `preauth()`, `completePartialAuth()`, and `refundEmv()` throw `KoardException` with `error.errorType` of `KoardErrorType.NotReady.NoActiveLocation` or `KoardErrorType.NotReady.NotEnrolled`. The order is fixed: authenticate, choose a location, then enroll. ```kotlin import com.koardlabs.merchant.sdk.KoardMerchantSdk import com.koardlabs.merchant.sdk.domain.exception.KoardException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext suspend fun setUpDevice() = withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() // 1. Fetch the merchant's locations and pick one. val locations = sdk.getLocations().getOrElse { error -> println("Could not load locations: ${error.message}") return@withContext } val location = locations.firstOrNull() ?: return@withContext try { // 2. Set the active location. Backend-authoritative: it re-binds the // terminal on the backend and only commits locally if that succeeds. sdk.setActiveLocation(location.id) // 3. Enroll the device. Returns a String status; throws on failure. val status = sdk.enrollDevice() println("Enrollment status: $status") } catch (e: KoardException) { println("Setup failed: ${e.error.shortMessage} (${e.error.errorType})") } } ``` ### `setActiveLocation(locationId)` * Suspend, worker thread only, returns `Unit` and **throws** on failure — it no longer silently no-ops. * On an unenrolled device it simply records the selection and flips readiness to `hasActiveLocation = true`. It does **not** enroll for you; you still have to call `enrollDevice()`. * If the device is **already enrolled** and you switch to a _different_ location, the SDK re-binds the terminal on the backend first and commits the new location locally only if the backend approves. A failed re-bind throws and leaves the previous location intact. It then pushes the new location's terminal/CVM profile down to the kernel. * Switching locations on an enrolled device **throws `InvalidRequest` when developer mode is on** — the Visa kernel will not accept a new profile in that state, so the SDK refuses the switch rather than half-applying it. * During the switch, readiness reports `NotReady.Preparing`, so a tap started mid-switch is rejected instead of running against a terminal that is still moving. Observe the resolved location reactively rather than caching your own copy — a private cache survives logout and goes stale, which blocks the next enrollment with "no active location set": ```kotlin sdk.activeLocation.collect { location -> // StateFlow updateUi(location?.name) } // On a cold start, resolve the persisted id into a full object once: sdk.refreshActiveLocation() // suspend, returns KoardLocation? sdk.activeLocationId // String? — the raw persisted id ``` ### `enrollDevice()` `suspend fun enrollDevice(): String` — worker thread only. It returns a status **String** and throws `KoardException` when it cannot proceed. Before calling it, the SDK refreshes its readiness checks and rejects the call when: * **Developer mode is enabled** — `InvalidRequest`, "Disable developer mode before enrolling device." * **The device is already enrolled** — `InvalidRequest`, "Clear enrollment data before enrolling again." Call `clearEnrollmentState()` (local only) or `unenrollDevice()` (clears the enrollment data held by the Tap to Pay Ready app, then clears local state) first. Note that `unenrollDevice()` does **not** fully deprovision the device on Visa's backend — see [Troubleshooting](/docs/guides/android-sdk/details/troubleshooting#tamperdetected-on-every-sale-after-a-failed-enrollment). * **No active location is set** — enrollment requires one, which is why Step 7 runs in this order. Because it is a user-visible, failure-prone operation, surface it as an explicit action in your UI (the demo app renders an **Enroll Device** button whenever `readinessState.enrollmentState` is not `Enrolled`) rather than firing it silently on login. Track progress through `sdk.readinessState`, whose `enrollmentState` moves `NotEnrolled` → `Enrolling` → `Enrolled` (or `Failed`). Use `readiness.notReadyReason()` to get the typed blocking reason without attempting a transaction. ## Step 8: Transaction Processing With installation complete you can now run Tap to Pay sessions. Keep these guardrails in mind: * `sdk.sale(...)`, `sdk.preauth(...)`, `sdk.completePartialAuth(...)`, `sdk.refundEmv(...)`, and `sdk.prepare()` are the APIs that drive a reader session, so they return a cold `Flow` that drives the reader UI. Other calls reach the thin client too (`isKernelAppInstalled()`, `checkKiCEligibility()`, `getTransactionConfig()`, `cancelTransaction()`, `resetKernelService()`, `unenrollDevice()`, the NFC registration pair), but they are one-shot and emit no reader events. * `sdk.refund(...)` is **backend-only** — it returns `Result` and never involves a tap. Use `sdk.refundEmv(...)` when the customer must present a card. * Every other payment action (capture, reverse, incremental auth, receipts, tip adjustment) is a suspend function that returns a `Result` and never emits reader events. * The SDK enforces readiness (`sdk.readinessState.value.isReadyForTransactions`) and blocks taps if the required tap-to-pay dependency is missing, developer mode is on, the device is not enrolled, or no active location is set. Rather than duplicating code here, the dedicated [Running Payments](/docs/setting-up-the-android-sdk/running-payments) guide shows: * How the demo’s `MainScreenViewModel` builds `PaymentBreakdown`, collects the Flow, and updates UI state * Mapping `KoardReaderStatus` + reader status codes to your own UX * Handling surcharge confirmation, canceling the reader, and orchestrating post-reader calls (capture/refund/reverse/incremental/tip adjust/receipt) Use that guide as the canonical reference when wiring payments into your application. ## Troubleshooting ### Common Installation Issues **SDK Not Initialized Error** ```plaintext IllegalStateException: Instance is null. Did you forget to call initialize? ``` **Solution**: Ensure `KoardMerchantSdk.initialize()` is called in `Application.onCreate()` on a worker thread before accessing `getInstance()`. **Thread Enforcement Error** **Solution**: All SDK operations must be called from a worker thread. Use `Dispatchers.IO`: ```kotlin lifecycleScope.launch(Dispatchers.IO) { // SDK operations here } ``` **Dependency Conflicts** **Solution**: Ensure you're using compatible versions of AndroidX and Kotlin libraries. Check for dependency conflicts with: ```bash ./gradlew app:dependencies ``` **Build Errors** **Solution**: * Verify you're using JDK 21 * Ensure minimum SDK is set to 31 (Android 12) * Clean and rebuild: `./gradlew clean build` **NFC Transactions Failing** **Solution**: * **Disable developer mode before running transactions** - This is required for security * Developer mode blocks tap to pay functionality * Workflow: Enable dev mode → Install app → Disable dev mode → Run transactions * Go to **Settings > System > Developer Options** and toggle OFF * Restart device after disabling developer mode ### Verification Checklist * [ ] Android Studio Hedgehog or newer installed * [ ] JDK 21 configured * [ ] Minimum SDK set to 31 (Android 12) * [ ] `mavenCentral()` present in repositories * [ ] SDK dependency added to build.gradle.kts * [ ] Application class created and registered * [ ] SDK.initialize() called on worker thread * [ ] Required permissions added to manifest (INTERNET, ACCESS\_NETWORK\_STATE, ACCESS\_WIFI\_STATE, REBOOT, NFC, WAKE\_LOCK) * [ ] `` tag added with `com.visa.kic.app.kernel` * [ ] NFC `TECH_DISCOVERED` intent filter and `@xml/nfc_tech_filter` meta-data added to your tap activity * [ ] `registerActivityForNfc()` / `unregisterActivityForNfc()` wired into `onResume()` / `onPause()` * [ ] `setActiveLocation(locationId)` called after login * [ ] `enrollDevice()` exposed as an explicit user action and completed successfully * [ ] Project builds without errors * [ ] Import statements work correctly * [ ] Tap-to-pay kernel dependency installed on test device (Visa Tap to Pay Ready) * [ ] Physical device with NFC available for testing * [ ] Aware of developer mode workflow (enable → install → disable → run transactions) ## Next Steps Once the SDK is installed and configured: * [Run the Demo App](/docs/setting-up-the-android-sdk/demo) - Test SDK functionality with the demo application * [Supported Devices & NFC Tap Location](/docs/setting-up-the-android-sdk/supported-devices) - Compatible devices, requirements, and where to tap * [Running Payments](/docs/setting-up-the-android-sdk/running-payments) - Implement tap-to-pay flows, surcharging, and post-reader actions * [SDK Response Codes](/docs/setting-up-the-android-sdk/sdk-response-codes) - Understand transaction outcomes, display messages, and error handling * [Troubleshooting](/docs/setting-up-the-android-sdk/troubleshooting) - Fix enrollment failures and taps that cancel instantly * [Understand Payment Lifecycle](/docs/payments/payment-lifecycle) - Learn about the complete payment flow ## See also This wraps up the SDK installation. See the links below for next steps in your integration: * [Demo App Setup](/docs/setting-up-the-android-sdk/demo) - Run the demo application * [SDK Response Codes](/docs/setting-up-the-android-sdk/sdk-response-codes) - Transaction outcomes, error codes, and display messages * [Payment Lifecycle](/docs/payments/payment-lifecycle) - Complete payment flow guide * [Supported Devices & NFC Tap Location](/docs/setting-up-the-android-sdk/supported-devices) - Device compatibility and tap guidance * [Troubleshooting](/docs/setting-up-the-android-sdk/troubleshooting) - Device-side fixes for enrollment and tap failures ## Best Practices * **Idempotency**: Generate a unique `eventId` (UUID) for every transaction mutation (sale, refund, adjust) and persist it with your POS records. * **Threading**: Always hop to `Dispatchers.IO` before calling any SDK method; return results to the main thread only after the call completes. * **Environment Flavors**: Mirror the demo’s `uat`/`prod` flavors so each build points at the correct Koard environment and credential set. Use `KoardEnvironment.Custom(...)` for anything else. * **Logging**: The SDK writes to Logcat under the fixed tag **`KoardSDK`**. Set verbosity once via `initialize(..., logLevel = KoardLogLevel.VERBOSE)` — there is no runtime setter, and the SDK no longer depends on Timber. Secrets (API key and token headers, device certificates, enrollment key material) are never logged at any level. * **Device Re-provisioning**: When wiping devices or reinstalling the tap-to-pay kernel app, call `refreshDeviceCertificates()` to regenerate keys and rerun enrollment. # SDK Response Codes & Error Handling Understand how the Koard Android SDK surfaces errors and transaction outcomes — including the `KoardException` / `KoardError` types your app catches, transaction response codes, display messages, and error scenarios. **What you learn** * The two error channels: **`KoardException`** (thrown) vs **`KoardTransactionResponse`** (emitted) * The `KoardError` and `KoardErrorType` sealed hierarchy your app inspects for error details * How the SDK wraps all underlying Visa KiC errors into Koard types — you never handle raw KiC exceptions * The final transaction statuses: **Approve**, **Decline**, **Abort**, **Failure**, **AltService**, and **Unknown** * What `statusCode` means and the numeric codes the underlying Visa KiC kernel sends * Display message IDs shown during the tap-to-pay flow * Common abort and error scenarios and how to handle them ## Error Model Overview The SDK surfaces errors through **two channels** depending on context: | Channel | When | How | What to inspect | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | --------------- | ---------------------------------------------------------------------- | | **`KoardException`** (thrown) | Non-transaction operations: enrollment, SDK init, API calls (`capture`, `refund`, `reverse`, `adjust`), validation | `try / catch` | `exception.error.errorType` — a `KoardErrorType` sealed class | | **`KoardTransactionResponse`** (emitted) | During `sale()` / `preauth()` / `completePartialAuth()` / `refundEmv()` tap flows | Callback / Flow | `response.actionStatus`, `response.finalStatus`, `response.statusCode` | **`refund()` is not a tap flow.** `sdk.refund(...)` (and its `refundTransaction(...)` alias) is a backend-only suspend call returning `Result`, so its errors arrive through the `KoardException` channel. The card-present refund is `sdk.refundEmv(...)`, which returns a `Flow` and uses the emitted channel. **You never handle raw KiC exceptions.** The SDK catches every `KiCSdkException` from the Visa Kernel in the Cloud SDK and maps it to a `KoardException` with a typed `KoardErrorType`. Your app only needs to handle Koard types. ## KoardException & KoardError `KoardException` is the main exception thrown by the SDK for all non-transaction-flow errors. It wraps a `KoardError` with a human-readable message and a typed error classification: ```kotlin class KoardException( cause: Throwable? = null, val error: KoardError = KoardError( shortMessage = "Koard Merchant SDK has encountered a fatal error. ...", errorType = KoardErrorType.GeneralError ) ) : Exception(error.shortMessage, cause) data class KoardError( val shortMessage: String, // Human-readable error description val errorType: KoardErrorType // Typed error classification (sealed hierarchy) ) ``` **The underlying engine exception is never attached as `cause`.** When the SDK maps a `KiCSdkException` from the Visa Kernel in the Cloud, it deliberately does _not_ set it as the `KoardException.cause` — this keeps third-party SDK identifiers out of your crash reporters and means you never need a transitive dependency on the engine's exception types. The numeric code and any human-readable detail are folded into `error.shortMessage` and `error.errorType`. ### Catching KoardException ```kotlin try { sdk.capture(transactionId, amount) } catch (e: KoardException) { when (e.error.errorType) { is KoardErrorType.KoardServiceErrorType.HttpError -> { val code = (e.error.errorType as KoardErrorType.KoardServiceErrorType.HttpError).errorCode showError("Server error (HTTP $code): ${e.error.shortMessage}") } is KoardErrorType.KoardServiceErrorType.ConnectionError -> showError("Network error — check your connection") is KoardErrorType.KoardServiceErrorType.Unauthorized -> showError("Session expired — please log in again") is KoardErrorType.VACEnrollmentError -> showError("Enrollment failed — re-enroll the device") else -> showError(e.error.shortMessage) } } ``` ## KoardErrorType Reference `KoardErrorType` is a sealed class hierarchy. Every error the SDK produces maps to one of these types. ### Top-Level Error Types | Error Type | When It Occurs | | --------------------------- | -------------------------------------------------------------------------- | | `GeneralError` | Catch-all for unmapped or unexpected errors | | `CertificateError` | TLS or certificate validation failure | | `BLEError` | Bluetooth/peripheral communication failure | | `MainThreadError` | SDK method called on the main thread (must use a worker thread) | | `NfcTransactionError` | NFC transaction-level failure | | `VACEligibilityError` | Device failed Visa Acceptance Cloud eligibility check (e.g., Android < 12) | | `DeviceNotProvisionedError` | Device has not been provisioned for Tap to Pay | | `VACEnrollmentError` | Enrollment with the Visa Acceptance Cloud failed | ### NotReady — Blocked Before the Tap Starts `sale()`, `preauth()`, `completePartialAuth()`, and `refundEmv()` refresh their readiness checks first and **throw** a `KoardException` whose `error.errorType` is a `KoardErrorType.NotReady.*` value if the SDK cannot transact. The Flow is never returned, so wrap the call itself — not just the collection — in `try / catch`. `KoardSdkReadiness.notReadyReason()` returns the same typed reason without attempting a transaction. | Error Type | Meaning | What to do | | ------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `KernelAppNotInstalled` | The Visa Tap to Pay Ready app isn't installed | Route the user to install it (`installKernelApp(activity)`) | | `DeveloperModeEnabled` | Developer options are on | Prompt the operator to disable developer mode | | `NotAuthenticated` | No merchant session | Call `login(...)` | | `NotEnrolled` | The device has not been enrolled | Set an active location, then call `enrollDevice()` | | `NoActiveLocation` | No active location selected | Call `setActiveLocation(locationId)` | | `ReaderNotStarted` | Enrolled with a location, but the reader connection was never started or was killed | Call `prepare()` to bring the session back up before retrying the tap | | `Preparing` | Enrolling, generating certificates, the payment processor is coming up, or a location switch is in flight | **Transient** — retry shortly | | `CertificateFailed(error)` | Certificate generation failed | Inspect `error`; re-run `refreshDeviceCertificates()` | | `EnrollmentFailed(error)` | Enrollment failed | Inspect `error`; see [Troubleshooting](/docs/guides/android-sdk/details/troubleshooting) | | `PaymentProcessorFailed(error)` | The thin client failed to start | Inspect `error`; `resetKernelService()` to release the IPC connection, then retry (optionally `prepare()` first to re-warm) | `KoardErrorType` and its nested `KoardServiceErrorType` / `KicConnectorError` are `sealed`, and 1.0.6 added the members above plus `KoardServiceErrorType.UnparseableResponse` and `KicConnectorError.KernelAppBusyWithAnotherMerchant`. Any exhaustive `when` over these types will stop compiling until you add the new branches or an `else`. ### KoardServiceErrorType — API / HTTP Errors Thrown when SDK methods call the Koard REST API (`capture`, `refund`, `reverse`, `adjust`, `getTransaction`, etc.): | Error Type | Description | | --------------------------- | --------------------------------------------------------------------------------------------- | | `HttpError(errorCode: Int)` | Server returned an HTTP error — inspect `errorCode` for the status (400, 401, 404, 500, etc.) | | `InvalidRequest` | Request validation failed before sending (e.g., negative amount, missing transaction ID) | | `NotFound` | Resource not found (404) | | `Unauthorized` | Missing or invalid API key / session (401) | | `UnexpectedError` | Unexpected server error or empty response body | | `UnparseableResponse` | The response body could not be deserialized | | `ConnectionError` | Network unreachable, DNS failure, or timeout | ### DeviceIntegrityError — Security Checks Thrown when the device fails security validation during enrollment or transaction preparation: | Error Type | KiC Code | Description | | ------------------------- | -------- | ------------------------------------------------------ | | `EmulatorDetected` | 1000 | Running on an emulator — use a physical device | | `RootDetected` | 1001 | Device is rooted or has superuser binaries | | `TamperDetected` | 1002 | Device tamper detection triggered | | `DeveloperModeEnabled` | 2000 | Developer options must be disabled | | `DebugModeEnabled` | 2001 | USB debugging must be disabled | | `HookDetected` | 2003 | Runtime instrumentation detected (Frida, Xposed, etc.) | | `GenericIntegrityFailure` | -1 | Generic device integrity attestation failure | ### TransactionErrorType — Card & Payment Errors These appear as the `errorType` on a `KoardException` when a transaction-level error is mapped from the KiC thin client. They correspond to EMV-level outcomes: | Error Type | Description | | ---------------------------------- | -------------------------------------------------- | | `TransactionAmountNonPositive` | Amount must be greater than zero | | `RefundMissingParentTransactionId` | Refund requires a parent transaction ID | | `CancelOrEnter` | Cardholder prompted to cancel or confirm | | `CardError` | Unrecoverable card data error | | `NotAuthorisedOrDeclined` | Issuer declined the transaction | | `PinRequired` | PIN entry is required | | `IncorrectPin` | Cardholder entered an incorrect PIN | | `ProcessingError` | Generic processing failure | | `TryAnotherCard` | Card cannot complete — try a different card | | `InsertOrSwipe` | Contactless not supported — use chip or mag-stripe | | `TryAnotherChoice` | Try a different payment method | | `Cancelled` | Transaction was cancelled | | `StrongCvm` | Strong Customer Verification required (SCA) | | `PinBypassed` | PIN entry was bypassed | | `PinNotProvided` | PIN was requested but not provided | | `TransactionNotAllowed` | Transaction type not allowed on this card/terminal | | `NotApplicable` | Status not applicable to this transaction type | | `UnknownStatus` | Unmapped status from the kernel | | `TransactionError` | Generic transaction error | | `EnableReader` | NFC reader needs to be enabled | | `NetworkError` | Network error during transaction processing | | `AuthenticationFailed` | Authentication with the payment backend failed | | `CouldNotAttestError` | Device attestation failed during transaction | | `AsiError` | Visa auth service interface error | | `TcConfigError` | Thin client configuration error | | `VACResponseFailedError` | VAC response indicated failure | | `VACResponseParseError` | Could not parse VAC response | | `TransactionInProgressError` | Another transaction is already in progress | | `DeviceDisabledError` | Device has been disabled for transactions | | `ErrorLoadingConfig` | Could not load transaction configuration | | `VACInternalError` | Internal VAC error | | `TransactionApprovedUploadFailed` | Transaction approved but receipt upload failed | ### KiC Connector Errors Thrown when the SDK cannot communicate with the Visa Tap to Pay Ready kernel app: | Error Type | KiC Code | Description | | ---------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `BindingError` | 92 | Failed to bind to the Visa kernel service | | `ConnectorSendError` | 93 | Sending a message to the kernel failed | | `KernelParseError` | 94 | Kernel response could not be parsed | | `ConnectorParseError` | 95 | Connector-side serialization failed | | `KernelAppNotInstalled` | 96 | Visa kernel app missing — install from Google Play | | `PlayProtectOrVerifyAppDisabled` | 97 | Google Play Protect must be enabled | | `KernelAppBusyWithAnotherMerchant` | 98 | Another merchant app on the device currently holds the lock on the Tap to Pay Ready kernel service (KiC multi-tenancy). Close the other app or wait for it to finish — calling `resetKernelService()` will **not** help because the lock is owned by a different process | ### KiC General Errors | Error Type | KiC Code | Description | | -------------------------------- | -------- | ---------------------------------------- | | `NoNetworkOrTimedOut` | 10 | Network unavailable or request timed out | | `UnsupportedAndroidVersion` | 21 | Device OS below Android 12 | | `TapToPayReadyAppUpdateRequired` | 62 | Visa Tap to Pay Ready app is outdated | ### KiC Eligibility Errors | Error Type | KiC Code | Description | | -------------------------------------------- | -------- | --------------------------------------- | | `UnsupportedOs` | 80 | OS build is unsupported | | `HardwareKeystoreNotPresent` | 81 | No hardware-backed keystore | | `ECEncryptionNotAvailable` | 82 | Elliptic-curve crypto unavailable | | `AESEncryptionNotAvailable` | 83 | AES crypto unavailable | | `DESEncryptionNotAvailable` | 84 | DES crypto unavailable | | `NfcNotAvailable` | 85 | NFC hardware missing or disabled | | `GooglePlayServicesNotAvailableOrOldVersion` | 86 | Google Play Services absent or outdated | | `EligibilityCheckFailed` | -1 | Generic eligibility failure | ### KiC Initialize Errors | Error Type | KiC Code | Description | | ----------------------- | -------- | ------------------------------------------- | | `AlreadyEnrolled` | 1 | Device already enrolled — no action needed | | `DeviceAuthPubKidEmpty` | 3 | Missing device-auth public key — re-enroll | | `VacDeviceIdEmpty` | 4 | VAC device ID not provided | | `XRandomValueEmpty` | 52 | Random nonce required by enrollment missing | | `Failed` | -1 | Generic initialization failure | ### KiC Prepare Errors Pre-transaction secure channel setup failures: | Error Type | KiC Code | Description | | -------------------------------- | -------- | ----------------------------------------------------- | | `AuthenticationFailed` | 7 | VAC authentication failed | | `SdkInitNotDone` | 11 | `init()` not completed before use | | `SdkEnrollNotDone` | 12 | `enrollDevice()` not completed — re-enroll | | `ErrorLoadingConfig` | 17 | Could not load config blobs | | `AsiError` | 18 | Visa auth service interface error | | `HardwareKeystoreNotPresent` | 20 | Hardware keystore missing during key prep | | `AttestationFailed` | 24 | Device attestation failed | | `DoLoginFailed` | 25 | Login exchange with Visa backend failed | | `NullLoginAssertion` | 26 | Login response missing assertion | | `NullLoginCrypto` | 27 | Login response missing crypto payload | | `NullLoginResponse` | 28 | Entire login response was null | | `NullLoginResponseBody` | 29 | Login HTTP body empty | | `NullLoginResponseAuthStatus` | 30 | Login response missing auth status | | `NullSharedSecret` | 31 | Shared secret not derived — re-enroll | | `NullSessionKeys` | 32 | Session keys missing — re-enroll | | `FailedResponseVerification` | 33 | MAC/signature mismatch — possible tampering | | `FailedMacTagVerification` | 34 | MAC tag verification failed | | `FailedAuthStatus` | 36 | Visa backend rejected authorization | | `EmptyAuthStatus` | 37 | Auth status element empty | | `GetSeedListFailure` | 49 | Could not fetch key-rotation seed list | | `CertificatePinningError` | 50 | TLS pinning failed — possible MITM | | `TransactionKeyDerivationFailed` | 51 | Could not derive transaction keys — re-enroll | | `KeyRotationNeeded` | 87 | Kernel requested key rotation (handled automatically) | | `KeyRotationNotNeeded` | 88 | Key rotation not needed (informational) | | `KeyRotationSuccess` | 89 | Key rotation completed (informational) | | `KeyRotationFailure` | 90 | Key rotation failed — re-enroll if transactions fail | | `KeyRotationNullResponse` | 91 | Kernel did not return rotation status | ## Transaction Response Flow Every `KoardTransactionResponse` emitted by `sdk.sale()` or `sdk.preauth()` includes an **action status** that tells your app what stage the transaction is in. Use this to drive your UI: | Action Status | Meaning | What to do | | ------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | | `OnProgress` | Transaction is in flight — the reader is active | Update your UI with the current `displayMessage` and `readerStatus` | | `OnFailure` | A non-recoverable error occurred before completion | Read the `statusCode` to determine the failure reason and display an appropriate error | | `OnComplete` | The transaction has finished — check `finalStatus` for the outcome | Route to your receipt, decline, or error screen based on `finalStatus` | ```kotlin when (response.actionStatus) { KoardTransactionActionStatus.OnProgress -> { showStatus(response.readerStatus.toString(), response.displayMessage) } KoardTransactionActionStatus.OnFailure -> { showError(response.statusCode, response.displayMessage ?: "Transaction failed") } KoardTransactionActionStatus.OnComplete -> { when (response.finalStatus) { KoardTransactionFinalStatus.Approve -> showReceipt(response.transaction!!) KoardTransactionFinalStatus.Decline -> showDeclined(response) KoardTransactionFinalStatus.Abort -> showAborted(response) KoardTransactionFinalStatus.Failure -> showFailure(response) } } } ``` ## Final Transaction Statuses When `actionStatus` is `OnComplete`, the SDK sets `finalStatus` to one of these values: | Final Status | Description | Typical Cause | | ------------------------ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | **`Approve`** | Transaction was authorized by the issuer | Successful payment — display receipt with approval code and transaction details | | **`Decline`** | Transaction was explicitly declined | Issuer denied the authorization, card restricted, insufficient funds, or Strong CVM required (SCA interface switch) | | **`Abort`** | Transaction was terminated before completion | User cancelled, PIN entry cancelled, device security issue, NFC read failure, timeout, or network loss | | **`Failure`** | An internal or system-level error prevented the transaction | SDK/kernel error, device misconfiguration, or unexpected processing failure | | **`AltService`** | Card requested an alternative service | The card network indicated that an alternative acceptance method should be used | | **`Unknown(rawStatus)`** | The kernel returned a status the SDK could not map | Inspect `rawStatus` for the original value; treat as a non-approval | The SDK consolidates the underlying processor response into these statuses so your app does not need to interpret raw processor-level codes. The original acquirer `responseCode` (ISO 8583 field 39) is still available in the transaction receipt for logging and support purposes. ## Persisted Transaction Status The reader `finalStatus` above describes the outcome of a single tap. The persisted transaction itself (`KoardTransaction.status`, also returned by `getTransaction`/`getTransactions` and post-reader operations) uses the `KoardTransactionStatus` enum: | Status | Description | | ------------------- | ------------------------------------------------------------------------------------------- | | `PENDING` | Transaction created but not yet finalized | | `AUTHORIZED` | Funds authorized (preauth/hold) | | `CAPTURED` | Authorization captured/settled | | `SETTLED` | Settled with the processor | | `DECLINED` | Declined by the issuer | | `REFUNDED` | Fully or partially refunded | | `REVERSED` | Reversed/voided | | `CANCELED` | Canceled before completion | | `ERROR` | Errored out | | `SURCHARGE_PENDING` | Awaiting customer surcharge confirmation — call `sdk.confirm(transactionId, confirm = ...)` | | `UNKNOWN` | Unmapped status | A transaction is refundable when its status is `AUTHORIZED`, `CAPTURED`, `SETTLED`, or `PENDING` (exposed as `KoardTransactionStatus.isRefundable`). ## Acquirer Authorization Statuses Behind the scenes, the acquirer returns a more granular `authStatus` in the authorization response. The SDK maps these to the final statuses above, but they are available in the transaction details for advanced use cases: | Auth Status | Description | Maps to Final Status | | ------------------ | ------------------------------------------------------------------------ | ------------------------------------------- | | `Approve` | Issuer approved the transaction | `Approve` | | `Decline` | Issuer declined the transaction (also used for internal acquirer errors) | `Decline` | | `PartialApproval` | Issuer approved a lesser amount than requested | `Approve` (with reduced `authorizedAmount`) | | `InvalidPIN` | The PIN entered by the cardholder was incorrect | `Decline` | | `UnableToGoOnline` | The terminal could not connect to the acquirer for online authorization | `Decline` or `Abort` | | `AdditionalInfo` | Acquirer returned supplementary information (e.g., referral) | Varies | ## Transaction Response Details When a transaction completes (regardless of outcome), the `KoardTransactionResponse` contains the following fields: | Field | Type | Description | | ----------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------- | | `transactionId` | `String` | Unique identifier for the transaction | | `finalStatus` | `KoardTransactionFinalStatus` | Terminal outcome: `Approve`, `Decline`, `Abort`, `Failure`, or `AltService` | | `actionStatus` | `KoardTransactionActionStatus` | Current action phase: `OnProgress`, `OnFailure`, or `OnComplete` | | `readerStatus` | `KoardReaderStatus` | Reader state: `preparing`, `readyForTap`, `cardDetected`, `processing`, `complete`, etc. | | `displayMessage` | `String?` | Human-readable message from the reader/kernel | | `statusCode` | `Int?` | Numeric status code from the Visa KiC kernel — see [Status Code Reference](#status-code-reference) below | | `statusCodeDescription` | `String?` | Human-readable description of the status code (auto-generated from the code) | | `transaction` | `KoardTransaction?` | Full transaction object (populated on completion) | ## Status Code Reference The `statusCode` field on `KoardTransactionResponse` is a numeric integer forwarded from the underlying **Visa Kernel in the Cloud (KiC)** SDK. These codes are passed through on the transaction response for troubleshooting and logging. **These same codes drive the `KoardErrorType` mapping.** When the SDK catches a `KiCSdkException` with one of these codes, it maps it to the corresponding `KoardErrorType` documented in the [KoardErrorType Reference](#koarderrortype-reference) above. You don't need to handle numeric codes directly — use `KoardErrorType` pattern matching instead. For most apps, routing on `actionStatus` + `finalStatus` is sufficient. The `statusCode` is useful for **debugging**, **logging**, and handling edge cases like re-enrollment (`12`) or developer mode (`2000`). The status codes fall into several categories based on what layer of the KiC stack generated them: ### Connector Status (92–97) — Service Binding Failures These indicate problems communicating between the Koard SDK and the Visa Tap to Pay Ready app installed on the device. | Code | Description | What to do | | ---- | --------------------------------------------------- | ---------------------------------------------------------------- | | `92` | Failed to bind to the Visa kernel service | Ensure the Visa Tap to Pay Ready app is installed and up to date | | `93` | Sending a message to the kernel service failed | Retry the operation; if persistent, restart both apps | | `94` | Kernel response payload could not be parsed | Update the Visa Tap to Pay Ready app | | `95` | Connector-side serialization/deserialization failed | Update the Koard SDK to the latest version | | `96` | Visa kernel service app missing on device | Install the Visa Tap to Pay Ready app from Google Play | | `97` | Google Play Protect / Verify Apps is disabled | Enable Play Protect in Google Play settings | ### General Status (10, 21, 62) — Environment Readiness | Code | Description | What to do | | ---- | -------------------------------------------- | ------------------------------------------------ | | `10` | Network unavailable or SDK request timed out | Check network connectivity and retry | | `21` | Device OS level not supported by Tap to Pay | Device must run Android 12 (API 31) or later | | `62` | Visa Tap to Pay Ready app is outdated | Update the Tap to Pay Ready app from Google Play | ### Eligibility Status (80–86) — Device Capability Checks Returned when `checkKiCEligibility()` detects a device hardware or software limitation. | Code | Description | What to do | | ---- | ---------------------------------------------- | ------------------------------------------------------------ | | `80` | OS flavor/build is unsupported | Device uses an incompatible Android build (e.g., custom ROM) | | `81` | Device lacks a hardware-backed keystore | Device does not meet security requirements | | `82` | Elliptic-curve crypto APIs missing or disabled | Device crypto hardware insufficient | | `83` | AES crypto acceleration unavailable | Device crypto hardware insufficient | | `84` | DES crypto unavailable | Device crypto hardware insufficient | | `85` | NFC hardware missing or disabled | Enable NFC in device settings, or device has no NFC | | `86` | Google Play Services absent or out of date | Install or update Google Play Services | ### Initialize Status (1, 3, 4, 52) — Enrollment & Bootstrap | Code | Description | What to do | | ---- | ---------------------------------------------- | ---------------------------------------------------------- | | `1` | Device already enrolled for Tap to Pay | No action needed — the device is already set up | | `3` | Missing device-auth public key identifier | Re-run the enrollment flow | | `4` | Merchant/VAC device ID not provided | Ensure the SDK is configured with a valid merchant profile | | `52` | Random nonce required by enrollment is missing | Re-run the enrollment flow | ### Security Status (1000–2003) — Device Integrity | Code | Description | What to do | | ------ | ------------------------------------------- | ----------------------------------------------------------- | | `1000` | Emulator detected | Tap to Pay cannot run on emulators — use a physical device | | `1001` | Device rooted or superuser binaries present | Device must not be rooted | | `1002` | Device tamper detection triggered | Device has been modified and is not trusted | | `2000` | Developer options must be disabled | Disable developer mode before running transactions | | `2001` | USB debugging/logging must be disabled | Turn off USB debugging in developer options | | `2003` | Runtime hook/instrumentation detected | Remove any instrumentation frameworks (Frida, Xposed, etc.) | ### Prepare Status (7–91) — Pre-Transaction Secure Channel These codes occur during `startUpSdk()` or when the SDK prepares for a transaction. They relate to the secure channel between the device and the Visa backend. | Code | Description | What to do | | ---- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `7` | VAC authentication call to Visa failed | Check API credentials and network connectivity | | `11` | `init()` not completed before use | Complete SDK initialization before starting transactions | | `12` | `enrollDevice()` not completed | Device needs enrollment — show the enrollment UI and re-enroll. This also occurs if the Tap to Pay Ready app was reinstalled | | `17` | Could not load enrollment/transaction config blobs | Re-initialize the SDK | | `18` | ASI (Visa auth service interface) returned error | Transient backend issue — retry | | `20` | Hardware keystore missing when preparing keys | Device does not meet security requirements | | `24` | Device attestation failed or invalid | Re-enroll the device; ensure Play Protect is enabled | | `25` | Login exchange with Visa backend failed | Check network; retry | | `26` | Login response missing assertion blob | Transient backend issue — retry | | `27` | Login response missing crypto payload | Transient backend issue — retry | | `28` | Entire login response was null | Transient backend issue — retry | | `29` | Login HTTP body empty | Transient backend issue — retry | | `30` | Login response missing auth status | Transient backend issue — retry | | `31` | Shared secret not derived | Re-enroll the device | | `32` | Session keys missing | Re-enroll the device | | `33` | MAC/signature mismatch in response | Possible tampering — re-enroll the device | | `34` | MAC tag verification failed | Possible tampering — re-enroll the device | | `36` | Visa backend explicitly rejected authorization | Check merchant configuration with Koard support | | `37` | Auth status element empty | Transient backend issue — retry | | `49` | Could not fetch key-rotation seed list | Check network connectivity | | `50` | TLS pinning check failed | Possible man-in-the-middle — check network security | | `51` | Could not derive transaction keys | Re-enroll the device | ### Key Rotation Status (87–91) | Code | Description | What to do | | ---- | ------------------------------------------- | ------------------------------------------------- | | `87` | Kernel requested key rotation | SDK handles this automatically — no action needed | | `88` | Key rotation already satisfied (not needed) | Informational — no action needed | | `89` | Key rotation completed successfully | Informational — no action needed | | `90` | Key rotation failed | Re-enroll the device if transactions fail | | `91` | Kernel did not return key rotation status | Re-enroll the device if transactions fail | ### Prepare Progress (70–75) — `prepare()` Warm-Up The `sdk.prepare()` warm-up flow emits `KoardPrepareResponse` objects whose `status` is a `KoardPrepareStatus`. Codes 70–75 are the normal lifecycle; anything in the 80–199 range is surfaced as `KoardPrepareStatus.Error(code)`, and an unrecognized code becomes `KoardPrepareStatus.Unknown(code)`. These progress codes are informational — wait for `Done` (or an `Error`) rather than handling each one. | Code | `KoardPrepareStatus` | Meaning | | ---------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `70` | `Called` | KiC accepted the `prepare()` invocation | | `71` | `AuthenticationInProgress` | Authenticating the device against the backend | | `72` | `AttestationInProgress` | Performing Play Integrity / device attestation | | `73` | `GettingConfigurations` | Downloading transaction configurations | | `74` | `ParsingConfigurations` | Parsing the downloaded configurations | | `75` | `Done` | Prepare completed — SDK is ready for transactions | | `80`–`199` | `Error(code)` | Prepare did not reach `Done` (auth/attestation failure, key rotation, network error, etc.) — retry on the next user action | ### Transaction Status — In-Progress Codes These codes appear during an active transaction and are reflected in the `readerStatus` field: | Code | Reader Status | Description | | ----- | --------------- | ----------------------------------------------------------- | | `109` | `readyForTap` | POS state started — reader is waiting for a card tap | | `112` | `preparing` | POS state message — reader is preparing for the transaction | | `106` | `readCompleted` | Card read completed successfully | ### Generic Failure (-1) A status code of `-1` indicates a generic failure. The SDK uses the `statusCodeDescription` field to provide more context: | Description Contains | Meaning | | -------------------------------- | -------------------------------------------- | | `"eligibility"` | Generic eligibility evaluation failure | | `"initialise"` or `"enrol"` | Generic initialization or enrollment failure | | `"integrity"` or `"attestation"` | Device integrity attestation failed | **Status code 12 (enrollment not done)** deserves special handling. The demo app treats `statusCode == 12` during `OnFailure` as retryable (alongside `42`, `53`, and a null status code) — it prompts "Please tap again" rather than surfacing an error. If the code persists, the Tap to Pay Ready app was most likely reinstalled or had its data cleared; the merchant app must then clear its stored enrollment data (`clearEnrollmentState()`) and re-run `enrollDevice()`. Re-enrollment is never automatic. ## Display Message IDs During the tap-to-pay flow, the SDK emits display messages via `displayMessage` on each `OnProgress` event. These correspond to standard EMV message identifiers from the kernel: | Message | ID | Description | | --------------------------------------- | ---- | -------------------------------------------------------------------- | | Approved | `03` | Authorization obtained — transaction approved | | Cancel or Enter | `05` | Prompt to cancel or confirm | | Card Error | `06` | Unrecoverable card data error | | Not Authorized / Declined | `07` | Transaction was declined by the issuer | | Please remove card | `10` | Card not yet removed from the reader field | | Please try again | `13` | Recoverable error — retry the tap | | Welcome | `14` | Idle state — reader is ready | | Present card | `15` | Prompt the cardholder to tap | | Processing | `16` | Transaction is being processed | | Card read OK / Remove card | `17` | Card was read successfully — may be removed | | Please insert or swipe card | `18` | Contactless not supported — try contact/mag-stripe | | Please present one card only | `19` | Card collision detected — present only one card | | Approved. Please Sign | `1A` | Approved; signature required | | Authorizing. Please Wait | `1B` | Online authorization in progress | | Insert, swipe, or try another card | `1C` | Contactless failed — use another interface or card | | Please insert card | `1D` | Chip card should be inserted into the slot | | _(Empty string)_ | `1E` | Clear the display | | See Phone for instructions | `20` | Mobile device CVM required (Touch ID, Face ID, etc.) | | Present card again | `21` | Recoverable error — present the card again | | Practice Mode | `40` | Successful test/practice transaction | | Partial Approval | `43` | Issuer approved a lesser amount | | Cancelled for Device Security | `46` | Transaction cancelled due to a device security issue | | Cancelled | `47` | Generic transaction cancellation | | Try another card - No contact interface | `48` | Card returned GPO error (SW 6984) — transaction aborted | | Strong CVM | `49` | SCA issuer response requires interface switch — transaction declined | ## Abort and Error Scenarios The SDK returns an `Abort` or `Failure` final status in several well-defined situations. Understanding these helps you build robust error handling: ### Transaction Abort Scenarios | Scenario | What Happens | Message ID | | ------------------------------- | ------------------------------------------------------------------------------- | ---------- | | **User cancels PIN entry** | User selects "Cancel Transaction" on the PIN keypad | — | | **PIN session timeout** | 1 minute of inactivity on the PIN keypad | — | | **PIN keypad interrupted** | Another app covers the PIN screen | — | | **Network loss during PIN** | Network drops before the PIN event is sent | — | | **Split screen mode** | Device enters split screen while on PIN screen — sends `CVEntrySecurity` cancel | — | | **Device security issue** | Security configuration problem detected | `46` | | **Generic cancellation** | User or system cancelled the transaction | `47` | | **Card NFC failure (GPO 6984)** | Card cannot complete contactless — abort with `MACompletion` indicator | `48` | | **Strong CVM / SCA switch** | Issuer requires contact interface (not supported) — decline with `MACompletion` | `49` | | **Developer mode enabled** | Developer options are on — reader blocks the transaction | — | ### OnFailure Status Codes When `actionStatus` is `OnFailure`, check `statusCode` for the specific reason: | Status Code Constant | Description | | ---------------------------------- | ------------------------------------------------------------------ | | `TRANSACTION_WINDOW_FOCUS_CHANGED` | The transaction window lost focus (another app came to foreground) | | `CAMERA_IS_ACTIVE` | Device camera is active — conflicts with the secure NFC session | | `DEVELOPER_MODE_ENABLED` | Developer options are enabled on the device | | `NFC_NOT_AVAILABLE` | Device NFC is disabled or unavailable | | `DEVICE_NOT_ENROLLED` | Device has not completed enrollment | | `SESSION_TIMEOUT` | The transaction session timed out | **Developer Mode**: The most common cause of unexpected transaction failures during development. Always disable developer mode before running transactions. Follow the workflow: **Enable dev mode → Install app → Disable dev mode → Run transactions**. ### Completion Indicators The receipt field `emv.tx.tm.CompletionIndicator` tells you how the transaction concluded at the kernel level: | Value | Meaning | | ---------------- | ----------------------------------------------------------------------------------------------------------------- | | `FullCompletion` | Transaction completed normally through the full authorization flow | | `MACompletion` | Transaction was terminated by the kernel (Merchant Application completion) — typically an abort or forced decline | ## Handling Responses in Practice The SDK uses two error channels. Here is a complete pattern for handling both: ### Channel 1: Transaction Flow (KoardTransactionResponse) For `sale()`, `preauth()`, `completePartialAuth()`, and the tap-based `refundEmv()` — errors come via the response callback: ```kotlin private fun handleTransactionEvent(response: KoardTransactionResponse) { when (response.actionStatus) { KoardTransactionActionStatus.OnProgress -> { // Update UI with reader status and display message updateUI( status = response.readerStatus.toString(), message = response.displayMessage ?: "Processing..." ) } KoardTransactionActionStatus.OnFailure -> { // Check for re-enrollment scenario if (response.statusCode == 12) { // Tap to Pay Ready app was reinstalled or cleared data // Clear stored enrollment info and re-run enrollment triggerReEnrollment() return } // Transaction could not proceed — show the reason val reason = buildString { append("Transaction Failed") response.displayMessage?.let { append("\n\n$it") } response.statusCodeDescription?.let { append("\n\n$it") } response.statusCode?.let { append("\n\nStatus Code: $it") } } showError(reason) } KoardTransactionActionStatus.OnComplete -> { when (response.finalStatus) { KoardTransactionFinalStatus.Approve -> { showReceipt(response.transaction!!) } KoardTransactionFinalStatus.Decline -> { showDeclined(response.displayMessage ?: "Transaction declined") } KoardTransactionFinalStatus.Abort -> { showAborted(response.displayMessage ?: "Transaction aborted") } KoardTransactionFinalStatus.Failure -> { showError(response.displayMessage ?: "Transaction failed") } is KoardTransactionFinalStatus.Unknown -> { showError("Unexpected status: ${response.finalStatus}") } } } else -> Unit } } ``` ### Channel 2: API & SDK Operations (KoardException) For `capture()`, `reverse()`, `refund()`, `adjust()`, `enrollDevice()`, `setActiveLocation()`, and other non-tap operations — errors are thrown as `KoardException`: ```kotlin private suspend fun capturePayment(transactionId: String, amount: Int) { try { val result = sdk.capture(transactionId, amount) showReceipt(result) } catch (e: KoardException) { when (val errorType = e.error.errorType) { // HTTP errors from the Koard API is KoardErrorType.KoardServiceErrorType.HttpError -> showError("Server error (HTTP ${errorType.errorCode}): ${e.error.shortMessage}") is KoardErrorType.KoardServiceErrorType.ConnectionError -> showError("Network error — check your connection and retry") is KoardErrorType.KoardServiceErrorType.Unauthorized -> showError("Session expired — please log in again") is KoardErrorType.KoardServiceErrorType.NotFound -> showError("Transaction not found") is KoardErrorType.KoardServiceErrorType.InvalidRequest -> showError("Invalid request: ${e.error.shortMessage}") // Device integrity failures is KoardErrorType.DeviceIntegrityError.DeveloperModeEnabled -> showError("Disable developer mode before processing payments") is KoardErrorType.DeviceIntegrityError -> showError("Device security check failed: ${e.error.shortMessage}") // Enrollment issues is KoardErrorType.VACEnrollmentError -> promptReEnrollment(e.error.shortMessage) is KoardErrorType.VACEligibilityError -> showError("Device not eligible for Tap to Pay: ${e.error.shortMessage}") // KiC connector/kernel errors is KoardErrorType.KicConnectorError.KernelAppNotInstalled -> showError("Install the Visa Tap to Pay Ready app from Google Play") is KoardErrorType.KicConnectorError -> showError("Kernel communication error: ${e.error.shortMessage}") is KoardErrorType.KicPrepareError.SdkEnrollNotDone -> promptReEnrollment("Device needs re-enrollment") // Fallback else -> showError(e.error.shortMessage) } } } ``` ## Next Steps * Review the [Running Payments](/docs/setting-up-the-android-sdk/running-payments) guide for the complete payment flow implementation * See the [Demo App](/docs/setting-up-the-android-sdk/demo) for a working example of response handling in `MainScreenViewModel` * Consult the [API Response Codes](/docs/api-reference/response-codes) for HTTP-level status codes from the Koard REST API # Payments Learn how to orchestrate Koard payment flows across sale, preauthorization, capture, adjustment, reversal, and refund operations. ## Available Guides * [Idempotency](/docs/payments/idempotency) – Using `event_id` to make payments safely retryable * [Sale](/docs/payments/methods/sale) – One-step auth + capture transactions * [Preauth](/docs/payments/methods/preauth) – Hold funds before finalizing totals * [Capture](/docs/payments/methods/capture) – Settle preauthorized amounts * [Incremental Auth](/docs/payments/methods/incremental-auth) – Increase an existing authorization * [Tip Adjust](/docs/payments/methods/tip-adjust) – Update gratuity before settlement * [Reverse](/docs/payments/methods/reverse) – Release funds from a preauth * [Refund](/docs/payments/methods/refund) – Return captured funds * [Payment Lifecycle](/docs/payments/payment-lifecycle) – End-to-end transaction flow * [Surcharging](/docs/payments/surcharging) - Implementing Surcharging **Looking for SDK usage?** Start with the [iOS Running Payments guide](/docs/guides/ios-sdk/details/running-payments.md) and pair it with these payment references. # Tax and Tip Handling Koard resolves **tax** settings hierarchically, and handles **tips** either at the time of sale or as a later adjustment. > Surcharge settings (`surcharge_rate`, `surcharge_basis`, `surcharge_confirmation_required`) follow the same account → location → terminal hierarchy described below, but are documented on the [Surcharging](/payments/surcharging) page. ## Hierarchical tax defaults `tax_rate` and `tax_basis` can be configured at **three** levels — the **account**, the **location**, and the **terminal**: | Field | Meaning | | ----------- | ------------------------------ | | `tax_rate` | Tax percentage. | | `tax_basis` | What the tax is calculated on. | **Resolution — most specific wins.** For a given transaction, Koard uses the value set on the **terminal** if present; otherwise it falls back to the **location**, then to the **account**. A `null` at a more specific level means "inherit." Set the broad default once on the account. Only set the field on a location or terminal when it needs to differ from the level above it. `tax_rate` and `tax_basis` here are **configuration defaults** (resolved from the account -> location -> terminal hierarchy). They are separate from the **transaction-breakdown** fields returned on a completed transaction: `taxAmount` (the computed tax, in **cents**) and `taxRate` (the rate actually applied to that transaction; see the transaction breakdown schema for its exact representation). Don't conflate the config input (`tax_rate`, a percentage) with the computed breakdown output (`taxRate` / `taxAmount`). ## Tip handling Tips are captured in one of two modes, tracked on the transaction as `tip_capture_mode`: | Mode | When | Notes | | ------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------- | | **Tip at sale** (`tip_at_sale`) | The tip is included on the original sale. | Any non-tip-adjust transaction that carries a tip. | | **Tip adjust** (`tip_adjust`) | The tip is added/changed **after** the sale (e.g. restaurant tip-on-receipt). | A dedicated `tip_adjust` transaction referencing the original. | Each carries a `tip_amount` and `tip_type` (e.g. `percentage` or a fixed amount). **Tip adjust does not apply surcharge.** A tip adjustment only changes the tip on an existing transaction — it does not recompute or add a surcharge. Surcharge is applied on the **sale**, independently of tips (see [Surcharging](/payments/surcharging)). # Troubleshooting Real-world fixes for issues merchants commonly hit when enrolling devices or running their first tap. If a device fails the SDK eligibility check, see [Supported Devices & NFC Tap Location](/docs/guides/android-sdk/details/supported-devices). For SDK error codes returned during a transaction, see [SDK Response Codes](/docs/guides/android-sdk/details/sdk-response-codes). **What you learn** In this guide, you'll learn: * How to fix the "tap was immediately cancelled" issue * How to resolve enrollment failures caused by device clock drift * How to recover from a `TamperDetected` error on every sale after a failed enrollment * What to do when the kernel is busy with another merchant app * How to fix the "SDK not initialized" error * Which device settings must be enabled before a merchant's first transaction * A pre-flight checklist to run before contacting Koard support ## Tap is Immediately Cancelled ### Symptom The merchant enrolls successfully and the SDK reports the device as eligible. When they attempt their first sale, the reader shows "Present card", but the moment the customer taps, the transaction is **immediately cancelled** — the SDK emits an `Abort` (or `OnFailure`) before the card is read. This has been observed across several Android devices, with **Samsung Galaxy S21** being the most common — but any Android device that exposes a separate **"NFC and contactless payments"** (or similarly named) toggle can hit it. ### Cause Many Android devices ship with **two distinct NFC settings**: 1. **NFC** — the basic NFC radio toggle (enabled by default on most devices). 2. **NFC and contactless payments** — a separate setting that authorizes the device to use NFC for **payment-related** Host Card Emulation (HCE) and Tap to Pay flows. Different OEMs label this differently (e.g. "NFC and contactless payments", "NFC and contactless transactions", "Contactless payments"). The SDK eligibility check verifies that NFC hardware is present and the radio is on, which is why enrollment and the eligibility check both pass. However, if the secondary **"NFC and contactless payments"** setting is disabled, the Visa Tap to Pay Ready kernel cannot complete the contactless card read — the device's payment subsystem blocks the read and the transaction is cancelled before any card data is exchanged. ### Fix On the merchant's device: 1. Open **Settings** 2. Tap **Connections** (or **Connected devices** depending on the OEM and Android version) 3. Tap **NFC and contactless payments** (sometimes labeled **NFC and contactless transactions** or simply **Contactless payments**) 4. Ensure the master toggle is **On** 5. Optionally set **Contactless payments → Default payment app** to your merchant app, or leave it as the system default if your app does not require the system payment role After enabling the setting, retry the sale. No re-enrollment is required. **Why the eligibility check doesn't catch this**: `checkKiCEligibility()` verifies that NFC hardware exists and the system NFC radio is enabled. The OEM-specific "contactless payments" toggle gates a higher layer (the payment HCE subsystem) and is not visible to the eligibility API. If a merchant sees taps cancelled instantly, always confirm this setting before deeper debugging. If "NFC and contactless payments" is enabled and the tap is still cancelled instantly, check whether a wallet app (Samsung Wallet, Google Wallet, etc.) is set as the default and is intercepting the tap. Temporarily clear the default payment app under **NFC and contactless payments → Contactless payments**, then retry. ## Enrollment Fails or Returns Errors ### Symptom Calls to `enrollDevice()` (or the SDK's enrollment flow) fail with errors such as: * `InvalidRequest` — "Developer mode is enabled", "Device already enrolled", or no active location was set. `enrollDevice()` requires an authenticated session **and** a prior `setActiveLocation(locationId)` call * `VACEnrollmentError` * `AuthenticationFailed` (status code `7`) * `AttestationFailed` (status code `24`) * `CouldNotAttestError` * Generic prepare errors in the `7`–`51` range The merchant sees an "Enrollment failed" screen and cannot proceed to take their first tap. ### Cause Enrollment establishes a secure channel between the device and the Visa Acceptance Cloud (VAC). That handshake includes a **device attestation step** that is sensitive to three things: 1. **Developer mode is enabled** — most enrollment endpoints reject devices with developer options on, because the device cannot produce a trustworthy attestation. 2. **The device clock has drifted** — VAC verifies signed timestamps during the handshake. If the device clock is off by more than a few minutes (a "clock drift" issue), signatures fail to verify and attestation is rejected. This is most common on devices that have been offline for a long time, recently factory-reset, or have automatic time disabled. 3. **Google Play Protect is disabled** — Play Protect provides the integrity signals VAC uses to attest the device. With Play Protect off, the attestation payload is incomplete and enrollment is rejected. ### Fix Walk the merchant through all three checks before retrying enrollment: #### 1. Disable Developer Mode 1. Open **Settings → System → Developer options** 2. Toggle **Developer options** to **Off** 3. If the option is missing entirely, developer mode is already off — proceed to the next step If developer mode was on, restart the device after disabling it. #### 2. Enable Automatic Date & Time 1. Open **Settings → General management → Date and time** (Samsung) or **Settings → System → Date & time** (stock Android) 2. Toggle **Automatic date and time** to **On** 3. Toggle **Automatic time zone** to **On** 4. Wait a few seconds for the device to sync with the network time source This corrects clock drift and is the most common fix when enrollment fails on a device that previously worked. #### 3. Enable Google Play Protect 1. Open the **Google Play Store** app 2. Tap your profile icon (top right) → **Play Protect** 3. Tap the **gear icon** (settings) in the top right of the Play Protect screen 4. Toggle **Scan apps with Play Protect** to **On** Play Protect must be enabled for the device attestation step to succeed. If the merchant cannot enable Play Protect (for example on a device without Google Mobile Services), the device is not eligible for Tap to Pay. After all three checks pass, restart the device and retry enrollment. If the SDK still returns an enrollment error, capture the `statusCode` and `errorType` and consult the [SDK Response Codes](/docs/guides/android-sdk/details/sdk-response-codes#kic-prepare-errors) reference. **Why clock drift matters**: The VAC attestation handshake relies on signed timestamps to prevent replay attacks. The SDK's enrollment flow includes a fresh nonce and a device-side timestamp; if the device clock is more than a few minutes off the server clock, the server rejects the timestamp and the handshake fails — often surfacing as a generic `AuthenticationFailed` (code `7`) or `AttestationFailed` (code `24`). Enabling automatic time syncs the device against a network time source and eliminates the drift. ## `TamperDetected` on Every Sale After a Failed Enrollment ### Symptom `enrollDevice()` (or `enableNfcTransactionsAsync(...)`) appears to finish but reports a non-success status. Afterwards, **every** sale throws `KoardErrorType.DeviceIntegrityError.TamperDetected` (status code `1002`). Clearing the app's data via Android Settings makes the next enrollment succeed — which points at corrupted local state rather than a genuinely tampered device. ### Cause A partial enrollment can leave the SDK's local preferences (certificates, VAC device ID, auth keys, x-via hint, x-random value) half-written and out of sync with the kernel's internal state. The mismatch surfaces as a false `TamperDetected` on the next transaction. ### Fix Call `clearEnrollmentState()` from a worker thread to wipe the local enrollment preferences, then re-attempt enrollment. The active location is intentionally preserved, and this is a local-only operation — it does **not** contact the backend. ```kotlin lifecycleScope.launch(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() sdk.clearEnrollmentState() sdk.enrollDevice() // or enableNfcTransactionsAsync(...) } ``` Use `clearEnrollmentState()` for half-enrolled recovery. If the device is **fully** enrolled, call `unenrollDevice()` instead: it asks the Tap to Pay Ready app to clean up the enrollment data it holds for your app, then clears the SDK's local state. Local state is flushed either way — if the kernel call fails, the returned string reports the failure but the device can still re-enroll.\ \ **This does not fully deprovision the device on Visa's backend.** Per KiC integration guide §3.4.20 (implementation _optional_), `unenrollDevice()` only cleans up enrollment data on the Tap to Pay Ready app; Visa requires the integrator to _also_ call the backend `manageDevice` API to disable the device, otherwise it keeps being billed. Coordinate that step with Koard support. ## Tap to Pay Is Busy with Another Merchant App ### Symptom A sale fails with `KoardErrorType.KicConnectorError.KernelAppBusyWithAnotherMerchant` (status code `98`). ### Cause Introduced with KiC multi-tenancy, this means a **different** merchant app on the device currently holds the lock on the Visa Tap to Pay Ready kernel service. ### Fix Ask the operator to close the other merchant app (or wait for it to finish its transaction), then retry. Calling `resetKernelService()` will **not** resolve this — the lock is owned by a different process, not your app. ## "SDK Not Initialized" Error ### Symptom ```text IllegalStateException: Instance is null. Did you forget to call initialize? ``` is thrown the first time you call `KoardMerchantSdk.getInstance()`. ### Cause `getInstance()` was called before `KoardMerchantSdk.initialize(...)` completed. ### Fix Initialize the SDK in `Application.onCreate()` on a worker thread (`Dispatchers.IO`) before any code accesses `getInstance()`. See the [Installing the SDK](/docs/guides/android-sdk/details/installing-sdk) guide for the recommended startup sequence. ## Pre-Flight Checklist Before contacting Koard support about a device that "won't take a tap" or "fails to enroll", confirm **all** of the following on the merchant's device: * Device is running **Android 12 (API 31) or later** * **NFC** is enabled in Settings * **NFC and contactless payments** is enabled (where present) * **Developer mode** is **off** * **Automatic date and time** is **on** * **Google Play Protect** is enabled * **Google Play Services** is installed and up to date * **Visa Tap to Pay Ready** kernel app is installed from the Play Store * Device is not rooted and is not running a custom ROM * `checkKiCEligibility()` returns no failures If every item is checked and the device still fails, gather the following before contacting support: * Device make, model, and Android version * **Koard SDK version** (the version of `koard-android` / the Koard Merchant SDK your app is built against) * The `statusCode` and `statusCodeDescription` from the failing `KoardTransactionResponse`, or the `KoardErrorType` from the thrown `KoardException` * A log excerpt from the time of the failure. The SDK logs to Logcat under the fixed tag **`KoardSDK`**; capture it with `adb logcat -s KoardSDK`. Verbosity is set once at startup via `KoardMerchantSdk.initialize(..., logLevel = KoardLogLevel.VERBOSE)` — there is no runtime setter, and the default is `NONE` in release builds, so ask the merchant for a build that opts in. The SDK never logs API keys, tokens, device certificates, or enrollment key material at any level. --- title: Sale --- # Sale A sale authorizes and captures a payment in a single step—use it when the final amount is known at checkout. ## Prerequisites - Authenticated merchant with `login()` - Active location set via `setActiveLocationID()` - Card reader prepared with `prepare()` (iOS) or device enrolled (Android) ## Basic Sale **iOS:** ```swift let breakdown = PaymentBreakdown( subtotal: 10000, // $100.00 in cents taxRate: 8.75, // 8.75% as a percent value taxAmount: 875, // $8.75 in cents tipAmount: 2000, // $20.00 tip tipType: .fixed ) let currency = CurrencyCode(currencyCode: "USD", displayName: "US Dollar") let response = try await KoardMerchantSDK.shared.sale( amount: 12875, // subtotal + tax + tip breakdown: breakdown, currency: currency, eventId: UUID().uuidString ) ``` **Android:** ```kotlin val breakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipAmount = 2000, tipType = "fixed" ) sdk.sale( activity = this, amount = 12875, breakdown = breakdown, eventId = UUID.randomUUID().toString() ).collect { event -> when (event.actionStatus) { ActionStatus.OnComplete -> { val txn = event.response?.transaction println("Sale complete: ${txn?.transactionId}") } ActionStatus.OnFailure -> { println("Sale failed: ${event.response?.message}") } else -> { /* reader progress */ } } } ``` ## Sale with Surcharge The surcharge percentage is applied to the **full transaction amount** (subtotal + tax + tip): ``` surcharge = (subtotal + taxAmount + tipAmount) × surchargeRate = (10000 + 875 + 2000) × 0.035 = 451 cents ($4.51) ``` **iOS:** ```swift let breakdown = PaymentBreakdown( subtotal: 10000, taxRate: 8.75, taxAmount: 875, tipAmount: 2000, tipType: .fixed, surcharge: PaymentBreakdown.Surcharge( amount: 451, // surcharge on full amount percentage: 0.035 ) ) let response = try await KoardMerchantSDK.shared.sale( amount: 13326, // 12875 + 451 surcharge breakdown: breakdown, currency: currency, eventId: UUID().uuidString ) ``` **Android:** ```kotlin val breakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipAmount = 2000, tipType = "fixed", surcharge = Surcharge( amount = 451, percentage = 0.035 ) ) sdk.sale( activity = this, amount = 13326, breakdown = breakdown, eventId = UUID.randomUUID().toString() ).collect { event -> when (event.actionStatus) { ActionStatus.OnComplete -> { println("Sale complete: ${event.response?.transaction?.transactionId}") } ActionStatus.OnConfirmSurcharge -> { val txn = event.response?.transaction sdk.confirm( transactionId = txn?.transactionId ?: "", confirm = true ) } ActionStatus.OnFailure -> { println("Sale failed: ${event.response?.message}") } else -> { /* reader progress */ } } } ``` ## Surcharge Confirmation If the terminal has surcharging enabled and the card is eligible (credit only—debit cards are automatically excluded), the transaction returns `surchargePending`. You **must** present the disclosure and confirm. **iOS:** ```swift if response.transaction?.status == .surchargePending { let disclosure = response.transaction?.surchargeDisclosure ?? "" let approved = await showSurchargeDisclosure(disclosure) let confirmed = try await KoardMerchantSDK.shared.confirm( transaction: response.transactionId ?? "", confirm: approved, amount: nil, breakdown: nil, eventId: nil ) } ``` **Android:** ```kotlin ActionStatus.OnConfirmSurcharge -> { val txn = event.response?.transaction // Present disclosure to customer, then: sdk.confirm( transactionId = txn?.transactionId ?: "", confirm = true ) } ``` ## Bypassing Automatic Surcharge Set `bypass: true` to skip the processor's automatic surcharge. Useful for [custom BIN-based surcharging](surcharging.md#bin-based-custom-surcharge): **iOS:** ```swift let breakdown = PaymentBreakdown( subtotal: 10000, taxRate: 8.75, taxAmount: 875, tipType: .fixed, surcharge: PaymentBreakdown.Surcharge(bypass: true) ) ``` **Android:** ```kotlin val breakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipType = "fixed", surcharge = Surcharge(bypass = true) ) ``` ## Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `amount` | `Int` | Yes | Total amount in minor units (cents) | | `breakdown` | `PaymentBreakdown?` | No | Itemized breakdown (see below) | | `currency` | `CurrencyCode` | Yes (iOS) | Currency for the transaction | | `eventId` | `String?` | No | Idempotency key (UUID recommended) | | `activity` | `Activity` | Yes (Android) | Android activity for NFC access | ### PaymentBreakdown | Field | Type | Description | |-------|------|-------------| | `subtotal` | `Int` | Base amount in minor units | | `taxRate` | `Double?` | Tax rate as a percent value (`8.75` = 8.75%) | | `taxAmount` | `Int` | Calculated tax in minor units | | `tipAmount` | `Int?` | Tip in minor units | | `tipRate` | `Double?` | Tip rate as decimal (alternative to fixed tip) | | `tipType` | `TipType` | `.fixed` / `.percentage` (iOS) or `"fixed"` / `"percentage"` (Android) | | `surcharge` | `Surcharge?` | Nested surcharge object | ### Surcharge | Field | Type | Default | Description | |-------|------|---------|-------------| | `amount` | `Int?` | `nil` | Fixed surcharge in minor units | | `percentage` | `Double?` | `nil` | Surcharge rate as decimal (`0.035` = 3.5%). Applied to subtotal + tax + tip. | | `bypass` | `Bool` | `false` | Skip automatic surcharge calculation | ## See Also - [Preauth](preauth.md) — Hold funds now, capture later - [Surcharging](surcharging.md) — Automatic and custom surcharge workflows - [Payment Lifecycle](payment-lifecycle.md) — End-to-end payment flow --- title: Capture --- # Capture Capture finalizes a previously authorized ([preauth](preauth.md)) transaction. You can capture at the original amount, a lower amount (partial capture), or with an updated breakdown that includes tip or surcharge. ## Prerequisites - An existing preauth transaction ID - Transaction must be in an authorized (uncaptured) state ## Full Capture **iOS:** ```swift let response = try await KoardMerchantSDK.shared.capture( transactionId: transactionId, amount: 10875 ) ``` **Android:** ```kotlin sdk.capture( transactionId = transactionId, amount = 10875 ) ``` ## Capture with Updated Breakdown Include the final breakdown when the tip or surcharge changed after the preauth: **iOS:** ```swift let finalBreakdown = PaymentBreakdown( subtotal: 10000, taxRate: 8.75, taxAmount: 875, tipAmount: 3000, // customer added a $30 tip tipType: .fixed, surcharge: PaymentBreakdown.Surcharge( amount: 486, // 3.5% of (10000 + 875 + 3000) = 486 percentage: 0.035 ) ) let response = try await KoardMerchantSDK.shared.capture( transactionId: transactionId, amount: 14361, // 10000 + 875 + 3000 + 486 breakdown: finalBreakdown ) ``` **Android:** ```kotlin val finalBreakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipAmount = 3000, tipType = "fixed", surcharge = Surcharge( amount = 486, percentage = 0.035 ) ) sdk.capture( transactionId = transactionId, amount = 14361, breakdown = finalBreakdown ) ``` ## Partial Capture Capture at a lower amount than the original hold: **iOS:** ```swift let response = try await KoardMerchantSDK.shared.capture( transactionId: transactionId, amount: 8000 // capture $80 of a $108.75 hold ) ``` **Android:** ```kotlin sdk.capture( transactionId = transactionId, amount = 8000 ) ``` > The remaining hold amount is automatically released back to the cardholder. ## Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `transactionId` | `String` | Yes | The preauth transaction to capture | | `amount` | `Int?` | No | Capture amount in minor units. Defaults to the original preauth amount. | | `breakdown` | `PaymentBreakdown?` | No | Updated breakdown with final tip/surcharge | | `eventId` | `String?` | No | Idempotency key | ## See Also - [Preauth](preauth.md) — Place a hold before capturing - [Incremental Auth](incremental-auth.md) — Increase the hold before capture - [Tip Adjust](tip-adjust.md) — Update tip before capture - [Surcharging](surcharging.md) — Include surcharge in capture breakdown --- title: Refund --- # Refund A refund returns funds to the cardholder **after** settlement. Use it for post-settlement returns, partial returns, or customer disputes. > For voiding a transaction **before** settlement, see [Reverse](reverse.md). ## Full Refund **iOS:** ```swift let response = try await KoardMerchantSDK.shared.refund( transactionId: transactionId, amount: 12875, eventId: UUID().uuidString ) ``` **Android:** ```kotlin sdk.refund( transactionId = transactionId, amount = 12875, eventId = UUID.randomUUID().toString() ) ``` ## Partial Refund Refund a portion of the original transaction: **iOS:** ```swift let response = try await KoardMerchantSDK.shared.refund( transactionId: transactionId, amount: 5000, // refund $50 of the original eventId: UUID().uuidString ) ``` **Android:** ```kotlin sdk.refund( transactionId = transactionId, amount = 5000, eventId = UUID.randomUUID().toString() ) ``` ## Tap-Based Refund (iOS) For card-present refunds where the customer taps their card: ```swift let response = try await KoardMerchantSDK.shared.refund( transactionId: transactionId, amount: 12875, tapBasedRefund: true, eventId: UUID().uuidString ) ``` > Tap-based refunds require the card reader to be prepared. The customer taps their card to confirm the refund. ## Surcharge Handling When refunding a surcharged transaction, the surcharge is **prorated automatically** by the processor. You do not need to pass a breakdown or calculate the surcharge portion—just pass the refund amount and the processor handles the rest. ## Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `transactionId` | `String` | Yes | Original transaction to refund | | `amount` | `Int` | Yes | Refund amount in minor units | | `eventId` | `String?` | No | Idempotency key | | `tapBasedRefund` | `Bool?` | No | iOS only—require card tap to confirm refund | | `activity` | `Activity` | Yes (Android) | Android activity for tap-based refunds | ## See Also - [Reverse](reverse.md) — Void before settlement - [Sale](sale.md) — Original payment - [Payment Lifecycle](payment-lifecycle.md) — End-to-end flow # Batch and Settlements Learn how to manage batch processing and settlement operations with Koard's payment platform. [Get started with Koard](/docs/getting-started-with-koard/introduction) Koard provides comprehensive batch processing and settlement management for transactions processed through our system. Whether you're an enterprise ISV, PSP, or building a payment platform, Koard offers flexible solutions to meet your settlement needs. **What you learn** In this guide, you'll learn: * How Koard's batch processing system works * The two main workflows for different business types * How to retrieve batch information and settlement data * Best practices for enterprise vs. platform implementations * How to integrate with Koard's settlement APIs ## Before you begin This guide covers batch processing and settlement management in Koard. For a better understanding of how to process payments, see our [Running Payments guide](/docs/setting-up-the-ios-sdk/running-payments). If you're ready to start accepting payments, see our [Getting Started guide](/docs/getting-started-with-koard/introduction). ## Batch Processing Overview Koard can run batches and settlements for transactions processed through our system. Our platform handles all scheduling and retries automatically, providing you with: * **Batch Lists**: Retrieve comprehensive batch information * **Batch Components**: Detailed breakdown of batch contents * **Settlement Breakdown**: Success and failure analysis * **Automated Scheduling**: Koard handles all timing and retries * **Per-Merchant Batching**: Batches are created per unique MID and TID combination ## Two Main Workflows Koard supports two distinct workflows based on your business model and technical requirements. ### Enterprise ISVs and PSPs **Recommended approach**: Use your own batching solution For enterprise ISVs and PSPs with existing batching infrastructure, we recommend maintaining your own batch processing and settlement management: * **Append to Existing Batches**: Add Koard transactions to your already open batches * **Trigger Batch Closure**: Close batches using your existing system * **Inform Koard**: Update Koard's systems when settlements occur * **Avoid Duplicate Issues**: Prevents duplicate batch IDs and synchronization problems **Important**: For TSYS, Fiserv, and Elavon processors, there's a high risk of duplicate batch IDs, out-of-order synchronization, and missing/invalid tags that may fail batches when using Koard's batching system. **Exception**: If you've created specific MID and TID combinations for Tap to Pay through Koard, you can safely use Koard's batching system without the above concerns. ### Everyone Else **Recommended approach**: Use Koard's settlement system If Koard is your sole authorization layer and you don't have existing batching infrastructure: * **Full Settlement Management**: Let Koard handle all batch processing * **Automated Scheduling**: Koard manages timing and retries * **Simplified Integration**: Single API for all settlement operations * **Comprehensive Reporting**: Built-in analytics and monitoring ## Key Features | Feature | Description | Enterprise ISVs | Platform Users | | ------------------------- | ------------------------------------- | ------------------------- | --------------------------- | | **Batch Creation** | Automatic batch creation per MID/TID | Use your own system | Koard handles automatically | | **Settlement Scheduling** | Automated timing and retries | Your existing schedule | Koard manages timing | | **Error Handling** | Robust retry logic and error recovery | Your error handling | Koard handles retries | | **Reporting** | Batch and settlement analytics | Your reporting system | Koard provides reports | | **API Access** | RESTful APIs for batch management | Limited to status updates | Full API access | ## Getting Started 1. **Determine Your Workflow**: Choose between enterprise or platform approach 2. **Set Up Integration**: Configure your chosen workflow 3. **Test Batch Processing**: Verify your implementation 4. **Monitor Operations**: Track batch and settlement status ## Best Practices ### For Enterprise ISVs and PSPs * **Maintain Existing Batches**: Don't create separate batches for Koard transactions * **Sync Settlement Data**: Keep Koard informed of settlement status * **Handle Edge Cases**: Implement proper error handling for processor-specific issues * **Monitor for Duplicates**: Watch for duplicate batch IDs across systems ### For Platform Users * **Use Koard APIs**: Leverage Koard's comprehensive batch management * **Monitor Settlement Status**: Track batch processing through Koard's dashboard * **Implement Webhooks**: Set up real-time notifications for settlement events * **Regular Reconciliation**: Verify settlement data against your records ## Integration Points * **API Integration**: RESTful APIs for batch management and status updates * **Webhook Notifications**: Real-time batch and settlement status updates * **Dashboard Monitoring**: Visual tracking of batch processing and settlements * **Reporting Tools**: Comprehensive analytics and settlement reports ## See also This wraps up the batch and settlements overview. See the links below for detailed implementation guides: * [Running Batches](/docs/batch-and-settlements/running-batches) - Step-by-step batch implementation * [Running Payments](/docs/setting-up-the-ios-sdk/running-payments) - Payment processing fundamentals * [Setting up Webhooks](/docs/webhooks/setting-up-webhooks) - Configure webhooks to receive real-time batch and settlement event notifications --- title: Tip Adjust --- # Tip Adjust Tip adjust updates the tip amount on a transaction that has already been authorized but not yet settled. Use it when the customer adds or changes a tip after the initial payment. ## Basic Tip Adjust **iOS:** ```swift let response = try await KoardMerchantSDK.shared.tipAdjust( transactionId: transactionId, tipAmount: 3000 // $30.00 tip ) ``` **Android:** ```kotlin sdk.adjust( transactionId = transactionId, tipAmount = 3000 ) ``` > **Note:** The iOS SDK method is `tipAdjust()` while the Android SDK method is `adjust()`. ## Surcharge Behavior When a tip is adjusted on a surcharged transaction, the surcharge is **not recalculated**. The original surcharge amount remains unchanged. If you need to recalculate the surcharge based on the new total (subtotal + tax + new tip), you should handle that in your capture flow instead. ## Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `transactionId` | `String` | Yes | Transaction to adjust | | `tipAmount` | `Int` | Yes | New tip amount in minor units | ## See Also - [Sale](sale.md) — One-step payment - [Preauth](preauth.md) — Hold with tip added later - [Capture](capture.md) — Finalize with updated tip # Automated Batch Scheduling ## Automated Batch Scheduling Koard supports automated batch close and re-open scheduling per terminal. Instead of manually closing batches at the end of each day, you can configure a schedule and Koard handles it automatically. ### Supported Processors | Processor | Supported | Notes | | ------------ | --------- | --------------------------------------------------- | | **TSYS** | ✅ | Full support. Batch numbers auto-managed (001-999). | | **Elavon** | ✅ | Full support via ViaConex TC 920/921/929. | | **Worldpay** | ✅ | Full support via 610 settlement interface. | | Fiserv | ❌ | Not supported for automated scheduling. | | Payroc | ❌ | Not supported for automated scheduling. | ### How It Works 1. **Configure a schedule** on the terminal via `PUT /v2/terminals/{terminal_id}` 2. **Koard's scheduler** runs every minute, checking for terminals due for batch close 3. When due, Koard **closes the current batch** and **opens a new one** 4. If a close fails, Koard **retries up to 10 times** with backoff 5. On persistent failure, a **webhook error event** is sent ### Setting Up a Schedule Add a `batch_schedule` field to your terminal update: ```json PUT /v2/terminals/{terminal_id} { "batch_schedule": { "timezone": "US/Eastern", "is_active": true, "schedule": [ { "day": "MON", "times": ["23:00"] }, { "day": "TUE", "times": ["23:00"] }, { "day": "WED", "times": ["23:00"] }, { "day": "THU", "times": ["23:00"] }, { "day": "FRI", "times": ["23:00"] }, { "day": "SAT", "times": ["23:00"] } ] } } ``` #### Response The terminal response includes confirmation that the schedule was saved: ```json { "terminal_id": "term-abc123", "name": "Front Counter POS", "mid": "886000001130", "tid": "00000001", "processor_config_id": "cfg-tsys-001", "status": "active", "var_sheet": { "applicationId": "B001" } } ``` ### Schedule Format #### Days Use 3-letter day codes: | Code | Day | | ----- | --------- | | `MON` | Monday | | `TUE` | Tuesday | | `WED` | Wednesday | | `THU` | Thursday | | `FRI` | Friday | | `SAT` | Saturday | | `SUN` | Sunday | #### Times Times are in 24-hour `HH:MM` format. You can set **multiple closes per day**: ```json { "day": "WED", "times": ["12:00", "18:00", "23:00"] } ``` This closes the batch at noon, 6pm, and 11pm on Wednesdays. #### No Close on a Day Simply omit the day from the schedule. If Saturday and Sunday are not listed, no batch close happens on weekends. ### Timezones **DST vs Fixed Timezones**: Choose carefully between DST-aware and fixed-offset timezones. Most merchants want DST-aware timezones so the batch close follows "wall clock" time. #### DST-Aware Timezones (Recommended) These follow daylight saving time transitions. `23:00 US/Eastern` means 11pm EDT in summer and 11pm EST in winter. | Timezone | Description | | ------------- | -------------------------- | | `US/Eastern` | Eastern Time (New York) | | `US/Central` | Central Time (Chicago) | | `US/Mountain` | Mountain Time (Denver) | | `US/Pacific` | Pacific Time (Los Angeles) | | `US/Alaska` | Alaska Time | | `US/Hawaii` | Hawaii Time (no DST) | | `US/Arizona` | Arizona Time (no DST) | You can also use full IANA zone names like `America/New_York`, `America/Chicago`, etc. #### Fixed-Offset Timezones These **never** change for DST. Use only if you want a fixed UTC offset year-round. | Timezone | UTC Offset | Notes | | -------- | ------------- | ------------------------------------ | | `EST` | UTC-5 always | Does **not** switch to EDT in summer | | `MST` | UTC-7 always | Does **not** switch to MDT in summer | | `HST` | UTC-10 always | Same as US/Hawaii | | `UTC` | UTC+0 always | Universal Coordinated Time | #### Example: DST Impact A batch close at `23:00 US/Eastern`: * **Winter (EST)**: Fires at 04:00 UTC * **Summer (EDT)**: Fires at 03:00 UTC A batch close at `23:00 EST`: * **Always**: Fires at 04:00 UTC (even in summer when "wall clock" Eastern time is EDT) ### Managing Schedules #### Update an Existing Schedule ```json PUT /v2/terminals/{terminal_id} { "batch_schedule": { "timezone": "US/Pacific", "schedule": [ { "day": "MON", "times": ["22:00"] }, { "day": "FRI", "times": ["14:00", "22:00"] } ] } } ``` #### Pause Scheduling (Keep Config) Set `is_active` to `false` to temporarily disable without losing your schedule: ```json PUT /v2/terminals/{terminal_id} { "batch_schedule": { "is_active": false } } ``` #### Resume Scheduling ```json PUT /v2/terminals/{terminal_id} { "batch_schedule": { "is_active": true, "schedule": [ { "day": "MON", "times": ["23:00"] }, { "day": "TUE", "times": ["23:00"] } ] } } ``` **Important**: An active schedule must have at least one day with times configured. Setting `is_active: true` with an empty schedule will return an error. #### Remove Schedule Entirely (Back to Manual) Set `batch_schedule` to `null`: ```json PUT /v2/terminals/{terminal_id} { "batch_schedule": null } ``` After removal, the terminal returns to manual batch management. #### Updates Without batch\_schedule Updating other terminal fields (name, MID, var\_sheet, etc.) does **not** affect the schedule: ```json PUT /v2/terminals/{terminal_id} { "name": "New Terminal Name" } ``` The existing batch schedule is preserved. ### Switching Between Auto and Manual #### Switching from Auto to Manual To switch a terminal back to manual batch management, remove the schedule: ```json PUT /v2/terminals/{terminal_id} { "batch_schedule": null } ``` **Important**: When switching to manual, the current open batch stays open. You are now responsible for: * Closing the current batch manually (`POST /v1/batches/{batch_id}/close`) * Opening new batches manually (`POST /v1/batches/open`) * Closing all future batches — they will no longer auto-close #### Switching from Manual to Auto Enable a schedule on an existing terminal: ```json PUT /v2/terminals/{terminal_id} { "batch_schedule": { "timezone": "US/Central", "is_active": true, "schedule": [ { "day": "MON", "times": ["22:00"] }, { "day": "TUE", "times": ["22:00"] }, { "day": "WED", "times": ["22:00"] }, { "day": "THU", "times": ["22:00"] }, { "day": "FRI", "times": ["22:00"] } ] } } ``` If the terminal already has an open batch, the scheduler will close it at the next scheduled time and open a new one automatically. ### Closing a Batch Early You can **always** close a batch early, even when automated scheduling is enabled: ```bash POST /v1/batches/{batch_id}/close ``` **You must open a new batch immediately after an early close.** Transactions cannot be processed without an open batch. Call `POST /v1/batches/open` right after the early close. When the scheduler fires later at its scheduled time: * If it finds an **open batch with transactions**, it closes and reopens normally * If it finds an **open batch with no transactions** (e.g., you just opened it), it cancels the empty batch and opens a fresh one * If it finds **no open batch** (e.g., you closed early and didn't reopen), it opens a new one This means early closes are safe and the scheduler self-heals on the next run. #### Example: Early Close at 3pm, Scheduled Close at 11pm 1. **3:00 PM** — You close the batch early via API 2. **3:01 PM** — You open a new batch via API 3. **3:01 PM – 11:00 PM** — Transactions accumulate in the new batch 4. **11:00 PM** — Scheduler fires, closes the batch (with transactions), opens a new one If you forget to reopen at step 2, the scheduler at 11pm will detect no open batch and open one for you — but any transactions between 3:01 PM and 11:00 PM will have failed because there was no open batch. ### TSYS-Specific Behavior #### Batch Number Auto-Management TSYS batch numbers must be between **001-999** and cannot be reused within **5 consecutive days**. Koard handles this automatically: 1. **On open**: Queries the last closed batch for the terminal and increments 2. **On close**: Verifies no conflicting batch number, auto-increments if needed 3. **On duplicate (QD)**: Automatically retries with the next batch number (up to 10 attempts) #### Batch Number Wrapping When the batch number reaches 999, it wraps around to 001. ### Error Handling | Scenario | Koard's Response | | -------------------------------- | ------------------------------------------ | | Processor unavailable | Retries up to 10 times with backoff | | Duplicate batch number (TSYS QD) | Auto-increments and retries | | All retries exhausted | Sends `batch.rejected` webhook, logs error | | No open batch to close | Skips close, opens a new batch | ### Webhook Events When using automated scheduling, you'll receive the standard batch webhook events: | Event | When | | ----------------- | ----------------------------------------------------------------------- | | `batch.submitted` | Batch sent to processor | | `batch.accepted` | Processor accepted the batch | | `batch.rejected` | Processor rejected the batch, or the scheduler failed after all retries | | `batch.opened` | New batch opened after close | ### Permissions The following roles can view and manage batch schedules: | Role | View | Create/Edit | Delete | | -------- | ---- | ----------- | ------ | | PSP | ✅ | ✅ | ✅ | | Partner | ✅ | ✅ | ✅ | | Merchant | ✅ | ✅ | ✅ | ### See also * [Batch and Settlements Overview](/docs/batch-and-settlements/overview) - Batch concepts * [Running Batches](/docs/batch-and-settlements/running-batches) - Manual batch management * [Setting up Webhooks](/docs/webhooks/setting-up-webhooks) - Webhook configuration # Developing with Apple Comprehensive guidelines and best practices for developing Tap to Pay on iPhone applications with Koard and Apple's payment technologies. ## Overview Developing Tap to Pay on iPhone applications requires careful attention to security, environment configuration, and compliance with Apple's requirements. This guide covers essential security measures, environment setup, and testing procedures to ensure a secure and compliant integration. ## Security Considerations ### Cardholder Data Protection Given the sensitive nature of cardholder data processed through Tap to Pay on iPhone, Koard has implemented multiple security measures to ensure secure development and operation: * **Secure SDK Architecture**: All card data is handled securely through Apple's ProximityReader framework * **Encrypted Communication**: All API communications use industry-standard encryption * **Tokenization**: Sensitive payment data is tokenized and never stored in plain text * **PCI DSS Compliance**: Koard maintains PCI DSS Level 1 compliance standards ### Entitlement Verification Before initiating any Tap to Pay functionality, verify that your application has the correct Tap to Pay on iPhone entitlement. **Check Entitlement Status** You can verify the entitlement by accessing the `readerIdentifier` property. If the app is missing the required entitlement, `readerIdentifier` will throw a `notAllowed` error. ```swift import ProximityReader do { let readerIdentifier = try await ProximityReader.readerIdentifier // Entitlement is present and valid } catch { // Handle notAllowed error if entitlement is missing print("Entitlement error: \(error)") } ``` **Important**: Ensure your SDK returns an appropriate error if the entitlement is missing, and handle this error gracefully in your application. ## Environment Configuration ### Production and Certificate Environments Koard provides two distinct environments for development and production use: #### Certificate Environment (CERT) The Certificate environment is designed for development and testing purposes. Use this environment when: * Developing internally with your team * Sharing builds with internal team members without a distribution certificate * Testing payment flows without processing real transactions * Conducting integration testing before production deployment **Best Practice**: Always use the Certificate environment for internal development and testing. This ensures that test transactions remain isolated from production payment processing. #### Production Environment The Production environment is used for live merchant transactions. All devices connect to the Koard production environment by default when configured for production use. **Note**: All customers working directly with Koard will have access to both environments via their API key configuration. ### Environment Selection To switch between environments, configure your SDK initialization: ```swift let options = KoardOptions( environment: .cert, // or .production loggingLevel: .debug ) KoardMerchantSDK.shared.initialize( options: options, apiKey: "your-api-key" ) ``` ## Sandbox Testing ### Sandbox Tester Account Setup If your SDK or API developers need to connect to Apple's Certificate environment through your test environment, you must create a Sandbox Tester Account. This account allows you to test Tap to Pay functionality without processing real transactions. ### Creating a Sandbox Tester Account Follow these steps to create a sandbox tester account: 1. **Sign in to App Store Connect** * Navigate to [App Store Connect](https://appstoreconnect.apple.com) * Sign in with your Apple Developer account credentials 2. **Access Sandbox Testers** * On the homepage, click **Users and Access** * In the top navigation, click **Sandbox** * Click the add button (+) * If this is your first time adding sandbox testers, click **Create Test Accounts** 3. **Complete Tester Information** * Enter a first and last name for your tester * Enter an email address that: * Has not been used as an Apple Account * Has not been used to purchase iTunes or App Store content * Consider creating a dedicated email address for each sandbox tester * Enter a strong password that meets Apple's requirements * Choose an App Store country or region 4. **Email Subaddressing (Optional)** If your email service provider supports email subaddressing with a plus sign (+), you can use subaddresses of a sandbox-specific address for multiple testers. For example: * Base email: `billjames2@icloud.com` * Subaddresses: `billjames2+UK@icloud.com`, `billjames2+US@icloud.com`, `billjames2+JP@icloud.com` All communications sent to the subaddresses are also sent to the base address. 5. **Invite the Tester** * Review all information * Click **Invite** to complete the setup 6. **Configure Testing Devices** * Sign out of your Apple Account on all testing devices * Sign back in with your new sandbox tester account **Important Notes**: * Once you create a tester, you cannot edit the name, email, or password * Each test account is associated with one of 175 App Store storefronts * You can edit a tester's App Store country or region after creation to test on different storefronts using the same Sandbox account ### Additional Resources For more detailed information on creating sandbox tester accounts, see [App Store Connect Help: Create a sandbox tester account](https://help.apple.com/app-store-connect/#/dev8b57d558e). ## Best Practices ### Development Workflow 1. **Use Certificate Environment** for all internal development and testing 2. **Verify Entitlements** before attempting to use Tap to Pay functionality 3. **Implement Error Handling** for missing entitlements and other error conditions 4. **Test Thoroughly** using sandbox tester accounts before production deployment ### Security Guidelines * Never log or store sensitive cardholder data * Implement proper error handling without exposing sensitive information * Use secure communication protocols (HTTPS/TLS) * Follow Apple's security guidelines for payment applications * Regularly update your SDK to the latest version for security patches ## Testing Checklist Before deploying to production, ensure: * [ ] Application has Tap to Pay on iPhone entitlement configured * [ ] Entitlement verification is implemented and tested * [ ] Error handling for missing entitlements is in place * [ ] Sandbox tester accounts are created and configured * [ ] Testing is performed in Certificate environment * [ ] All payment flows are tested with test cards * [ ] Production environment is properly configured before go-live ## Support and Resources ### Apple Documentation * [Apple Pay Developer Guide](https://developer.apple.com/apple-pay/) * [ProximityReader Framework Reference](https://developer.apple.com/documentation/proximityreader) * [App Store Connect Help](https://help.apple.com/app-store-connect/) * [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/) ### Koard Resources * [Setting Up the Entitlement](/docs/setting-up-the-ios-sdk/setting-up-the-entitlement-for-tap-to-pay-on-iphone) * [SDK Installation Guide](/docs/setting-up-the-ios-sdk/installing-the-sdk) * [Payment Lifecycle Guide](/docs/guides/payments/details/payment-lifecycle.md) * [Test Cards Reference](/docs/appendix/resources#resources__test-cards) # Payment Configurations Please email support@koard.com for more details on customized payment configurations. At the moment, the only payment configurations that can be set are handled by Bleu on the backend. All partners working directly with Apple will have their own Apple Terminal Profile configurations set up and will need to share the Terminal Profile configurations with the Bleu team. # Available Events Complete list of webhook events Koard can deliver, with the **exact payload your endpoint receives**. ## Payload shape Almost every webhook request body is the **resource object itself, as flat JSON** — for example a `batch.accepted` body is a Batch, a `transaction.sale` body is a Transaction. There is **no `{"event": ..., "data": ...}` envelope**; the fields shown below are top-level. The exception is the **`*.deleted` events** (`account.deleted`, `terminal.deleted`, `location.deleted`), which carry a sparse **id-only** payload — just the resource id (and `account_id` where applicable), not the full object, since the resource no longer exists. Each event's exact body is shown below. **The event type is not in the request body.** Koard delivers webhooks through Svix, so the body is the raw payload and the delivery metadata rides in headers (`svix-id`, `svix-timestamp`, `svix-signature`). To know which event an endpoint received, **subscribe that endpoint to specific event types** in the developer portal (one endpoint per event or per category). Transactions and batches also carry discriminators in the body — branch on `transaction_type` / `status` for transactions and on `status` for batches. Conventions used in every payload: * **Money is in minor units** (cents) — `5000` means $50.00. * **Timestamps:** transaction `created_at` is a Unix timestamp in **milliseconds**; every other resource (`batch`, `account`, `terminal`, `location`, `api_key`, `credential`) uses **ISO-8601** strings (`opened_at`, `created_at`, …). * **Secrets are masked:** the card is truncated (`4113********4242`), an API key is `"****" + last4`, and a credential PIN is `"****"`. Full secrets and hash material are never sent. * Unset optional fields are present with a `null` value. ## Transaction outcomes Transaction webhooks are named after the **operation that was attempted** — not its result. A sale attempt always fires `transaction.sale`, an authorization always fires `transaction.authorize`, and so on — whether the attempt was **approved, declined, or errored**. The **outcome** lives in the payload: * **`status`** — the resulting state (e.g. `authorized`, `captured`, `declined`, `error`). * **`status_reason`** — a machine-readable reason for that status (e.g. `approved`, `insufficient_funds`, `payment_failed`). So your handler should **branch on `status`** to tell success from failure. **Failed and declined attempts fire the same event as a success.** A declined or errored **auth** is still delivered as `transaction.authorize`; a failed **sale** as `transaction.sale`. There is no separate `transaction.failed` or `transaction.declined` event — read `status` / `status_reason` to handle the result. ### Transaction statuses `status` reflects the resulting state of the transaction: | `status` | Meaning | Outcome | | ------------------- | ------------------------------------------------------------------ | --------------- | | `authorized` | Funds held, not yet captured | ✅ Success | | `captured` | Funds captured (sale or capture) | ✅ Success | | `settled` | Funds finalized in a settled batch | ✅ Success | | `refunded` | Refund completed | ✅ Success | | `reversed` | Authorization reversed / voided | ✅ Success | | `pending` | Awaiting external input | ⏳ In progress | | `surcharge_pending` | Awaiting surcharge confirmation | ⏳ In progress | | `declined` | Declined by the issuer | ❌ Failure | | `error` | The attempt could not be completed (host / format / network error) | ❌ Failure | | `canceled` | Canceled by the user or system | ⛔ Not completed | ### Status reasons `status_reason` explains **why** a transaction ended in its `status`. Common values: | `status_reason` | Meaning | | --------------------------------------------------- | ------------------------------------------------------------------------- | | `approved` | The operation was approved | | `declined` | The issuer declined the transaction | | `insufficient_funds` | Declined — the account had insufficient funds | | `invalid_card` | The card was invalid or expired | | `payment_failed` | The host / gateway returned an unclassified failure (not a clean decline) | | `processor_timeout` / `timed_out` | No response from the processor in time | | `network_connectivity_error` | Could not reach the processor | | `invalid_gateway_response` / `format_error` | The processor response could not be parsed | | `invalid_terminal` | The terminal / BIN is not boarded or is misconfigured | | `partial_approval` | Approved for less than the requested amount | | `incremental_auth_declined` | An incremental authorization was declined | | `invalid_tip_adjustment` | A tip adjustment is not allowed for this transaction | | `exceeds_original_auth` | A capture / adjustment exceeds the original authorization | | `invalid_capture_sequence` | A capture was attempted in an invalid state | | `surcharge_declined_by_payer` / `surcharge_expired` | The cardholder declined the surcharge, or the prompt expired | | `user_canceled` / `merchant_canceled` | Canceled by the cardholder or the merchant | | `duplicate_event` | A duplicate event ID was received | ### Parsing a transaction webhook Verify the Svix signature first (see [Setting up Webhooks](/docs/webhooks/setting-up-webhooks)), then read the **flat** transaction body. This endpoint is subscribed to transaction events, so the body is a Transaction; route on `transaction_type` and branch on `status`. ```python Python # POST https://your-app.com/webhooks/koard (subscribed to transaction.* events) from flask import request, jsonify SUCCESS_STATUSES = {"authorized", "captured", "settled", "refunded", "reversed"} IN_PROGRESS_STATUSES = {"pending", "surcharge_pending"} @app.route("/webhooks/koard", methods=["POST"]) def koard_webhook(): txn = request.get_json() # the body IS the transaction (flat, no envelope) operation = txn["transaction_type"] # sale / auth / capture / refund / reverse / ... status = txn.get("status") if status in SUCCESS_STATUSES: handle_success(operation, txn) elif status in IN_PROGRESS_STATUSES: handle_in_progress(operation, status, txn) # awaiting input; not final yet elif status == "canceled": handle_canceled(operation, txn) # not completed else: # only declined / error land here — status_reason explains why handle_failure(operation, status, txn.get("status_reason"), txn) return jsonify(received=True), 200 ``` ```javascript Node.js // POST https://your-app.com/webhooks/koard (subscribed to transaction.* events) const SUCCESS_STATUSES = new Set([ "authorized", "captured", "settled", "refunded", "reversed", ]); const IN_PROGRESS_STATUSES = new Set(["pending", "surcharge_pending"]); app.post("/webhooks/koard", (req, res) => { const txn = req.body; // the body IS the transaction (flat, no envelope) const { transaction_type: operation, status, status_reason } = txn; if (SUCCESS_STATUSES.has(status)) { handleSuccess(operation, txn); } else if (IN_PROGRESS_STATUSES.has(status)) { handleInProgress(operation, status, txn); // awaiting input; not final yet } else if (status === "canceled") { handleCanceled(operation, txn); // not completed } else { // only declined / error land here — `status_reason` explains why handleFailure(operation, status, status_reason, txn); } res.status(200).json({ received: true }); }); ``` ```ruby Ruby # POST https://your-app.com/webhooks/koard (subscribed to transaction.* events) SUCCESS_STATUSES = %w[authorized captured settled refunded reversed].freeze IN_PROGRESS_STATUSES = %w[pending surcharge_pending].freeze post "/webhooks/koard" do txn = JSON.parse(request.body.read) # the body IS the transaction (flat) operation = txn["transaction_type"] status = txn["status"] if SUCCESS_STATUSES.include?(status) handle_success(operation, txn) elsif IN_PROGRESS_STATUSES.include?(status) handle_in_progress(operation, status, txn) # awaiting input; not final yet elsif status == "canceled" handle_canceled(operation, txn) # not completed else # only declined / error land here handle_failure(operation, status, txn["status_reason"], txn) end status 200 { received: true }.to_json end ``` ```java Java // POST https://your-app.com/webhooks/koard (subscribed to transaction.* events) private static final Set SUCCESS_STATUSES = Set.of("authorized", "captured", "settled", "refunded", "reversed"); private static final Set IN_PROGRESS_STATUSES = Set.of("pending", "surcharge_pending"); @PostMapping("/webhooks/koard") public ResponseEntity koardWebhook(@RequestBody Map txn) { String operation = (String) txn.get("transaction_type"); String status = (String) txn.get("status"); if (SUCCESS_STATUSES.contains(status)) { handleSuccess(operation, txn); } else if (IN_PROGRESS_STATUSES.contains(status)) { handleInProgress(operation, status, txn); // awaiting input; not final yet } else if ("canceled".equals(status)) { handleCanceled(operation, txn); // not completed } else { // only declined / error land here handleFailure(operation, status, (String) txn.get("status_reason"), txn); } return ResponseEntity.ok().build(); } ``` ```php PHP true]); ``` The examples below show the exact body delivered for each event. ## Transaction Events ### transaction.authorize Card authorized — funds held, not yet captured. ```json { "transaction_id": "6f6b6d2e-1c3a-4a9e-9d21-2f0e7b8c9a10", "event_id": "6F6B6D2E-1C3A-4A9E-9D21-2F0E7B8C9A10", "mid": "445566778899", "tid": "TERM0001", "processor_mid": "5289003", "processor_tid": "5289003", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "device_id": "dev_5Hj3Kl8Mn2Pq6Rt", "processor": "tsys", "gateway": "sierra", "currency": "USD", "location_id": "loc_7Bd4Cf9Gh2Jk5Lm", "gateway_transaction_id": "BG6RJZHK8Q", "subtotal": 4200, "tip_amount": 800, "tip_type": "amount", "tip_capture_mode": null, "tax_amount": 0, "tax_rate": 0, "tax_source": null, "tax_basis": null, "total_amount": 5000, "surcharge_applied": false, "surcharge_amount": 0, "surcharge_rate": 0, "surcharge_source": null, "surcharge_basis": null, "surcharge_confirmation_required": null, "refunded": 0, "reversed": 0, "created_at": 1768470600000, "status": "authorized", "status_reason": "approved", "payment_method": "contactlessIcc", "card_type": "credit", "card_brand": "visa", "card": "4113********4242", "additional_details": {}, "gateway_transaction_response": { "responseCode": "A", "responseMessage": "APPROVAL", "approvalCode": "522841", "processorResponseCode": "00" }, "processor_response_code": "00", "processor_response_message": "Approved", "transaction_type": "auth", "apple_transaction_id": "", "reader_identifier": "rdr_8Wq2Er5Ty7Ui", "owner_id": "acct_2Nk9Lm4Pq7Rs1Tv", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "merchant_name": "Blue Bottle Coffee", "psp": null, "batch_id": "batch_6Yh3Uj8Ik2Ol", "device_type": null, "terminal_details": null, "history": [] } ``` ### transaction.sale Sale — card authorized and captured in one step. ```json { "transaction_id": "6f6b6d2e-1c3a-4a9e-9d21-2f0e7b8c9a10", "event_id": "6F6B6D2E-1C3A-4A9E-9D21-2F0E7B8C9A10", "mid": "445566778899", "tid": "TERM0001", "processor_mid": "5289003", "processor_tid": "5289003", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "device_id": "dev_5Hj3Kl8Mn2Pq6Rt", "processor": "tsys", "gateway": "sierra", "currency": "USD", "location_id": "loc_7Bd4Cf9Gh2Jk5Lm", "gateway_transaction_id": "BG6RJZHK8Q", "subtotal": 4200, "tip_amount": 800, "tip_type": "amount", "tip_capture_mode": null, "tax_amount": 0, "tax_rate": 0, "tax_source": null, "tax_basis": null, "total_amount": 5000, "surcharge_applied": false, "surcharge_amount": 0, "surcharge_rate": 0, "surcharge_source": null, "surcharge_basis": null, "surcharge_confirmation_required": null, "refunded": 0, "reversed": 0, "created_at": 1768470600000, "status": "captured", "status_reason": "approved", "payment_method": "contactlessIcc", "card_type": "credit", "card_brand": "visa", "card": "4113********4242", "additional_details": {}, "gateway_transaction_response": { "responseCode": "A", "responseMessage": "APPROVAL", "approvalCode": "522841", "processorResponseCode": "00" }, "processor_response_code": "00", "processor_response_message": "Approved", "transaction_type": "sale", "apple_transaction_id": "", "reader_identifier": "rdr_8Wq2Er5Ty7Ui", "owner_id": "acct_2Nk9Lm4Pq7Rs1Tv", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "merchant_name": "Blue Bottle Coffee", "psp": null, "batch_id": "batch_6Yh3Uj8Ik2Ol", "device_type": null, "terminal_details": null, "history": [] } ``` ### transaction.capture A prior authorization was captured for settlement. ```json { "transaction_id": "6f6b6d2e-1c3a-4a9e-9d21-2f0e7b8c9a10", "event_id": "6F6B6D2E-1C3A-4A9E-9D21-2F0E7B8C9A10", "mid": "445566778899", "tid": "TERM0001", "processor_mid": "5289003", "processor_tid": "5289003", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "device_id": "dev_5Hj3Kl8Mn2Pq6Rt", "processor": "tsys", "gateway": "sierra", "currency": "USD", "location_id": "loc_7Bd4Cf9Gh2Jk5Lm", "gateway_transaction_id": "BG6RJZHK8Q", "subtotal": 4200, "tip_amount": 800, "tip_type": "amount", "tip_capture_mode": null, "tax_amount": 0, "tax_rate": 0, "tax_source": null, "tax_basis": null, "total_amount": 5000, "surcharge_applied": false, "surcharge_amount": 0, "surcharge_rate": 0, "surcharge_source": null, "surcharge_basis": null, "surcharge_confirmation_required": null, "refunded": 0, "reversed": 0, "created_at": 1768470600000, "status": "captured", "status_reason": "approved", "payment_method": "contactlessIcc", "card_type": "credit", "card_brand": "visa", "card": "4113********4242", "additional_details": {}, "gateway_transaction_response": { "responseCode": "A", "responseMessage": "APPROVAL", "approvalCode": "522841", "processorResponseCode": "00" }, "processor_response_code": "00", "processor_response_message": "Approved", "transaction_type": "capture", "apple_transaction_id": "", "reader_identifier": "rdr_8Wq2Er5Ty7Ui", "owner_id": "acct_2Nk9Lm4Pq7Rs1Tv", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "merchant_name": "Blue Bottle Coffee", "psp": null, "batch_id": "batch_6Yh3Uj8Ik2Ol", "device_type": null, "terminal_details": null, "history": [] } ``` ### transaction.cancel Transaction cancelled before settlement. ```json { "transaction_id": "6f6b6d2e-1c3a-4a9e-9d21-2f0e7b8c9a10", "event_id": "6F6B6D2E-1C3A-4A9E-9D21-2F0E7B8C9A10", "mid": "445566778899", "tid": "TERM0001", "processor_mid": "5289003", "processor_tid": "5289003", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "device_id": "dev_5Hj3Kl8Mn2Pq6Rt", "processor": "tsys", "gateway": "sierra", "currency": "USD", "location_id": "loc_7Bd4Cf9Gh2Jk5Lm", "gateway_transaction_id": "BG6RJZHK8Q", "subtotal": 4200, "tip_amount": 800, "tip_type": "amount", "tip_capture_mode": null, "tax_amount": 0, "tax_rate": 0, "tax_source": null, "tax_basis": null, "total_amount": 5000, "surcharge_applied": false, "surcharge_amount": 0, "surcharge_rate": 0, "surcharge_source": null, "surcharge_basis": null, "surcharge_confirmation_required": null, "refunded": 0, "reversed": 0, "created_at": 1768470600000, "status": "canceled", "status_reason": "user_canceled", "payment_method": "contactlessIcc", "card_type": "credit", "card_brand": "visa", "card": "4113********4242", "additional_details": {}, "gateway_transaction_response": { "responseCode": "A", "responseMessage": "APPROVAL", "approvalCode": "522841", "processorResponseCode": "00" }, "processor_response_code": "00", "processor_response_message": "Approved", "transaction_type": "sale", "apple_transaction_id": "", "reader_identifier": "rdr_8Wq2Er5Ty7Ui", "owner_id": "acct_2Nk9Lm4Pq7Rs1Tv", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "merchant_name": "Blue Bottle Coffee", "psp": null, "batch_id": "batch_6Yh3Uj8Ik2Ol", "device_type": null, "terminal_details": null, "history": [] } ``` ### transaction.create A transaction record was created. ```json { "transaction_id": "6f6b6d2e-1c3a-4a9e-9d21-2f0e7b8c9a10", "event_id": "6F6B6D2E-1C3A-4A9E-9D21-2F0E7B8C9A10", "mid": "445566778899", "tid": "TERM0001", "processor_mid": "5289003", "processor_tid": "5289003", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "device_id": "dev_5Hj3Kl8Mn2Pq6Rt", "processor": "tsys", "gateway": "sierra", "currency": "USD", "location_id": "loc_7Bd4Cf9Gh2Jk5Lm", "gateway_transaction_id": "BG6RJZHK8Q", "subtotal": 4200, "tip_amount": 800, "tip_type": "amount", "tip_capture_mode": null, "tax_amount": 0, "tax_rate": 0, "tax_source": null, "tax_basis": null, "total_amount": 5000, "surcharge_applied": false, "surcharge_amount": 0, "surcharge_rate": 0, "surcharge_source": null, "surcharge_basis": null, "surcharge_confirmation_required": null, "refunded": 0, "reversed": 0, "created_at": 1768470600000, "status": "pending", "status_reason": "pending", "payment_method": "contactlessIcc", "card_type": "credit", "card_brand": "visa", "card": "4113********4242", "additional_details": {}, "gateway_transaction_response": { "responseCode": "A", "responseMessage": "APPROVAL", "approvalCode": "522841", "processorResponseCode": "00" }, "processor_response_code": "00", "processor_response_message": "Approved", "transaction_type": "sale", "apple_transaction_id": "", "reader_identifier": "rdr_8Wq2Er5Ty7Ui", "owner_id": "acct_2Nk9Lm4Pq7Rs1Tv", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "merchant_name": "Blue Bottle Coffee", "psp": null, "batch_id": null, "device_type": null, "terminal_details": null, "history": [] } ``` ### transaction.reverse Authorization voided/undone after auth, before settlement. ```json { "transaction_id": "6f6b6d2e-1c3a-4a9e-9d21-2f0e7b8c9a10", "event_id": "6F6B6D2E-1C3A-4A9E-9D21-2F0E7B8C9A10", "mid": "445566778899", "tid": "TERM0001", "processor_mid": "5289003", "processor_tid": "5289003", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "device_id": "dev_5Hj3Kl8Mn2Pq6Rt", "processor": "tsys", "gateway": "sierra", "currency": "USD", "location_id": "loc_7Bd4Cf9Gh2Jk5Lm", "gateway_transaction_id": "BG6RJZHK8Q", "subtotal": 4200, "tip_amount": 800, "tip_type": "amount", "tip_capture_mode": null, "tax_amount": 0, "tax_rate": 0, "tax_source": null, "tax_basis": null, "total_amount": 5000, "surcharge_applied": false, "surcharge_amount": 0, "surcharge_rate": 0, "surcharge_source": null, "surcharge_basis": null, "surcharge_confirmation_required": null, "refunded": 0, "reversed": 5000, "created_at": 1768470600000, "status": "reversed", "status_reason": "approved", "payment_method": "contactlessIcc", "card_type": "credit", "card_brand": "visa", "card": "4113********4242", "additional_details": {}, "gateway_transaction_response": { "responseCode": "A", "responseMessage": "APPROVAL", "approvalCode": "522841", "processorResponseCode": "00" }, "processor_response_code": "00", "processor_response_message": "Approved", "transaction_type": "reverse", "apple_transaction_id": "", "reader_identifier": "rdr_8Wq2Er5Ty7Ui", "owner_id": "acct_2Nk9Lm4Pq7Rs1Tv", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "merchant_name": "Blue Bottle Coffee", "psp": null, "batch_id": "batch_6Yh3Uj8Ik2Ol", "device_type": null, "terminal_details": null, "history": [] } ``` ### transaction.refund Funds refunded to the cardholder after settlement. ```json { "transaction_id": "6f6b6d2e-1c3a-4a9e-9d21-2f0e7b8c9a10", "event_id": "6F6B6D2E-1C3A-4A9E-9D21-2F0E7B8C9A10", "mid": "445566778899", "tid": "TERM0001", "processor_mid": "5289003", "processor_tid": "5289003", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "device_id": "dev_5Hj3Kl8Mn2Pq6Rt", "processor": "tsys", "gateway": "sierra", "currency": "USD", "location_id": "loc_7Bd4Cf9Gh2Jk5Lm", "gateway_transaction_id": "BG6RJZHK8Q", "subtotal": 4200, "tip_amount": 800, "tip_type": "amount", "tip_capture_mode": null, "tax_amount": 0, "tax_rate": 0, "tax_source": null, "tax_basis": null, "total_amount": 5000, "surcharge_applied": false, "surcharge_amount": 0, "surcharge_rate": 0, "surcharge_source": null, "surcharge_basis": null, "surcharge_confirmation_required": null, "refunded": 5000, "reversed": 0, "created_at": 1768470600000, "status": "refunded", "status_reason": "approved", "payment_method": "contactlessIcc", "card_type": "credit", "card_brand": "visa", "card": "4113********4242", "additional_details": {}, "gateway_transaction_response": { "responseCode": "A", "responseMessage": "APPROVAL", "approvalCode": "522841", "processorResponseCode": "00" }, "processor_response_code": "00", "processor_response_message": "Approved", "transaction_type": "refund", "apple_transaction_id": "", "reader_identifier": "rdr_8Wq2Er5Ty7Ui", "owner_id": "acct_2Nk9Lm4Pq7Rs1Tv", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "merchant_name": "Blue Bottle Coffee", "psp": null, "batch_id": "batch_6Yh3Uj8Ik2Ol", "device_type": null, "terminal_details": null, "history": [] } ``` ### transaction.increment An existing authorization amount was increased (incremental auth). ```json { "transaction_id": "6f6b6d2e-1c3a-4a9e-9d21-2f0e7b8c9a10", "event_id": "6F6B6D2E-1C3A-4A9E-9D21-2F0E7B8C9A10", "mid": "445566778899", "tid": "TERM0001", "processor_mid": "5289003", "processor_tid": "5289003", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "device_id": "dev_5Hj3Kl8Mn2Pq6Rt", "processor": "tsys", "gateway": "sierra", "currency": "USD", "location_id": "loc_7Bd4Cf9Gh2Jk5Lm", "gateway_transaction_id": "BG6RJZHK8Q", "subtotal": 6200, "tip_amount": 800, "tip_type": "amount", "tip_capture_mode": null, "tax_amount": 0, "tax_rate": 0, "tax_source": null, "tax_basis": null, "total_amount": 7000, "surcharge_applied": false, "surcharge_amount": 0, "surcharge_rate": 0, "surcharge_source": null, "surcharge_basis": null, "surcharge_confirmation_required": null, "refunded": 0, "reversed": 0, "created_at": 1768470600000, "status": "authorized", "status_reason": "approved", "payment_method": "contactlessIcc", "card_type": "credit", "card_brand": "visa", "card": "4113********4242", "additional_details": {}, "gateway_transaction_response": { "responseCode": "A", "responseMessage": "APPROVAL", "approvalCode": "522841", "processorResponseCode": "00" }, "processor_response_code": "00", "processor_response_message": "Approved", "transaction_type": "incremental_auth", "apple_transaction_id": "", "reader_identifier": "rdr_8Wq2Er5Ty7Ui", "owner_id": "acct_2Nk9Lm4Pq7Rs1Tv", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "merchant_name": "Blue Bottle Coffee", "psp": null, "batch_id": "batch_6Yh3Uj8Ik2Ol", "device_type": null, "terminal_details": null, "history": [] } ``` ### transaction.tip\_adjust A tip was added or changed on a captured transaction. ```json { "transaction_id": "6f6b6d2e-1c3a-4a9e-9d21-2f0e7b8c9a10", "event_id": "6F6B6D2E-1C3A-4A9E-9D21-2F0E7B8C9A10", "mid": "445566778899", "tid": "TERM0001", "processor_mid": "5289003", "processor_tid": "5289003", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "device_id": "dev_5Hj3Kl8Mn2Pq6Rt", "processor": "tsys", "gateway": "sierra", "currency": "USD", "location_id": "loc_7Bd4Cf9Gh2Jk5Lm", "gateway_transaction_id": "BG6RJZHK8Q", "subtotal": 4200, "tip_amount": 1200, "tip_type": "amount", "tip_capture_mode": null, "tax_amount": 0, "tax_rate": 0, "tax_source": null, "tax_basis": null, "total_amount": 5400, "surcharge_applied": false, "surcharge_amount": 0, "surcharge_rate": 0, "surcharge_source": null, "surcharge_basis": null, "surcharge_confirmation_required": null, "refunded": 0, "reversed": 0, "created_at": 1768470600000, "status": "captured", "status_reason": "approved", "payment_method": "contactlessIcc", "card_type": "credit", "card_brand": "visa", "card": "4113********4242", "additional_details": {}, "gateway_transaction_response": { "responseCode": "A", "responseMessage": "APPROVAL", "approvalCode": "522841", "processorResponseCode": "00" }, "processor_response_code": "00", "processor_response_message": "Approved", "transaction_type": "tip_adjust", "apple_transaction_id": "", "reader_identifier": "rdr_8Wq2Er5Ty7Ui", "owner_id": "acct_2Nk9Lm4Pq7Rs1Tv", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "merchant_name": "Blue Bottle Coffee", "psp": null, "batch_id": "batch_6Yh3Uj8Ik2Ol", "device_type": null, "terminal_details": null, "history": [] } ``` ## Batch Events ### batch.opened A settlement batch was opened. ```json { "id": "batch_6Yh3Uj8Ik2Ol", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "processor_name": "tsys", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "status": "open", "captured_amount": 0, "refunded_amount": 0, "transaction_count": 0, "processor_batch_id": null, "opened_at": "2026-01-15T08:00:00Z", "closed_at": null, "response": null, "batch_metadata": {} } ``` ### batch.rejected The processor rejected the batch. ```json { "id": "batch_6Yh3Uj8Ik2Ol", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "processor_name": "tsys", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "status": "rejected", "captured_amount": 128400, "refunded_amount": 5000, "transaction_count": 37, "processor_batch_id": "TSYS-BATCH-000481", "opened_at": "2026-01-15T08:00:00Z", "closed_at": null, "response": { "rejection_reason": "Host unavailable \u2014 retry" }, "batch_metadata": {} } ``` ### batch.accepted The processor accepted the batch for settlement. ```json { "id": "batch_6Yh3Uj8Ik2Ol", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "processor_name": "tsys", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "status": "accepted", "captured_amount": 128400, "refunded_amount": 5000, "transaction_count": 37, "processor_batch_id": "TSYS-BATCH-000481", "opened_at": "2026-01-15T08:00:00Z", "closed_at": "2026-01-15T20:00:00Z", "response": null, "batch_metadata": {} } ``` ### batch.partially\_accepted The processor accepted the batch with some rejected records. ```json { "id": "batch_6Yh3Uj8Ik2Ol", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "processor_name": "tsys", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "status": "partially_accepted", "captured_amount": 128400, "refunded_amount": 5000, "transaction_count": 37, "processor_batch_id": "TSYS-BATCH-000481", "opened_at": "2026-01-15T08:00:00Z", "closed_at": "2026-01-15T20:00:00Z", "response": { "accepted": 35, "rejected": 2 }, "batch_metadata": {} } ``` ### batch.edited A batch's transactions were edited before close. ```json { "id": "batch_6Yh3Uj8Ik2Ol", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "processor_name": "tsys", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "status": "open", "captured_amount": 128400, "refunded_amount": 5000, "transaction_count": 36, "processor_batch_id": "TSYS-BATCH-000481", "opened_at": "2026-01-15T08:00:00Z", "closed_at": null, "response": null, "batch_metadata": {} } ``` ### batch.submitted A batch was submitted to the processor for settlement. ```json { "id": "batch_6Yh3Uj8Ik2Ol", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "processor_name": "tsys", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "status": "submitted", "captured_amount": 128400, "refunded_amount": 5000, "transaction_count": 37, "processor_batch_id": "TSYS-BATCH-000481", "opened_at": "2026-01-15T08:00:00Z", "closed_at": null, "response": null, "batch_metadata": {} } ``` ## Account Events ### account.created A new account was created. ```json { "id": "acct_2Nk9Lm4Pq7Rs1Tv", "organization_id": null, "has_access_to_mms": false, "has_access_to_apple_config": false, "available_processor_configs": [], "tax_id": null, "mcc": "5812", "type": "merchant", "status": "active", "name": "Blue Bottle Coffee", "description": "Specialty coffee roaster", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "address": { "street_line1": "1500 Market St", "street_line2": "Suite 200", "city": "San Francisco", "state": "CA", "zip": "94103", "country": "US" }, "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "created_at": "2026-01-15T09:30:00Z", "deleted_at": null, "updated_at": null } ``` ### account.updated An account's details were updated. ```json { "id": "acct_2Nk9Lm4Pq7Rs1Tv", "organization_id": null, "has_access_to_mms": false, "has_access_to_apple_config": false, "available_processor_configs": [], "tax_id": null, "mcc": "5812", "type": "merchant", "status": "active", "name": "Blue Bottle Coffee Co.", "description": "Specialty coffee roaster", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "address": { "street_line1": "1500 Market St", "street_line2": "Suite 200", "city": "San Francisco", "state": "CA", "zip": "94103", "country": "US" }, "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "created_at": "2026-01-15T09:30:00Z", "deleted_at": null, "updated_at": null } ``` ### account.blocked An account was blocked (deactivated). ```json { "id": "acct_2Nk9Lm4Pq7Rs1Tv", "organization_id": null, "has_access_to_mms": false, "has_access_to_apple_config": false, "available_processor_configs": [], "tax_id": null, "mcc": "5812", "type": "merchant", "status": "blocked", "name": "Blue Bottle Coffee", "description": "Specialty coffee roaster", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "address": { "street_line1": "1500 Market St", "street_line2": "Suite 200", "city": "San Francisco", "state": "CA", "zip": "94103", "country": "US" }, "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "created_at": "2026-01-15T09:30:00Z", "deleted_at": null, "updated_at": null } ``` ### account.unblocked A blocked account was reactivated. ```json { "id": "acct_2Nk9Lm4Pq7Rs1Tv", "organization_id": null, "has_access_to_mms": false, "has_access_to_apple_config": false, "available_processor_configs": [], "tax_id": null, "mcc": "5812", "type": "merchant", "status": "active", "name": "Blue Bottle Coffee", "description": "Specialty coffee roaster", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "address": { "street_line1": "1500 Market St", "street_line2": "Suite 200", "city": "San Francisco", "state": "CA", "zip": "94103", "country": "US" }, "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "created_at": "2026-01-15T09:30:00Z", "deleted_at": null, "updated_at": null } ``` ### account.deleted An account was deleted (soft-deleted). ```json { "id": "acct_2Nk9Lm4Pq7Rs1Tv" } ``` ## Terminal Events ### terminal.created A new terminal was created. ```json { "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "name": "Front Counter \u2014 Register 1", "description": "Android EMV COTS device", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "mid": "445566778899", "tid": "TERM0001", "store_number": "SN-COTS-00481", "bin": "412345", "vid": null, "agent_bank_number": null, "merchant_category_code": "5812", "terminal_capability": "5", "currency_code": "USD", "country_code": "US", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "status": "active", "created_at": "2026-01-15T09:30:00Z", "deleted_at": null, "updated_at": null, "merchant_name": "Blue Bottle Coffee", "transactions": [], "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "var_sheet": null, "batching": null } ``` ### terminal.updated A terminal's configuration was updated. ```json { "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "name": "Front Counter \u2014 Register 2", "description": "Android EMV COTS device", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "mid": "445566778899", "tid": "TERM0001", "store_number": "SN-COTS-00481", "bin": "412345", "vid": null, "agent_bank_number": null, "merchant_category_code": "5812", "terminal_capability": "5", "currency_code": "USD", "country_code": "US", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "status": "active", "created_at": "2026-01-15T09:30:00Z", "deleted_at": null, "updated_at": null, "merchant_name": "Blue Bottle Coffee", "transactions": [], "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "var_sheet": null, "batching": null } ``` ### terminal.blocked A terminal was blocked (deactivated). ```json { "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "name": "Front Counter \u2014 Register 1", "description": "Android EMV COTS device", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "mid": "445566778899", "tid": "TERM0001", "store_number": "SN-COTS-00481", "bin": "412345", "vid": null, "agent_bank_number": null, "merchant_category_code": "5812", "terminal_capability": "5", "currency_code": "USD", "country_code": "US", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "status": "blocked", "created_at": "2026-01-15T09:30:00Z", "deleted_at": null, "updated_at": null, "merchant_name": "Blue Bottle Coffee", "transactions": [], "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "var_sheet": null, "batching": null } ``` ### terminal.unblocked A blocked terminal was reactivated. ```json { "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "name": "Front Counter \u2014 Register 1", "description": "Android EMV COTS device", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "mid": "445566778899", "tid": "TERM0001", "store_number": "SN-COTS-00481", "bin": "412345", "vid": null, "agent_bank_number": null, "merchant_category_code": "5812", "terminal_capability": "5", "currency_code": "USD", "country_code": "US", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "status": "active", "created_at": "2026-01-15T09:30:00Z", "deleted_at": null, "updated_at": null, "merchant_name": "Blue Bottle Coffee", "transactions": [], "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "var_sheet": null, "batching": null } ``` ### terminal.deleted A terminal was deleted (soft-deleted). ```json { "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv" } ``` ## Location Events ### location.created A new location was created. ```json { "id": "loc_7Bd4Cf9Gh2Jk5Lm", "name": "Ferry Building", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "address": { "street_line1": "1500 Market St", "street_line2": "Suite 200", "city": "San Francisco", "state": "CA", "zip": "94103", "country": "US" }, "phone": "+14155551234", "email": "ferry@bluebottle.example", "status": "active", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "country_code": "US", "currency": "USD", "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "metadata": null, "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "created_at": null, "updated_at": null, "deleted_at": null } ``` ### location.updated A location's details were updated. ```json { "id": "loc_7Bd4Cf9Gh2Jk5Lm", "name": "Ferry Building Plaza", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "address": { "street_line1": "1500 Market St", "street_line2": "Suite 200", "city": "San Francisco", "state": "CA", "zip": "94103", "country": "US" }, "phone": "+14155551234", "email": "ferry@bluebottle.example", "status": "active", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "country_code": "US", "currency": "USD", "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "metadata": null, "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "created_at": null, "updated_at": null, "deleted_at": null } ``` ### location.blocked A location was blocked (deactivated). ```json { "id": "loc_7Bd4Cf9Gh2Jk5Lm", "name": "Ferry Building", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "address": { "street_line1": "1500 Market St", "street_line2": "Suite 200", "city": "San Francisco", "state": "CA", "zip": "94103", "country": "US" }, "phone": "+14155551234", "email": "ferry@bluebottle.example", "status": "blocked", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "country_code": "US", "currency": "USD", "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "metadata": null, "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "created_at": null, "updated_at": null, "deleted_at": null } ``` ### location.unblocked A blocked location was reactivated. ```json { "id": "loc_7Bd4Cf9Gh2Jk5Lm", "name": "Ferry Building", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "address": { "street_line1": "1500 Market St", "street_line2": "Suite 200", "city": "San Francisco", "state": "CA", "zip": "94103", "country": "US" }, "phone": "+14155551234", "email": "ferry@bluebottle.example", "status": "active", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "country_code": "US", "currency": "USD", "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "metadata": null, "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "created_at": null, "updated_at": null, "deleted_at": null } ``` ### location.deleted A location was deleted (soft-deleted). ```json { "id": "loc_7Bd4Cf9Gh2Jk5Lm", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv" } ``` ## API Key Events ### api\_key.created A new API key was issued. ```json { "id": "key_1Ab2Cd3Ef4Gh5Ij", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "name": "Production integration", "status": "active", "key": "****4x9Q", "key_last4": "4x9Q", "expires_at": null, "created_at": "2026-01-15T09:30:00Z" } ``` ### api\_key.revoked An API key was revoked (denied at auth until reinstated). ```json { "id": "key_1Ab2Cd3Ef4Gh5Ij", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "name": "Production integration", "status": "revoked", "key": "****4x9Q", "key_last4": "4x9Q", "expires_at": null, "created_at": "2026-01-15T09:30:00Z" } ``` ### api\_key.reinstated A previously revoked API key was reinstated. ```json { "id": "key_1Ab2Cd3Ef4Gh5Ij", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "name": "Production integration", "status": "active", "key": "****4x9Q", "key_last4": "4x9Q", "expires_at": null, "created_at": "2026-01-15T09:30:00Z" } ``` ### api\_key.deleted An API key was deleted. ```json { "id": "key_1Ab2Cd3Ef4Gh5Ij", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "name": "Production integration", "status": "deleted", "key": "****4x9Q", "key_last4": "4x9Q", "expires_at": null, "created_at": "2026-01-15T09:30:00Z" } ``` ## Credential Events ### credential.created A new merchant credential was created. ```json { "id": "cred_7Kl8Mn9Op0Qr1St", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "code": "bluebottle-ferry", "pin": "****", "is_active": true, "created_at": "2026-01-15T09:30:00Z" } ``` ### credential.blocked A merchant credential was blocked (deactivated). ```json { "id": "cred_7Kl8Mn9Op0Qr1St", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "code": "bluebottle-ferry", "pin": "****", "is_active": false, "created_at": "2026-01-15T09:30:00Z" } ``` ### credential.unblocked A blocked merchant credential was reactivated. ```json { "id": "cred_7Kl8Mn9Op0Qr1St", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "code": "bluebottle-ferry", "pin": "****", "is_active": true, "created_at": "2026-01-15T09:30:00Z" } ``` ### credential.deleted A merchant credential was deleted. ```json { "id": "cred_7Kl8Mn9Op0Qr1St", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "code": "bluebottle-ferry", "pin": "****", "is_active": false, "created_at": "2026-01-15T09:30:00Z" } ``` ## Event Processing ### Identifying the event Because the event type is **not** in the body, don't `switch` on a body field. Instead: 1. **Recommended — subscribe per event type.** Point one endpoint at each event type (or category) you care about in the developer portal. The endpoint's URL then tells you what it received. 2. **In-body discriminators.** Transactions carry `transaction_type` + `status`; batches carry `status`. Lifecycle events for accounts, terminals, locations, API keys, and credentials do **not** carry the verb in the body (`terminal.created` and `terminal.updated` have identical shapes), so rely on the endpoint subscription for those. ### Idempotency Delivery is **at-least-once** — the same message may arrive more than once (e.g. on retry). **Deduplicate on the `svix-id` header**: it is the canonical delivery id and stays _constant across every retry_ of a message, so it's the key to track processed events (store it, e.g. in Redis with a 24-hour expiry, and skip anything you've already seen). The transaction `event_id` field is a separate, application-level correlation id for the underlying transaction event — useful for tying a webhook back to a transaction, but **not** the delivery-dedup key. ### Event ordering Events are generally delivered in the order they occurred, but retries can cause out-of-order delivery. Use the timestamps in the payload / `svix-timestamp` header if you need strict ordering. ### Event filtering Configure filters per endpoint in the developer portal: * **Event type** — subscribe to specific event types (recommended for production). * **Transaction type** — branch on `transaction_type` (`sale`, `auth`, `capture`, `refund`, `reverse`, `tip_adjust`, `incremental_auth`). * **Status** — branch on `status` (`authorized`, `captured`, `declined`, `refunded`, `reversed`, …). * **Processor** — branch on `processor` (`tsys`, `payroc`, …). ## Complete Event List | Event | Description | Category | | -------------------------- | ------------------------------------------------------------------ | ----------- | | `transaction.authorize` | Card authorized — funds held, not yet captured. | transaction | | `transaction.sale` | Sale — card authorized and captured in one step. | transaction | | `transaction.capture` | A prior authorization was captured for settlement. | transaction | | `transaction.cancel` | Transaction cancelled before settlement. | transaction | | `transaction.create` | A transaction record was created. | transaction | | `transaction.reverse` | Authorization voided/undone after auth, before settlement. | transaction | | `transaction.refund` | Funds refunded to the cardholder after settlement. | transaction | | `transaction.increment` | An existing authorization amount was increased (incremental auth). | transaction | | `transaction.tip_adjust` | A tip was added or changed on a captured transaction. | transaction | | `batch.opened` | A settlement batch was opened. | batch | | `batch.rejected` | The processor rejected the batch. | batch | | `batch.accepted` | The processor accepted the batch for settlement. | batch | | `batch.partially_accepted` | The processor accepted the batch with some rejected records. | batch | | `batch.edited` | A batch's transactions were edited before close. | batch | | `batch.submitted` | A batch was submitted to the processor for settlement. | batch | | `account.created` | A new account was created. | account | | `account.updated` | An account's details were updated. | account | | `account.blocked` | An account was blocked (deactivated). | account | | `account.unblocked` | A blocked account was reactivated. | account | | `account.deleted` | An account was deleted (soft-deleted). | account | | `terminal.created` | A new terminal was created. | terminal | | `terminal.updated` | A terminal's configuration was updated. | terminal | | `terminal.blocked` | A terminal was blocked (deactivated). | terminal | | `terminal.unblocked` | A blocked terminal was reactivated. | terminal | | `terminal.deleted` | A terminal was deleted (soft-deleted). | terminal | | `location.created` | A new location was created. | location | | `location.updated` | A location's details were updated. | location | | `location.blocked` | A location was blocked (deactivated). | location | | `location.unblocked` | A blocked location was reactivated. | location | | `location.deleted` | A location was deleted (soft-deleted). | location | | `api_key.created` | A new API key was issued. | api\_key | | `api_key.revoked` | An API key was revoked (denied at auth until reinstated). | api\_key | | `api_key.reinstated` | A previously revoked API key was reinstated. | api\_key | | `api_key.deleted` | An API key was deleted. | api\_key | | `credential.created` | A new merchant credential was created. | credential | | `credential.blocked` | A merchant credential was blocked (deactivated). | credential | | `credential.unblocked` | A blocked merchant credential was reactivated. | credential | | `credential.deleted` | A merchant credential was deleted. | credential | ## See also * [Setting up Webhooks](/docs/webhooks/setting-up-webhooks) — configuring endpoints, event subscriptions, and signature verification # Authentication All Koard API requests must be authenticated using an API key passed in the `x-koard-apikey` header. curl https://api.koard.com/v1/accounts/YOUR_ACCOUNT_ID \ -H "x-koard-apikey: YOUR_API_KEY" \ -H "Accept: application/json" ## Headers | Header | Required | Value | |--------|----------|-------| | `x-koard-apikey` | Always | Your Koard API key | | `Accept` | Always | `application/json` | | `Content-Type` | When sending a body | `application/json` | The header name is case-insensitive — `x-koard-apikey` and `X-Koard-apikey` are equivalent. ## API Keys API keys are provisioned per account. A partner-level key can manage merchants and terminals under it. A merchant-level key (a key bound to a `merchant` account) is limited to that merchant's own operations. Retrieve or rotate your API key from the [Koard MMS](https://app.koard.com) under your account settings, or manage keys programmatically via the [API Keys endpoints](/docs/api-reference/apikeys). Keep your API key secret. Never expose it in client-side code or public repositories. ### Scoped Permissions Each v5 key carries an explicit list of permissions. A permission is a string in the form `resource:action` (for example `payments:read`, `terminals:create`) or `resource:action:subtype` (for example `accounts:create:merchant`). A key can do **only** what it has been granted — there are no implicit grants. In particular, holding a write permission does **not** imply the matching read permission. Two macro permissions exist: `all` grants every concrete permission (for trusted server-side backends), and `legacy_all` is a grandfathered bucket set only on migrated pre-v5 keys (it cannot be granted to new keys). | Resource | Actions | |----------|---------| | `payments` | `tap-ios`, `tap-android`, `read`, `refund`, `tipadjust`, `capture`, `incremental-auth`, `void`, `confirm` | | `batches` | `read`, `open`, `edit`, `close` | | `terminals` | `read`, `create`, `edit`, `delete` | | `locations` | `read`, `create`, `edit`, `delete` | | `accounts` | `read`, `create:partner`, `create:merchant`, `edit`, `delete` | | `credentials` | `read`, `create`, `edit`, `delete` | | `apikeys` | `read`, `create`, `edit`, `delete` (and the `:sub` variants below) | | `webhooks` | `read`, `create`, `edit`, `delete` (Koard PSP only) | #### Own account vs. sub-accounts API-key management permissions are split by surface. The base form (`apikeys:read`, `apikeys:create`, `apikeys:edit`, `apikeys:delete`) governs keys on **your own** account. The `:sub` variants (`apikeys:read:sub`, `apikeys:create:sub`, `apikeys:edit:sub`, `apikeys:delete:sub`) govern keys on **descendant** (sub-) accounts. These are distinct grants — holding the self permission never satisfies a sub-account operation, and vice versa. #### Privilege-escalation guard When you create a key, you may only grant permissions you yourself hold. Requesting a permission outside your own grant is rejected with `403`. The `webhooks:*` permissions can only be granted by Koard PSP accounts. ### Visibility: 401 vs 404 When a key lacks a permission, the response distinguishes between "denied" and "invisible": - If your key holds **some** permission on the target resource type but not the specific operation, you get `401`. - If your key holds **zero** permissions on the target resource type (the resource is invisible to you), you get `404` — out-of-scope resources are hidden, never advertised. ### Key Lifecycle | Action | Effect | |--------|--------| | **Create** (`POST`) | Issues the key and returns the plaintext once. Defaults to a long expiry; pass `expires_at` to set your own. | | **Revoke** (`PUT` with `status: "revoked"`) | Disables the key for authentication. **Recoverable** — reinstate with `PUT status: "active"`. | | **Reinstate** (`PUT` with `status: "active"`) | Re-enables a revoked key. Does not work on a deleted key. | | **Delete** (`DELETE`) | Permanent soft-delete. The key can never be reinstated. Idempotent — repeating the call returns the same deleted key. | | **Expire** (`expires_at` passes) | The key stops authenticating automatically. | Permissions are **immutable** after creation — a `PUT` may change `name`, `expires_at`, and `status` only. To change a key's permissions, create a new key and delete the old one. Revoking or deleting a key takes effect immediately: in-flight sessions authenticated with that key are locked out. # Setting up the Merchant Learn how to set up merchant accounts in the Koard Merchant Management System (MMS) and configure them for tap-to-pay payments. [![link icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXNxdWFyZS1jb2RlLWljb24gbHVjaWRlLXNxdWFyZS1jb2RlIj48cGF0aCBkPSJtMTAgOS0zIDMgMyAzIi8+PHBhdGggZD0ibTE0IDE1IDMtMy0zLTMiLz48cmVjdCB4PSIzIiB5PSIzIiB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHJ4PSIyIi8+PC9zdmc+) Access UAT MMS ](https://app.uat.koard.com)[ Access Production MMS](https://app.koard.com) For the safety and security of your merchant accounts, complete our [merchant checklist](#setting-up-the-merchant__verification-and-testing) before going live. Immediately after you create a merchant account, you can use it in testing environments. In a _sandbox_ (A sandbox is an isolated test environment that allows you to test Koard functionality without affecting your live integration. Use sandboxes to safely experiment with new features and changes), simulate transactions and use all of Koard's features without moving any money. To accept real payments, you must activate your merchant account to use live mode. **Prerequisites** Before you begin, ensure you have: * **Access to the Koard Merchant Management System** * **Valid business registration documents** * **Tax identification number** for each merchant * **Merchant Category Code (MCC)** for each business * **Processor credentials** (MID, TID, VIN) or Partner ID * **Sandbox Apple Account on dedicated test iPhone** for validating Tap to Pay flows **Dedicated Test Hardware**: Make sure your Sandbox Apple Account is signed in on a separate test iPhone. You'll need it to validate Tap to Pay flows before onboarding merchants in production. ## Access the Merchant Management System ### UAT Environment For testing and development, use the UAT MMS: **** ### Production Environment For live merchant onboarding: **** ## Create a Merchant To create a merchant, fill out the merchant application requesting basic information about the business, processor details, and location information. After creating the merchant, you can immediately start configuring terminals and processing payments. Koard's merchant onboarding requirements ensure compliance with payment processor regulations and Apple's tap-to-pay guidelines. These requirements come from our financial partners and Apple, and are intended to prevent abuse of the payment system. We review the information you provide internally to make sure that it complies with our merchant agreement. After you create a merchant account, you can't change its country. If you need to use Koard in a different country that we support, you must create a new merchant account. Privacy and security are priorities for Koard. Our merchant data handling follows industry standards for payment processing and Apple's security requirements. **1. Navigate to Accounts** 1. Log into the MMS 2. Go to the **Accounts** tab 3. You'll see a list of existing Partners 4. Click [![link icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik01IDEyaDE0Ii8+PHBhdGggZD0iTTEyIDV2MTQiLz48L3N2Zz4=) Add Account](#) ![getting-started-1](/getting-started-1.png) **2. Fill Required Fields** Every merchant requires these mandatory fields: * **Name**: Business name or legal entity name * **Tax ID**: Unique tax identification number (merchants with the same Tax ID will be grouped together) * **MCC Code**: 4-digit Merchant Category Code * **HQ Country**: Headquarters country location ![getting-started-2](/getting-started-2.png) **3. Save Merchant** Click **Save** to create the merchant account. ## Assign a Terminal **1. Create New Terminal** 1. After creating the merchant, click [![link icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIi8+PHBhdGggZD0iTTggMTJoOCIvPjxwYXRoIGQ9Ik0xMiA4djgiLz48L3N2Zz4=) New Terminal](#) 2\. You'll see the terminal configuration view ![getting-started-3](/getting-started-3.png) **2. Configure Terminal Details** For most processors, enter: * **MID**: Merchant ID from your processor * **TID**: Terminal ID from your processor * **VIN**: Vendor ID from your processor For processors like Payroc where merchants have their own ID: * Replace MID, TID, and VIN with the **Partner ID** (or Processing Terminal ID) 1. Configure the terminal details by clicking [![link icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xMSA0SDRhMiAyIDAgMCAwLTIgMnYxNGEyIDIgMCAwIDAgMiAyaDE0YTIgMiAwIDAgMCAyLTJ2LTciLz48cGF0aCBkPSJNMTguNSAyLjVhMi4xMjEgMi4xMjEgMCAwIDEgMyAzTDEyIDE1bC00IDEtMS00IDkuNS05LjV6Ii8+PC9zdmc+) Edit Terminal](#) ![getting-started-4](/getting-started-4.png) **3. Assign Location** 1. Go back to the merchant account to access the **Locations** tab ![locations-1](/locations-1.png) 2. Click [![link icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIi8+PHBhdGggZD0iTTggMTJoOCIvPjxwYXRoIGQ9Ik0xMiA4djgiLz48L3N2Zz4=) New Location](#) 3. Assign the terminal to that location 4. This gives the merchant a physical location to accept payments from ![getting-started-5](/getting-started-5.png) ## Create Merchant Credentials Your merchants will use these credentials to authenticate with the Koard SDK and process payments. The credentials are tied to specific locations and terminals, ensuring secure payment processing. **1. Generate Credentials** 1. Navigate to the merchant's credential section 2. Click **Create Merchant Credentials** 3. Generate a unique **Code** and **PIN** for the merchant ![getting-started-6](/getting-started-6.png) **2. SDK Integration** Merchants can use these credentials to log into the SDK: ```swift KoardMerchantSDK.shared.login( "Code": "YOUR_CODE", "PIN": "YOUR_PIN" ) ``` **3. Location Configuration** * Merchants can set their location from the SDK or your mPOS app * Location is required to create a card reader session on the iPhone * This ensures payments are processed at the correct merchant location **Security Note**: Keep merchant credentials confidential. Store them securely on your servers and never share them in client-side code or public repositories. ## Multiple Processor Support Each merchant can be configured with multiple MIDs based on the supported processors you have access to: * **Primary Processor**: Main payment processor for the merchant * **Secondary Processors**: Backup or specialized processors * **Regional Processors**: Location-specific payment processing ## Verification and Testing **1. Verify Configuration** 1. Check that all required fields are completed 2. Verify processor credentials are correct 3. Ensure location is properly assigned **2. Test Integration** 1. Use the UAT environment to test merchant login 2. Verify terminal assignment works correctly 3. Test payment processing with test cards **3. Go Live** 1. Move merchant to production environment 2. Update SDK credentials for production 3. Monitor initial transactions ## Keep your merchant accounts safe After you set up your merchant accounts, you'll want to keep them secure. Here are our recommendations: * **Keep private information private**: Don't share merchant credentials and keep your secret API keys confidential on your own servers. As a reminder, Koard employees will never ask you for your keys. * **Use unique credentials**: Generate unique codes and PINs for each merchant. If you reuse credentials across merchants and one account is compromised, an attacker could access multiple merchant accounts. * **Use team members to provide others with access**: You can invite others (with limited access) to your Koard MMS account so that they can log in and take certain actions without full administrative access. * **Update your computer and browser regularly**: We recommend configuring your computer to automatically download and install updates. This helps protect your system against automated attacks and malware. * **Beware of phishing**: All genuine Koard sites use the `koard.com` domain and HTTPS. If you get an email from us that you don't expect, go directly to our site to log in. Don't enter your password after clicking a link in an email. * **Enable two-factor verification**: When you enable two-factor authentication, you'll need to provide an additional unique code from your mobile device to complete the login process. This means that even if someone steals your username and password, they won't be able to log in. ## Best Practices * **Unique Tax IDs**: Ensure each merchant has a unique tax identification * **Proper MCC Codes**: Use accurate Merchant Category Codes for compliance * **Location Accuracy**: Verify physical locations match processor records * **Credential Security**: Store merchant credentials securely * **Regular Audits**: Periodically review merchant configurations ## Troubleshooting ### Common Issues * **Duplicate Tax IDs**: Merchants with same Tax ID will be grouped together * **Invalid MCC Codes**: Ensure 4-digit codes are valid for your region * **Processor Mismatch**: Verify processor credentials match your configuration * **Location Errors**: Ensure locations are properly assigned to terminals ### Support For technical issues with merchant setup: * Check the [Resources](/docs/appendix/resources) section * Contact Koard support for processor-specific issues * Review [Apple Best Practices](/docs/appendix/apple-best-practices-and-guidelines) for compliance ## See also * [iOS SDK Installation](/docs/setting-up-the-ios-sdk/installing-the-sdk) - Install and integrate the Koard Merchant SDK into your iOS application * [Tap to Pay Configuration](/docs/setting-up-the-ios-sdk/adding-support-for-tap-to-pay-on-iphone) - Configure Tap to Pay on iPhone functionality in your app * [Webhook Setup](/docs/webhooks/setting-up-webhooks) - Configure webhooks to receive real-time payment event notifications * [Payment Testing](/docs/setting-up-the-ios-sdk/running-payments) - Learn how to process and test payments with the Koard SDK * [Apple Best Practices](/docs/appendix/apple-best-practices-and-guidelines) - Follow Apple's guidelines and best practices for Tap to Pay development # Getting Started with Koard Welcome to Koard's Tap to Pay on iPhone integration guide. This comprehensive documentation will help you integrate Koard's payment solutions into your iOS applications, enabling secure tap-to-pay transactions for your merchants. [](https://www.koard.com/videos/apple-tap-to-pay.mp4) ## What is Koard? Koard is a payment platform that specializes in Tap to Pay on iPhone solutions, helping Payment Service Providers (PSPs) and Independent Software Vendors (ISVs) integrate Apple's tap-to-pay technology into their existing applications. We streamline the complex process of Apple certification and merchant onboarding, allowing you to go live with tap-to-pay payments in under 6 weeks instead of a multi-year process. ## Key Benefits * **Fast Time to Market**: Launch tap-to-pay payments in under 6 weeks * **Apple Partnership**: Bypass L3 certification requirements through our Apple partnership * **Comprehensive Support**: End-to-end guidance from Apple setup to merchant onboarding * **Precompiled SDK**: Easy integration with our .xcframework distribution * **Merchant Management**: Complete portal for merchant configuration and credential management * **Payment Routing**: Flexible integration with multiple payment processors ## Who Can Use Koard? Koard is designed for: * **Payment Service Providers (PSPs)** serving multiple merchants * **Independent Software Vendors (ISVs)** building payment applications * **Large merchants** processing significant transaction volumes * **US and Europe-based enterprises** with existing POS infrastructure **Prerequisites** **Business Requirements** * US or Europe-based enterprise PSP or ISV * Serve multiple merchants or process large transaction volumes * Existing experience with physical POS terminals or similar technology * Ability to track items and orders in your own database * Capacity to update your app for Apple compliance requirements **Technical Requirements** * **iPhone Model**: iPhone XS or later * **iOS Version**: iOS 17.4 or later * **Apple Developer Account**: Organization-level account required * **GitHub Account**: Required for SDK dependency management * **Supported PSP**: Integration with a Koard-supported Payment Service Provider * **Sandbox Apple Account**: Dedicated Sandbox tester signed in on a test iPhone that will be used for certification and QA **Test Device Required**: Plan for a dedicated test iPhone running iOS 17.4 or later with your Sandbox Apple Account signed in. Production Apple IDs cannot be used for Sandbox testing. ## Integration Phases ### Phase 1: Apple Partnership Setup * Establish relationship with Apple through Koard * Bypass L3 certification requirements * Set up Apple Business Register Account * Configure environment and KEK exchange processes ### Phase 2: Merchant Onboarding * Create merchant configurations via Koard's portal * Set up merchant credentials and processor integrations * Upload VAR sheets for supported processors and gateways * Configure payment routing with chosen processors ### Phase 3: SDK Integration * Integrate Koard's precompiled .xcframework * Implement payment flows in your iOS application * Test transactions in certification environment * Prepare for production deployment ### Phase 4: Launch and Scale * Deploy to production environment * Onboard merchants and provision iPhone terminals * Monitor transactions and optimize performance * Scale your tap-to-pay business ## Quick Start Path 1. **Verify Prerequisites**: Confirm you meet all business and technical requirements 2. **Contact Koard**: Reach out to our team to discuss your integration needs 3. **Set Up Apple Partnership**: Work with Koard to establish your Apple relationship 4. **Configure Merchants**: Use our portal to set up your merchant configurations 5. **Integrate SDK**: Follow our technical integration guide 6. **Test and Launch**: Complete testing and go live with tap-to-pay payments ## Next Steps * [Setting up the Merchant](/docs/getting-started-with-koard/setting-up-the-merchant) - Learn how to configure merchant accounts and credentials * [Creating a Sandbox Apple Account](/docs/setting-up-the-ios-sdk/creating-a-sandbox-apple-account) - Configure dedicated testers and devices * [Installing the SDK](/docs/setting-up-the-ios-sdk/installing-the-sdk) - Technical integration guide * [Payment Lifecycle](/docs/payments/payment-lifecycle) - Understand the complete payment flow * [Getting Ready for Production](/docs/setting-up-the-ios-sdk/getting-ready-for-production) - Prepare schemes and API keys for launch ## Support For questions or assistance with your integration: * **Technical Support**: Contact our development team * **Business Inquiries**: Reach out to Behailu at * **PSP Partnerships**: Ask about our updated list of supported Payment Service Providers # Setting up the Merchant via API Automate merchant onboarding with Koard's REST API. This guide walks through creating the merchant account, provisioning a terminal, associating it with a location, and issuing SDK credentials without using the Merchant Management System UI. - **Koard API key** with permission to manage merchant accounts - **Processor configuration IDs** that the merchant should use - **Location identifiers** (either newly created via API or existing records) linked to the merchant - **Dedicated test device** enrolled in the appropriate Apple sandbox for Tap to Pay validation For each request, send `X-Koard-apikey: {API_KEY}` and specify the target environment (`https://api.uat.koard.com` for sandbox or `https://api.koard.com` for production). Use `POST /v2/accounts` to create the merchant record tied to your processor configuration. The payload must include the account `type`, `name`, `description`, and an `address` object. **Request** curl https://api.uat.koard.com/v2/accounts \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "merchant", "name": "Bluebird Coffee Roasters", "description": "Retail coffee shop using Tap to Pay on iPhone", "address": { "street_line1": "123 Market Street", "city": "San Francisco", "state": "CA", "zip": "94105" }, "tax_id": "12-3456789", "mcc": "5812", "available_processor_configs": ["prc_live_payroc_us"] }' **200 Response** { "id": "100200300001", "type": "merchant", "name": "Bluebird Coffee Roasters", "description": "Retail coffee shop using Tap to Pay on iPhone", "status": "active", "tax_id": "12-3456789", "mcc": "5812", "address": { "street_line1": "123 Market Street", "city": "San Francisco", "state": "CA", "zip": "94105" }, "available_processor_configs": ["prc_live_payroc_us"], "created_at": "2024-10-15T18:21:04.123Z" } Store the returned `id`—you will use it when you create the terminal and credentials. In the examples that follow we will reference the ID `100200300001`. Provision a terminal with `POST /v2/terminals`. Provide the merchant's processor details using the processor-specific VAR sheet format. **Request** curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_live_payroc_us", "terminal_name": "Front Counter iPhone", "var_sheet": { "acquirerBin": "123456", "merchantNumber": "123456789012", "storeNumber": "0001", "terminalNumber": "0001", "merchantCategoryCode": "5812", "merchantName": "Bluebird Coffee Roasters", "merchantLocation": "San Francisco", "merchantState": "CA", "cityCode": "94105", "acceptorStreetAddress": "123 Market Street", "industryCode": "R", "acceptorPhone": "4155551234", "acceptorCustomerServicePhone": "4155551234" } }' **201 Response** { "terminal_id": "500600700001", "name": "Front Counter iPhone", "account_id": "100200300001", "mid": "123456789012", "tid": "0001", "processor_config_id": "prc_live_payroc_us", "status": "active", "created_at": "2024-10-15T18:21:05.015Z", "var_sheet": { "acquirerBin": "123456", "merchantNumber": "123456789012", "storeNumber": "0001", "terminalNumber": "0001", "merchantCategoryCode": "5812", "merchantName": "Bluebird Coffee Roasters", "merchantLocation": "San Francisco", "merchantState": "CA", "cityCode": "94105", "acceptorStreetAddress": "123 Market Street", "industryCode": "R", "acceptorPhone": "4155551234", "acceptorCustomerServicePhone": "4155551234" } } The response contains the terminal configuration with an auto-generated `terminal_id`. See the API reference for the full `TSYSVarSheet` schema with all required and optional fields. Update an existing location with `PUT /v1/locations/{location_id}` so that the location references the new terminal ID. Include any additional fields you need to change (for example, contact info or status). **Request** curl https://api.uat.koard.com/v1/locations/300400500001 \ -X PUT \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Bluebird Coffee HQ", "terminal_id": "500600700001", "processor_config_id": "prc_live_payroc_us", "status": "active" }' **200 Response** { "id": "300400500001", "name": "Bluebird Coffee HQ", "account_id": "100200300001", "terminal_id": "500600700001", "processor_config_id": "prc_live_payroc_us", "status": "active", "updated_at": "2024-10-15T18:21:06.287Z" } If you do not yet have a location record, create one first with `POST /v1/locations`, then repeat this update call to attach the terminal. Generate the merchant's SDK login credentials using `POST /v1/accounts/credentials`. You may supply a custom `code` and `pin`, or let Koard create randomized values by omitting them. **Request** curl https://api.uat.koard.com/v1/accounts/credentials \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001" }' **200 Response** { "id": "900100200003", "account_id": "100200300001", "code": "483920123456", "pin": "739051", "is_active": true, "created_at": "2024-10-15T18:21:07.431Z" } The response returns the `code` and `pin` only once. Store them securely and deliver them to your merchant through a trusted channel so they can authenticate with the Koard SDK. ## Next steps - Verify the credentials by logging into the Koard iOS SDK test harness. - Run a test payment in the UAT environment to confirm the terminal and location configuration. - When ready for production, repeat the flow against `https://api.koard.com` with live processor credentials. # Boarding a Merchant with Fiserv You can board a Fiserv merchant either through the Koard Merchant Management System (MMS) UI or programmatically via the API. Koard targets Fiserv's **UMF Rapid Connect** interface (spec `UMF_RMY019_2026.02.16`, v15.04.4) over the Datawire transport. Per the UMF spec, a Fiserv merchant is identified at the integrator boundary by three merchant-supplied values — **MID**, **TID**, **MCC** — plus the **TPPID** that Koard owns and sets once per environment. Everything else needed to run an authorization is configured by Koard (TPPID, terminal-capability constants); a Datawire ID (DID) is used **only when Datawire is enabled** on the terminal. ## Fiserv Flavors Fiserv Rapid Connect fronts several acquiring platforms. Koard selects the right one per merchant via the **processor config** — you board with the same VAR-sheet shape shown on this page regardless of flavor; only the `processor_config_id` and the Fiserv-assigned `GroupID` differ. | Flavor | Front-end | Group ID | Capture / settlement | |---|---|---|---| | **Nashville** (Classic) | Nashville (Envoy) | `10001` | Host capture | | **Nashville North** | Nashville front-end → North back-end | `10001` | Terminal capture (North PTS) — pass `settlement_mid` | | **Cardnet North** | Cardnet / North | `30001` | Terminal capture (North PTS) — pass `settlement_mid` | | **Omaha** | Omaha (FDR) | `40001` | Hybrid-host capture | The `GroupID` in the VAR packet tells you which front-end the merchant sits on. Use the `processor_config_id` for that flavor; everything else on this page is identical across flavors. ## Before You Start Fiserv provisions the merchant on their side; Koard never makes a "create merchant" call. Once a merchant is set up on Fiserv's platform, your VAR sheet packet contains: | Provided by Fiserv | UMF tag | Format | What it is | |---|---|---|---| | **Merchant ID** (MID) | `MerchID` | `an` ..16 | Fiserv-assigned merchant identifier (UMF §3.1.11 — "A unique ID used to identify the Merchant. The merchant must use the value assigned by Fiserv.") | | **Terminal ID** (TID) | `TermID` | `an` ..8 | Per-terminal identifier (UMF §3.1.10 — "A unique ID assigned by Fiserv to identify a terminal."). In certification, all transactions must run on TID `00000001`. | | **Merchant Category Code** | `MerchCatCode` | `N` 4 | ISO 18245 4-digit MCC (UMF §3.1.13) | | **Group ID** (GID) | `GroupID` | `an` 5..13 | Assigned by Fiserv to identify the individual merchant or group of merchants (UMF §3.1.23). **Spec defines no default**. | | **Datawire ID** (DID) | *(Datawire transport, not a UMF body field)* | — | Used **only when the terminal has Datawire enabled** (`var_sheet.datawire_enabled: true`, the default). With Datawire enabled: paste the Fiserv-issued DID, or omit it and Koard provisions one at boarding. With `datawire_enabled: false`, no DID is used — sending one is rejected. | The UMF `TPPID` field (Rapid Connect ID assigned by Fiserv for a specific version of vendor/merchant software, UMF §3.1.9) is **not** on the merchant VAR sheet — it identifies the **integrator's certified SDK build**, not the merchant. Koard owns it and sends a fixed value (`RMY019` for the current Koard build) on every transaction. ## Via the MMS After creating the merchant account, click **New Terminal** and select Fiserv as the processor. You'll be presented with the **Fiserv VAR Sheet Information** form. Fill in all required fields (marked with `*`) using the values from the merchant's Fiserv VAR sheet: | MMS Label | Required | Notes | |---|---|---| | Merchant ID | Yes | Fiserv-assigned MID, up to 16 alphanumeric | | Terminal ID | Yes | Up to 8 alphanumeric. Zero-padded to 8 chars when used as Datawire `AuthKey2`. | | Merchant Category Code | Yes | 4-digit MCC. UMF treats it as `O\|C` (optional in request — the boarded MID record carries the default), but Koard requires it on the API for surcharge and reporting. | | Group ID | Yes | 5-13 alphanumeric. No spec default; supply the value Fiserv assigned in your VAR packet. | | Datawire Enabled | No | Whether the terminal routes over Datawire. Default on. When off, no Datawire ID is used. | | Datawire ID | No | Only when Datawire is enabled. Paste from the VAR packet if provided; if left blank, Koard provisions one at boarding. | Once saved, assign the terminal to a location and generate merchant credentials as usual. ## Via the API Send `X-Koard-apikey: {API_KEY}` with every request. Use `https://api.uat.koard.com` for sandbox and `https://api.koard.com` for production. ### Create Terminal — `POST /v2/terminals` **Request body** | Field | Required | Description | |---|---|---| | `account_id` | Yes | Merchant account ID | | `processor_config_id` | Yes | Fiserv processor configuration ID | | `terminal_name` | Yes | Display name for the terminal | | `terminal_description` | No | Optional description | | `mid` | Yes | Fiserv `MerchID` (top-level on the request — not in `var_sheet`) | | `tid` | Yes | Fiserv `TermID` (top-level) | | `mcc` | Yes | 4-digit MCC (top-level) | | `var_sheet.group_id` | Yes | Fiserv `GroupID` — assigned by Fiserv, no spec default | | `var_sheet.datawire_enabled` | No | Bool, default `true`. `true` = route over Datawire (a DID is used). `false` = no DID (sending `did` alongside is rejected as contradictory). | | `var_sheet.did` | No | Datawire ID. Only when `datawire_enabled` is `true`: send an already-issued DID, or omit it and Koard provisions one at boarding. | **Required vs conditional fields** Every Fiserv board — regardless of flavor — **requires** the top-level `account_id`, `processor_config_id`, `terminal_name`, `mid`, `tid`, `mcc`, plus the `var_sheet` **address** (`merchant_street_address`, `merchant_city`, `merchant_state`, `merchant_postal_code`), `country_code`, and `industry`. The rest is **conditional on the flavor**: | `var_sheet` field | Conditional? | When | |---|---|---| | `group_id` | Resolved from the processor config | Don't send it unless you're overriding the config's Group ID. | | `settlement_mid` | Conditional | North-settling flavors (**Nashville North**, **Cardnet North**). Optional — defaults to a copy of `mid` when omitted. | | `equipment` | Optional | POS Solution Name (enum) — boarding metadata only; the wire identifies the build via `TPPID`. | | `datawire_enabled` | Optional | Default `true`. `false` ⇒ no Datawire ID is used (and sending `did` is rejected as contradictory). | | `did` | Optional | Only when `datawire_enabled` is `true`: supply an issued DID, or omit it to have Koard provision one at boarding. | **Example — DID already issued** curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_live_fiserv_us", "terminal_name": "Front Counter iPhone", "mid": "RCTST1000118756", "tid": "00000003", "mcc": "5812", "var_sheet": { "group_id": "40001", "did": "00067045767186571068" } }' **Example — DID provisioned at boarding (Datawire enabled)** With `datawire_enabled` true (the default), omit `did` and Koard provisions a Datawire ID at boarding and persists it on the terminal. curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_live_fiserv_us", "terminal_name": "Front Counter iPhone", "mid": "RCTST1000118756", "tid": "00000099", "mcc": "5812", "var_sheet": { "group_id": "40001" } }' The response includes the persisted `var_sheet.did` so you can confirm registration succeeded. ### Update Terminal — `PUT /v2/terminals/{terminal_id}` To correct or update VAR sheet fields after creation, send a `PUT` with a `var_sheet` object containing only the fields you want to change. curl https://api.uat.koard.com/v2/terminals/500600700001 \ -X PUT \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "var_sheet": { "did": "00067045767186571068" } }' ## Batch Management Fiserv is **host capture** today — the host manages the batch and settles on its cutoff. There is no merchant-driven batch open/close model, and the MMS batch panel is read-only for Fiserv terminals. Merchant-driven batch management (see [Automated Batch Scheduling](/docs/guides/batch-settlements/details/automated-batch-scheduling)) is on the roadmap. Until then, leave batch management disabled in the MMS. ## Gotchas - **Omaha settlement** — for merchants on Fiserv DD / sponsor-bank funding via FDC, confirm the Omaha flavor is provisioned before boarding. - **`tid` is zero-padded to 8 characters as `AuthKey2`** in the Datawire envelope. Don't pad it yourself in the VAR packet — supply the raw value (Koard pads). - **`TranFee` (Merchant Surcharge) triggers full declines without enrollment.** The UMF spec is explicit: "Failure to do this will cause all credit card transactions with the surcharge amount field populated to be declined." Don't enable surcharge until Fiserv confirms enrollment. ## Troubleshooting **`400 Bad Request` on create** - Confirm `mid`, `tid`, and `mcc` are at the top level of the request, NOT inside `var_sheet`. - Confirm `group_id` is inside `var_sheet` and is 5-13 alphanumeric chars. - Verify `processor_config_id` is a valid Fiserv config ID for your environment. **Transactions erroring with `INVALID MERCHANT`** - The acquirer MID is platform-level. If your merchant requires a different acquirer relationship, contact Koard support — per-merchant acquirer routing is a future enhancement, not configurable on the VAR sheet today. **Need Omaha settlement** - Confirm the Omaha flavor is provisioned for the merchant before boarding. # Fiserv Nashville North Nashville North uses the **Nashville front-end** settling to the **North (PTS) back-end** — i.e. terminal capture. Board it like any Fiserv terminal (see the [Fiserv Overview](/boarding-a-merchant/fiserv/fiserv-overview) for the shared shape, SRS, and UMF coverage) using the **Nashville North** processor config, and supply the North settlement MID. ## Boarding spec | Field | Value / format | Notes | |---|---|---| | **Group ID** | `10001` | Nashville **front-end** — same Group ID as Nashville Classic. The difference is the back-end/capture, not the Group ID. | | **Merchant ID** (MID) | 7 digits (`MerchID`) | Nashville front-end MID. Top-level `mid`. | | **Terminal ID** (TID) | 7 digits (`TermID`) | Top-level `tid`. | | **Settlement MID** | 12 digits | The North Settlement MID. VAR-sheet `settlement_mid` — **optional on the request; defaults to a copy of `mid` if omitted**. | | **MCC** | 4-digit | Top-level `mcc`. | | **Equipment** (POS Solution) | `CreditCallLTDGTWRC` or `CRDCallResellerRCSS` | North terminal-capture solutions. VAR-sheet `equipment`. | ## Request shape curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_fiserv_nashville_north", "terminal_name": "Register 1", "mid": "9446055", "tid": "9259755", "mcc": "5045", "var_sheet": { "group_id": "10001", "settlement_mid": "445197000368", "industry": "retail_qsr_grocery", "equipment": "CreditCallLTDGTWRC", "merchant_street_address": "102 Woodmont Blvd Ste 125", "merchant_city": "Nashville", "merchant_state": "TN", "merchant_postal_code": "37205", "country_code": "840" } }' ## Capture & settlement **Terminal capture (North PTS).** The gateway holds transactions and submits the batch at cutoff. Boarding must match the host's configured capture mode — if you board terminal capture but the host has the MID as host capture (or vice-versa), settlement breaks. ## Gotchas - **Same Group ID as Nashville Classic (`10001`).** Nashville North is *not* a different Group ID — it's the Nashville front-end paired with the North back-end. The flavor is chosen by the processor config, not the Group ID. - **`settlement_mid` is the 12-digit North Settlement MID.** Omit it and Koard copies `mid`; supply it explicitly when the North settlement MID differs. - **Capture-mode mismatch = broken settlement.** Confirm the host has the MID set to terminal capture before going live. - **`tid` is zero-padded to 8** as the Datawire `AuthKey2`. # Fiserv Omaha Omaha (FDR — First Data Resources) is a **hybrid-host** front-end: First Data holds the transaction detail (host-style) but the gateway initiates settlement so both sides reconcile. Board it like any Fiserv terminal (see the [Fiserv Overview](/boarding-a-merchant/fiserv/fiserv-overview) for the shared shape, SRS, and UMF coverage) using the **Omaha** processor config. ## Boarding spec | Field | Value / format | Notes | |---|---|---| | **Group ID** | `40001` | Omaha front-end. | | **Merchant ID** (MID) | 7 digits (`MerchID`) | Omaha front-end MID. Top-level `mid`. | | **Terminal ID** (TID) | 7 digits (`TermID`) | The Bank TID. Top-level `tid`. | | **Settlement MID** | — | Not used — Omaha is hybrid-host, not North terminal capture. | | **MCC** | 4-digit | Top-level `mcc`. | | **Equipment** (POS Solution) | `CREDITCALLHCRC` | VAR-sheet `equipment`. | ## Request shape curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_fiserv_omaha", "terminal_name": "Register 1", "mid": "9446123", "tid": "9259801", "mcc": "5045", "var_sheet": { "group_id": "40001", "industry": "retail_qsr_grocery", "equipment": "CREDITCALLHCRC", "merchant_street_address": "102 Woodmont Blvd Ste 125", "merchant_city": "Nashville", "merchant_state": "TN", "merchant_postal_code": "37205", "country_code": "840" } }' ## Capture & settlement **Hybrid-host capture.** First Data holds the detail, but the gateway initiates settlement by sending batch totals so both sides reconcile. As with the other flavors, the MMS batch panel is read-only. ## Gotchas - **Group ID `40001`.** This is the Omaha front-end. (Historically a `40001` default has been used as a sandbox/test group elsewhere — always use the value on the merchant's VAR packet.) - **No `settlement_mid`.** Omaha is hybrid-host, not North terminal capture — don't send a settlement MID. - **`tid` is zero-padded to 8** as the Datawire `AuthKey2`; supply the raw 7-digit value. - **Hybrid-host means settlement is initiated by the gateway** — funding is FDC/back-office driven; confirm the merchant's Omaha funding relationship before boarding. # Boarding a Merchant with Worldpay You can board a Worldpay merchant either through the Koard Merchant Management System (MMS) UI or programmatically via the API. Koard targets Worldpay's **610 interface** (TPS / Vantage) for mPOS EMV COTS. Worldpay assigns the merchant a **Merchant ID** and **Terminal ID** out-of-band. Those two values plus `mcc` are all Koard needs from the merchant. The other 610 credentials (User ID, Password, Network Routing, Bank ID) are configured by Koard once per environment. ## Before You Start Worldpay provisions merchants on their side; Koard never makes a "create merchant" call. The packet you get from Worldpay for a new merchant contains: | Provided by Worldpay | What it is | |---|---| | **Merchant ID** (MID) | up to 12 digits — Worldpay-assigned merchant identifier (610 §3 Field 42 "Card Acceptor ID Code") | | **Terminal ID** (TID) | 3 digits — lane/device identifier (610 §3 Field 41) | | **Merchant Category Code** | 4-digit MCC — held in the Worldpay merchant profile, not on the wire | That's it from the merchant. The other 610 fields you may have heard about (`userid`, `password`, `network_routing`, `bank_id`) are owned by Koard and configured once per environment — never paste them into a `POST /v2/terminals` body. ## Via the MMS After creating the merchant account, click **New Terminal** and select Worldpay as the processor. | MMS Label | Required | Notes | |---|---|---| | Merchant ID | Yes | Worldpay-assigned, up to 12 digits | | Terminal ID | Yes | Worldpay-assigned, 3 digits | | Merchant Category Code | Yes | 4-digit MCC | | Currency | No | Defaults to `USD`. The 610 message has no wire currency field — currency is inferred from the MID profile on Worldpay's side. | Once saved, assign the terminal to a location and generate merchant credentials as usual. ## Via the API Send `X-Koard-apikey: {API_KEY}` with every request. Use `https://api.uat.koard.com` for sandbox and `https://api.koard.com` for production. ### Create Terminal — `POST /v2/terminals` **Request body** | Field | Required | Description | |---|---|---| | `account_id` | Yes | Merchant account ID | | `processor_config_id` | Yes | Worldpay processor configuration ID | | `terminal_name` | Yes | Display name for the terminal | | `terminal_description` | No | Optional description | | `mid` | Yes | Worldpay Merchant ID — up to 12 digits | | `tid` | Yes | Worldpay Terminal ID — 3 digits | | `mcc` | Yes | 4-digit MCC | | `var_sheet` | No | Currently no required merchant fields — reserved for future extensions. | **Example** curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_live_worldpay_us", "terminal_name": "Front Counter iPhone", "mid": "000038462929", "tid": "001", "mcc": "5812" }' ### Update Terminal — `PUT /v2/terminals/{terminal_id}` Send a `PUT` with only the fields you want to change. ## Batch & Settlement The 610 spec describes Worldpay as "host capture" with "host settlement requirements" (610 §1.1.1). The integrator can drive batch release explicitly: | Operation | Message | Trigger | |---|---|---| | Batch Inquiry | MTI `0500` / PC `920000` | Read current batch counts and amounts (610 §2.5) | | Batch Release | MTI `0500` / PC `930000` | Close the current batch, "initiates the closing of the current batch by settling all transactions" (610 §2.5 line 33922) | The spec is **silent on whether Worldpay can be configured to auto-release** — that would be a per-merchant boarding option not documented in the 610 reference. Koard exposes a `batch_schedule` on the terminal to drive Batch Release on a schedule; see [Automated Batch Scheduling](/docs/guides/batch-settlements/details/automated-batch-scheduling). ## Currency & Country The 610 base message has **no wire currency field**. Field 04 (Amount) is `n9` cents and the spec layers on ISO 8583's "amount expressed in U.S. Dollars" (ISO 8583 §F004). Multi-currency on a USD-boarded MID is not supported by the 610 message set. The 610 base message has **no wire country field** either. Country is inferred from the MID profile. For EMV transactions only, the terminal country surfaces via EMV TLV tag `9F1A` inside G035 chip data; Koard builds this internally. `currency` exposed at the API level defaults to `USD` and is reserved for future multi-currency support. ## Reading the Response A Worldpay 610 response comes back with a 21-byte TPS header prefix, the MTI, then a 2-character **Bitmap Type** that tells you whether the transaction was approved or declined: | Bitmap Type | Meaning | What to look at next | |---|---|---| | `90` / `91` | Approved | `F65` auth code (offset 30, 6 chars); `F37` retrieval reference (offset 22, 8 chars); `F120.3` 4-char card brand mnemonic (`VI` / `MC` / `DI` / `AX`) | | `99` | Declined / error | `F123.1` 20-char error text (offset 44); `F123.2` 3-char response code (offset 64) | The full canonical response-code list is in **Appendix A** of the Worldpay 610 Interface Reference Guide. ## Express vs 610 vs RAFT Worldpay offers three integration surfaces. Koard targets **610** for mPOS EMV COTS because it's the only interface in the spec that exposes device classes `6 — SoftPOS Device` and `9 — MPOS` explicitly in fields F25 and F107. | Interface | What it is | Koard support | |---|---|---| | **610** | Host-capture controller message set, ISO 8583-derived flat positional format | **Production today** | | **Express** | XML over HTTPS (SOAP is deprecated per the spec). Requires `AccountID + AccountToken + AcceptorID + TerminalID` instead of 610's credential set. | Not currently used | | **RAFT** | Worldpay's internal authorization platform — referenced in 610 messages (e.g. `R997 RAFT=…`) but not a separate integrator interface in the supplied spec set | Internal — not a customer-facing option | ## Gotchas - **`mid` and `tid` are top-level fields** on the request — not inside `var_sheet`. Matches every other processor on Koard. - **`userid`, `password`, `network_routing`, `bank_id` are configured by Koard once per environment.** Don't ask the merchant for these. - **The 610 wire has no currency or country field.** Both are inferred from the MID. If the merchant needs multi-currency, contact Koard support — it requires Worldpay-side reconfiguration. - **MCC is not on the 610 wire either** — it's held in the Worldpay merchant profile. Koard still requires it on the API for surcharge calculation and reporting. - **F22 / F25 / F107 are device-class constants** (SoftPOS), set by Koard once per device class. Not configurable per merchant. ## Troubleshooting **`400 Bad Request` on create** - Verify `mid`, `tid`, and `mcc` are at the top level. - Confirm `processor_config_id` is a valid Worldpay config ID for your environment. **Transactions erroring with `FORMAT ERROR` (response code `730`)** - Usually an EMV TLV issue — Worldpay's whitelist of allowed EMV tags is narrow. Capture the request body and forward to Koard support. **Transactions erroring with `CALL OPER` (response code `701`)** - Card-side decline. Have the merchant ask the cardholder to call their bank. **Need multi-currency** - Not supported on the 610 wire as written. Requires Worldpay-side reconfiguration of the MID; contact Koard support. **Need explicit batch close** - Use `batch_schedule` on the terminal to schedule `MTI 0500 / PC 930000` Batch Release on a recurring cadence, or trigger manually via the batch API. # Getting Ready for Production Prepare your Koard integration to move beyond the Sandbox by configuring Xcode schemes, build configurations, and API keys for both development and production. **What You Learn** In this guide, you'll learn how to: * Create separate build configurations and schemes in Xcode * Inject environment-specific Koard API keys and endpoints * Automate build-time switching between Sandbox and production settings * Validate your production readiness checklist before submitting to the App Store **Prerequisites** Before you begin, ensure you have: * **Koard API keys** for both Sandbox and production * **Info.plist or `.xcconfig` access** to store environment values * **Xcode project admin access** to edit schemes and build settings * **Dedicated test devices** with Sandbox Apple Accounts for final verification ## 1. Duplicate Build Configurations 1. In Xcode, select your project in the Project Navigator. 2. Under **PROJECT → Info**, duplicate your existing `Debug` and `Release` configurations. Name the copies `Debug-Prod` and `Release-Prod`. 3. Point the production build configurations to `.xcconfig` files (optional but recommended) such as `Koard-Dev.xcconfig` and `Koard-Prod.xcconfig`. ```text Koard-Dev.xcconfig KOARD_API_BASE_URL = https://sandbox-api.koard.com KOARD_API_KEY = ${KoardSandboxAPIKey} Koard-Prod.xcconfig KOARD_API_BASE_URL = https://api.koard.com KOARD_API_KEY = ${KoardProductionAPIKey} ``` Store sensitive values in your CI/CD environment or use Xcode build setting macros rather than hardcoding secrets in source control. ## 2. Customize Schemes for Each Environment Following Apple’s [Customizing the Build Schemes](https://developer.apple.com/documentation/xcode/customizing-the-build-schemes-for-a-project) guidance, create two top-level schemes: * `KoardApp-Dev` mapped to the `Debug`/`Release` configurations * `KoardApp-Prod` mapped to the `Debug-Prod`/`Release-Prod` configurations 1. Open **Product → Scheme → Manage Schemes**. 2. Duplicate your primary scheme and rename it `KoardApp-Prod`. 3. Assign the correct build configuration for each action (Build, Run, Archive, etc.). 4. Uncheck **Shared** while iterating, then re-enable sharing once the configuration is stable so teammates receive the new scheme in source control. **Tip**: Keep the production scheme archived with **Release-Prod** to ensure App Store submissions always use production endpoints and credentials. ## 3. Switch API Keys at Build Time Expose the Koard API key and environment to your app using Info.plist substitutions or Swift build flags. ### Option A: Info.plist placeholders 1. Add keys like `KOARD_API_BASE_URL` and `KOARD_API_KEY` to your Info.plist. 2. Reference them using `${KOARD_API_BASE_URL}` placeholders. 3. Resolve them in code at runtime: ```swift struct KoardAppConfig { static let baseURL: URL = { guard let urlString = Bundle.main.object(forInfoDictionaryKey: "KOARD_API_BASE_URL") as? String, let url = URL(string: urlString) else { fatalError("Missing or invalid KOARD_API_BASE_URL") } return url }() static let apiKey: String = { guard let key = Bundle.main.object(forInfoDictionaryKey: "KOARD_API_KEY") as? String else { fatalError("Missing KOARD_API_KEY") } return key }() } ``` ### Option B: Swift compilation conditions 1. Add custom flags in **Build Settings → Swift Compiler - Custom Flags** (e.g., `-DKOARD_ENV_SANDBOX` and `-DKOARD_ENV_PRODUCTION`). 2. Use those flags to branch logic: ```swift #if KOARD_ENV_PRODUCTION let environment: KoardEnvironment = .production #else let environment: KoardEnvironment = .uat #endif let options = KoardOptions( environment: environment, loggingLevel: environment == .production ? .error : .debug ) KoardMerchantSDK.shared.initialize(options: options, apiKey: apiKey) ``` Use this approach when you prefer compile-time enforcement of environment differences, such as disabling test-specific UI in production builds. **Environment and logging**: `KoardOptions.environment` accepts `.uat`, `.production`, or `.custom(String)`. Lower the `loggingLevel` for production builds (for example `.error` or `.none`) so debug logs don't ship to release users. ## 4. Verify Device and Account Setup * Install the **Dev** build on a dedicated test iPhone that remains signed in with your Sandbox Apple Account. * Install the **Prod** build on a separate device or reset the test device before signing in with the production Apple ID. * Run smoke tests for Tap to Pay, refunds, reversals, and settlement cutovers in each environment. **Deploy Deliberately**: Never archive or submit to App Store Connect using a Sandbox scheme. Require production code reviews to confirm the `KoardApp-Prod` scheme was used for the final archive. **Authentication and Keychain**: Login is session-token only — the SDK persists the session token and never stores the merchant code, PIN, or alias. `logout()` clears only the SDK's own Keychain entries (it no longer performs a blanket delete of the host app's Keychain items), so signing out will not disturb other credentials your app stores. ## 5. Pre-launch Checklist * [ ] Xcode schemes map to the correct build configurations. * [ ] Production API keys and endpoints live outside of source control. * [ ] Secrets are injected via CI/CD, `.xcconfig`, or secure build settings. * [ ] Sandbox and production devices are authenticated with the appropriate Apple IDs. * [ ] A rollback plan is documented in case production rollout needs to be paused. ## Next Steps * [Creating a Sandbox Apple Account](/docs/setting-up-the-ios-sdk/creating-a-sandbox-apple-account) to keep development builds isolated. * [Running Payments](/docs/setting-up-the-ios-sdk/running-payments) to validate production behavior before launch. * Coordinate with your Koard partner manager to schedule final Apple certification checks. # Adding Support for Tap to Pay on iPhone Enable Tap to Pay on iPhone to allow merchants to accept contactless payments directly on their iPhone without additional hardware. If you're ready to start developing, see our [iOS SDK installation guide](/docs/setting-up-the-ios-sdk/installing-the-sdk). [Get started with Koard](/docs/getting-started-with-koard/introduction) **What you learn** In this guide, you'll learn: * How to request the Tap to Pay entitlement from Apple * How to configure your Xcode project for Tap to Pay * How to set up testing in developer mode * How to distribute your app for testing and production **Prerequisites** Before you begin, ensure you have: * **iOS 17.4 or later** (minimum required version) * **iPhone XS or later** (supported hardware) * **Apple Developer Account** (organization-level account required) * **Koard iOS SDK** (installed in your project) * **Valid merchant account** (configured in Koard MMS) * **Sandbox Apple Account signed in on test device** (dedicated iPhone in Developer Mode) **Use a Dedicated Test iPhone**: Keep your Sandbox Apple Account signed in on a separate test device. Production Apple IDs cannot complete Sandbox Tap to Pay transactions. ## Enable the Tap to Pay Entitlement Enabling the Tap to Pay Entitlement is a critical step that is handled by Apple. Typically, you will need to request the entitlement from Apple through their developer portal. ### Requesting Tap to Pay Entitlement from Apple To enable Tap to Pay on iPhone, follow these steps: 1. **Log in to your Apple Developer account** as the account holder 2. **Navigate to Certificates, Identifiers & Profiles** 3. **Select Tap to Pay on iPhone Entitlement** and submit a request 4. **Wait for approval** - Apple will add the entitlement under Managed Capabilities **Processing Time**: The process to get approval from Apple typically takes **one or two business days**. You will need to start with the development certificate to get access to the Apple CERT environment. ## Configure Your Xcode Project Once you have the entitlement, configure your Xcode project: ### 1. Enable Tap to Pay Capability 1. **Sign in to your Apple Developer Account** 2. **Create an App ID** (if you don't have one already) 3. **Go to Certificates, Identifiers & Profiles > Identifiers** 4. **Select your app and go to Additional Capabilities** 5. **Enable Tap to Pay on iPhone and save** ### 2. Create a Provisioning Profile 1. **Open Xcode and select your project** 2. **Navigate to Signing & Capabilities** 3. **Under Provisioning Profile, select Download Profile** 4. **Choose the new provisioning profile** ### 3. Add Entitlements File 1. **In Xcode, select your project in Project Navigator** 2. **Create a new Property List file** (File > New > File > Resource) 3. **Name the file `[ProjectName].entitlements`** 4. **Open Build Settings and locate Code Signing Entitlements** 5. **Set its value to the path of the .entitlements file** Open the `.entitlements` file and add the following key-value pair: ```xml com.apple.developer.proximity-reader.payment.acceptance ``` **Additional Resources**: More details on enabling the tap to pay entitlement can be found in [Apple's Developer Documentation](https://developer.apple.com/documentation/passkit/tap_to_pay_on_iphone). ## Testing in Developer Mode To test the iOS app in developer mode, follow these additional steps: ### Enable Developer Mode 1. **On the test iPhone, go to Settings > Privacy & Security** 2. **Enable Developer Mode** ### Use a Sandbox Apple Account The sandbox account must be: * **Freshly created in App Store Connect** * **Signed into iCloud** * **Linked to the test iPhone** **Important**: Existing accounts attempting to test an App with Tap to Pay in cert mode will be blocked from creating a card reader session due to Apple's security policies. ### Register a Test Device 1. **Retrieve the UDID of the test device** 2. **Add the UDID to Allowed Devices in the Apple Developer account** 3. **Update the Provisioning Profile to include the allowed devices** **Device Management**: Any new test device will need to have the UDID uploaded and a **NEW provisioning profile** will need to be added anytime new devices are added. The app will then need a new archive file that will be shared with the new device. ### Distribute via .ipa File 1. **Create an .ipa build file in Xcode** 2. **Share the build with your internal team for testing** **Testing Recommendation**: It is highly recommended that you have a **secondary iPhone or higher** to test transactions. Otherwise, developer mode means that engineers will have to sign into a test account onto their own devices to test the mPOS app with Tap to Pay. This is a hard limitation set by Apple and has no workaround at the moment. ### TestFlight and App Store Distribution **TestFlight beta testing** and **App Store submissions** require a separate entitlement that allows distribution. If you've already completed your testing with the non-distribution entitlement, respond to the original email and re-request the Tap to Pay on iPhone Entitlement. ## Testing Environment Running a transaction in the cert environment will route payments to all the processor and card brands test environment. Transactions sent through this setup will **NOT authorize a real transaction** but will be production-like in terms of workflow. ### Test Cards Use these test cards to verify your integration: * **Test Card**: `4242 4242 4242 4242` * **Expiry**: Any future date * **CVV**: Any 3 digits ## Production Deployment Before going live: 1. **Complete merchant verification** in Koard MMS 2. **Switch to production environment** in your app configuration 3. **Test with real payment methods** (in a controlled environment) 4. **Submit for App Store review** with the distribution entitlement 5. **Review scheme setup** in [Getting Ready for Production](/docs/setting-up-the-ios-sdk/getting-ready-for-production) to ensure the correct API keys ship with your archive. ## Card Reader Lifecycle in the SDK Once the entitlement and provisioning are in place, the SDK manages the Apple ProximityReader card reader. After authenticating the merchant, link the account and prepare the reader before taking payments. ```swift import KoardSDK // 1. Check whether the merchant account is already linked to Apple Tap to Pay let isLinked = try await KoardMerchantSDK.shared.isAccountLinked() // 2. Link the account if needed (this requires user interaction) if !isLinked { // Synchronous variant try KoardMerchantSDK.shared.linkAccount() // Or the async variant (recommended with Swift Concurrency) // try await KoardMerchantSDK.shared.linkAccountAsync() } // 3. Prepare the reader for accepting payments try await KoardMerchantSDK.shared.prepare() ``` ### Monitor Reader Events and Status Observe live reader events with the `readerEvents` async stream, and check `status` to see whether the reader is ready: ```swift Task { for await event in KoardMerchantSDK.shared.readerEvents { print("Reader event: \(event.description)") } } // Current reader status let status = KoardMerchantSDK.shared.status ``` ### Present the Tap to Pay Tutorial On iOS 18 and later, you can present Apple's built-in "How to Tap" tutorial from a visible view controller: ```swift if #available(iOS 18.0, *) { try KoardMerchantSDK.shared.presentTutorial(from: viewController) } ``` **Reader operations are serialized**: The SDK serializes ProximityReader operations (prepare, linkAccount, isAccountLinked, and reads), so a sale started while `prepare()` is still running waits for readiness instead of failing with a "reader busy" error. If you switch the active location, the reader re-prepares for the new location before the next charge. **Handle cancellation**: When the customer cancels at the Apple Tap to Pay sheet, the sale, pre-auth, and card-present refund flows throw `KoardMerchantSDKError.TTPPaymentFailed(.canceled)`. Treat this as a benign "canceled" outcome rather than a failure. ## Troubleshooting ### Common Issues * **Entitlement Not Found**: Ensure you've requested and received approval from Apple * **Provisioning Profile Issues**: Make sure your provisioning profile includes the Tap to Pay capability * **Device Registration**: Verify test devices are properly registered in your Apple Developer account * **Sandbox Account Issues**: Use a fresh Apple ID created specifically for testing ### Support For technical issues with Tap to Pay integration: * Check the [Apple Best Practices](/docs/appendix/apple-best-practices-and-guidelines) guide * Review [Payment Configurations](/docs/appendix/payment-configurations) for advanced settings * Contact Koard support for SDK-specific issues ## See also This wraps up the Tap to Pay setup. See the links below for next steps in your integration: * [Creating a Sandbox Apple Account](/docs/setting-up-the-ios-sdk/creating-a-sandbox-apple-account) - Set up test identities * [Installing the SDK](/docs/setting-up-the-ios-sdk/installing-the-sdk) - Complete SDK integration guide * [Running Payments](/docs/setting-up-the-ios-sdk/running-payments) - Implement payment processing * [Payment Lifecycle](/docs/payments/payment-lifecycle) - Understand the complete payment flow * [Getting Ready for Production](/docs/setting-up-the-ios-sdk/getting-ready-for-production) - Configure schemes for launch * [Apple Best Practices](/docs/appendix/apple-best-practices-and-guidelines) - Follow Apple's guidelines # Creating a Sandbox Apple Account Set up an Apple Sandbox tester account so you can exercise Koard payment flows safely without charging real cards. **What You Learn** In this guide, you'll learn how to: * Create and manage Sandbox tester accounts in App Store Connect * Enable Developer Mode so your test devices can run Sandbox builds * Sign in to a dedicated test iPhone with the Sandbox Apple ID * Reset test data between sessions **Prerequisites** Before you begin, make sure you have: * **Apple Developer role access** (_Account Holder_, _Admin_, _App Manager_, or _Developer_) in App Store Connect * **Unique email addresses** for every Sandbox tester you plan to create * **A dedicated test iPhone** running iOS 17 or later with Developer Mode enabled **Important**: Always sign in to your Sandbox tester account on a dedicated test device. Production Apple IDs cannot make Sandbox purchases, and mixing test and production accounts on the same hardware regularly causes authentication issues. ## Step 1: Enable Developer Mode on Your Test Device 1. Connect the iPhone to your Mac and open Xcode. 2. From the menubar, choose **Window → Devices and Simulators**. 3. Select your device, then click **Enable Developer Mode**. 4. Follow the on-device prompts to reboot and confirm Developer Mode. **Why this matters**: Developer Mode is required before a physical device can run apps signed with a development profile or interact with Sandbox services. ## Step 2: Create a Sandbox Tester in App Store Connect 1. Sign in to [App Store Connect](https://appstoreconnect.apple.com/). 2. Navigate to **Users and Access → Sandbox → Test Accounts**. 3. Click the **Add** button (`+`) and fill in the tester’s first and last name. 4. Provide an email address that has never been used for an Apple ID purchase. Email subaddressing (`tester+us@example.com`) works well when supported by your provider.\ _Apple will send all test purchase receipts and account notices to this address._ 5. Choose a strong password that meets Apple’s complexity requirements. 6. Select the App Store country or region you want to test against. 7. Click **Create** to save the tester. Apple allows up to 10,000 Sandbox testers per team, so create regional variants as needed for localization or tax testing. [Source](https://developer.apple.com/help/app-store-connect/test-in-app-purchases/create-a-sandbox-apple-account/). ## Step 3: Sign In on the Test iPhone 1. On the dedicated test device, open **Settings → App Store**. 2. Scroll to the bottom and tap **Sandbox Account**. 3. Sign in with the newly created Sandbox Apple ID. 4. Confirm the Sandbox indicator appears when making in-app purchases. When prompted inside the Koard demo app or your integration, always use the Sandbox credentials you signed into Settings with—never production Apple IDs. ## Step 4: Reset or Remove Sandbox Testers If you encounter inconsistent billing states or need a clean slate: * In App Store Connect, open the tester record and click **Reset** to clear purchase history. * To delete a tester, select it in the Sandbox list and choose **Delete Account**. You must remove the tester from any Sandbox Test Families first. * After deletion, the associated email can be re-used for a brand-new tester if necessary. ## Troubleshooting Tips * **Purchase dialogs ask for payment details**: Verify you’re signed in with the Sandbox account under **Settings → App Store → Sandbox Account**. * **Device won’t install development build**: Confirm Developer Mode is enabled and your provisioning profile includes the test device UDID. * **Sandbox credential lockouts**: Apple temporarily locks accounts after multiple bad password attempts. Wait 30 minutes before trying again, or delete and recreate the tester. * **Test receipts missing**: Check the tester’s email inbox (including spam) for Sandbox receipts, or reset the tester record and attempt the purchase again. ## Next Steps * [Install the SDK](/docs/setting-up-the-ios-sdk/installing-the-sdk) to start building against the Koard Sandbox environment. * [Run payments](/docs/setting-up-the-ios-sdk/running-payments) using Sandbox credentials to validate flows end-to-end. * Move on to [Getting Ready for Production](/docs/setting-up-the-ios-sdk/getting-ready-for-production) once your Sandbox tests succeed. # Retrieving Your API Key To retrieve your API key for your account, follow these steps: **1. Navigate to the Developer page** In the sidebar, click on ![link icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWZpbGUtdGV4dC1pY29uIGx1Y2lkZS1maWxlLXRleHQiPjxwYXRoIGQ9Ik02IDIyYTIgMiAwIDAgMS0yLTJWNGEyIDIgMCAwIDEgMi0yaDhhMi40IDIuNCAwIDAgMSAxLjcwNC43MDZsMy41ODggMy41ODhBMi40IDIuNCAwIDAgMSAyMCA4djEyYTIgMiAwIDAgMS0yIDJ6Ii8+PHBhdGggZD0iTTE0IDJ2NWExIDEgMCAwIDAgMSAxaDUiLz48cGF0aCBkPSJNMTAgOUg4Ii8+PHBhdGggZD0iTTE2IDEzSDgiLz48cGF0aCBkPSJNMTYgMTdIOCIvPjwvc3ZnPg==) Developer to access the Developer Tools page. **2. Access API Keys** Once on the Developer page, you'll see the **API Keys** tab. Click on it to view your API key management section. **3. View your API key** Your API key will be displayed in the API Key Management section. You can: * View the masked API key * Click the eye icon to reveal the full key * Click the copy icon to copy the key to your clipboard ![API Key Management](/api.png) **Security Note**: Keep your API key confidential and never share it in client-side code or public repositories. Rotate and revoke keys regularly to maintain security. ## Using Your API Key in the SDK Pass your API key to the SDK when you initialize it, along with a `KoardOptions` value that selects the environment and logging level: ```swift import KoardSDK let options = KoardOptions( environment: .uat, // .uat | .production | .custom(String) loggingLevel: .debug // .none | .error | .warning | .debug | .verbose ) KoardMerchantSDK.shared.initialize(options: options, apiKey: "your-koard-api-key") ``` * **`environment`** accepts `.uat`, `.production`, or `.custom(String)` for a custom base URL. * **`loggingLevel`** accepts `.none`, `.error`, `.warning`, `.debug`, or `.verbose`. Use a lower level (such as `.error` or `.none`) for production builds. Call `initialize(options:apiKey:)` once, early in your app lifecycle (for example in `AppDelegate`), before using any payment functionality. # KoardMerchantSDK Demo App ## Setup To run the demo app, you need to get the project and configure your API credentials: **1. Get the Demo Project:** * The demo app now lives in its own repository, separate from the SDK. * Clone the Git repository: `https://github.com/koardlabs/demos.git` * Or [download the ZIP file](https://github.com/koardlabs/demos/archive/refs/heads/main.zip) **2. Open the Project in Xcode:** * Open Xcode * Select **File > Open Existing Project** * Navigate to the demo's `KoardDemo.xcodeproj` and open it **3. Copy the template file to \`config.plist\`:** ```bash cp KoardMerchantSDK-Demo/Config.plist.template KoardMerchantSDK-Demo/Config.plist ``` **4. Edit the configuration:** Open `KoardMerchantSDK-Demo/Config.plist` and replace the placeholder values: * `YOUR_API_KEY_HERE` - Your [Koard API key](/docs/setting-up-the-ios-sdk/retrieving-your-api-key) * `YOUR_MERCHANT_CODE_HERE` - Your [merchant code](/docs/getting-started-with-koard/setting-up-the-merchant#setting-up-the-merchant__create-merchant-credentials) * `YOUR_MERCHANT_PIN_HERE` - Your [merchant PIN](/docs/getting-started-with-koard/setting-up-the-merchant#setting-up-the-merchant__create-merchant-credentials) ##### Configuration Format The `Config.plist` file should contain: ```xml apiKey your_api_key merchantCode your_merchant_code merchantPin your_merchant_pin ``` **5. Add to Xcode project:** * Open the Demo project in Xcode * Drag `Config.plist` into the project navigator * Ensure it's added to the app target **Important:** The app will crash on startup if Config.plist is missing or contains template values The demo app automatically reads credentials from Config.plist at startup. If the file is missing or contains placeholder values, the app will display an error message and fail to initialize. ## Running the Demo Build the Project: Build and run the project in Xcode. You should see a display like this: ![Demo Main Screen](/ios8.png) Hit Authenticate Merchant You will know if it was successful if you see this screen: ![Authenticate Merchant Success](/ios10.png) Next, hit Setup Card Reader Answer the prompts and agree to the terms and conditions as you see fit. If everything has been set up correctly, you will see this screen: ![Card Reader Setup Success](/ios11.png) Hit Process Sample Transaction and enter a sample dollar amount like so: ![Transaction Entry](/ios9.png) Complete Tap to Pay Transaction: You will see a simulated Tap to Pay transaction: ![Tap to Pay Transaction](/ios12.png) ![Transaction Complete](/ios13.png) ## Security Notes * `Config.plist` is gitignored to prevent accidentally committing credentials * Never commit the actual `Config.plist` file to version control * Only commit the `Config.plist.template` file for reference * Consider using environment variables or secure credential management in production # Running Payments on Android Learn how to drive live Tap to Pay on Android sessions with the Koard SDK and how to handle every follow-up transaction step. **What you learn** * Which APIs emit reader events, and why `refund()` is not one of them * How to validate SDK readiness before launching the thin client * Handling `Flow` to power custom UIs * Running Practice Mode taps to validate the online-PIN flow * Managing surcharge confirmation, tip/tax/surcharge breakdowns, and telemetry * Post-reader operations such as capture, refund, EMV refund, reverse, incremental auth, and tip adjustments ## Step 1: Install the Visa Kernel App Before writing any Tap to Pay code, install the Visa Kernel app on every NFC-enabled development device. Download it directly from the [Google Play Store](https://play.google.com/store/apps/details?id=com.visa.kic.app.kernel), launch it once to complete setup, and keep it installed for all future builds. ## Prerequisites Before starting any NFC session: * Initialize the SDK in `Application.onCreate()` and authenticate the merchant * Call `setActiveLocation(locationId)` and then `enrollDevice()` — both are mandatory, and neither happens automatically. See [Installing the SDK](/docs/setting-up-the-android-sdk/installing-the-sdk) * Register the hosting activity for NFC in `onResume()` / `onPause()` * Disable developer mode on the device (reader emits `developerModeEnabled` if it’s left on) * Observe `KoardMerchantSdk.getInstance().readinessState` and block the UI unless `isReadyForTransactions` is `true` ```kotlin val sdk = KoardMerchantSdk.getInstance() val readiness = sdk.readinessState.value if (!readiness.isReadyForTransactions) { Log.w("Checkout", "SDK not ready: ${readiness.getStatusMessage()}") return } ``` ## Launching a Sale Session `sdk.sale`, `sdk.preauth`, `sdk.completePartialAuth`, and `sdk.refundEmv` are the APIs that drive a reader session, so they return cold `Flow` streams. `sdk.prepare` also returns a `Flow`, but it is a warm-up that emits progress toward `Done` — it never runs a transaction. Collect the flow on a worker thread, and feed each event back to the UI. (Other calls reach the thin client too — `isKernelAppInstalled`, `checkKiCEligibility`, `getTransactionConfig`, `cancelTransaction`, `resetKernelService`, `unenrollDevice`, and the NFC registration pair — but they are one-shot and do not emit reader events.) ```kotlin suspend fun sale( activity: Activity, amount: Int, // amount in cents breakdown: PaymentBreakdown? = null, buttonProperties: List? = null, currency: String = "USD", eventId: String? = null, tapTimeoutMs: Long? = null, // auto-cancel the tap after this many ms transactionType: String = "Payment" // "TestWithPin" for Practice Mode ): Flow ``` Pass `buttonProperties` to customize the on-screen reader buttons, and `tapTimeoutMs` to automatically cancel the reader session if no card is presented in time. `currency` must be one of the currency codes the kernel reports for this device. Per KiC §3.4.8 the transaction's `currencyCode` "can be one of the currencies that comes out of the `getTransactionConfig` api", so if you support anything beyond a single fixed currency, read the live list from `sdk.getTransactionConfig()` (a suspend call returning `KoardTransactionConfig?`, wrapping KiC §3.4.16) rather than hard-coding it. The same object carries the supported `transactionTypes`, `supportedButtons`, `supportedCountries`, and `supportedLanguages`. Visa marks `getTransactionConfig` **Implementation: Conditional** — it is there to be called on demand when you need the latest config. **`preauth` no longer matches `sale`'s signature.** The trailing `transactionType` parameter is new in 1.0.6 and exists on `sale()` only. `preauth()` takes the same first seven parameters (`activity` through `tapTimeoutMs`) and nothing more. **Readiness is enforced by throwing, not by emitting.** `sale()`, `preauth()`, `completePartialAuth()`, and `refundEmv()` refresh their readiness checks and **throw `KoardException`** before returning a Flow when the SDK is not ready. Inspect `e.error.errorType`: it is a `KoardErrorType.NotReady.*` value such as `KernelAppNotInstalled`, `DeveloperModeEnabled`, `NotAuthenticated`, `NotEnrolled`, `NoActiveLocation`, `ReaderNotStarted`, or `Preparing`. Retry on `Preparing`; prompt the operator on the rest. Wrap the call in `try/catch`, not just the collection. ### Practice Mode (`TestWithPin`) `transactionType` is the KiC transaction type sent to the kernel: | Value | Behavior | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `"Payment"` (default) | A real sale. The kernel picks the CVM from the card and terminal profile — usually signature or no-CVM, never a guaranteed PIN | | `"TestWithPin"` | **Practice Mode** (KiC Annex A-2). Forces the PIN-entry UI so you can validate the online-PIN flow without routing to the acquirer | ```kotlin sdk.sale( activity = activity, amount = 1000, transactionType = "TestWithPin" // Practice Mode — exercises the PIN keypad ).collect { response -> handleTransactionEvent(response) } ``` Practice Mode is only valid when the device enrollment's supported transaction types include `TestWithPin`. Read the enrollment's supported types from `sdk.getEnrollmentDetails()?.transactionTypes` before offering it in your UI. ```kotlin fun startSale(activity: Activity, amountInCents: Int) { val sdk = KoardMerchantSdk.getInstance() // No pre-flight readiness guard here: sale() refreshes the readiness checks // itself and throws with the typed reason, so a guard that returns early // would swallow the retryable Preparing case. Let the catch below decide. val eventId = UUID.randomUUID().toString() lifecycleScope.launch(Dispatchers.IO) { try { sdk.sale( activity = activity, amount = amountInCents, breakdown = PaymentBreakdown(subtotal = amountInCents), currency = "USD", eventId = eventId ).collect { response -> // The Flow is collected on Dispatchers.IO; hop to Main for UI work. withContext(Dispatchers.Main) { handleTransactionEvent(response) } } } catch (e: KoardException) { // sale() runs the readiness checks itself and throws before it // returns the Flow, so this is where a not-ready SDK surfaces. withContext(Dispatchers.Main) { when (e.error.errorType) { // Transient: the reader is still coming up — retry shortly. is KoardErrorType.NotReady.Preparing -> scheduleRetry { startSale(activity, amountInCents) } // Every other NotReady state needs operator action // (install the kernel app, disable developer mode, log in, // enroll the device, select a location). else -> showError(message = e.error.shortMessage, errorType = e.error.errorType) } } } } } private fun handleTransactionEvent(response: KoardTransactionResponse) { when (response.actionStatus) { KoardTransactionActionStatus.OnProgress -> { showStatus(response.readerStatus.toString(), response.displayMessage) } KoardTransactionActionStatus.OnFailure -> { showError( code = response.statusCode, message = response.displayMessage ?: "Transaction failed", finalStatus = response.finalStatus ) } KoardTransactionActionStatus.OnComplete -> { val transaction = response.transaction ?: return showReceipt(transaction) } else -> Unit } } ``` ### Event payloads Every emitted `KoardTransactionResponse` contains the data you need to build a bespoke reader UI: * **`readerStatus`** – a `KoardReaderStatus` enum value: `preparing`, `readyForTap`, `cardDetected`, `pinEntryRequested`, `pinEntryCompleted`, `readCompleted`, `readNotCompleted`, `readCancelled`, `readRetry`, `processing`, `complete`, `developerModeEnabled`, or `unknown` * **`displayMessage`** – Reader-provided instructions ("Present card", "Processing", etc.) * **`statusCode`** / **`statusCodeDescription`** – numeric status (and its description) for advanced troubleshooting * **`actionStatus`** – `OnProgress`, `OnFailure`, or `OnComplete` (these are the only three action statuses) * **`finalStatus`** – `Approve`, `Decline`, `Abort`, `Failure`, `AltService`, or `Unknown` after reader completion * **`transaction`** – populated on completion; includes IDs, surcharge state, and breakdown so you can print receipts or prompt for confirmation **Surcharge confirmation is signaled on the transaction, not the action status.** There is no `OnConfirmSurcharge` action status. When the completion event carries a transaction whose `transaction.status == KoardTransactionStatus.SURCHARGE_PENDING`, prompt the customer and call `sdk.confirm(transactionId, confirm = true/false)` with their decision. For a complete reference of every `finalStatus`, `statusCode`, display message ID, and error scenario the SDK can return, see the [SDK Response Codes](/docs/setting-up-the-android-sdk/sdk-response-codes) guide. The demo’s `MainScreenViewModel` (see `src/main/java/com/koard/android/ui/MainScreenViewModel.kt`) shows a complete Compose-based implementation that you can adapt for your UI stack. ### Preauthorization Call `sdk.preauth(...)` when you need a hold + later capture. It takes the same parameters as `sale` except the trailing `transactionType` (Practice Mode is sale-only), the Flow emits the same events, and `response.transaction?.transactionId` is the ID you’ll pass into capture/refund APIs later. ### Completing a partial approval When an authorization is only partially approved (the issuer approved less than the requested amount), call `sdk.completePartialAuth(...)` to authorize the remaining amount using the original card data — no new tap is required for the card itself, but because it drives the reader session it returns a `Flow` just like `sale`/`preauth`: ```kotlin suspend fun completePartialAuth( activity: Activity, transactionId: String, amount: Int, breakdown: PaymentBreakdown? = null, buttonProperties: List? = null, currency: String = "USD", eventId: String? = null, tapTimeoutMs: Long? = null ): Flow ``` ### Warming up the reader `sdk.prepare()` is a **latency optimization, not a precondition for tapping.** Visa marks the pre-transaction warm-up API **Implementation: Optional** (KiC integration guide §3.4.7, "Preparing the SDK for Point-of-Sale Transactions"), and `startTransaction` (§3.4.11) lists no warm-up among its preconditions. What warming up buys you is timing: §3.4.7 describes the session-establishment and device-attestation work as happening "before startTransaction" either way, and frames the API as a fix for "the delay seen if the startTransaction is called for the first time after app restart". Calling it moves that cost off the critical user path. The one precondition §3.4.7 does state is that **the device must already be enrolled** before you warm up, so call `prepare()` after `enrollDevice()` has succeeded, not before. Call it early (for example in `Application.onCreate`, once enrollment is confirmed). It returns a `Flow` that emits progress (`Called` → `AuthenticationInProgress` → `AttestationInProgress` → `GettingConfigurations` → `ParsingConfigurations` → `Done`); key off the `Done` emission and treat any error variant as "retry on next user action". Nothing else in your flow needs to block on it — a fire-and-forget collect on a worker scope is a valid usage pattern, since the only effect is the warm-up. ```kotlin // collect() is a suspend function — call it from a coroutine on a worker scope. applicationScope.launch(Dispatchers.IO) { sdk.prepare().collect { response -> if (response.status is KoardPrepareStatus.Done) { Log.i("Checkout", "Reader is warmed up") } } } ``` ## Post-Reader Operations All backend-only operations are suspend functions that return a `Result`, so they never emit reader events and can run from any worker thread. The payload varies by call: `capture`, `reverse`, `refund`, `adjust`, and `getTransaction` return `Result`; `getTransactions` returns `Result` (which carries the paging fields); and `sendReceipt` returns `Result`. `cancelTransaction()` and `resetKernelService()` also return a `Result` (`Result`), but they are **not** backend calls — they talk to the Tap to Pay Ready app over IPC to abort a tap or release the service connection. See [Canceling the reader session](#canceling-the-reader-session). ### Capture ```kotlin suspend fun captureTransaction(transactionId: String, amountOverride: Int? = null) { withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() sdk.capture(transactionId = transactionId, amount = amountOverride) .onSuccess { println("Capture successful: ${it.transactionId}") } .onFailure { println("Capture failed: ${it.message}") } } } ``` ### Incremental authorization ```kotlin suspend fun incrementalAuth(transactionId: String, additionalAmountCents: Int) { withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() sdk.incrementalAuth(transactionId, additionalAmountCents) .onSuccess { println("Incremental auth approved") } .onFailure { println("Incremental auth failed: ${it.message}") } } } ``` ### Reverse / void ```kotlin suspend fun reverseTransaction(transactionId: String, amountCents: Int? = null) { withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() sdk.reverse(transactionId, amountCents) .onSuccess { println("Reverse successful: ${it.transactionId}") } .onFailure { println("Reverse failed: ${it.message}") } } } ``` ### Refund (backend-only) `sdk.refund(...)` is a plain backend call — **no card is presented and no reader events are emitted**. Use it to refund against a transaction you already hold an ID for: ```kotlin suspend fun refundTransaction(transactionId: String, amount: Int? = null) { withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() sdk.refund( transactionId = transactionId, amount = amount, // null refunds the full amount eventId = UUID.randomUUID().toString() ).onSuccess { println("Refund successful: ${it.transactionId}") }.onFailure { println("Refund failed: ${it.message}") } } } ``` `sdk.refundTransaction(transactionId, amount, eventId)` is an older alias with the same signature and behavior; prefer `refund(...)` in new code. ### EMV refund (tap required) When the customer must present their card to receive the refund, use `sdk.refundEmv(...)`. This is the **only** refund entry point that drives the thin client, so it returns a `Flow` and emits the same reader events as `sale`: ```kotlin suspend fun refundEmv( activity: Activity, transactionId: String, amount: Int, // amount in cents, required breakdown: PaymentBreakdown? = null, buttonProperties: List? = null, currency: String = "USD", eventId: String? = null ): Flow ``` ```kotlin lifecycleScope.launch(Dispatchers.IO) { sdk.refundEmv( activity = activity, transactionId = transactionId, amount = amountInCents, eventId = UUID.randomUUID().toString() ).collect { response -> handleTransactionEvent(response) // same handler as sale } } ``` Like `sale()`, `refundEmv()` throws a `KoardErrorType.NotReady.*` `KoardException` before returning a Flow when the SDK is not ready, and it has no `tapTimeoutMs` parameter. ### Tip adjustments ```kotlin suspend fun adjustTip(transactionId: String, newTipCents: Int) { withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() sdk.adjust( transactionId = transactionId, type = AmountType.FIXED, amount = newTipCents ).onSuccess { println("Tip adjusted. New total: ${it.totalAmount}") }.onFailure { println("Tip adjustment failed: ${it.message}") } } } ``` ### Confirming a surcharge When a completion event carries a transaction with `status == KoardTransactionStatus.SURCHARGE_PENDING`, prompt the customer and submit their decision with `sdk.confirm(...)`: ```kotlin suspend fun confirmSurcharge(transactionId: String, accepted: Boolean) { withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() sdk.confirm(transactionId = transactionId, confirm = accepted) .onSuccess { println("Surcharge ${if (accepted) "accepted" else "declined"}: ${it.totalAmount}") } .onFailure { println("Confirm failed: ${it.message}") } } } ``` ### Canceling the reader session There are two teardown paths. They do different things — one aborts a tap in flight, the other releases the IPC connection to the Tap to Pay Ready app. | Call | Use for | Effect | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sdk.cancelTransaction()` | Programmatic cancel, tap timeouts | Calls KiC `cancelTransaction()`, which aborts the current tap and emits a cancel terminal through the active flow, then clears the SDK's transaction guard. It does **not** call `unbindKernelService()`, so the SDK-connector's binding to the Tap to Pay Ready app is left in place | | `sdk.resetKernelService()` | Navigating away from the transaction screen (including after a completed transaction, before the receipt screen), and recovery: the kernel app died, an unrecoverable error, or "another tap is already active" | Unbinds the IPC connection to the Tap to Pay Ready app and clears the SDK's transaction state. Enrollment and the kernel app itself are untouched. The next tap re-initializes lazily; call `prepare()` afterward only to avoid paying that latency on the critical path | ```kotlin suspend fun cancelTap() { withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() sdk.cancelTransaction() // abort the tap; leaves the IPC binding in place .onSuccess { println("Tap cancelled") } .onFailure { println("Cancel failed: ${it.message}") } } } suspend fun releaseReaderSession() { withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() sdk.resetKernelService() // unbind IPC; safe after a completed transaction .onSuccess { println("Reader session reset") } .onFailure { println("Reset failed: ${it.message}") } } } ``` **The underlying KiC `cancelTransaction()` API is deprecated.** Visa's 25.06.10 release notes list "Deprecated cancelTransaction() Api" alongside the change that makes the Tap to Pay Ready app call cancel itself when the user presses its cancel button (it now emits `CancelTransactionInitiated` instead of asking your app to cancel). The KiC integration guide has no §3.4 section specifying `cancelTransaction()`'s behavior; it appears only in §3.4.10 as one of the calls that returns a `TransactionResponseMessage`.\ \ Koard's `cancelTransaction()` still calls it, and it is the path the SDK's own tap-timeout timer uses. Treat customer-pressed cancel as handled by the Tap to Pay Ready app, and reach for this wrapper when _your_ code needs to abort a tap. If a future kernel release removes the API, this wrapper is the surface that breaks — pin your integration tests to it. `resetKernelService()` wraps KiC `unbindKernelService()` (KiC integration guide §3.4.12, "Resetting the Service Connection State", implementation _optional_). Visa's own sample calls it once a transaction completes, before navigating to the receipt screen — so it is routine lifecycle hygiene, not just an emergency lever. It resets the connection between the SDK-connector and the Tap to Pay Ready app; it does **not** unenroll the device, uninstall the kernel app, or discard credentials.\ \ It always clears the SDK's internal state, even when the underlying unbind throws (for example because the kernel app is already dead) — the returned `Result` reports the unbind failure, but the next tap flow can still start. Use `sdk.hasActiveTapTransaction()` to check whether a tap is in flight before tearing anything down. ### Account, location, and terminal details Use these suspend lookups to read merchant, location, and terminal configuration (including surcharge and tax details) when building your checkout UI: ```kotlin sdk.getMerchantAccount() // Result sdk.getLocation(locationId) // Result sdk.getTerminal(terminalId) // Result ``` ### Transaction history and receipts ```kotlin // Offset paging over the active location's transactions. KoardTransactionDetails // carries limit/offset/page/total, so advance offset and compare offset + loaded // against total to implement "load more". sdk.getTransactions(limit = 50, offset = 0) // Result sdk.getTransaction(transactionId) // Result // Email, SMS, or both — pass null for the channel you don't want. sdk.sendReceipt( transactionId = transactionId, email = "customer@example.com", phoneNumber = null ) // Result ``` ## Next steps * Review the [Installing the SDK](/docs/setting-up-the-android-sdk/installing-the-sdk) guide for initialization, location selection, and enrollment * Explore the [Demo App](/docs/setting-up-the-android-sdk/demo) to see `MainScreenViewModel` in action * See [SDK Response Codes](/docs/setting-up-the-android-sdk/sdk-response-codes) for the full reference of final statuses, display messages, and error scenarios * Consult `KoardPaymentModels.kt` for `PaymentBreakdown`, `Surcharge`, and other metadata helpers * Check [Supported Devices & NFC Tap Location](/docs/setting-up-the-android-sdk/supported-devices) for device compatibility and where customers should tap ## Best Practices * **Idempotency everywhere**: Pass a stable `eventId` into `sale`, `preauth`, refunds, reversals, and adjustments so Koard can safely dedupe retried requests. * **UI threading**: Collect the payment Flow on `Dispatchers.IO`, but dispatch UI updates (Compose state, views, dialogs) back to the main thread to avoid lifecycle crashes. * **Surface readiness early**: Bind `readinessState` to visible UI (like the demo Settings screen) so operators see why the reader is blocked (developer mode, missing tap-to-pay dependency, no active location). * **Telemetry hooks**: Each `KoardTransactionResponse` carries `statusCode`, `readerStatus`, and `finalStatus`. Log or export these fields for device fleet monitoring and support. * **Lifecycle hygiene**: Cancel the Flow when the hosting Activity/Fragment stops, and call `sdk.cancelTransaction()` for programmatic cancels and timeouts — it aborts the tap without unbinding. Call `sdk.resetKernelService()` when navigating away from the transaction screen — including after a completed transaction, per KiC §3.4.12 — to release the IPC connection. * **NFC registration**: Keep `registerActivityForNfc()` / `unregisterActivityForNfc()` wired into `onResume()` / `onPause()` on every activity that can host a tap. Visa marks these **optional** (KiC §3.3.15.1–2), but they claim NFC foreground dispatch so another `TECH_DISCOVERED` app cannot jump in front of your tap (§3.4.14). # Supported Devices & NFC Tap Location A guide to compatible Android devices for Tap to Pay, device requirements, and where customers should tap their card on each device. **What you learn** In this guide, you'll learn: * Which Android devices are compatible with Tap to Pay * Minimum device requirements for NFC contactless payments * Where the NFC antenna is located on popular devices * How to instruct customers to tap their card correctly ## Device Requirements To accept Tap to Pay on Android, the merchant's device must meet **all** of the following requirements: | Requirement | Details | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | | **Android version** | Android 12 (API level 31) or later | | **NFC hardware** | Built-in NFC chip (virtually all modern flagship and mid-range Android phones) | | **Hardware keystore** | TEE or StrongBox-backed keystore for secure key storage | | **Google Play Services** | Google Mobile Services (GMS) must be installed and up to date | | **Google Play Protect** | Must be enabled for device integrity verification | | **Developer mode** | Must be **disabled** during live transactions | | **Device integrity** | Passes Play Integrity: release-signed APK, locked bootloader, no root/Magisk. No custom ROMs | | **Visa Kernel app** | The Visa Tap to Pay Ready (TTPR) kernel app must be installed (from the Google Play Store, or via `installKernelApp()`) | **Developer Mode**: Tap to Pay transactions will fail if developer mode is enabled. Always disable developer mode before processing payments. See the [Installing the SDK](/docs/guides/android-sdk/details/installing-sdk) guide for the recommended workflow. ### Play Integrity Gating In addition to the hardware/software requirements above, the underlying contactless engine enforces **Play Integrity** at enrollment time. The device must be running a **release-signed APK on a locked bootloader**, with **no root/Magisk** and **Developer Options turned off**. Debug-signed builds installed via Run ▶ from Android Studio are rejected during enrollment, even on otherwise-eligible hardware. ### Eligibility Check The SDK provides a built-in eligibility check that verifies device requirements before attempting a transaction. Call `checkKiCEligibility()` on a worker thread; it returns a `KoardKiCEligibility` with an `isEligible` flag and the set of `failureCodes` when the device is not eligible: ```kotlin withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() val eligibility = sdk.checkKiCEligibility() if (eligibility.isEligible) { // Device is eligible for Tap to Pay } else { // Handle ineligibility — inspect failure codes eligibility.failureCodes.forEach { code -> Log.w("TapToPay", "Eligibility failure code: $code") } } } ``` The numeric `failureCodes` map to the eligibility error codes documented in [SDK Response Codes](/docs/guides/android-sdk/details/sdk-response-codes#kic-eligibility-errors). ### Visa Kernel App Tap to Pay requires the Visa Tap to Pay Ready (TTPR) kernel app on the device. Check for it and trigger an in-app install through the SDK: ```kotlin withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() if (!sdk.isKernelAppInstalled()) { sdk.installKernelApp(activity) // launches the install flow } } ``` ## Tested & Certified Devices The following devices have been tested and certified by Visa for use with the Kernel in the Cloud (KiC) Tap to Pay Ready application: | Device | Android Version | Form Factor | | ------------------ | --------------- | ----------- | | Google Pixel 3a | Android 12 | Phone | | Google Pixel 4 | Android 13 | Phone | | Google Pixel 6 | Android 15 | Phone | | Google Pixel 8 | Android 16 | Phone | | Google Pixel 9 | Android 15 | Phone | | Samsung Galaxy S22 | Android 13 | Phone | | Samsung Galaxy S23 | Android 14 | Phone | | Oona Tablet | — | Tablet | **Not limited to this list**: These are the devices Visa has explicitly tested and certified. In practice, **any Android device** that meets all the requirements listed above (Android 12+, NFC, hardware keystore, GMS) should work with Tap to Pay. The SDK's `checkKiCEligibility()` check will confirm compatibility at runtime. ### Full Supported Device List Beyond the Visa-certified list, Tap to Pay on Android is supported across a wide range of manufacturers and models. The following devices are confirmed compatible (when running Android 12+): | Brand | Supported Models | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Asus** | Zenfone 9, Zenfone 10, Zenfone 11 Ultra, Zenfone 12 Ultra, ROG Phone 6, ROG Phone 7, ROG Phone 8, ROG Phone 9 | | **Google Pixel** | Pixel 6, Pixel 6a, Pixel 7, Pixel 7a, Pixel 8, Pixel 8a, Pixel 9, Pixel 9a, Pixel 10 | | **Nokia** | G22, G42, G60, G400, X30, XR21 | | **Honor** | 70, 70 Lite, 80, 90, 90 Lite, Magic5, Magic6, Magic7, Magic8 | | **Infinix** | Hot 30, Hot 40, Hot 50, Hot 60i, Zero 20, Zero 30, Zero 40 | | **Motorola** | Edge 2023, Edge 2024, Edge 2025, Moto G 2025, Razr 40, Razr 50, Razr 60 | | **OnePlus** | Nord 3, Nord 4, Nord 5, Nord CE3, Nord CE4, Nord CE5, Nord N30, 11, 11R, 12, 12R, 13, 13R | | **Oppo** | A60, A77, A78, A98, Find X5, Find X6, Find X7, Find X8, Find X9, Reno8, Reno9, Reno10, Reno11, Reno12, Reno13, Reno14, Reno15 | | **Samsung Galaxy** | A04s, A05s, A13, A14, A15, A16, A17, A24, A25, A26, A33, A34, A35, A36, A53, A54, A55, A56, A73, S22, S23, S24, S25, S26 Ultra, Z Flip4, Z Flip5, Z Flip6, Z Flip7, Z Fold4, Z Fold5, Z Fold6, Z Fold7 | | **Xiaomi** | 12, 12S, 12T, 13, 13T, 14, 14T, 15, 15T, Redmi 12, Redmi 12C, Redmi 13, Redmi 13C, Redmi 14C, Redmi 15, Redmi Note 12, Redmi Note 13, Redmi Note 14 | **Always verify at runtime**: NFC support can vary by region and carrier variant — especially for mid-range devices. Use the SDK's `checkKiCEligibility()` check at runtime rather than relying solely on a static device list. ## NFC Antenna Location & Tap Guidance The NFC antenna location determines where the customer should hold or tap their contactless card or device. Getting this right is critical for a smooth payment experience. ### General Rule On virtually all Android phones, the **NFC antenna is located on the back of the device, in the upper-center area** (roughly behind the rear camera module). Customers should hold their card flat against the **upper-middle portion of the phone's back**. ### NFC Antenna Location Summary The table below summarizes NFC antenna placement by manufacturer. For per-model details, see the device tables in the [Full Supported Device List](#full-supported-device-list) above. | Manufacturer | NFC Antenna Location | Tap Zone Guidance | | ------------------------------ | ------------------------------------------------------- | -------------------------------------------------- | | **Asus** (Zenfone / ROG Phone) | Center to upper-center back | Hold card against the center-top third of the back | | **Google Pixel** | Upper-center back, near/behind the rear camera bar | Hold card against the top third of the back | | **Nokia** | Upper-center back | Hold card against the top third of the back | | **Honor** | Upper-center back; Magic series slightly above midpoint | Hold card against the top third of the back | | **Infinix** | Upper-center back | Hold card against the top third of the back | | **Motorola** (Edge / Moto G) | Upper-center back, near the camera or Motorola logo | Hold card against the top third of the back | | **Motorola** (Razr foldables) | Upper half of back (when folded) | Tap on the upper portion of the folded device | | **OnePlus** (flagships) | Upper-center back, near camera module | Hold card against the top third of the back | | **OnePlus** (Nord series) | Upper-center back | Hold card against the top third of the back | | **Oppo** (Find X series) | Center back, near camera module | Hold card against the center of the back | | **Oppo** (A / Reno series) | Upper-center back | Hold card against the top third of the back | | **Samsung Galaxy S** series | Center back, slightly above the midpoint | Hold card against the center of the back | | **Samsung Galaxy A** series | Upper-center to center back (varies by tier) | Hold card against the center of the back | | **Samsung Galaxy Z Flip** | Upper half of the back (when folded) | Tap on the upper portion of the folded device | | **Samsung Galaxy Z Fold** | Center of the back panel (when closed) | Tap on the center of the back when folded | | **Xiaomi** (flagships) | Upper-center back, near camera module | Hold card against the top third of the back | | **Xiaomi** (Redmi series) | Upper-center back | Hold card against the top third of the back | | **Tablets** | Varies — typically center back or near one edge | Check manufacturer documentation | ### Visual Tap Guide For the best tap experience, instruct customers to: 1. **Remove the card from any wallet or sleeve** — Other cards or RFID-blocking material can interfere with the NFC signal 2. **Hold the card flat** against the back of the phone — Do not tap at an angle 3. **Position the card** over the NFC antenna zone (upper-center back on most devices) 4. **Hold steady for 1–2 seconds** — Do not pull away until the phone confirms the read 5. **Listen/watch for confirmation** — The device will display a status message and/or vibrate when the card is read successfully ```plaintext ┌─────────────────────┐ │ │ ← Phone (back view) │ ┌───────────┐ │ │ │ 📷 Camera │ │ │ └───────────┘ │ │ ╔═══════════════╗ │ │ ║ NFC ANTENNA ║ │ ← Tap card here │ ║ TAP ZONE ║ │ │ ╚═══════════════╝ │ │ │ │ │ │ │ │ │ └─────────────────────┘ ``` **Phone cases**: Thin phone cases generally do not interfere with NFC reads. However, thick rugged cases, metal cases, or cases with built-in card holders may block or weaken the NFC signal. If customers experience read failures, try removing the case. ### Handling Tap Failures If the card does not read on the first attempt: 1. Reposition the card slightly — move it toward the camera area 2. Ensure the card is flat and not angled 3. Remove any phone case that may be interfering 4. Check that the device screen shows the "Present card" or "Tap card" prompt 5. If the issue persists, the SDK will surface an appropriate [display message](/docs/guides/android-sdk/details/sdk-response-codes#display-message-ids) or an `OnFailure` status code such as `NFC_NOT_AVAILABLE` ## Troubleshooting Device Compatibility | Issue | Possible Cause | Solution | | ------------------------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Eligibility check returns failure codes | Device doesn't meet one or more requirements | Inspect the failure codes from `checkKiCEligibility()` and address each (e.g., enable Google Play Protect, update Google Play Services, disable developer mode) | | NFC transactions fail on a supported device | Developer mode enabled | Disable developer mode in Settings → Developer options | | Card not reading | NFC disabled in device settings | Go to Settings → Connected devices → Connection preferences → NFC and ensure it's toggled on | | Intermittent read failures | Card positioned incorrectly | Guide the customer to tap in the correct NFC zone (see table above) | | "Kernel app not found" error | Visa Tap to Pay Ready app not installed | Install the Visa Kernel app from the Google Play Store | | Transactions fail after app update | Developer mode was re-enabled for deployment | Disable developer mode after installing updates, then restart the device | ## Next Steps * [Installing the SDK](/docs/guides/android-sdk/details/installing-sdk) — Set up your development environment * [Running Payments](/docs/guides/android-sdk/details/running-payments) — Process your first Tap to Pay transaction * [SDK Response Codes](/docs/guides/android-sdk/details/sdk-response-codes) — Understand transaction outcomes and error handling * [Running the Demo App](/docs/guides/android-sdk/details/demo) — Test with the sample application # Demo ## Setup To run the demo app, you need to get the project and configure your API credentials: **1. Get the Demo Project** * Clone the Git repository: `https://github.com/koardlabs/koard-android.git` * Or [download the ZIP file](https://github.com/koardlabs/koard-android/archive/refs/heads/main.zip) **2. Open the Project in Android Studio** * Open Android Studio * Select **File > Open** * Navigate to the `koard-android` folder and open it * Wait for Gradle sync to complete The `koard-android` repository is fully standalone—no parent project or linked modules required. The demo app is the root Gradle module, and the SDK itself resolves from Maven Central as `com.koard`. **3. Configure Your Credentials** Open `build.gradle.kts` and set your **API key** for each build flavor via the `API_KEY` `buildConfigField`. The flavor's `ENVIRONMENT` field selects the matching `KoardEnvironment` at startup: ```kotlin android { flavorDimensions += "environment" productFlavors { create("uat") { dimension = "environment" isDefault = true buildConfigField("String", "ENVIRONMENT", "\"UAT\"") buildConfigField("String", "API_KEY", "\"YOUR_API_KEY\"") applicationIdSuffix = ".uat" } create("prod") { dimension = "environment" buildConfigField("String", "ENVIRONMENT", "\"PROD\"") buildConfigField("String", "API_KEY", "\"YOUR_API_KEY\"") } } } ``` Replace `YOUR_API_KEY` with your Koard API key. The merchant **code** and **PIN** are not build config values — you enter them on the demo's login screen at runtime, where they are passed to `sdk.login(merchantCode, merchantPin)`. The active location is selected in-app after login. **Important**: For production use, externalize these credentials using secure storage or environment variables. Never commit actual credentials to version control. **4. Select Build Flavor** The demo app has two build flavors: | Flavor | API Environment | Application ID Suffix | | ------ | --------------------- | --------------------- | | uat | UAT/Testing (default) | `.uat` | | prod | Production | (none) | In Android Studio: * Go to **Build > Select Build Variant** * Choose your desired flavor (e.g., `uatDebug` for testing) There is no `dev` flavor. To point the demo at a development backend, switch the SDK to `KoardEnvironment.Custom(...)` in `DemoApplication`. **5. Build the Project** Build the project using one of these methods: **Via Android Studio:** * Click **Build > Make Project** (⌘+F9 / Ctrl+F9) **Via Command Line:** ```bash # Build UAT flavor ./gradlew assembleUatDebug # Build production flavor ./gradlew assembleProdRelease ``` **6. Install on Physical Device** **Physical Device Required**: Tap to Pay on Android requires a physical device with NFC hardware. The emulator does not support NFC payments. **Prerequisites:** * Physical Android device with NFC support (Android 12+) * Tap-to-pay kernel app installed on the device (see the Running Payments guide) * USB debugging enabled on the device **Install the app:** **Via Android Studio:** * Connect your device via USB * Click **Run > Run 'app'** (Shift+F10) **Via Command Line:** ```bash # Install UAT build ./gradlew installUatDebug # Install production build ./gradlew installProdRelease ``` ## Running the Demo Launch the App Open the Koard Demo app on your device. You should see the main screen. Authenticate Merchant Enter your merchant **code** and **PIN** on the login screen — these are entered at runtime, not baked into the build — and submit. The demo calls `sdk.login(merchantCode, merchantPin)`. You will know authentication was successful if you see: * The app navigates past the login screen * The location selector and enrollment panel become available Select Location Tap the location selector in the top bar and choose your active location. The demo calls `sdk.setActiveLocation(location.id)` with your choice. The selected location is used for all transactions, and **enrollment cannot proceed without it**. Enroll the Device **Enrollment is not automatic.** Nothing enrolls as a side effect of logging in or picking a location — you must press the button. Until enrollment completes, the demo's home screen shows the enrollment panel instead of the transaction form, and any tap API would throw `KoardErrorType.NotReady.NotEnrolled`. While `readinessState.enrollmentState` is anything other than `Enrolled`, the demo's home screen renders an enrollment panel in place of the transaction form. Press **Enroll Device** to run `sdk.enrollDevice()`. Under the hood the demo: 1. Checks the tap-to-pay kernel app is installed via `sdk.isKernelAppInstalled()`, and surfaces "Visa Kernel app is not installed." if it isn't 2. Calls `sdk.enrollDevice()`, which generates device certificates and enrolls the device with Koard's payment services 3. Treats the returned `String` as an error message if it contains "failed" or "error", and catches `KoardException` for typed failures You will know enrollment was successful if you see: * The enrollment panel is replaced by the transaction form * `readinessState.enrollmentState` becomes `Enrolled` and the reader status banner clears * NFC transactions can be initiated Enrollment is rejected outright if developer mode is on, if no active location is set, or if the device is already enrolled. To re-enroll, use **Unenroll Device** on the Settings screen (which calls `sdk.unenrollDevice()`) first. Disable Developer Mode **IMPORTANT: Turn Off Developer Mode** Before processing any tap to pay transactions, you MUST disable developer mode on your Android device: 1. Go to **Settings > System > Developer Options** 2. Toggle **Developer Mode** to OFF 3. Restart your device (recommended) Tap to Pay transactions will fail if developer mode is enabled. This is a security requirement from the payment processor. **Developer Workflow:** * **Enable Developer Mode** → Install/update app → **Disable Developer Mode** → Run transactions Process Sample Transaction Test a contactless payment: 1. Tap **Process Transaction** or **New Payment** 2. Enter a sample dollar amount (e.g., $10.00) 3. Select transaction type (Sale or Preauth) 4. Tap **Start** or **Continue** Complete Tap to Pay Transaction Complete the payment flow: 1. Prompt the customer to tap their card or phone 2. Hold the card/phone against the device's NFC reader 3. Wait for the transaction to process 4. View the transaction result (approved/declined) Transaction details will include: * Transaction ID * Authorization code * Card type and last 4 digits * Amount charged * Transaction status View Transaction History Navigate to **Transaction History** to: * View all completed transactions * Filter by date or status * View detailed transaction information * Send receipts via email or SMS ## Demo App Features The demo app showcases the following SDK capabilities: ### Merchant Authentication * Login with merchant code and PIN * Session management * Automatic token refresh ### Device Enrollment * NFC configuration with Koard's tap-to-pay services * Certificate management * Device provisioning status ### Location Management * Retrieve all merchant locations * Select active location * Location-based transaction processing ### Transaction Processing * Sale transactions (immediate capture) * Preauthorization (auth hold) * Capture (settle preauth) * Refund processing * Transaction reversal * Tip adjustment ### Transaction History * List all transactions * View transaction details * Filter and search * Transaction status tracking ### Digital Receipts * Send receipts via email * Send receipts via SMS * Receipt customization ## Project Structure The demo app structure: ```plaintext koard-android/ ├── src/ │ └── main/ │ ├── java/com/koard/android/ │ │ ├── DemoApplication.kt # SDK initialization │ │ ├── MainActivity.kt # NFC register/unregister in onResume/onPause │ │ ├── navigation/ # Tab navigation (History, Home, Settings) │ │ └── ui/ │ │ ├── LoginScreen.kt / LoginViewModel.kt │ │ ├── MainScreen.kt / MainScreenViewModel.kt │ │ ├── SettingsScreen.kt │ │ ├── TransactionDetailsScreen.kt │ │ └── TransactionHistoryScreen.kt │ ├── res/ │ │ └── xml/nfc_tech_filter.xml # IsoDep tech filter for TECH_DISCOVERED │ └── AndroidManifest.xml ├── build.gradle.kts # Build configuration + SDK coordinate ├── settings.gradle.kts # google() + mavenCentral() only └── README.md ``` ### SDK Integration The demo resolves the SDK straight from Maven Central — there is no bundled AAR and no local repository to configure: ```kotlin // build.gradle.kts implementation("com.koard:koard-android-sdk:1.0.6") ``` The published artifact is a **shaded fat AAR**: it bundles the tap-to-pay engine (KiC) and relocates OkHttp, Okio, Retrofit, kotlinx.serialization, and Ktor under `com.koardlabs.*`, so those never clash with your app's own versions. It also means they are **not** transitive dependencies — see [Installing the SDK](/docs/guides/android-sdk/details/installing-sdk) for what you must declare yourself. The environment (UAT vs. PROD) is chosen at runtime from the flavor's `ENVIRONMENT` `buildConfigField`, not by swapping artifacts: * **UAT builds** → `KoardEnvironment.UAT` * **Prod builds** → `KoardEnvironment.PROD` ### How the Demo Initializes the SDK `DemoApplication.onCreate()` maps `BuildConfig.ENVIRONMENT` to a `KoardEnvironment` and initializes the SDK on a worker thread, passing the API key from `BuildConfig`: ```kotlin val environment = when (BuildConfig.ENVIRONMENT) { "PROD" -> KoardEnvironment.PROD "UAT" -> KoardEnvironment.UAT else -> KoardEnvironment.UAT } KoardMerchantSdk.initialize( application = this@DemoApplication, apiKey = BuildConfig.API_KEY, environment = environment, timeoutSeconds = 30L ) ``` Merchant credentials are supplied later, on the login screen, via `sdk.login(merchantCode, merchantPin)`. To shave first-tap latency, you can warm the reader up after init by collecting `sdk.prepare()` in the background and keying off the `KoardPrepareStatus.Done` emission. See [Running Payments](/docs/guides/android-sdk/details/running-payments) for details. ## Build Commands Reference ```bash # Clean build ./gradlew clean # Build all flavors ./gradlew build # Build specific flavor ./gradlew assembleUatDebug ./gradlew assembleProdRelease # Install on device ./gradlew installUatDebug ./gradlew installProdRelease # Uninstall from device ./gradlew uninstallUatDebug ./gradlew uninstallProd # Run tests ./gradlew test # Generate APK ./gradlew assembleDebug ./gradlew assembleRelease ``` ## Troubleshooting ### Common Issues **App crashes on startup** **Solution**: * Verify credentials are configured correctly in `build.gradle.kts` * Check that you're not using placeholder values (YOUR\_API\_KEY\_HERE) * Review Logcat for specific error messages **"Device not provisioned" error** **Solution**: * Ensure the tap-to-pay kernel app is installed (see the Running Payments guide for setup steps) * Confirm you selected a location and then pressed **Enroll Device** — enrollment never runs on its own * Check NFC hardware is functioning * Verify the kernel app is up to date **NFC not working** **Solution**: * Confirm device has NFC hardware * Enable NFC in device settings * Verify device enrollment was successful * Test with a physical payment card * **IMPORTANT**: Ensure developer mode is DISABLED on the device **"Transaction failed" or "NFC error" during tap to pay** **Solution**: * **Disable developer mode** - This is the most common cause of transaction failures * Go to **Settings > System > Developer Options** and toggle OFF * Restart your device after disabling developer mode * Ensure the tap-to-pay kernel app is running and up to date * Check that NFC is enabled in device settings **Build errors** **Solution**: * Verify JDK 21 is configured * Check minimum SDK is set to 31 * Clean and rebuild: `./gradlew clean build` * Invalidate caches: **File > Invalidate Caches / Restart** **Network errors** **Solution**: * Check device has internet connection * Verify API key and credentials are correct * Ensure you're using the correct build flavor for your environment (uat/prod) ### Debugging Tips **Enable verbose logging:** SDK log verbosity is set **once**, at initialization — there is no runtime setter. Pass a `KoardLogLevel` in `DemoApplication.onCreate()`: ```kotlin import com.koardlabs.merchant.sdk.domain.KoardLogLevel KoardMerchantSdk.initialize( application = this@DemoApplication, apiKey = BuildConfig.API_KEY, environment = environment, timeoutSeconds = 30L, logLevel = KoardLogLevel.VERBOSE ) ``` Levels are `VERBOSE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, and `NONE`. The default is `DEBUG` in debug builds and `NONE` in release, and the level applies in release builds too if you opt in. Secrets — API key and token headers, device certificates, and enrollment key material — are never logged at any level. **Check Logcat:** The SDK logs under the fixed tag `KoardSDK`: ```bash adb logcat -s KoardSDK ``` **Verify SDK version:** Check which SDK version the demo is using: ```bash ./gradlew dependencies | grep koard ``` ## Security Notes * Never commit actual credentials to version control * Use environment variables or secure credential management for production * The `build.gradle.kts` credentials are for demo purposes only * Consider using Android Keystore for sensitive data in production apps * Keep the demo app on devices used exclusively for testing ## Updating the SDK Updating the SDK is a one-line change — bump the Maven coordinate in `build.gradle.kts` and rebuild: ```kotlin implementation("com.koard:koard-android-sdk:") ``` There is no AAR to download or copy: the artifact resolves from `mavenCentral()`, which `settings.gradle.kts` already declares. 1.0.6 changed bytecode signatures (`initialize`, `getTransactions`, and `sale` gained defaulted parameters) and replaced the leaked Visa `ButtonProperties` type with `KoardButtonProperties`. Always do a clean rebuild after bumping the version rather than dropping a new artifact onto pre-compiled call sites. ## Next Steps After running the demo app successfully: * [Install SDK in Your App](/docs/setting-up-the-android-sdk/installing-the-sdk) - Integrate SDK into your own application, including login, location selection, and enrollment * [Running Payments](/docs/setting-up-the-android-sdk/running-payments) - Drive tap-to-pay sessions and post-reader operations * [SDK Response Codes](/docs/setting-up-the-android-sdk/sdk-response-codes) - Understand transaction outcomes, error codes, and error handling * [Payment Lifecycle](/docs/payments/payment-lifecycle) - Understand payment flows * [Supported Devices & NFC Tap Location](/docs/setting-up-the-android-sdk/supported-devices) - Device compatibility and where to tap * [Troubleshooting](/docs/setting-up-the-android-sdk/troubleshooting) - Fix enrollment failures and taps that cancel instantly ## Support For technical support or questions: * Email: * Documentation: * GitHub Issues: # 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](/docs/payments/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_id`s 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_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`: ```http GET /v1/transactions/event/{event_id} X-Koard-apikey: ``` 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](/docs/webhooks/setting-up-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 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_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. --- title: Surcharging --- # Surcharging Surcharging adds a fee to credit card transactions to offset processing costs. Koard supports both **automatic surcharging** (processor-calculated) and **custom surcharging** (merchant-calculated, e.g., BIN-based). > **Debit cards:** Surcharges must not be applied to debit card transactions. Koard automatically excludes debit cards from surcharging for US-based transactions. The SDK will not trigger a `surchargePending` status for debit cards. > **Partner responsibility:** It is the partner's responsibility to ensure that merchants configure the correct surcharge rates, disclosure text, and comply with applicable card brand rules and state/local regulations. Koard provides the surcharge infrastructure, but legal compliance—including rate caps, signage, and receipt requirements—is the merchant's obligation. > **Disclosure requirement:** Most card brand rules and state laws require that the surcharge is disclosed to the cardholder *before* the transaction is completed. Koard's SDK handles this via the surcharge confirmation flow, but the partner must ensure the disclosure content is accurate and legally compliant. ## How Automatic Surcharging Works 1. The merchant initiates a [sale](sale.md). 2. The processor evaluates the card—only **credit cards** in eligible regions are surcharged. Debit cards are automatically excluded. 3. If eligible, the transaction returns with status `surchargePending`. 4. The SDK surfaces the surcharge amount and disclosure text. 5. The merchant app presents the disclosure to the customer. 6. The merchant calls `confirm()` with the customer's decision. 7. If confirmed, the transaction finalizes with the surcharge included. > **Note:** Surcharge pending only applies to **sale** transactions. Preauth transactions do not trigger `surchargePending`—use the [custom BIN surcharge flow](#flow-2-custom-bin-surcharge-via-preauth) instead. ## Surcharge Calculation Basis The surcharge percentage is applied to the **full transaction amount** (subtotal + tax + tip), not just the subtotal: ``` surcharge = (subtotal + taxAmount + tipAmount) * surchargeRate ``` For example, with a 3.5% surcharge on a $100 subtotal + $8.75 tax + $20 tip: ``` surcharge = (10000 + 875 + 2000) * 0.035 = 12875 * 0.035 = $4.51 (451 cents) ``` ## Rate Hierarchy Surcharge rates are resolved in priority order: | Priority | Source | Description | |----------|--------|-------------| | 1 (highest) | `PaymentBreakdown.surcharge` | Per-transaction override passed in the SDK call | | 2 | Terminal configuration | Rate set on the terminal | | 3 | Location configuration | Rate set on the location | | 4 | Account configuration | Default rate on the merchant account | | 5 (lowest) | Processor default | Fallback rate from the payment processor | ## Surcharge Settings (Account / Location / Terminal) Beyond the per-transaction override, surcharge behavior is configured on the **account**, **location**, and **terminal** records. Each level can set: | Field | Meaning | |---|---| | `surcharge_rate` | Surcharge percentage applied to eligible credit-card transactions. | | `surcharge_basis` | What the surcharge is calculated on (e.g. subtotal vs. subtotal + tax + tip). | | `surcharge_confirmation_required` | Whether the cardholder must confirm the surcharge before the sale completes. | **Resolution — most specific wins.** Koard uses the value on the **terminal** if present, else the **location**, else the **account** (a `null` at a more specific level means "inherit"). This is the same account → location → terminal hierarchy used for [tax](/payments/tax-and-tip-handling). ### Surcharge Confirmation When `surcharge_confirmation_required` resolves to `true`, the surcharge must be **confirmed by the cardholder** before the sale completes — the SDK surfaces the surcharge and disclosure (`surchargePending`) and requires acknowledgement via `confirm()` (see [How Automatic Surcharging Works](#how-automatic-surcharging-works)). When it resolves to `false`, the surcharge is applied without a separate confirmation step. Note that card-brand rules and many state laws **require** disclosure before completion regardless of this flag — see the **Disclosure requirement** above. ## The `Surcharge` Object Both SDKs use a nested `Surcharge` object inside `PaymentBreakdown`: ::::scalar-tabs :::scalar-tab{ title="iOS" } ```swift PaymentBreakdown.Surcharge( amount: Int?, // fixed surcharge in cents percentage: Double?, // surcharge rate as decimal (0.035 = 3.5%) bypass: Bool // skip automatic surcharge (default: false) ) ``` ::: :::scalar-tab{ title="Android" } ```kotlin Surcharge( amount: Int?, // fixed surcharge in cents percentage: Double?, // surcharge rate as decimal bypass: Boolean // skip automatic surcharge (default: false) ) ``` ::: :::: ### Usage in PaymentBreakdown ::::scalar-tabs :::scalar-tab{ title="iOS" } ```swift let breakdown = PaymentBreakdown( subtotal: 10000, taxRate: 8.75, taxAmount: 875, tipAmount: 2000, tipType: .fixed, surcharge: PaymentBreakdown.Surcharge( amount: 451, // surcharge on (10000 + 875 + 2000) at 3.5% percentage: 0.035 ) ) ``` ::: :::scalar-tab{ title="Android" } ```kotlin val breakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipAmount = 2000, tipType = "fixed", surcharge = Surcharge( amount = 451, // surcharge on (10000 + 875 + 2000) at 3.5% percentage = 0.035 ) ) ``` ::: :::: --- ## API Flows ### Flow 1: Automatic Surcharge (Sale) This is the standard flow where the processor automatically determines surcharge eligibility. Surcharge pending **only** triggers on sale transactions. #### Step 1 — Initiate Sale via SDK The sale is initiated through the Koard SDK on the device. The SDK handles card reading, encryption, and communication with Koard's servers. ::::scalar-tabs :::scalar-tab{ title="iOS" } ```swift let breakdown = PaymentBreakdown( subtotal: 10000, taxRate: 8.75, taxAmount: 875, tipAmount: 2000, tipType: .fixed ) let result = try await koard.createSale(amount: 12875, breakdown: breakdown) // result.status may be "surcharge_pending" for eligible credit cards ``` ::: :::scalar-tab{ title="Android" } ```kotlin val breakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipAmount = 2000, tipType = TipType.FIXED ) val result = koard.createSale(amount = 12875, breakdown = breakdown) // result.status may be "surcharge_pending" for eligible credit cards ``` ::: :::: #### Step 2 — Handle `surchargePending` Response If the card is a credit card and surcharge rules apply: ```json { "transaction_id": "txn_abc123", "status": "surcharge_pending", "total_amount": 13326, "subtotal": 10000, "tax_amount": 875, "tip_amount": 2000, "surcharge_applied": true, "surcharge_amount": 451, "surcharge_rate": 0.035, "card_brand": "visa", "card_type": "credit", "payment_method": "contactless" } ``` > If the card is debit, the response will be `captured` immediately with no surcharge. No confirm step is needed. #### Step 3 — Confirm or Decline Surcharge Present the surcharge disclosure to the customer, then confirm: ```bash POST /v1/payments/{transaction_id}/confirm X-Koard-apikey: {api_key} { "confirm": true, "event_id": "evt_confirm_id" } ``` **Response (confirmed):** ```json { "transaction_id": "txn_abc123", "status": "captured", "total_amount": 13326, "subtotal": 10000, "tax_amount": 875, "tip_amount": 2000, "surcharge_applied": true, "surcharge_amount": 451, "surcharge_rate": 0.035 } ``` **Response (declined — `confirm: false`):** ```json { "transaction_id": "txn_abc123", "status": "cancelled", "total_amount": 0, "surcharge_applied": false, "surcharge_amount": 0 } ``` --- ### Flow 2: Custom BIN Surcharge via Preauth For merchants who calculate surcharges based on the card's BIN. Use preauth with `bypass: true` to skip automatic surcharge, then add the surcharge via incremental auth. #### Step 1 — Preauth with Bypassed Surcharge via SDK Initiate a preauth through the SDK with `bypass: true` to skip automatic surcharge calculation: ::::scalar-tabs :::scalar-tab{ title="iOS" } ```swift let breakdown = PaymentBreakdown( subtotal: 10000, taxRate: 8.75, taxAmount: 875, tipAmount: 2000, tipType: .fixed, surcharge: PaymentBreakdown.Surcharge(bypass: true) ) let result = try await koard.createPreauth(amount: 12875, breakdown: breakdown) ``` ::: :::scalar-tab{ title="Android" } ```kotlin val breakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipAmount = 2000, tipType = TipType.FIXED, surcharge = Surcharge(bypass = true) ) val result = koard.createPreauth(amount = 12875, breakdown = breakdown) ``` ::: :::: **Response:** ```json { "transaction_id": "txn_preauth_123", "status": "authorized", "total_amount": 12875, "subtotal": 10000, "tax_amount": 875, "tip_amount": 2000, "surcharge_applied": false, "surcharge_amount": 0, "card_brand": "visa", "card_type": "credit" } ``` #### Step 2 — BIN Lookup and Calculate Surcharge Use the card BIN from the response to determine surcharge eligibility: ```javascript const bin = response.transaction.bin; const isDebit = await checkIsDebitCard(bin); if (isDebit) { // Debit card — skip surcharge, capture at original amount await capture(transactionId, 12875); return; } // Credit card — calculate surcharge const baseAmount = 12875; const surchargeRate = lookupSurchargeRate(bin); // e.g., 0.035 const surchargeAmount = Math.round(baseAmount * surchargeRate); // 451 ``` #### Step 3 — Incremental Auth for Surcharge Amount Add the surcharge as an incremental authorization on the existing preauth: ```bash POST /v3/payments/{transaction_id}/auth X-Koard-apikey: {api_key} { "amount": 451, "breakdown": { "subtotal": 0, "surcharge": { "amount": 451, "percentage": 0.035 } }, "event_id": "evt_inc_auth_id" } ``` **Response:** ```json { "transaction_id": "txn_preauth_123", "status": "authorized", "total_amount": 13326, "subtotal": 10000, "tax_amount": 875, "tip_amount": 2000, "surcharge_applied": true, "surcharge_amount": 451, "surcharge_rate": 0.035 } ``` #### Step 4 — Capture the Full Amount ```bash POST /v4/payments/{transaction_id}/capture X-Koard-apikey: {api_key} { "amount": 13326, "breakdown": { "subtotal": 10000, "taxRate": 8.75, "taxAmount": 875, "tipAmount": 2000, "tipType": "fixed", "surcharge": { "amount": 451, "percentage": 0.035 } }, "event_id": "evt_capture_id" } ``` **Response:** ```json { "transaction_id": "txn_preauth_123", "status": "captured", "total_amount": 13326, "subtotal": 10000, "tax_amount": 875, "tip_amount": 2000, "surcharge_applied": true, "surcharge_amount": 451, "surcharge_rate": 0.035, "batch_id": "batch_456" } ``` --- ### Flow 3: Preauth with Surcharge, Then Remove on Capture For cases where you preauth with surcharge included initially, but then discover the surcharge can't be applied (e.g., BIN lookup reveals a debit card). Capture at the lower amount with an updated breakdown. #### Step 1 — Preauth with Surcharge Included via SDK Initiate a preauth with the surcharge pre-calculated and included in the total: ::::scalar-tabs :::scalar-tab{ title="iOS" } ```swift let breakdown = PaymentBreakdown( subtotal: 10000, taxRate: 8.75, taxAmount: 875, tipAmount: 2000, tipType: .fixed, surcharge: PaymentBreakdown.Surcharge(amount: 451, percentage: 0.035) ) // Total = 10000 + 875 + 2000 + 451 = 13326 let result = try await koard.createPreauth(amount: 13326, breakdown: breakdown) ``` ::: :::scalar-tab{ title="Android" } ```kotlin val breakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipAmount = 2000, tipType = TipType.FIXED, surcharge = Surcharge(amount = 451, percentage = 0.035) ) // Total = 10000 + 875 + 2000 + 451 = 13326 val result = koard.createPreauth(amount = 13326, breakdown = breakdown) ``` ::: :::: **Response:** ```json { "transaction_id": "txn_preauth_456", "status": "authorized", "total_amount": 13326, "subtotal": 10000, "tax_amount": 875, "tip_amount": 2000, "surcharge_applied": true, "surcharge_amount": 451, "surcharge_rate": 0.035 } ``` #### Step 2 — BIN Lookup Reveals Debit Card Your BIN lookup returns `debit: true`. Surcharges cannot be applied to debit cards. #### Step 3 — Capture Without Surcharge (Lower Amount) Capture at the original amount without surcharge. The processor releases the unused hold automatically: ```bash POST /v4/payments/{transaction_id}/capture X-Koard-apikey: {api_key} { "amount": 12875, "breakdown": { "subtotal": 10000, "taxRate": 8.75, "taxAmount": 875, "tipAmount": 2000, "tipType": "fixed", "surcharge": { "bypass": true } }, "event_id": "evt_capture_no_surcharge" } ``` **Response:** ```json { "transaction_id": "txn_preauth_456", "status": "captured", "total_amount": 12875, "subtotal": 10000, "tax_amount": 875, "tip_amount": 2000, "surcharge_applied": false, "surcharge_amount": 0, "surcharge_rate": 0, "batch_id": "batch_789" } ``` > The difference between the authorized amount ($133.26) and the captured amount ($128.75) is automatically released back to the cardholder. --- ## Bypassing Automatic Surcharge Set `bypass: true` to skip the processor's automatic surcharge calculation. Required for custom surcharge workflows: ::::scalar-tabs :::scalar-tab{ title="iOS" } ```swift let breakdown = PaymentBreakdown( subtotal: 10000, taxRate: 8.75, taxAmount: 875, tipType: .fixed, surcharge: PaymentBreakdown.Surcharge(bypass: true) ) ``` ::: :::scalar-tab{ title="Android" } ```kotlin val breakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipType = "fixed", surcharge = Surcharge(bypass = true) ) ``` ::: :::: ## Transaction Response — Surcharge Fields | Field | Type | Description | |-------|------|-------------| | `surcharge_applied` | boolean | Whether a surcharge was applied to this transaction | | `surcharge_amount` | integer | Surcharge amount in cents | | `surcharge_rate` | float | Surcharge rate as decimal (0.035 = 3.5%) | ## Surcharging on Other Operations | Operation | Surcharge Behavior | |-----------|-------------------| | [Sale](sale.md) | Surcharge calculated automatically; triggers `surchargePending` for confirmation | | [Preauth](preauth.md) | No automatic surcharge pending. Use `bypass: true` + incremental auth for custom surcharge | | [Capture](capture.md) | Include surcharge in breakdown for accurate settlement. Can capture less to remove surcharge | | [Incremental Auth](incremental-auth.md) | Used to add custom surcharge amounts to existing preauth | | [Refund](refund.md) | Surcharge prorated automatically—no breakdown needed | | [Reverse](reverse.md) | Full surcharge released automatically—no breakdown needed | | [Tip Adjust](tip-adjust.md) | Surcharge preserved—not recalculated on tip change | ## See Also - [Sale](sale.md) — One-step payment with automatic surcharge - [Preauth](preauth.md) — Hold with surcharge bypass option - [Incremental Auth](incremental-auth.md) — Add custom surcharge to existing auth - [Payment Lifecycle](payment-lifecycle.md) — End-to-end payment flow --- title: Preauth --- # Preauth A preauthorization places a hold on the cardholder's funds without capturing. Use it when the final amount may change (e.g., tips, adjustments, custom surcharging). ## Prerequisites - Authenticated merchant with `login()` - Active location set via `setActiveLocationID()` - Card reader prepared with `prepare()` (iOS) or device enrolled (Android) ## Basic Preauth **iOS:** ```swift let breakdown = PaymentBreakdown( subtotal: 10000, taxRate: 8.75, taxAmount: 875, tipType: .fixed ) let currency = CurrencyCode(currencyCode: "USD", displayName: "US Dollar") let response = try await KoardMerchantSDK.shared.preauth( amount: 10875, breakdown: breakdown, currency: currency, eventId: UUID().uuidString ) let transactionId = response.transactionId! ``` **Android:** ```kotlin val breakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipType = "fixed" ) sdk.preauth( activity = this, amount = 10875, breakdown = breakdown, eventId = UUID.randomUUID().toString() ).collect { event -> when (event.actionStatus) { ActionStatus.OnComplete -> { val txn = event.response?.transaction println("Preauth hold placed: ${txn?.transactionId}") } ActionStatus.OnConfirmSurcharge -> { val txn = event.response?.transaction sdk.confirm( transactionId = txn?.transactionId ?: "", confirm = true ) } ActionStatus.OnFailure -> { println("Preauth failed: ${event.response?.message}") } else -> { /* reader progress */ } } } ``` ## Preauth with Surcharge Bypass To calculate surcharges yourself (e.g., BIN-based logic), bypass the processor's automatic surcharge: **iOS:** ```swift let breakdown = PaymentBreakdown( subtotal: 10000, taxRate: 8.75, taxAmount: 875, tipAmount: 2000, tipType: .fixed, surcharge: PaymentBreakdown.Surcharge(bypass: true) ) let response = try await KoardMerchantSDK.shared.preauth( amount: 12875, // subtotal + tax + tip (no surcharge yet) breakdown: breakdown, currency: currency, eventId: UUID().uuidString ) ``` **Android:** ```kotlin val breakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipAmount = 2000, tipType = "fixed", surcharge = Surcharge(bypass = true) ) sdk.preauth( activity = this, amount = 12875, breakdown = breakdown, eventId = UUID.randomUUID().toString() ).collect { /* handle response */ } ``` After the preauth completes, use the BIN from the response to calculate a custom surcharge, then apply it via [incremental auth](incremental-auth.md). See the [BIN-based surcharging workflow](surcharging.md#bin-based-custom-surcharge) for the full flow. ## Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `amount` | `Int` | Yes | Hold amount in minor units (cents) | | `breakdown` | `PaymentBreakdown?` | No | Itemized breakdown | | `currency` | `CurrencyCode` | Yes (iOS) | Currency for the transaction | | `eventId` | `String?` | No | Idempotency key (UUID recommended) | | `activity` | `Activity` | Yes (Android) | Android activity for NFC access | ## After Preauth A preauth hold must be followed by one of: | Action | Description | |--------|-------------| | [Capture](capture.md) | Finalize at the same or lower amount | | [Incremental Auth](incremental-auth.md) | Increase the hold (e.g., add surcharge) | | [Tip Adjust](tip-adjust.md) | Update the tip before capture | | [Reverse](reverse.md) | Void the hold entirely | ## See Also - [Sale](sale.md) — One-step authorize + capture - [Capture](capture.md) — Finalize a preauth - [Surcharging](surcharging.md) — Bypass and custom surcharge workflows --- title: Incremental Auth --- # Incremental Auth Incremental authorization increases the hold amount on an existing [preauth](preauth.md). Common uses: - Adding a custom surcharge after BIN lookup - Increasing the hold for additional items or services - Adjusting the authorization before [capture](capture.md) ## Basic Incremental Auth **iOS:** ```swift let response = try await KoardMerchantSDK.shared.auth( transactionId: transactionId, amount: 2000 // increase hold by $20 ) ``` **Android:** ```kotlin sdk.incrementalAuth( transactionId = transactionId, amount = 2000 ) ``` ## Incremental Auth with Surcharge Breakdown When adding a custom surcharge via incremental auth, include the surcharge details in the breakdown. The surcharge percentage is applied to the **full transaction amount** (subtotal + tax + tip): **iOS:** ```swift let baseAmount = 10000 + 875 + 2000 // subtotal + tax + tip = 12875 let surchargeRate = 0.035 let surchargeAmount = Int(Double(baseAmount) * surchargeRate) // 451 let surchargeBreakdown = PaymentBreakdown( subtotal: 0, taxAmount: 0, tipType: .fixed, surcharge: PaymentBreakdown.Surcharge( amount: surchargeAmount, percentage: surchargeRate ) ) let response = try await KoardMerchantSDK.shared.auth( transactionId: transactionId, amount: surchargeAmount, breakdown: surchargeBreakdown ) ``` **Android:** ```kotlin val baseAmount = 10000 + 875 + 2000 // 12875 val surchargeRate = 0.035 val surchargeAmount = (baseAmount * surchargeRate).toInt() // 451 val surchargeBreakdown = PaymentBreakdown( subtotal = 0, taxAmount = 0, tipType = "fixed", surcharge = Surcharge( amount = surchargeAmount, percentage = surchargeRate ) ) sdk.incrementalAuth( transactionId = transactionId, amount = surchargeAmount, breakdown = surchargeBreakdown ) ``` ## Custom Surcharging via BIN Lookup The most common use of incremental auth is the [BIN-based surcharging workflow](surcharging.md#bin-based-custom-surcharge): 1. **Preauth** with `surcharge: Surcharge(bypass: true)` to get the card BIN 2. **BIN lookup** to determine if the card is credit (surchargeable) or debit (not surchargeable) 3. **Incremental auth** to add the calculated surcharge amount 4. **Capture** at the full amount with the complete breakdown > **Reminder:** Surcharges must not be applied to debit cards. Always verify the card type from the BIN before adding a surcharge via incremental auth. ## Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `transactionId` | `String` | Yes | Existing preauth to increase | | `amount` | `Int` | Yes | Additional amount to authorize (in minor units) | | `breakdown` | `PaymentBreakdown?` | No | Breakdown for the incremental amount | > **Note:** The iOS SDK method is `auth()` while the Android SDK method is `incrementalAuth()`. ## See Also - [Preauth](preauth.md) — Initial authorization hold - [Capture](capture.md) — Finalize after incrementing - [Surcharging](surcharging.md) — BIN-based custom surcharge workflow --- title: Reverse (Void) --- # Reverse (Void) A reverse voids a transaction **before** settlement, releasing the hold on the cardholder's funds immediately. Use it to cancel a sale or preauth that hasn't settled yet. > For returning funds **after** settlement, see [Refund](refund.md). ## Full Reverse **iOS:** ```swift let response = try await KoardMerchantSDK.shared.reverse( transactionId: transactionId, eventId: UUID().uuidString ) ``` **Android:** ```kotlin sdk.reverse( transactionId = transactionId, eventId = UUID.randomUUID().toString() ) ``` ## Partial Reverse Reduce the authorized amount without voiding the entire transaction: **iOS:** ```swift let response = try await KoardMerchantSDK.shared.reverse( transactionId: transactionId, amount: 5000, // reduce hold by $50 eventId: UUID().uuidString ) ``` **Android:** ```kotlin sdk.reverse( transactionId = transactionId, amount = 5000, eventId = UUID.randomUUID().toString() ) ``` ## Surcharge Handling When reversing a surcharged transaction, the **full surcharge is released automatically**. No breakdown is needed—the processor handles the surcharge reversal. ## When to Use Reverse vs. Refund | | Reverse | Refund | |---|---------|--------| | **Timing** | Before settlement | After settlement | | **Speed** | Immediate hold release | 3–5 business days | | **Surcharge** | Full surcharge voided | Surcharge prorated | | **Use case** | Cancel, customer changed mind | Post-settlement return | ## Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `transactionId` | `String` | Yes | Transaction to reverse | | `amount` | `Int?` | No | Partial reverse amount in minor units. Omit for full void. | | `eventId` | `String?` | No | Idempotency key | ## See Also - [Refund](refund.md) — Return funds after settlement - [Sale](sale.md) — Original payment - [Preauth](preauth.md) — Authorization hold # Payment Lifecycle Understand how Koard transactions progress from initial authorization through capture, adjustment, reversal, and refund. **What You Learn** * How sale and preauthorization flows differ * Which follow-up operations are available and when to use them * iOS SDK entry points and their matching REST endpoints * How to monitor state transitions and handle errors ## Before You Begin * Review the transaction-specific guides: [Sale](/docs/payments/methods/sale), [Preauth](/docs/payments/methods/preauth), [Capture](/docs/payments/methods/capture), [Incremental Auth](/docs/payments/methods/incremental-auth), [Tip Adjust](/docs/payments/methods/tip-adjust), [Reverse](/docs/payments/methods/reverse), and [Refund](/docs/payments/methods/refund). * For implementation details in Swift, start with the [Running Payments](/docs/setting-up-the-ios-sdk/running-payments) guide. * Make sure you have a dedicated test iPhone with your Sandbox Apple Account signed in so you can exercise Tap to Pay flows end-to-end. ## Transaction Categories ### Tap-Initiated Transactions (card present) | Transaction | Description | iOS SDK Method | API Entry Point | | ----------- | ----------------------- | ----------------------------------- | ------------------ | | Sale | One-step auth + capture | `KoardMerchantSDK.shared.sale()` | `POST /v4/payment` | | Preauth | Authorization hold | `KoardMerchantSDK.shared.preauth()` | `POST /v4/preauth` | These operations **require** card data from Tap to Pay or another compliant reader. ### Follow-Up Operations (card-not-present) | Transaction | Purpose | iOS SDK | REST Endpoint | | ---------------- | --------------------------------- | ----------- | -------------------------------- | | Capture | Settle an authorized amount | `capture()` | `POST /v3/payments/{id}/capture` | | Incremental Auth | Increase an existing hold | `auth()` | `POST /v3/payments/{id}/auth` | | Tip Adjust | Update gratuity before settlement | `adjust()` | `POST /v1/payments/{id}/adjust` | | Reverse | Release held funds | `reverse()` | `POST /v1/payments/{id}/reverse` | | Refund | Return captured funds | `refund()` | `POST /v1/payments/{id}/refund` | **REST vs SDK**: After a successful tap, you can perform every follow-up operation via the REST API, the iOS SDK, or both—choose the channel that fits your workflow. ## Lifecycle Flows ### Sale Flow ```plaintext Tap → Sale (status: captured) → [Optional] Refund → Complete ``` Sales capture funds immediately. Refunds return money after settlement. ### Preauth Flow ```plaintext Tap → Preauth (status: authorized) ├─ Incremental Auth (optional, status stays authorized) ├─ Capture (status: captured) → Refund (optional) └─ Reverse (status: reversed) ``` Preauths require an explicit capture to collect funds. If plans change, reverse the authorization instead of refunding. **Successive auths**: If a follow-up authorization is declined, Koard automatically reverts to the last successfully authorized amount. ## Swift SDK Reference ```swift // Sale (tap required) KoardMerchantSDK.shared.sale( amount: Int, breakdown: PaymentBreakdown? = nil, currency: CurrencyCode, transactionId: String? = nil, type: PaymentType = .sale ) async throws -> TransactionResponse // Preauthorization (tap required) KoardMerchantSDK.shared.preauth( amount: Int, currency: CurrencyCode, transactionId: String? = nil, breakdown: PaymentBreakdown? = nil ) async throws -> TransactionResponse // Follow-up operations KoardMerchantSDK.shared.capture(transactionId: String, amount: Int? = nil, breakdown: PaymentBreakdown? = nil) KoardMerchantSDK.shared.auth(transactionId: String, amount: Int, breakdown: PaymentBreakdown? = nil) KoardMerchantSDK.shared.adjust(transactionId: String, type: AdjustmentType, amount: Int? = nil, percentage: Double? = nil) KoardMerchantSDK.shared.reverse(transactionId: String, amount: Int? = nil) KoardMerchantSDK.shared.refund(transactionId: String, amount: Int? = nil) ``` ### Updated Breakdown Example ```swift let breakdown = PaymentBreakdown( subtotal: 2500, taxRate: 8.75, // 8.75% as a percent value taxAmount: 219, tipAmount: 500, tipType: .fixed, surcharge: PaymentBreakdown.Surcharge( amount: 75, // $0.75 surcharge in cents percentage: 0.03, // 3% surcharge rate bypass: false ) ) ``` `taxRate` is a floating-point percent value (e.g., `8.75` for 8.75%). Surcharge details are passed as a nested `Surcharge` object with `amount`, `percentage`, and `bypass` fields. ## REST Quick Reference | Operation | Endpoint | Notes | | ---------------- | -------------------------------------------- | --------------------------------------------- | | Sale | `POST /v4/payment` | Requires encrypted card data from Tap to Pay | | Preauth | `POST /v4/preauth` | Returns `transaction_id` for follow-ups | | Capture | `POST /v3/payments/{transaction_id}/capture` | Include breakdown to reconcile tips/surcharge | | Incremental Auth | `POST /v3/payments/{transaction_id}/auth` | Amount is the incremental delta | | Tip Adjust | `POST /v1/payments/{transaction_id}/adjust` | `percentage` is a decimal (e.g., `0.18`) | | Reverse | `POST /v1/payments/{transaction_id}/reverse` | Releases uncaptured funds | | Refund | `POST /v1/payments/{transaction_id}/refund` | Works on captured transactions | See the individual transaction guides for full payload examples. ## Transaction States | State | Description | Transitions | | ------------ | ------------------ | ------------------------------------------------ | | `pending` | Request accepted | → `processing`, `failed` | | `processing` | Gateway evaluating | → `authorized`, `captured`, `declined`, `failed` | | `authorized` | Funds on hold | → `captured`, `reversed`, `cancelled` | | `captured` | Funds collected | → `refunded`, `cancelled` | | `declined` | Processor rejected | Terminal | | `failed` | Processing error | Terminal | | `reversed` | Hold released | Terminal | | `refunded` | Funds returned | Terminal | | `cancelled` | Flow cancelled | Terminal | ### Visual Flow ```plaintext Sale: pending → processing → captured → [refunded | cancelled] Preauth: pending → processing → authorized ├─ capture → captured → [refunded | cancelled] └─ reverse → reversed ``` ## Monitoring State Changes * **Webhooks**: Subscribe to `transaction.*` events (created, authorized, captured, adjusted, reversed, refunded, settled). See [Available Events](/docs/webhooks/available-events). * **iOS SDK**: Inspect `TransactionResponse.transaction.status` to update UI immediately. ```swift switch transaction.status { case .approved, .captured: // Success case .declined: // Inform user case .error: // Retry or escalate default: // Handle intermediate states } ``` ## Error Handling & Retries ```swift func processPaymentWithRetry(maxRetries: Int = 3) async throws { var attempts = 0 while attempts < maxRetries { do { let response = try await KoardMerchantSDK.shared.sale(...) // Success return } catch { attempts += 1 if attempts >= maxRetries { throw error } try await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(attempts))) * 1_000_000_000) } } } ``` ## Best Practices * **Choose the right entry point**: Use sales for immediate capture; use preauth when totals may change. * **Store transaction IDs**: Needed for every follow-up operation and webhook reconciliation. * **Keep breakdowns accurate**: Supply tax, tip, and surcharge data with the latest values to keep reports aligned. * **Use idempotency keys**: Provide `transaction_id` or `event_id` to guard against duplicate requests. * **Monitor via webhooks**: Use asynchronous events to update order states reliably. ## Troubleshooting Checklist * **Transaction not found**: Confirm the transaction belongs to your Koard account and that the ID is spelled correctly. * **Invalid state transition**: Verify the current state (`authorized`, `captured`, etc.) before calling a new operation. * **Amount validation errors**: Capture/Refund amounts cannot exceed the available balances; partial operations require explicit amounts. * **Tap to Pay issues**: Ensure the device has Developer Mode enabled, an active Sandbox Apple Account, and that `prepare()` was called. * **SDK errors**: Authenticate with `login()`, set an active location, and handle `KoardMerchantSDKError` cases explicitly. ## See Also * [Sale](/docs/payments/methods/sale) * [Preauth](/docs/payments/methods/preauth) * [Capture](/docs/payments/methods/capture) * [Incremental Auth](/docs/payments/methods/incremental-auth) * [Tip Adjust](/docs/payments/methods/tip-adjust) * [Reverse](/docs/payments/methods/reverse) * [Refund](/docs/payments/methods/refund) * [Running Payments](/docs/setting-up-the-ios-sdk/running-payments) * [Webhooks – Available Events](/docs/webhooks/available-events) # Running Batches Learn how to create, manage, and process batches with Koard's payment platform. [Get started with Koard](/docs/getting-started-with-koard/introduction) Koard provides comprehensive batch processing capabilities for managing transactions and settlements. Whether you choose to let Koard handle batches automatically or manage them yourself, this guide covers all the essential operations and best practices. **What you learn** In this guide, you'll learn: * How to choose between Koard-managed and self-managed batch processing * Core batch operations: opening, closing, and editing batches * Required fields for self-managed batch processing * Webhook integration for real-time batch updates * Processor-specific considerations and constraints * Best practices for batch management and error handling ## Before you begin This guide covers batch processing operations in Koard. For a better understanding of batch concepts, see our [Batch and Settlements overview](/docs/batch-and-settlements/introduction). If you're ready to start processing payments, see our [Running Payments guide](/docs/setting-up-the-ios-sdk/running-payments). ## Two Batch Processing Approaches Koard supports two distinct approaches for batch processing based on your technical requirements and business model. ### Via Koard (Recommended) **Best for**: Tap to Pay terminals with specific gateway configurations Koard can handle batches entirely for a MID/TID combination. This approach is recommended if you have specific terminals configured on the gateway for Tap to Pay. **Key benefits:** * **Automatic Management**: Koard handles all batch operations automatically * **Status Tracking**: Automatic transaction status tracking and reconciliation * **Error Handling**: Built-in retry logic and error recovery * **Webhook Integration**: Real-time updates for all batch events ### Self-Managed Batches **Best for**: Enterprise ISVs and PSPs with existing batch infrastructure If you prefer to manage batches yourself, you'll need to handle the core batch operations and maintain the required transaction data. **Key requirements:** * **Required Fields**: Maintain all necessary transaction data * **Webhook Integration**: Process transaction events for batch management * **Sequential Ordering**: Maintain proper batch number sequencing * **Error Handling**: Implement retry logic for failed operations ## Core Batch Operations Koard provides several core functions that you can apply to batches and transactions: | Operation | Description | When to Use | API Endpoint | | ---------------- | ------------------------------------------ | ------------------------------------------ | ----------------------------------- | | **List Batches** | Retrieve paginated list of batches | Viewing batch history, filtering by status | `GET /v1/batches` | | **Get Batch** | Retrieve specific batch details | View batch status and transactions | `GET /v1/batches/{batch_id}` | | **Open Batch** | Create a new batch for transactions | Start of business day or when needed | `POST /v1/batches/open` | | **Close Batch** | Finalize and submit batch for processing | End of business day or when ready | `POST /v1/batches/{batch_id}/close` | | **Edit Batch** | Add or remove transactions from open batch | Before closing the batch | `PUT /v1/batches/{batch_id}/edit` | ### Listing Batches ```bash GET /v1/batches?limit=50&offset=0&statuses=open ``` **Query Parameters:** * `account_id` (optional): Filter by owning account * `terminal_id` (optional): Filter by terminal ID * `statuses` (optional): Filter by batch lifecycle statuses — accepts multiple values (open, closed, submitted, accepted, partially\_accepted, rejected, cancelled) * `processor_config_id` (optional): Filter by processor config ID * `limit` (optional): Maximum items per page (1-500, default: 50) * `offset` (optional): Number of items to skip (default: 0) ### Getting a Batch ```bash GET /v1/batches/{batch_id}?include_transactions=true ``` **Query Parameters:** * `include_transactions` (optional): If true, embed transactions with the batch (default: false) ### Opening a Batch ```bash POST /v1/batches/open { "terminal_id": "tid_987654321", "processor_batch_id": "batch_001" } ``` **Required Fields:** * `terminal_id`: The terminal identifier * `processor_batch_id`: The batch ID for the processor (must be unique per MID/TID combination) ### Closing a Batch ```bash POST /v1/batches/{batch_id}/close ``` No request body is required. The batch must be in an `open` status to be closed. ### Editing an Open Batch Only open batches can be edited. ```bash PUT /v1/batches/{batch_id}/edit { /*"added_transactions": ["17279734-fa7b-4f26-a945-19a8a98b6258"], "removed_transactions": [],*/ "processor_batch_id": "6" } ``` **Body Fields (all optional):** * `processor_batch_id`: The processor batch ID * `added_transactions`: List of transaction IDs to add to the batch * `removed_transactions`: List of transaction IDs to remove from the batch ## Self-Managed Batch Requirements If you choose to run batches yourself, you'll need to maintain these essential fields for each transaction: ### Required Transaction Fields | Field | Description | Example | | -------------------------------- | --------------------------------------- | --------------------------------- | | **Approval Code** | Authorization approval code | `123456` | | **Response Code** | Transaction response code | `00` | | **Transaction Identifier** | Unique transaction ID | `txn_abc123def456` | | **Local Date/Time** | Transaction timestamp | `2024-01-15T14:30:00Z` | | **Amounts** | Auth, settled, tip, surcharge, cashback | `{"auth": 1000, "settled": 1000}` | | **ACI/Void/Reversal Indicators** | Transaction type indicators | `{"aci": "Y", "void": "N"}` | ### Webhook Integration Every transaction processed through Koard triggers a webhook event. Upon delivery, you'll receive: * **Transaction Data**: All required fields for batch processing * **TLV Tags**: General TLV tags from the payment device * **Status Information**: Real-time transaction status updates * **Batch Context**: Information about which batch the transaction belongs to ```json { "event": "transaction.processed", "data": { "transactionId": "txn_abc123def456", "approvalCode": "123456", "responseCode": "00", "amount": { "auth": 1000, "settled": 1000, "tip": 0 }, "timestamp": "2024-01-15T14:30:00Z", "batchId": "batch_xyz789", "tlvTags": { "aci": "Y", "void": "N" } } } ``` ## Batch Lifecycle Management ### Batch Statuses A batch progresses through several statuses during its lifecycle: | Status | Description | Transitions To | | ----------------------- | -------------------------------------------------- | --------------------------------------- | | **open** | Batch is open and accepting transactions | closed | | **closed** | Batch has been closed and submitted for settlement | submitted, rejected | | **submitted** | Batch has been submitted to the processor | accepted, partially\_accepted, rejected | | **accepted** | Batch has been fully accepted by the processor | (terminal state) | | **partially\_accepted** | Some transactions were accepted, others rejected | (terminal state) | | **rejected** | Batch was rejected by the processor | (terminal state) | | **cancelled** | Batch was cancelled before settlement | (terminal state) | ### Automatic Transaction Addition When a batch is open on a terminal: * **Captured Transactions**: Automatically added to the open batch * **Refunds**: Automatically added to the open batch * **Real-time Updates**: Webhook events provide immediate status updates ### Batch Closure Process 1. **Close at Any Time**: Batches can be closed at any point (typically end of business) 2. **Constraint**: You can only close an open batch 3. **Rejected Batches**: If a batch is rejected or partially approved: * Add all rejected transactions to a new open batch * Close the new batch again * Maintain batch number sequential order ### Editing Open Batches You can edit an open batch by: * **Adding Transactions**: Include new transactions before closing * **Removing Transactions**: Remove transactions if needed * **Constraint**: Only open batches can be edited ## Processor-Specific Considerations ### TSYS Constraints TSYS has specific batch ID requirements that affect batch management: | Constraint | Description | Impact | | ------------------ | --------------------------------- | ------------------------------------ | | **Batch ID Range** | Numbered batch IDs from 1-999 | Limited batch ID availability | | **Sliding Window** | 5-day sliding window for reuse | Batch IDs can be reused after 5 days | | **Uniqueness** | Unique per MID/TID combination | Prevents duplicate batch IDs | | **Error Handling** | Reusing batch ID results in error | Requires proper ID management | ### Batch ID Management * **Default Behavior**: Koard automatically configures batch IDs * **Override Option**: You can override batch IDs when running your own batches * **Webhook Integration**: Set batch ID via webhook when managing batches yourself * **Sequential Order**: Maintain proper batch number sequencing ### Splitting Batches **Not Recommended**: Splitting batches is not recommended due to various constraints imposed by processors like TSYS, Fiserv, and Elavon. **Best Practice**: Each MID/TID should have one batch per day to avoid potential duplicate batch ID issues. ## Error Handling and Recovery ### Failed/Rejected Batches When batches fail or are rejected: 1. **Status Information**: Koard returns detailed status information 2. **Failure Details**: Specific information about what failed 3. **Retry Logic**: Automatic retry mechanisms for recoverable errors 4. **Manual Approval**: Rejected batches can be manually approved via the portal on TSYS ### Monitoring and Alerts * **Real-time Status**: Monitor batch processing status in real-time * **Error Notifications**: Receive alerts for batch failures * **Webhook Events**: Subscribe to batch status change events * **Dashboard Monitoring**: Visual tracking of batch operations ## Best Practices ### Batch Timing * **End of Business**: Close batches at the end of each business day * **Peak Hours**: Avoid processing batches during peak transaction times * **Timezone Considerations**: Consider your merchant's timezone for batch windows * **Business Hours**: Process batches during business hours for faster support ### Error Handling * **Retry Logic**: Implement retry logic for failed batch operations * **Partial Failures**: Handle cases where some transactions in a batch fail * **Monitoring**: Set up alerts for batch processing failures * **Recovery Procedures**: Have clear procedures for handling rejected batches ### Performance Optimization * **Batch Size**: Optimize batch sizes for your transaction volume * **Sequential Processing**: Maintain proper batch number sequencing * **Resource Management**: Monitor system resources during batch processing * **Webhook Processing**: Ensure reliable webhook event processing ## See also This wraps up the running batches guide. See the links below for related information: * [Batch and Settlements Overview](/docs/batch-and-settlements/overview) - High-level batch concepts * [Running Payments](/docs/setting-up-the-ios-sdk/running-payments) - Payment processing fundamentals * [Setting up Webhooks](/docs/webhooks/setting-up-webhooks) - Webhook configuration * [Available Events](/docs/webhooks/available-events) - Webhook event reference # Online PIN Validation Koard supports **online PIN only**. When a PIN is required, it is validated in real time against the issuer during the authorization request — Koard does not support offline PIN, where the PIN would be verified locally on the device without issuer involvement. The encrypted PIN block is forwarded to the acquirer and issuer as part of the authorization. The issuer validates the PIN against the cardholder's account and will hard decline the transaction if the PIN is incorrect — this decision is made entirely by the issuer and acquirer, not by Koard. When a cardholder enters a PIN during a Tap to Pay on iPhone transaction, Apple encrypts the PIN data before it leaves the device. Koard handles the full decryption and validation flow online — the encrypted PIN never passes through your application unprotected. ## How It Works 1. The iOS SDK captures the cardholder's PIN and returns encrypted cardholder data, encrypted PIN data, and a transaction ID to Koard. 2. Koard calls Apple's Proximity Payment Service to exchange the encrypted data for single-use decryption keys. 3. Apple returns keys scoped to that transaction. Koard validates and decrypts the data, then forwards the PIN block to the payment processor in the authorization request. ## Supported Scenarios | Scenario | PIN Captured | Notes | |----------|-------------|-------| | Cardholder data only | No | Standard contactless — no PIN required | | Cardholder data + PIN | Yes | PIN collected inline during the tap | | Cardholder data + PIN token | Yes | PIN collected and tokenized | | PIN fallback | Yes | Used when the card requires PIN but cannot use standard flow | # Setting up Webhooks Learn how to configure and use webhooks to receive real-time updates from Koard's payment platform. [Get started with Koard](/docs/getting-started-with-koard/introduction) Koard allows you to add URLs that receive POST requests when specific events occur in your payment system. Each endpoint can be configured to receive a specific set of events, enabling real-time integration with your applications. **What you learn** In this guide, you'll learn: * How to set up webhook endpoints in Koard * How to configure event subscriptions and filters * How to test and verify webhook functionality * How to monitor webhook delivery and troubleshoot issues * Best practices for webhook security and reliability ## Before you begin This guide covers webhook configuration and management in Koard. For a better understanding of available events, see our [Available Events guide](/docs/webhooks/available-events). If you're ready to start processing payments, see our [Running Payments guide](/docs/setting-up-the-ios-sdk/running-payments). ## What are Webhooks? Webhooks are HTTP callbacks that Koard sends to your application when specific events occur. This allows you to receive real-time notifications about payment events, transaction updates, and other important changes without constantly polling our APIs. **Key benefits:** * **Real-time Updates**: Get instant notifications about payment events * **Automated Processing**: Trigger automated workflows based on events * **Reduced Polling**: Eliminate the need to constantly poll for updates * **Better User Experience**: Provide immediate feedback to users ## Setting Up Webhook Endpoints ### Access the Developer Portal To set up a new webhook endpoint, navigate to one of the following URLs based on your environment: | Environment | URL | | -------------- | ------------------------------------- | | **UAT** | | | **Production** | | ![webhooks-1](/webhooks-1.png) _Koard Developer Portal - Webhook Configuration_ ### Create a New Endpoint 1. **Navigate to Add Endpoint**: Click "Add Endpoint" on the right side of the developer portal page 2. **Configure HTTPS Endpoint**: Enter your HTTPS endpoint URL that will receive POST requests 3. **Select Events**: Choose which events you want to subscribe to 4. **Save Configuration**: Complete the setup process ![webhooks-2](/webhooks-2.png) _Adding a new webhook endpoint in the Koard Developer Portal_ **Important**: Currently, webhooks can only be managed via the Portal, but API management is on the roadmap for creating and managing endpoints and event filters. ### Webhook Endpoint Requirements Your webhook endpoint must meet these requirements: * **HTTPS Only**: All webhook endpoints must use HTTPS * **POST Method**: Endpoints must accept POST requests * **JSON Payload**: Events are sent as JSON in the request body * **Quick Response**: Return a 2xx status code quickly (within 30 seconds) ## Webhook Configuration ### Event Selection Choose which events you want to receive based on your integration needs. If you don't specify any event types, by default your endpoint will receive all events, regardless of type. This can be helpful for getting started and testing, but we recommend selecting specific events for production. | Event Category | Description | Common Events | | ---------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Transaction Events** | Transaction lifecycle events | `transaction.create`, `transaction.authorize`, `transaction.sale`, `transaction.capture`, `transaction.increment`, `transaction.tip_adjust`, `transaction.reverse`, `transaction.cancel`, `transaction.refund` | | **Account Events** | Account management events | `account.created`, `account.updated`, `account.blocked`, `account.unblocked`, `account.deleted` | | **Terminal Events** | Terminal configuration events | `terminal.created`, `terminal.updated`, `terminal.blocked`, `terminal.unblocked`, `terminal.deleted` | | **Location Events** | Location management events | `location.created`, `location.updated`, `location.blocked`, `location.unblocked`, `location.deleted` | | **Batch Events** | Batch processing and settlement | `batch.opened`, `batch.submitted`, `batch.accepted`, `batch.partially_accepted`, `batch.rejected`, `batch.edited` | | **API Key Events** | API key management events | `api_key.created`, `api_key.revoked`, `api_key.reinstated`, `api_key.deleted` | | **Credential Events** | Merchant credential management events | `credential.created`, `credential.blocked`, `credential.unblocked`, `credential.deleted` | For a complete list of available events with schemas and examples, see our [Available Events guide](/docs/webhooks/available-events). ### Webhook Headers Koard uses industry-standard webhook headers powered by Svix for maximum compatibility: ```plaintext Content-Type: application/json svix-id: msg_p5jXN8AQM9LWM0D4loKWxJek svix-timestamp: 1614265330 svix-signature: v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE= User-Agent: Svix-Webhooks/1.0 ``` **Header Descriptions:** * `svix-id`: Unique message identifier for idempotency * `svix-timestamp`: Unix timestamp when the webhook was sent (for replay attack prevention) * `svix-signature`: HMAC signature for verifying webhook authenticity * `Content-Type`: Always `application/json` ### Retry Policy Koard implements a robust retry policy with exponential backoff for failed webhook deliveries: | Retry Attempt | Delay | Time from First Attempt | | ------------- | ---------- | ----------------------- | | 1st | Immediate | 0 seconds | | 2nd | 5 seconds | 5 seconds | | 3rd | 5 minutes | \~5 minutes | | 4th | 30 minutes | \~35 minutes | | 5th | 2 hours | \~2 hours 35 minutes | | 6th | 5 hours | \~7 hours 35 minutes | | 7th | 10 hours | \~17 hours 35 minutes | | 8th | 10 hours | \~27 hours 35 minutes | **Key Points:** * **Response Timeout**: 15 seconds per attempt * **Success Criteria**: 2xx status code (200-299) indicates success * **Failure Criteria**: Any other status code or timeout triggers a retry * **Endpoint Disabling**: After 5 days of consecutive failures, endpoints are automatically disabled * **Manual Recovery**: Use the dashboard to recover or resend failed messages **Note**: When responding to webhooks, return a 2xx status code quickly. Process complex workflows asynchronously to avoid timeouts. ## Testing Your Webhook ### Create a Test Endpoint To test your webhook submission, create a webhook that can accept all transaction events and use the Koard SDK to process some transactions: ```javascript // Express.js example webhook endpoint const express = require('express'); const app = express(); // Use express.raw so req.body is the exact bytes Svix signed. Do NOT JSON-parse // and re-stringify the body — that can change the bytes and fail verification. app.post('/webhooks/koard', express.raw({ type: 'application/json' }), (req, res) => { const payload = req.body; // raw Buffer, exactly as received const headers = { 'svix-id': req.headers['svix-id'], 'svix-timestamp': req.headers['svix-timestamp'], 'svix-signature': req.headers['svix-signature'] }; try { // Verify webhook signature using Svix library const wh = new Webhook(process.env.KOARD_WEBHOOK_SECRET); const verifiedPayload = wh.verify(payload, headers); // Process webhook after verification processWebhook(verifiedPayload); res.status(200).send('OK'); plaintext } catch (err) { console.error('Webhook verification failed:', err); res.status(400).send('Invalid signature'); } }); function processWebhook(payload) { // The body IS the resource object (flat) — there is no { event, data } envelope, // and the event type is not in the body. Subscribe each endpoint to specific // event types (developer portal), and/or branch on payload fields. For a // transaction endpoint, route on transaction_type and read status. const txn = payload; switch (txn.transaction_type) { case 'sale': handleSale(txn); break; case 'capture': handleCapture(txn); break; // Add more: 'auth', 'refund', 'reverse', 'tip_adjust', 'incremental_auth' default: console.log('Unhandled transaction type:', txn.transaction_type); } } app.listen(3000, () => { console.log('Webhook endpoint listening on port 3000'); }); ``` ### Test with Koard SDK Use the Koard SDK to process transactions and trigger webhook events: ```swift // iOS SDK example import KoardMerchantSDK // Process a test transaction let paymentRequest = PaymentRequest( amount: 1000, // $10.00 currency: .USD, description: "Test webhook transaction" ) KoardMerchantSDK.shared.processPayment(paymentRequest) { result in switch result { case .success(let response): print("Payment successful: (response.transactionId)") // This will trigger webhook events case .failure(let error): print("Payment failed: (error)") } } ``` ![webhooks-4](/webhooks-4.png) _Testing webhook functionality with Koard SDK transactions_ ## Webhook Security ### Signature Verification **Why Verify Webhooks?** Webhook signatures let you verify that webhook messages are actually sent by Koard and not a malicious actor. This prevents: * **Spoofing Attacks**: Malicious actors sending fake webhooks * **Replay Attacks**: Old webhooks being resent * **Man-in-the-Middle Attacks**: Webhooks being intercepted and modified For a detailed explanation, see [why you should verify webhooks](https://docs.svix.com/receiving/verifying-payloads/why). ### Using Svix Libraries (Recommended) Koard uses [Svix](https://www.svix.com/) for webhook delivery, which provides official libraries for easy verification: ```javascript Node.js const { Webhook } = require("svix"); const secret = process.env.KOARD_WEBHOOK_SECRET; // Get from Koard dashboard // Use express.raw so req.body is the exact bytes Svix signed. Do NOT JSON-parse // and re-stringify the body — that can change the bytes and fail verification. app.post('/webhooks/koard', express.raw({ type: 'application/json' }), (req, res) => { const payload = req.body; // raw Buffer, exactly as received const headers = { 'svix-id': req.headers['svix-id'], 'svix-timestamp': req.headers['svix-timestamp'], 'svix-signature': req.headers['svix-signature'] }; try { const wh = new Webhook(secret); const verifiedPayload = wh.verify(payload, headers); // Webhook is verified - process it processWebhook(verifiedPayload); res.status(200).json({ success: true }); } catch (err) { console.error('Webhook verification failed:', err.message); res.status(400).json({ error: 'Invalid signature' }); } }); ``` ```python Python from svix.webhooks import Webhook import os webhook_secret = os.environ['KOARD_WEBHOOK_SECRET'] @app.route('/webhooks/koard', methods=['POST']) def webhook_handler(): payload = request.get_data() headers = { 'svix-id': request.headers.get('svix-id'), 'svix-timestamp': request.headers.get('svix-timestamp'), 'svix-signature': request.headers.get('svix-signature') } try: wh = Webhook(webhook_secret) msg = wh.verify(payload, headers) # Webhook is verified - process it process_webhook(msg) return jsonify({'success': True}), 200 except Exception as e: print(f'Webhook verification failed: {e}') return jsonify({'error': 'Invalid signature'}), 400 ``` ```go Go import ( "encoding/json" svix "github.com/svix/svix-webhooks/go" ) func webhookHandler(w http.ResponseWriter, r *http.Request) { webhookSecret := os.Getenv("KOARD_WEBHOOK_SECRET") payload, _ := ioutil.ReadAll(r.Body) headers := http.Header{} headers.Set("svix-id", r.Header.Get("svix-id")) headers.Set("svix-timestamp", r.Header.Get("svix-timestamp")) headers.Set("svix-signature", r.Header.Get("svix-signature")) wh, _ := svix.NewWebhook(webhookSecret) err := wh.Verify(payload, headers) if err != nil { w.WriteHeader(http.StatusBadRequest) return } // Verified — decode the flat body before routing on its fields var txn map[string]interface{} if err := json.Unmarshal(payload, &txn); err != nil { w.WriteHeader(http.StatusBadRequest) return } processWebhook(txn) w.WriteHeader(http.StatusOK) } ``` For more examples in other languages (Ruby, PHP, Java, Rust, Kotlin, C#), see the [Svix webhook verification documentation](https://docs.svix.com/receiving/verifying-payloads/how). ### Manual Verification (Advanced) If you prefer to verify signatures manually without using the Svix library: ```javascript const crypto = require('crypto'); function verifyWebhookSignature(payload, headers, secret) { const timestamp = headers['svix-timestamp']; const signature = headers['svix-signature']; const msgId = headers['svix-id']; // Check timestamp to prevent replay attacks (optional but recommended) const timestampSeconds = parseInt(timestamp); const now = Math.floor(Date.now() / 1000); if (Math.abs(now - timestampSeconds) > 300) { // 5 minute tolerance throw new Error('Webhook timestamp too old'); } // Create the signed content const signedContent = ${msgId}.${timestamp}.${payload}; // Compute expected signature const expectedSignature = crypto .createHmac('sha256', secret) .update(signedContent, 'utf8') .digest('base64'); // Extract signature from header (format: "v1,signature") const signatureParts = signature.split(','); const headerSignature = signatureParts[1]; // Compare signatures return crypto.timingSafeEqual( Buffer.from(headerSignature), Buffer.from(expectedSignature) ); } ``` **Important**: Use the **raw payload body** for verification, not the parsed JSON. Different JSON parsers may produce different string representations. ### HTTPS Requirements * **HTTPS Only**: Webhook endpoints must use HTTPS * **Valid SSL Certificates**: SSL certificates must be valid and trusted * **No Self-Signed Certificates**: Self-signed certificates are not allowed * **TLS 1.2+**: Minimum TLS version 1.2 required ### Security Best Practices * **Verify Signatures**: Always verify webhook signatures * **Use HTTPS**: Only accept webhooks over HTTPS * **Validate Payloads**: Validate webhook payloads before processing * **Rate Limiting**: Implement rate limiting to prevent abuse * **Logging**: Log all webhook events for debugging and security ## Monitoring and Logging ### Webhook Logs All webhooks come with detailed logging around deliverability, attempts, and activity history: 1. **Access Logs**: Go to the Logs tab in the developer portal 2. **Filter Events**: Filter by event type and message content 3. **View Details**: Click on individual events to see delivery details 4. **Monitor Status**: Track delivery status and retry attempts ![webhooks-3](/webhooks-3.png) _Webhook logs and delivery monitoring in the Koard Developer Portal_ ### Delivery Status Monitor webhook delivery status: | Status | Description | | ------------- | --------------------------------------------------- | | **Delivered** | Webhook successfully delivered and acknowledged | | **Pending** | Webhook delivery in progress or scheduled for retry | | **Failed** | Webhook delivery failed after all retry attempts | ### Troubleshooting Common webhook issues and solutions: **Not Using the Raw Payload Body** This is the most common issue. When generating the signed content, Koard uses the raw string body of the message payload. If you convert JSON payloads into strings using methods like `JSON.stringify()`, different implementations may produce different string representations, leading to verification failures. **Solution:** Use the raw request body exactly as received. In Express.js: use `express.raw()` or access `req.body` before JSON parsing. **Missing or Wrong Secret Key** Using an incorrect or outdated webhook secret will cause all verifications to fail. **Solution:** Get your webhook secret from the Koard Developer Portal. Remember that secrets are unique to each endpoint. **Timestamp Too Old** Webhooks with timestamps older than 5 minutes are rejected to prevent replay attacks. **Solution:** Ensure your server's system time is synchronized (use NTP). **Sending Wrong Response Codes** When Koard receives a 2xx status code (200-299), it's interpreted as successful delivery, even if your response payload indicates a failure. **Solution:** Return appropriate status codes: * `200` for successful processing * `400-499` for client errors (will not retry) * `500-599` for server errors (will retry) **Response Timeouts** Webhooks that don't respond within 15 seconds are considered failed and will be retried. **Solution:** Respond immediately with `200 OK` and process webhooks asynchronously: ```javascript app.post('/webhooks/koard', async (req, res) => { // Verify signature const wh = new Webhook(secret); const payload = wh.verify(req.body, req.headers); // Respond immediately res.status(200).send('OK'); // Process asynchronously queue.add('process-webhook', payload); }); ``` ### Failure Recovery **Re-enable a Disabled Endpoint** If all attempts to a specific endpoint fail for 5 days, the endpoint will be automatically disabled. **To re-enable:** 1. Go to the Koard Developer Portal 2. Navigate to Webhooks 3. Find the disabled endpoint 4. Click "Enable Endpoint" **Recovering Failed Messages** **Single Message Recovery:** 1. Find the message in the Developer Portal 2. Click the options menu next to the attempt 3. Click "Resend" to retry delivery **Bulk Message Recovery:** 1. Go to the endpoint details page 2. Click "Options" → "Recover Failed Messages" 3. Choose a time window to recover from 4. All failed messages in that window will be resent **Recovery from Specific Timestamp:** 1. Find any message near your desired recovery point 2. Click the options menu on that message 3. Select "Replay all failed messages since this time" ## Best Practices ### Endpoint Design * **Quick Response**: Return 2xx status codes quickly (within 15 seconds) * **Respond First, Process Later**: Acknowledge receipt immediately, then process asynchronously * **Disable CSRF Protection**: Disable CSRF checks for webhook endpoints * **Use Raw Body**: Access raw request body for signature verification * **Error Handling**: Implement proper error handling and logging ### Event Processing - Idempotency **Why Idempotency Matters:** Webhooks may be delivered more than once due to network issues, retries, or recovery operations. Your endpoint must handle duplicate events gracefully. **Implementation:** Use the `svix-id` header (message ID) to track processed events: ```javascript const processedEvents = new Set(); // In production, use a database app.post('/webhooks/koard', (req, res) => { const messageId = req.headers['svix-id']; // Check if we've already processed this event if (processedEvents.has(messageId)) { console.log('Duplicate event ignored:', messageId); return res.status(200).send('OK'); // Return success for duplicates } // Verify and process webhook const wh = new Webhook(secret); const payload = wh.verify(req.body, req.headers); // Mark as processed BEFORE processing to prevent race conditions processedEvents.add(messageId); // Process the webhook processWebhook(payload); res.status(200).send('OK'); }); ``` **Best Practices:** * Store message IDs in a database (Redis, PostgreSQL, etc.) * Set expiration on stored IDs (e.g., 7 days) to prevent infinite growth * Use database transactions to ensure idempotency * Return `200 OK` for duplicate events (they're already processed) ### Event Ordering **Important:** Delivery is best-effort ordered — events may arrive out of sequence due to network conditions, retries, or processing delays. **Order by the payload's `created_at`.** Koard is event-sourced: a transaction's lifecycle (`authorize` → `incremental_auth` → `capture` → `refund` → …) is a series of events that **share one `transaction_id`**, each with its own `event_id` and its own `created_at`. That `created_at` is assigned as `max(now, previous_event + 1)` in **milliseconds**, so within a `transaction_id` it is **strictly increasing** — a reliable per-event ordering key. Don't use the `svix-timestamp` header for ordering — it's the delivery-_attempt_ time and changes on retries. **Persist atomically** so out-of-order and concurrent deliveries can't regress state: upsert and only advance when the incoming `created_at` is greater than the stored watermark (a single statement, no read-then-write race). Deduplicate exact retries on `svix-id` (see the Idempotency section above). ```javascript app.post('/webhooks/koard', express.raw({ type: 'application/json' }), async (req, res) => { const wh = new Webhook(secret); const txn = wh.verify(req.body, { 'svix-id': req.headers['svix-id'], 'svix-timestamp': req.headers['svix-timestamp'], 'svix-signature': req.headers['svix-signature'], }); // created_at is a per-event, strictly increasing (ms) watermark for a // transaction_id. Upsert atomically and only advance when this event is // newer, so out-of-order or concurrent deliveries can't regress the state. await db.query( INSERT INTO transactions (transaction_id, status, payload, last_created_at) VALUES ($1, $2, $3, $4) ON CONFLICT (transaction_id) DO UPDATE SET status = EXCLUDED.status, payload = EXCLUDED.payload, last_created_at = EXCLUDED.last_created_at WHERE EXCLUDED.last_created_at > transactions.last_created_at, [txn.transaction_id, txn.status, txn, txn.created_at], ); res.status(200).send('OK'); }); ``` If you need strict, guaranteed ordering, use [Svix FIFO endpoints](https://docs.svix.com/advanced-endpoints/fifo-endpoints). Alternatively, treat the webhook as a signal and re-fetch the authoritative transaction from the Koard API before acting. ### Security * **Always Verify Signatures**: Never skip signature verification in production * **Use Svix Libraries**: Use official Svix libraries for proper verification * **HTTPS Only**: Use HTTPS for all webhook endpoints (required by Koard) * **Validate Timestamps**: Reject webhooks with old timestamps (>5 minutes) * **Secret Management**: Securely store webhook secrets (use environment variables) * **Rotate Secrets**: Periodically rotate webhook secrets * **Monitor Failures**: Alert on repeated verification failures (potential attack) ### Monitoring and Alerting * **Track Delivery**: Monitor webhook delivery success rates * **Set Up Alerts**: Alert on repeated failures or timeouts * **Log Everything**: Log all webhook events for debugging * **Monitor Processing Time**: Ensure webhooks process within timeout * **Dashboard Review**: Regularly review webhook logs in Koard Developer Portal ## See also This wraps up the webhook setup guide. See the links below for related information: * [Available Events](/docs/webhooks/available-events) - Complete list of webhook events * [Running Payments](/docs/setting-up-the-ios-sdk/running-payments) - Payment processing fundamentals * [Batch and Settlements](/docs/batch-and-settlements/overview) - Batch processing overview * [Getting Started](/docs/introduction) - Koard platform introduction # Apple Best Practices and Guidelines Comprehensive best practices for developing Tap to Pay on iPhone applications with Apple's ProximityReader framework and Koard SDK. ## Overview Building Tap to Pay on iPhone applications requires adherence to Apple's strict guidelines and best practices to ensure security, usability, and App Store approval. This guide covers essential practices for UI/UX design, security implementation, and the dual review process required for Tap to Pay applications. ## Apple's Dual Review Process Apple requires two distinct review processes for Tap to Pay on iPhone applications: ### 1. Tap to Pay Review The Tap to Pay review is a comprehensive security and compliance assessment focused on: * **Security Implementation**: Ensuring safe and secure payment processing * **Merchant Safety**: Verifying that merchants can safely accept payments on their devices * **Branding and Messaging**: Reviewing UI/UX workflows specifically around Tap to Pay functionality * **Payment Flow Design**: Evaluating the complete payment experience from initiation to completion * **Error Handling**: Assessing how payment errors and edge cases are managed * **Data Protection**: Verifying compliance with Apple's data handling requirements ### 2. App Store Review The standard App Store review process covers: * **General App Functionality**: Core app features and user experience * **TestFlight Distribution**: Ability to share the app via TestFlight for testing * **App Store Submission**: Final approval for public distribution * **Guideline Compliance**: Adherence to App Store Review Guidelines **Important**: Both reviews must be passed successfully before your app can be distributed through the App Store. ## User Experience Best Practices ### Feedback and User Actions Apple emphasizes providing clear feedback when users take explicit actions. This is crucial for Tap to Pay applications: #### Provide Immediate Feedback ```swift // Launch Tap to Pay screen with clear visual feedback func presentTapToPayScreen() { // Show loading indicator showProgressIndicator() // Launch Tap to Pay interface Task { do { let reader = try await ProximityReader.readerIdentifier // Present Tap to Pay UI presentTapToPayInterface(reader: reader) } catch { // Handle error appropriately handleTapToPayError(error) } } } ``` #### Progress Indicators During prepare() Calls ```swift func preparePaymentReader() { // Show progress indicator while prepare() completes showProgressIndicator(message: "Preparing payment reader...") Task { do { // This is a long-running operation let reader = try await ProximityReader.readerIdentifier await prepareReader(reader) // Hide progress indicator hideProgressIndicator() } catch { hideProgressIndicator() handlePreparationError(error) } } } ``` ### Error Handling Best Practices #### Avoid Modal Alerts for Background Operations **❌ Incorrect Approach:** ```swift // Never show modal alerts during background prepare() calls func prepareReaderInBackground() { Task { do { let reader = try await ProximityReader.readerIdentifier await prepareReader(reader) } catch { // DON'T: Show modal alert during app launch DispatchQueue.main.async { let alert = UIAlertController(title: "Error", message: "Failed to prepare reader", preferredStyle: .alert) self.present(alert, animated: true) } } } } ``` **✅ Correct Approach:** ```swift // Use non-modal feedback for background operations func prepareReaderInBackground() { Task { do { let reader = try await ProximityReader.readerIdentifier await prepareReader(reader) } catch { // Use banner notification or other non-modal method DispatchQueue.main.async { self.showBannerNotification( message: "Payment reader preparation failed. Please try again.", type: .error ) } } } } ``` #### Appropriate Error Display Methods ```swift enum ErrorDisplayMethod { case bannerNotification // For background operations case modalAlert // For user-initiated actions case inlineMessage // For form validation case toastNotification // For non-critical errors } func displayError(_ error: Error, method: ErrorDisplayMethod) { switch method { case .bannerNotification: showBannerNotification(message: error.localizedDescription) case .modalAlert: showModalAlert(title: "Error", message: error.localizedDescription) case .inlineMessage: showInlineError(message: error.localizedDescription) case .toastNotification: showToast(message: error.localizedDescription) } } ``` ## ProximityReader Framework Best Practices ### Reader Management Based on Apple's [ProximityReader documentation](https://developer.apple.com/documentation/proximityreader), proper reader management is essential: ```swift import ProximityReader class TapToPayManager: ObservableObject { @Published var isReaderAvailable = false @Published var readerIdentifier: String? func checkReaderAvailability() async { do { let identifier = try await ProximityReader.readerIdentifier await MainActor.run { self.readerIdentifier = identifier self.isReaderAvailable = true } } catch { await MainActor.run { self.isReaderAvailable = false self.readerIdentifier = nil } // Handle error appropriately handleReaderError(error) } } } ``` ### Secure Payment Processing ```swift class SecurePaymentProcessor { func processPayment(amount: Decimal, currency: String) async throws -> PaymentResult { // Verify reader availability before processing guard try await ProximityReader.readerIdentifier != nil else { throw PaymentError.readerNotAvailable } // Process payment securely let paymentData = try await capturePaymentData(amount: amount, currency: currency) // Send to secure backend return try await sendPaymentToBackend(paymentData) } private func capturePaymentData(amount: Decimal, currency: String) async throws -> PaymentData { // Implementation for capturing payment data securely // This should follow Apple's security guidelines } } ``` ## Security Best Practices ### Data Protection and Privacy ```swift class PaymentDataManager { // Never store sensitive payment data private let keychain = Keychain(service: "com.koard.payments") func storeNonSensitiveData(_ data: PaymentMetadata) { // Only store non-sensitive metadata keychain["payment_id"] = data.paymentId keychain["merchant_id"] = data.merchantId // Never store card numbers, CVV, or other sensitive data } func processPaymentSecurely(_ paymentData: PaymentData) async throws { // All sensitive processing should happen on secure backend let encryptedData = try encryptPaymentData(paymentData) try await sendToSecureBackend(encryptedData) } } ``` ### Entitlement Verification ```swift class EntitlementManager { func verifyTapToPayEntitlement() async -> Bool { do { _ = try await ProximityReader.readerIdentifier return true } catch { // Handle entitlement errors logEntitlementError(error) return false } } private func logEntitlementError(_ error: Error) { // Log error for debugging but don't expose sensitive information print("Entitlement verification failed: \(error.localizedDescription)") } } ``` ## UI/UX Design Guidelines ### Payment Flow Design ```swift class PaymentFlowViewController: UIViewController { @IBOutlet weak var amountLabel: UILabel! @IBOutlet weak var tapToPayButton: UIButton! @IBOutlet weak var progressIndicator: UIActivityIndicatorView! override func viewDidLoad() { super.viewDidLoad() setupAccessibility() configurePaymentFlow() } private func setupAccessibility() { // VoiceOver support tapToPayButton.accessibilityLabel = "Pay with Tap to Pay" tapToPayButton.accessibilityHint = "Double tap to initiate payment" amountLabel.accessibilityLabel = "Total amount: $\(formattedAmount)" } private func configurePaymentFlow() { // Clear payment intent amountLabel.text = formattedAmount amountLabel.font = UIFont.preferredFont(forTextStyle: .headline) amountLabel.adjustsFontForContentSizeCategory = true // Minimal steps - single tap to pay tapToPayButton.setTitle("Tap to Pay", for: .normal) } } ``` ### Progress and Loading States ```swift class PaymentProgressManager { func showProgress(for operation: PaymentOperation) { switch operation { case .preparingReader: showProgressIndicator(message: "Preparing payment reader...") case .processingPayment: showProgressIndicator(message: "Processing payment...") case .completingTransaction: showProgressIndicator(message: "Completing transaction...") } } func hideProgress() { hideProgressIndicator() } } ``` ## Testing and Validation ### Comprehensive Testing Strategy ```swift class TapToPayTests: XCTestCase { func testReaderAvailability() async { let manager = TapToPayManager() await manager.checkReaderAvailability() // Test on device with Tap to Pay capability XCTAssertTrue(manager.isReaderAvailable) } func testPaymentFlow() async throws { let processor = SecurePaymentProcessor() let result = try await processor.processPayment(amount: 10.00, currency: "USD") XCTAssertNotNil(result) XCTAssertEqual(result.status, .success) } func testErrorHandling() { // Test various error scenarios let errorHandler = PaymentErrorHandler() let networkError = PaymentError.networkError let userMessage = errorHandler.getUserFriendlyMessage(for: networkError) XCTAssertFalse(userMessage.isEmpty) XCTAssertFalse(userMessage.contains("technical")) } } ``` ### TestFlight Preparation ```swift // Prepare for TestFlight distribution class TestFlightManager { func prepareForTestFlight() { // Ensure all test scenarios are covered validatePaymentFlows() testErrorScenarios() verifyAccessibilityCompliance() checkSecurityImplementation() } private func validatePaymentFlows() { // Test all payment scenarios // Verify UI/UX workflows // Ensure proper error handling } } ``` ## App Store Review Preparation ### Documentation Requirements 1. **Payment Flow Documentation**: Complete walkthrough of payment process 2. **Security Implementation**: Details of security measures and data protection 3. **Error Handling**: Documentation of all error scenarios and user feedback 4. **Accessibility Compliance**: VoiceOver and Dynamic Type support verification 5. **Test Account Credentials**: Sandbox accounts for review team testing ### Review Checklist * [ ] Tap to Pay entitlement properly configured * [ ] Reader availability checked before payment initiation * [ ] Proper error handling for all scenarios * [ ] No modal alerts during background operations * [ ] Clear user feedback for all actions * [ ] Accessibility compliance verified * [ ] Security best practices implemented * [ ] Test accounts provided for review * [ ] Complete payment flow documented ## Performance Optimization ### Memory Management ```swift class OptimizedPaymentManager { weak var delegate: PaymentManagerDelegate? private var readerSession: ProximityReader.Session? func startPaymentSession() { // Use weak references to avoid retain cycles readerSession = ProximityReader.Session() readerSession?.delegate = self } deinit { // Clean up resources readerSession?.invalidate() } } ``` ### Network Optimization ```swift class NetworkOptimizer { private let session: URLSession init() { let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 30 config.timeoutIntervalForResource = 60 self.session = URLSession(configuration: config) } func processPayment(_ data: PaymentData) async throws -> PaymentResult { // Implement retry logic and timeout handling return try await withRetry(maxAttempts: 3) { try await sendPaymentRequest(data) } } } ``` ## Resources and References ### Apple Documentation * [ProximityReader Framework](https://developer.apple.com/documentation/proximityreader) - Core framework for Tap to Pay functionality * [Apple Pay Developer Guide](https://developer.apple.com/apple-pay/) - Payment processing guidelines * [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/) - UI/UX design principles * [App Store Review Guidelines](https://developer.apple.com/app-store/review/guidelines/) - App Store submission requirements ### Koard Resources * [Setting Up the Entitlement](/docs/setting-up-the-ios-sdk/setting-up-the-entitlement-for-tap-to-pay-on-iphone) - Koard SDK integration guide * [Payment Lifecycle Guide](/docs/guides/payments/details/payment-lifecycle.md) - Complete payment flow documentation * [Test Cards Reference](/docs/appendix/resources#resources__test-cards) - Testing with test card numbers * [Security Guidelines](/docs/appendix/developing-with-apple) - Security best practices ### Additional Support * [Apple Developer Forums](https://developer.apple.com/forums/) - Community support and discussions * [WWDC Sessions](https://developer.apple.com/videos/) - Latest Tap to Pay and payment processing sessions * [Koard Developer Support](mailto:developers@koard.com) - Direct support for Koard SDK integration # Resources Comprehensive resources for developing Tap to Pay on iPhone applications with Koard and Apple's payment technologies. ## Apple Resources Essential resources from Apple for working with Tap to Pay on iPhone technology. ### ProximityReader Framework The ProximityReader framework provides the core functionality for reading contactless payment cards on iPhone. | Resource | Description | Link | | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | **ProximityReader Framework Documentation** | Complete framework reference including setup steps, API documentation, and implementation guides for integrating Tap to Pay on iPhone | [View Documentation](https://developer.apple.com/documentation/proximityreader) | | **Quick Start Guide** | Step-by-step guide to begin using Tap to Pay on iPhone to read contactless payment cards | [Developer Documentation](https://developer.apple.com/documentation/proximityreader) | | **Framework Reference** | Complete API reference for all ProximityReader classes, methods, and properties | [API Reference](https://developer.apple.com/documentation/proximityreader) | **Key Topics Covered:** * Reader initialization and configuration * Card reading and payment processing * Error handling and recovery * Security and entitlement requirements * Session management and lifecycle ### Apple Business Register Documentation Comprehensive documentation portal for Payment Service Providers (PSPs) working with Tap to Pay on iPhone. | Resource | Description | Link | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- | | **Apple Business Register** | Central hub for PSP-specific Tap to Pay on iPhone documentation, including registration, certification, and integration guides | [Apple Business Register](https://register.apple.com) | | **PSP Integration Guide** | Complete integration guide for Payment Service Providers | \[For PSPs only] | | **Certification Process** | Step-by-step certification requirements and procedures | [Apple Business Register](https://register.apple.com) | **Note**: Apple Business Register documentation is available for Payment Service Providers (PSPs) only. If you're building as a PSP, contact Koard to access these resources. ### Human Interface Guidelines Apple's design guidelines and best practices for Tap to Pay on iPhone applications. | Resource | Description | Link | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | | **Tap to Pay Human Interface Guidelines** | Design principles, UI/UX best practices, and accessibility guidelines specifically for Tap to Pay on iPhone applications | [View Guidelines](https://developer.apple.com/design/human-interface-guidelines/tap-to-pay-on-iphone) | | **Design Patterns** | Recommended UI patterns and components for payment flows | [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/tap-to-pay-on-iphone) | | **Accessibility Guidelines** | VoiceOver, Dynamic Type, and other accessibility requirements | [Accessibility Guide](https://developer.apple.com/design/human-interface-guidelines/tap-to-pay-on-iphone) | **Key Topics Covered:** * User interface design principles * Payment flow UX best practices * Error handling and user feedback * Accessibility compliance * Branding and messaging guidelines ### Merchant Education Educational materials and training resources for PSPs and app developers. | Resource | Description | Link | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- | | **Accept Payments Guide** | Comprehensive guide on how to accept payments using Tap to Pay on iPhone, including merchant setup, training materials, and best practices | [View Guide](https://developer.apple.com/tap-to-pay/how-to-accept-payments/) | | **Merchant Training Materials** | Educational resources for training merchants on using Tap to Pay | [Merchant Education](https://developer.apple.com/tap-to-pay/how-to-accept-payments/) | | **Developer Resources** | Developer-focused educational content and implementation guides | [Developer Resources](https://developer.apple.com/tap-to-pay/how-to-accept-payments/) | **Key Topics Covered:** * Merchant onboarding process * Payment acceptance workflows * Device setup and configuration * Troubleshooting common issues * Training and support materials ### Tap to Pay on iPhone FAQs Frequently asked questions and answers about Tap to Pay on iPhone technology. | Resource | Description | Link | | -------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | | **Tap to Pay FAQs** | Comprehensive FAQ covering common questions about Tap to Pay on iPhone implementation, requirements, and troubleshooting | [View FAQs](https://register.apple.com/tap-to-pay-on-iphone) | | **Technical FAQs** | Technical implementation questions and answers | [Apple Business Register](https://register.apple.com/tap-to-pay-on-iphone) | | **Integration FAQs** | Common integration questions and solutions | [Apple Business Register](https://register.apple.com/tap-to-pay-on-iphone) | **Common Topics:** * Device requirements and compatibility * Entitlement and certification questions * Integration and implementation queries * Security and compliance questions * Troubleshooting and support ### Additional Apple Resources | Resource | Description | Link | | --------------------------- | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | **WWDC Sessions** | Video sessions from Apple's Worldwide Developers Conference covering Tap to Pay and payment technologies | [WWDC Videos](https://developer.apple.com/videos/) | | **Apple Developer Forums** | Community forums for discussing Tap to Pay implementation and troubleshooting | [Developer Forums](https://developer.apple.com/forums/) | | **Apple Developer Support** | Direct support channels for Apple Developer Program members | [Developer Support](https://developer.apple.com/support/) | | **Security Documentation** | Apple's security guidelines and best practices for payment applications | [Security Guide](https://developer.apple.com/documentation/proximityreader) | ## Koard Documentation ### API Documentation | Resource | Description | Link | | ------------------------- | ---------------------------------------------------------------------------------------------- | --------------------------------------------------- | | **REST API Reference** | Complete API documentation with examples, request/response schemas, and authentication details | [API Reference](/api-reference) | | **Webhook Documentation** | Webhook setup guide, event reference, and testing procedures | [Webhook Guide](/docs/webhooks/setting-up-webhooks) | | **API Authentication** | Authentication methods, API keys, and security best practices | [API Reference](/api-reference) | ### SDK Documentation | Resource | Description | Link | | ---------------------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | **iOS SDK Guide** | Complete iOS SDK integration guide with code examples | [Setting Up the Entitlement](/docs/setting-up-the-ios-sdk/setting-up-the-entitlement-for-tap-to-pay-on-iphone) | | **SDK Installation** | Step-by-step installation instructions for iOS SDK | [Installing SDK](/docs/setting-up-the-ios-sdk/installing-the-sdk) | | **Payment Lifecycle** | Complete guide to payment processing and lifecycle management | [Payment Lifecycle](/docs/guides/payments/details/payment-lifecycle.md) | | **Tap to Pay Configuration** | Guide to adding Tap to Pay functionality to your iOS app | [Tap to Pay Guide](/docs/setting-up-the-ios-sdk/adding-support-for-tap-to-pay-on-iphone) | ### Integration Guides | Resource | Description | Link | | ----------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------- | | **Getting Started** | Complete getting started guide for new Koard integrations | [Getting Started](/docs/getting-started-with-koard/introduction) | | **Merchant Setup** | Guide to setting up merchant accounts and configurations | [Merchant Setup](/docs/getting-started-with-koard/setting-up-the-merchant) | | **Batch Settlements** | Guide to batch processing and settlement workflows | [Batch Settlements](/docs/batch-and-settlements/introduction) | | **Webhook Integration** | Complete webhook integration guide with examples | [Webhook Setup](/docs/webhooks/setting-up-webhooks) | ### Developer Resources | Resource | Description | Link | | ----------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------- | | **Code Examples** | Sample implementations in Swift, Objective-C, and other languages | [Code Examples](/docs/setting-up-the-ios-sdk/installing-the-sdk) | | **Sandbox Environment** | Test environment setup and configuration guide | [Getting Started](/docs/getting-started-with-koard/introduction) | | **Best Practices** | Apple best practices and development guidelines | [Apple Best Practices](/docs/appendix/apple-best-practices-and-guidelines) | | **Security Guidelines** | Security best practices and Apple development guidelines | [Security Guide](/docs/appendix/developing-with-apple) | ## SDK Downloads ### iOS SDK ```bash # CocoaPods pod 'KoardSDK' # Swift Package Manager https://github.com/koardlabs/koard-ios ``` ### Android SDK ```gradle // Add to build.gradle implementation 'com.koard:koard-android-sdk:1.0.6' ``` ## Testing Resources ### Test Cards Use these test card numbers to verify your Tap to Pay integration in the Certificate environment. These cards simulate different payment scenarios without processing real transactions. | Card Type | Number | CVV | Expiry | Use Case | | ---------------- | ------------------- | ---- | --------------- | -------------------------- | | Visa | 4242 4242 4242 4242 | 123 | Any future date | Successful payment testing | | Mastercard | 5555 5555 5555 4444 | 123 | Any future date | Successful payment testing | | American Express | 3782 822463 10005 | 1234 | Any future date | Successful payment testing | | Discover | 6011 1111 1111 1117 | 123 | Any future date | Successful payment testing | ### Test Scenarios Test various payment scenarios using these test card numbers: | Scenario | Card Number | Expected Result | | ---------------------- | ------------------------------------------ | ------------------------------------------- | | **Successful Payment** | Use any test card above with valid details | Payment processes successfully | | **Declined Payment** | 4000 0000 0000 0002 | Payment is declined | | **Insufficient Funds** | 4000 0000 0000 9995 | Transaction fails due to insufficient funds | | **Invalid CVV** | Any test card with wrong CVV | CVV validation error | | **Expired Card** | Any test card with past expiry date | Card expiration error | ### Testing Environment | Environment | Description | Use Case | | ---------------------- | ------------------------------------- | ------------------------------------------------- | | **Certificate (CERT)** | Test environment for development | Use for all internal testing and development | | **Production** | Live merchant transaction environment | Use only after thorough testing and certification | **Important**: Always use the Certificate environment for testing. Test cards work only in the Certificate environment and will not process real transactions. ### Testing Checklist Before deploying to production, ensure you've tested: * [ ] Successful payment processing with all card types * [ ] Error handling for declined payments * [ ] Error handling for invalid card details * [ ] Reader availability checking * [ ] Entitlement verification * [ ] Payment flow UI/UX * [ ] Error messaging and user feedback * [ ] Accessibility compliance (VoiceOver, Dynamic Type) * [ ] Network error handling * [ ] Background/foreground transitions ## Support ### Developer Support Get help with technical questions, integration issues, and development challenges. | Support Channel | Description | Link | | ----------------- | -------------------------------------------- | ------------------------------------------------------------------ | | **Documentation** | Comprehensive documentation and guides | [Documentation Hub](/docs/getting-started-with-koard/introduction) | | **GitHub Issues** | Report bugs and request features for iOS SDK | [GitHub Issues](https://github.com/koardlabs/koard-sdk/issues) | ### Business Support Get assistance with merchant accounts, business inquiries, and partnership opportunities. ### Support Resources | Resource | Description | Link | | ------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | **FAQ Section** | Frequently asked questions and answers | [Apple FAQs](https://register.apple.com/tap-to-pay-on-iphone) | | **Troubleshooting Guide** | Common issues and solutions | [Troubleshooting](/docs/getting-started-with-koard/setting-up-the-merchant#setting-up-the-merchant__troubleshooting) | | **Status Page** | Real-time system status and maintenance updates | [Status Page](https://status.koard.com) | ## Community ### Developer Community * **GitHub**: * **Stack Overflow**: Tag questions with `koard` * **Reddit**: r/koard * **Discord**: Koard Developer Discord ### Events and Webinars * **Monthly Webinars**: Product updates and best practices * **Developer Meetups**: Local developer meetups * **Conference Talks**: Industry conference presentations * **Workshops**: Hands-on development workshops ## Tools and Utilities ### Development Tools * **Koard CLI**: Command-line interface for API testing * **Webhook Tester**: Local webhook testing tool * **API Explorer**: Interactive API documentation * **Postman Collection**: Pre-configured API requests ### Monitoring Tools * **Dashboard**: Real-time transaction monitoring * **Analytics**: Payment analytics and reporting * **Alerts**: Custom alert configuration * **Logs**: Detailed transaction logs ## Compliance and Security ### Security Resources * **Security Best Practices**: Comprehensive security guide * **PCI Compliance**: PCI DSS compliance information * **Data Protection**: GDPR and privacy compliance * **Security Audit**: Third-party security audits ### Compliance Documentation * **Terms of Service**: Legal terms and conditions * **Privacy Policy**: Data handling and privacy policy * **Cookie Policy**: Cookie usage and management * **GDPR Compliance**: European data protection compliance ## Training and Certification ### Developer Certification * **Koard Developer Certification**: Official developer certification * **Payment Processing Fundamentals**: Core payment concepts * **API Integration Specialist**: Advanced API integration * **Security Specialist**: Payment security best practices ### Training Materials * **Video Tutorials**: Step-by-step video guides * **Interactive Courses**: Hands-on learning modules * **Documentation**: Comprehensive written guides * **Code Examples**: Real-world implementation examples # Koard API Reference Koard API provides unified access to account onboarding, terminal management, payment processing, and reporting across all Koard environments. ## Base URLs | Environment | URL | | ----------- | --------------------------- | | UAT | `https://api.uat.koard.com` | | Production | `https://api.koard.com` | UAT is an isolated sandbox environment — transactions processed there will not appear in card or merchant histories and no money moves. ## API Versioning All Koard API endpoints are versioned with a path prefix (`/v1`, `/v2`, `/v3`, etc.). The version is part of the URL path and is **required** for every request. ```bash # Example: v1 endpoint curl https://api.koard.com/v1/accounts/{account_id} \ -H "x-koard-apikey: YOUR_API_KEY" # Example: v2 endpoint curl https://api.koard.com/v2/terminals \ -H "x-koard-apikey: YOUR_API_KEY" # Example: v3 endpoint curl https://api.koard.com/v3/payments/{transaction_id}/capture \ -H "x-koard-apikey: YOUR_API_KEY" ``` ### Current Versions by Resource | Resource | Version(s) | Path Prefix | Notes | | ------------------- | ---------- | -------------------------------------------- | -------------------------------------------------------------------------------- | | **Accounts** | v1, v2 | `/v1/accounts`, `/v2/accounts` | v2 provisions MMS (Clerk org, SVIX app, API key) on creation | | **Terminals** | v2 | `/v2/terminals` | Paginated listing, search, and full CRUD | | **Locations** | v1 | `/v1/locations` | Create, update, and retrieve locations | | **Transactions** | v1 | `/v1/transactions` | List, search, retrieve, and passthrough-only edit transactions | | **Payments** | v3 | `/v3/payment`, `/v3/preauth` | Card-present payment initiation via Tap to Pay | | **Payment Actions** | v1, v3 | `/v1/payments/{id}/…`, `/v3/payments/{id}/…` | Capture, auth increment, refund, reverse, adjust, confirm | | **Refunds** | v3 | `/v3/refund` | Standalone refund on completed transactions | | **Batches** | v1 | `/v1/batches` | Open, close, and edit settlement batches | | **API Keys** | v5 | `/v5/apikeys` | Scoped-permission keys; see [Authentication](/docs/api-reference/authentication) | The `/v5` surface currently exposes **API-key management only** (`/v5/apikeys`). Other resources such as payments, terminals, and accounts are served by their `/v1`–`/v3` paths above — there is no `/v5/payments`, `/v5/terminals`, etc. ### Version Lifecycle * **Newer versions** may change request/response schemas, add required fields, or alter default behavior. Always use the version shown in the endpoint path. * **Older versions** remain functional but may not include the latest features. We recommend migrating to the latest available version for each resource. * **Breaking changes** are only introduced in new major versions — existing versioned paths remain backward-compatible. > **Note:** The version prefix (e.g. `/v1/`, `/v2/`, `/v3/`) is required in all request paths. Requests without a version prefix will return a `404`. > **Passthrough-only transaction edits:** `PUT /v1/transactions/{transaction_id}` is reserved for PSP passthrough EMV flows where Koard is only forwarding EMV data and later receiving the final transaction outcome back from the PSP. It is not available for TSYS, Elavon, Fiserv, Worldpay, or other directly managed processor flows. Unsupported processor edits return `401`, while both missing and inaccessible transactions return `404`. # Response Codes Koard uses standard HTTP status codes. | Code | Meaning | |------|---------| | `200` | Success | | `201` | Resource created | | `400` | Validation failure — check the response body for details | | `401` | Missing or invalid API key | | `403` | Insufficient permissions for this operation | | `404` | Resource not found | | `409` | Conflict — resource already exists or state mismatch | | `423` | Locked — merchant account is blocked and cannot perform this operation | | `429` | Rate limited — slow down and retry | | `500` | Unexpected server error | ## Error Body Format Errors return a structured envelope with a machine-readable `error` code, a short `message` category, and a human-readable `details` string. Branch on `error` in client code; `details` is always a string (never a list or object): { "error": "validation_error", "message": "Request validation failed", "details": "query: sort_by: Input should be 'name' or 'volume'" } `500` responses never leak internal exception messages or stack traces — the incident is logged server-side and `details` carries only a generic string. ### Envelope Error Codes When an error is returned as the structured envelope, the `error` field is one of: | `error` | HTTP | `message` | |---------|------|-----------| | `validation_error` | 400 | Request validation failed | | `authentication_required` | 401 | Authentication required | | `permission_denied` | 403 | Permission denied | | `not_found` | 404 | Resource not found | | `conflict` | 409 | Resource state conflict | | `rate_limited` | 429 | Rate limit exceeded | | `upstream_error` | 502 | Upstream processor error | | `internal_error` | 500 | Internal server error | # Fiserv Nashville North Nashville North uses the **Nashville front-end** settling to the **North (PTS) back-end** — i.e. terminal capture. Board it like any Fiserv terminal (see the [Fiserv Overview](/boarding-a-merchant/fiserv/fiserv-overview) for the shared shape, SRS, and UMF coverage) using the **Nashville North** processor config, and supply the North settlement MID. ## Boarding spec | Field | Value / format | Notes | |---|---|---| | **Group ID** | `10001` | Nashville **front-end** — same Group ID as Nashville Classic. The difference is the back-end/capture, not the Group ID. | | **Merchant ID** (MID) | 7 digits (`MerchID`) | Nashville front-end MID. Top-level `mid`. | | **Terminal ID** (TID) | 7 digits (`TermID`) | Top-level `tid`. | | **Settlement MID** | 12 digits | The North Settlement MID. VAR-sheet `settlement_mid` — **optional on the request; defaults to a copy of `mid` if omitted**. | | **MCC** | 4-digit | Top-level `mcc`. | | **Equipment** (POS Solution) | `CreditCallLTDGTWRC` or `CRDCallResellerRCSS` | North terminal-capture solutions. VAR-sheet `equipment`. | ## Request shape curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_fiserv_nashville_north", "terminal_name": "Register 1", "mid": "9446055", "tid": "9259755", "mcc": "5045", "var_sheet": { "group_id": "10001", "settlement_mid": "445197000368", "industry": "retail_qsr_grocery", "equipment": "CreditCallLTDGTWRC", "merchant_street_address": "102 Woodmont Blvd Ste 125", "merchant_city": "Nashville", "merchant_state": "TN", "merchant_postal_code": "37205", "country_code": "840" } }' ## Capture & settlement **Terminal capture (North PTS).** The gateway holds transactions and submits the batch at cutoff. Boarding must match the host's configured capture mode — if you board terminal capture but the host has the MID as host capture (or vice-versa), settlement breaks. ## Gotchas - **Same Group ID as Nashville Classic (`10001`).** Nashville North is *not* a different Group ID — it's the Nashville front-end paired with the North back-end. The flavor is chosen by the processor config, not the Group ID. - **`settlement_mid` is the 12-digit North Settlement MID.** Omit it and Koard copies `mid`; supply it explicitly when the North settlement MID differs. - **Capture-mode mismatch = broken settlement.** Confirm the host has the MID set to terminal capture before going live. - **`tid` is zero-padded to 8** as the Datawire `AuthKey2`. # Boarding a Merchant with Worldpay You can board a Worldpay merchant either through the Koard Merchant Management System (MMS) UI or programmatically via the API. Koard targets Worldpay's **610 interface** (TPS / Vantage) for mPOS EMV COTS. Worldpay assigns the merchant a **Merchant ID** and **Terminal ID** out-of-band. Those two values plus `mcc` are all Koard needs from the merchant. The other 610 credentials (User ID, Password, Network Routing, Bank ID) are configured by Koard once per environment. ## Before You Start Worldpay provisions merchants on their side; Koard never makes a "create merchant" call. The packet you get from Worldpay for a new merchant contains: | Provided by Worldpay | What it is | |---|---| | **Merchant ID** (MID) | up to 12 digits — Worldpay-assigned merchant identifier (610 §3 Field 42 "Card Acceptor ID Code") | | **Terminal ID** (TID) | 3 digits — lane/device identifier (610 §3 Field 41) | | **Merchant Category Code** | 4-digit MCC — held in the Worldpay merchant profile, not on the wire | That's it from the merchant. The other 610 fields you may have heard about (`userid`, `password`, `network_routing`, `bank_id`) are owned by Koard and configured once per environment — never paste them into a `POST /v2/terminals` body. ## Via the MMS After creating the merchant account, click **New Terminal** and select Worldpay as the processor. | MMS Label | Required | Notes | |---|---|---| | Merchant ID | Yes | Worldpay-assigned, up to 12 digits | | Terminal ID | Yes | Worldpay-assigned, 3 digits | | Merchant Category Code | Yes | 4-digit MCC | | Currency | No | Defaults to `USD`. The 610 message has no wire currency field — currency is inferred from the MID profile on Worldpay's side. | Once saved, assign the terminal to a location and generate merchant credentials as usual. ## Via the API Send `X-Koard-apikey: {API_KEY}` with every request. Use `https://api.uat.koard.com` for sandbox and `https://api.koard.com` for production. ### Create Terminal — `POST /v2/terminals` **Request body** | Field | Required | Description | |---|---|---| | `account_id` | Yes | Merchant account ID | | `processor_config_id` | Yes | Worldpay processor configuration ID | | `terminal_name` | Yes | Display name for the terminal | | `terminal_description` | No | Optional description | | `mid` | Yes | Worldpay Merchant ID — up to 12 digits | | `tid` | Yes | Worldpay Terminal ID — 3 digits | | `mcc` | Yes | 4-digit MCC | | `var_sheet` | No | Currently no required merchant fields — reserved for future extensions. | **Example** curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_live_worldpay_us", "terminal_name": "Front Counter iPhone", "mid": "000038462929", "tid": "001", "mcc": "5812" }' ### Update Terminal — `PUT /v2/terminals/{terminal_id}` Send a `PUT` with only the fields you want to change. ## Batch & Settlement The 610 spec describes Worldpay as "host capture" with "host settlement requirements" (610 §1.1.1). The integrator can drive batch release explicitly: | Operation | Message | Trigger | |---|---|---| | Batch Inquiry | MTI `0500` / PC `920000` | Read current batch counts and amounts (610 §2.5) | | Batch Release | MTI `0500` / PC `930000` | Close the current batch, "initiates the closing of the current batch by settling all transactions" (610 §2.5 line 33922) | The spec is **silent on whether Worldpay can be configured to auto-release** — that would be a per-merchant boarding option not documented in the 610 reference. Koard exposes a `batch_schedule` on the terminal to drive Batch Release on a schedule; see [Automated Batch Scheduling](/docs/guides/batch-settlements/details/automated-batch-scheduling). ## Currency & Country The 610 base message has **no wire currency field**. Field 04 (Amount) is `n9` cents and the spec layers on ISO 8583's "amount expressed in U.S. Dollars" (ISO 8583 §F004). Multi-currency on a USD-boarded MID is not supported by the 610 message set. The 610 base message has **no wire country field** either. Country is inferred from the MID profile. For EMV transactions only, the terminal country surfaces via EMV TLV tag `9F1A` inside G035 chip data; Koard builds this internally. `currency` exposed at the API level defaults to `USD` and is reserved for future multi-currency support. ## Reading the Response A Worldpay 610 response comes back with a 21-byte TPS header prefix, the MTI, then a 2-character **Bitmap Type** that tells you whether the transaction was approved or declined: | Bitmap Type | Meaning | What to look at next | |---|---|---| | `90` / `91` | Approved | `F65` auth code (offset 30, 6 chars); `F37` retrieval reference (offset 22, 8 chars); `F120.3` 4-char card brand mnemonic (`VI` / `MC` / `DI` / `AX`) | | `99` | Declined / error | `F123.1` 20-char error text (offset 44); `F123.2` 3-char response code (offset 64) | The full canonical response-code list is in **Appendix A** of the Worldpay 610 Interface Reference Guide. ## Express vs 610 vs RAFT Worldpay offers three integration surfaces. Koard targets **610** for mPOS EMV COTS because it's the only interface in the spec that exposes device classes `6 — SoftPOS Device` and `9 — MPOS` explicitly in fields F25 and F107. | Interface | What it is | Koard support | |---|---|---| | **610** | Host-capture controller message set, ISO 8583-derived flat positional format | **Production today** | | **Express** | XML over HTTPS (SOAP is deprecated per the spec). Requires `AccountID + AccountToken + AcceptorID + TerminalID` instead of 610's credential set. | Not currently used | | **RAFT** | Worldpay's internal authorization platform — referenced in 610 messages (e.g. `R997 RAFT=…`) but not a separate integrator interface in the supplied spec set | Internal — not a customer-facing option | ## Gotchas - **`mid` and `tid` are top-level fields** on the request — not inside `var_sheet`. Matches every other processor on Koard. - **`userid`, `password`, `network_routing`, `bank_id` are configured by Koard once per environment.** Don't ask the merchant for these. - **The 610 wire has no currency or country field.** Both are inferred from the MID. If the merchant needs multi-currency, contact Koard support — it requires Worldpay-side reconfiguration. - **MCC is not on the 610 wire either** — it's held in the Worldpay merchant profile. Koard still requires it on the API for surcharge calculation and reporting. - **F22 / F25 / F107 are device-class constants** (SoftPOS), set by Koard once per device class. Not configurable per merchant. ## Troubleshooting **`400 Bad Request` on create** - Verify `mid`, `tid`, and `mcc` are at the top level. - Confirm `processor_config_id` is a valid Worldpay config ID for your environment. **Transactions erroring with `FORMAT ERROR` (response code `730`)** - Usually an EMV TLV issue — Worldpay's whitelist of allowed EMV tags is narrow. Capture the request body and forward to Koard support. **Transactions erroring with `CALL OPER` (response code `701`)** - Card-side decline. Have the merchant ask the cardholder to call their bank. **Need multi-currency** - Not supported on the 610 wire as written. Requires Worldpay-side reconfiguration of the MID; contact Koard support. **Need explicit batch close** - Use `batch_schedule` on the terminal to schedule `MTI 0500 / PC 930000` Batch Release on a recurring cadence, or trigger manually via the batch API. # Adding Support for Tap to Pay on iPhone Enable Tap to Pay on iPhone to allow merchants to accept contactless payments directly on their iPhone without additional hardware. If you're ready to start developing, see our [iOS SDK installation guide](/docs/setting-up-the-ios-sdk/installing-the-sdk). [Get started with Koard](/docs/getting-started-with-koard/introduction) **What you learn** In this guide, you'll learn: * How to request the Tap to Pay entitlement from Apple * How to configure your Xcode project for Tap to Pay * How to set up testing in developer mode * How to distribute your app for testing and production **Prerequisites** Before you begin, ensure you have: * **iOS 17.4 or later** (minimum required version) * **iPhone XS or later** (supported hardware) * **Apple Developer Account** (organization-level account required) * **Koard iOS SDK** (installed in your project) * **Valid merchant account** (configured in Koard MMS) * **Sandbox Apple Account signed in on test device** (dedicated iPhone in Developer Mode) **Use a Dedicated Test iPhone**: Keep your Sandbox Apple Account signed in on a separate test device. Production Apple IDs cannot complete Sandbox Tap to Pay transactions. ## Enable the Tap to Pay Entitlement Enabling the Tap to Pay Entitlement is a critical step that is handled by Apple. Typically, you will need to request the entitlement from Apple through their developer portal. ### Requesting Tap to Pay Entitlement from Apple To enable Tap to Pay on iPhone, follow these steps: 1. **Log in to your Apple Developer account** as the account holder 2. **Navigate to Certificates, Identifiers & Profiles** 3. **Select Tap to Pay on iPhone Entitlement** and submit a request 4. **Wait for approval** - Apple will add the entitlement under Managed Capabilities **Processing Time**: The process to get approval from Apple typically takes **one or two business days**. You will need to start with the development certificate to get access to the Apple CERT environment. ## Configure Your Xcode Project Once you have the entitlement, configure your Xcode project: ### 1. Enable Tap to Pay Capability 1. **Sign in to your Apple Developer Account** 2. **Create an App ID** (if you don't have one already) 3. **Go to Certificates, Identifiers & Profiles > Identifiers** 4. **Select your app and go to Additional Capabilities** 5. **Enable Tap to Pay on iPhone and save** ### 2. Create a Provisioning Profile 1. **Open Xcode and select your project** 2. **Navigate to Signing & Capabilities** 3. **Under Provisioning Profile, select Download Profile** 4. **Choose the new provisioning profile** ### 3. Add Entitlements File 1. **In Xcode, select your project in Project Navigator** 2. **Create a new Property List file** (File > New > File > Resource) 3. **Name the file `[ProjectName].entitlements`** 4. **Open Build Settings and locate Code Signing Entitlements** 5. **Set its value to the path of the .entitlements file** Open the `.entitlements` file and add the following key-value pair: ```xml com.apple.developer.proximity-reader.payment.acceptance ``` **Additional Resources**: More details on enabling the tap to pay entitlement can be found in [Apple's Developer Documentation](https://developer.apple.com/documentation/passkit/tap_to_pay_on_iphone). ## Testing in Developer Mode To test the iOS app in developer mode, follow these additional steps: ### Enable Developer Mode 1. **On the test iPhone, go to Settings > Privacy & Security** 2. **Enable Developer Mode** ### Use a Sandbox Apple Account The sandbox account must be: * **Freshly created in App Store Connect** * **Signed into iCloud** * **Linked to the test iPhone** **Important**: Existing accounts attempting to test an App with Tap to Pay in cert mode will be blocked from creating a card reader session due to Apple's security policies. ### Register a Test Device 1. **Retrieve the UDID of the test device** 2. **Add the UDID to Allowed Devices in the Apple Developer account** 3. **Update the Provisioning Profile to include the allowed devices** **Device Management**: Any new test device will need to have the UDID uploaded and a **NEW provisioning profile** will need to be added anytime new devices are added. The app will then need a new archive file that will be shared with the new device. ### Distribute via .ipa File 1. **Create an .ipa build file in Xcode** 2. **Share the build with your internal team for testing** **Testing Recommendation**: It is highly recommended that you have a **secondary iPhone or higher** to test transactions. Otherwise, developer mode means that engineers will have to sign into a test account onto their own devices to test the mPOS app with Tap to Pay. This is a hard limitation set by Apple and has no workaround at the moment. ### TestFlight and App Store Distribution **TestFlight beta testing** and **App Store submissions** require a separate entitlement that allows distribution. If you've already completed your testing with the non-distribution entitlement, respond to the original email and re-request the Tap to Pay on iPhone Entitlement. ## Testing Environment Running a transaction in the cert environment will route payments to all the processor and card brands test environment. Transactions sent through this setup will **NOT authorize a real transaction** but will be production-like in terms of workflow. ### Test Cards Use these test cards to verify your integration: * **Test Card**: `4242 4242 4242 4242` * **Expiry**: Any future date * **CVV**: Any 3 digits ## Production Deployment Before going live: 1. **Complete merchant verification** in Koard MMS 2. **Switch to production environment** in your app configuration 3. **Test with real payment methods** (in a controlled environment) 4. **Submit for App Store review** with the distribution entitlement 5. **Review scheme setup** in [Getting Ready for Production](/docs/setting-up-the-ios-sdk/getting-ready-for-production) to ensure the correct API keys ship with your archive. ## Card Reader Lifecycle in the SDK Once the entitlement and provisioning are in place, the SDK manages the Apple ProximityReader card reader. After authenticating the merchant, link the account and prepare the reader before taking payments. ```swift import KoardSDK // 1. Check whether the merchant account is already linked to Apple Tap to Pay let isLinked = try await KoardMerchantSDK.shared.isAccountLinked() // 2. Link the account if needed (this requires user interaction) if !isLinked { // Synchronous variant try KoardMerchantSDK.shared.linkAccount() // Or the async variant (recommended with Swift Concurrency) // try await KoardMerchantSDK.shared.linkAccountAsync() } // 3. Prepare the reader for accepting payments try await KoardMerchantSDK.shared.prepare() ``` ### Monitor Reader Events and Status Observe live reader events with the `readerEvents` async stream, and check `status` to see whether the reader is ready: ```swift Task { for await event in KoardMerchantSDK.shared.readerEvents { print("Reader event: \(event.description)") } } // Current reader status let status = KoardMerchantSDK.shared.status ``` ### Present the Tap to Pay Tutorial On iOS 18 and later, you can present Apple's built-in "How to Tap" tutorial from a visible view controller: ```swift if #available(iOS 18.0, *) { try KoardMerchantSDK.shared.presentTutorial(from: viewController) } ``` **Reader operations are serialized**: The SDK serializes ProximityReader operations (prepare, linkAccount, isAccountLinked, and reads), so a sale started while `prepare()` is still running waits for readiness instead of failing with a "reader busy" error. If you switch the active location, the reader re-prepares for the new location before the next charge. **Handle cancellation**: When the customer cancels at the Apple Tap to Pay sheet, the sale, pre-auth, and card-present refund flows throw `KoardMerchantSDKError.TTPPaymentFailed(.canceled)`. Treat this as a benign "canceled" outcome rather than a failure. ## Troubleshooting ### Common Issues * **Entitlement Not Found**: Ensure you've requested and received approval from Apple * **Provisioning Profile Issues**: Make sure your provisioning profile includes the Tap to Pay capability * **Device Registration**: Verify test devices are properly registered in your Apple Developer account * **Sandbox Account Issues**: Use a fresh Apple ID created specifically for testing ### Support For technical issues with Tap to Pay integration: * Check the [Apple Best Practices](/docs/appendix/apple-best-practices-and-guidelines) guide * Review [Payment Configurations](/docs/appendix/payment-configurations) for advanced settings * Contact Koard support for SDK-specific issues ## See also This wraps up the Tap to Pay setup. See the links below for next steps in your integration: * [Creating a Sandbox Apple Account](/docs/setting-up-the-ios-sdk/creating-a-sandbox-apple-account) - Set up test identities * [Installing the SDK](/docs/setting-up-the-ios-sdk/installing-the-sdk) - Complete SDK integration guide * [Running Payments](/docs/setting-up-the-ios-sdk/running-payments) - Implement payment processing * [Payment Lifecycle](/docs/payments/payment-lifecycle) - Understand the complete payment flow * [Getting Ready for Production](/docs/setting-up-the-ios-sdk/getting-ready-for-production) - Configure schemes for launch * [Apple Best Practices](/docs/appendix/apple-best-practices-and-guidelines) - Follow Apple's guidelines # Retrieving Your API Key To retrieve your API key for your account, follow these steps: **1. Navigate to the Developer page** In the sidebar, click on ![link icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWZpbGUtdGV4dC1pY29uIGx1Y2lkZS1maWxlLXRleHQiPjxwYXRoIGQ9Ik02IDIyYTIgMiAwIDAgMS0yLTJWNGEyIDIgMCAwIDEgMi0yaDhhMi40IDIuNCAwIDAgMSAxLjcwNC43MDZsMy41ODggMy41ODhBMi40IDIuNCAwIDAgMSAyMCA4djEyYTIgMiAwIDAgMS0yIDJ6Ii8+PHBhdGggZD0iTTE0IDJ2NWExIDEgMCAwIDAgMSAxaDUiLz48cGF0aCBkPSJNMTAgOUg4Ii8+PHBhdGggZD0iTTE2IDEzSDgiLz48cGF0aCBkPSJNMTYgMTdIOCIvPjwvc3ZnPg==) Developer to access the Developer Tools page. **2. Access API Keys** Once on the Developer page, you'll see the **API Keys** tab. Click on it to view your API key management section. **3. View your API key** Your API key will be displayed in the API Key Management section. You can: * View the masked API key * Click the eye icon to reveal the full key * Click the copy icon to copy the key to your clipboard ![API Key Management](/api.png) **Security Note**: Keep your API key confidential and never share it in client-side code or public repositories. Rotate and revoke keys regularly to maintain security. ## Using Your API Key in the SDK Pass your API key to the SDK when you initialize it, along with a `KoardOptions` value that selects the environment and logging level: ```swift import KoardSDK let options = KoardOptions( environment: .uat, // .uat | .production | .custom(String) loggingLevel: .debug // .none | .error | .warning | .debug | .verbose ) KoardMerchantSDK.shared.initialize(options: options, apiKey: "your-koard-api-key") ``` * **`environment`** accepts `.uat`, `.production`, or `.custom(String)` for a custom base URL. * **`loggingLevel`** accepts `.none`, `.error`, `.warning`, `.debug`, or `.verbose`. Use a lower level (such as `.error` or `.none`) for production builds. Call `initialize(options:apiKey:)` once, early in your app lifecycle (for example in `AppDelegate`), before using any payment functionality. # Running Payments on Android Learn how to drive live Tap to Pay on Android sessions with the Koard SDK and how to handle every follow-up transaction step. **What you learn** * Which APIs emit reader events, and why `refund()` is not one of them * How to validate SDK readiness before launching the thin client * Handling `Flow` to power custom UIs * Running Practice Mode taps to validate the online-PIN flow * Managing surcharge confirmation, tip/tax/surcharge breakdowns, and telemetry * Post-reader operations such as capture, refund, EMV refund, reverse, incremental auth, and tip adjustments ## Step 1: Install the Visa Kernel App Before writing any Tap to Pay code, install the Visa Kernel app on every NFC-enabled development device. Download it directly from the [Google Play Store](https://play.google.com/store/apps/details?id=com.visa.kic.app.kernel), launch it once to complete setup, and keep it installed for all future builds. ## Prerequisites Before starting any NFC session: * Initialize the SDK in `Application.onCreate()` and authenticate the merchant * Call `setActiveLocation(locationId)` and then `enrollDevice()` — both are mandatory, and neither happens automatically. See [Installing the SDK](/docs/setting-up-the-android-sdk/installing-the-sdk) * Register the hosting activity for NFC in `onResume()` / `onPause()` * Disable developer mode on the device (reader emits `developerModeEnabled` if it’s left on) * Observe `KoardMerchantSdk.getInstance().readinessState` and block the UI unless `isReadyForTransactions` is `true` ```kotlin val sdk = KoardMerchantSdk.getInstance() val readiness = sdk.readinessState.value if (!readiness.isReadyForTransactions) { Log.w("Checkout", "SDK not ready: ${readiness.getStatusMessage()}") return } ``` ## Launching a Sale Session `sdk.sale`, `sdk.preauth`, `sdk.completePartialAuth`, and `sdk.refundEmv` are the APIs that drive a reader session, so they return cold `Flow` streams. `sdk.prepare` also returns a `Flow`, but it is a warm-up that emits progress toward `Done` — it never runs a transaction. Collect the flow on a worker thread, and feed each event back to the UI. (Other calls reach the thin client too — `isKernelAppInstalled`, `checkKiCEligibility`, `getTransactionConfig`, `cancelTransaction`, `resetKernelService`, `unenrollDevice`, and the NFC registration pair — but they are one-shot and do not emit reader events.) ```kotlin suspend fun sale( activity: Activity, amount: Int, // amount in cents breakdown: PaymentBreakdown? = null, buttonProperties: List? = null, currency: String = "USD", eventId: String? = null, tapTimeoutMs: Long? = null, // auto-cancel the tap after this many ms transactionType: String = "Payment" // "TestWithPin" for Practice Mode ): Flow ``` Pass `buttonProperties` to customize the on-screen reader buttons, and `tapTimeoutMs` to automatically cancel the reader session if no card is presented in time. `currency` must be one of the currency codes the kernel reports for this device. Per KiC §3.4.8 the transaction's `currencyCode` "can be one of the currencies that comes out of the `getTransactionConfig` api", so if you support anything beyond a single fixed currency, read the live list from `sdk.getTransactionConfig()` (a suspend call returning `KoardTransactionConfig?`, wrapping KiC §3.4.16) rather than hard-coding it. The same object carries the supported `transactionTypes`, `supportedButtons`, `supportedCountries`, and `supportedLanguages`. Visa marks `getTransactionConfig` **Implementation: Conditional** — it is there to be called on demand when you need the latest config. **`preauth` no longer matches `sale`'s signature.** The trailing `transactionType` parameter is new in 1.0.6 and exists on `sale()` only. `preauth()` takes the same first seven parameters (`activity` through `tapTimeoutMs`) and nothing more. **Readiness is enforced by throwing, not by emitting.** `sale()`, `preauth()`, `completePartialAuth()`, and `refundEmv()` refresh their readiness checks and **throw `KoardException`** before returning a Flow when the SDK is not ready. Inspect `e.error.errorType`: it is a `KoardErrorType.NotReady.*` value such as `KernelAppNotInstalled`, `DeveloperModeEnabled`, `NotAuthenticated`, `NotEnrolled`, `NoActiveLocation`, `ReaderNotStarted`, or `Preparing`. Retry on `Preparing`; prompt the operator on the rest. Wrap the call in `try/catch`, not just the collection. ### Practice Mode (`TestWithPin`) `transactionType` is the KiC transaction type sent to the kernel: | Value | Behavior | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `"Payment"` (default) | A real sale. The kernel picks the CVM from the card and terminal profile — usually signature or no-CVM, never a guaranteed PIN | | `"TestWithPin"` | **Practice Mode** (KiC Annex A-2). Forces the PIN-entry UI so you can validate the online-PIN flow without routing to the acquirer | ```kotlin sdk.sale( activity = activity, amount = 1000, transactionType = "TestWithPin" // Practice Mode — exercises the PIN keypad ).collect { response -> handleTransactionEvent(response) } ``` Practice Mode is only valid when the device enrollment's supported transaction types include `TestWithPin`. Read the enrollment's supported types from `sdk.getEnrollmentDetails()?.transactionTypes` before offering it in your UI. ```kotlin fun startSale(activity: Activity, amountInCents: Int) { val sdk = KoardMerchantSdk.getInstance() // No pre-flight readiness guard here: sale() refreshes the readiness checks // itself and throws with the typed reason, so a guard that returns early // would swallow the retryable Preparing case. Let the catch below decide. val eventId = UUID.randomUUID().toString() lifecycleScope.launch(Dispatchers.IO) { try { sdk.sale( activity = activity, amount = amountInCents, breakdown = PaymentBreakdown(subtotal = amountInCents), currency = "USD", eventId = eventId ).collect { response -> // The Flow is collected on Dispatchers.IO; hop to Main for UI work. withContext(Dispatchers.Main) { handleTransactionEvent(response) } } } catch (e: KoardException) { // sale() runs the readiness checks itself and throws before it // returns the Flow, so this is where a not-ready SDK surfaces. withContext(Dispatchers.Main) { when (e.error.errorType) { // Transient: the reader is still coming up — retry shortly. is KoardErrorType.NotReady.Preparing -> scheduleRetry { startSale(activity, amountInCents) } // Every other NotReady state needs operator action // (install the kernel app, disable developer mode, log in, // enroll the device, select a location). else -> showError(message = e.error.shortMessage, errorType = e.error.errorType) } } } } } private fun handleTransactionEvent(response: KoardTransactionResponse) { when (response.actionStatus) { KoardTransactionActionStatus.OnProgress -> { showStatus(response.readerStatus.toString(), response.displayMessage) } KoardTransactionActionStatus.OnFailure -> { showError( code = response.statusCode, message = response.displayMessage ?: "Transaction failed", finalStatus = response.finalStatus ) } KoardTransactionActionStatus.OnComplete -> { val transaction = response.transaction ?: return showReceipt(transaction) } else -> Unit } } ``` ### Event payloads Every emitted `KoardTransactionResponse` contains the data you need to build a bespoke reader UI: * **`readerStatus`** – a `KoardReaderStatus` enum value: `preparing`, `readyForTap`, `cardDetected`, `pinEntryRequested`, `pinEntryCompleted`, `readCompleted`, `readNotCompleted`, `readCancelled`, `readRetry`, `processing`, `complete`, `developerModeEnabled`, or `unknown` * **`displayMessage`** – Reader-provided instructions ("Present card", "Processing", etc.) * **`statusCode`** / **`statusCodeDescription`** – numeric status (and its description) for advanced troubleshooting * **`actionStatus`** – `OnProgress`, `OnFailure`, or `OnComplete` (these are the only three action statuses) * **`finalStatus`** – `Approve`, `Decline`, `Abort`, `Failure`, `AltService`, or `Unknown` after reader completion * **`transaction`** – populated on completion; includes IDs, surcharge state, and breakdown so you can print receipts or prompt for confirmation **Surcharge confirmation is signaled on the transaction, not the action status.** There is no `OnConfirmSurcharge` action status. When the completion event carries a transaction whose `transaction.status == KoardTransactionStatus.SURCHARGE_PENDING`, prompt the customer and call `sdk.confirm(transactionId, confirm = true/false)` with their decision. For a complete reference of every `finalStatus`, `statusCode`, display message ID, and error scenario the SDK can return, see the [SDK Response Codes](/docs/setting-up-the-android-sdk/sdk-response-codes) guide. The demo’s `MainScreenViewModel` (see `src/main/java/com/koard/android/ui/MainScreenViewModel.kt`) shows a complete Compose-based implementation that you can adapt for your UI stack. ### Preauthorization Call `sdk.preauth(...)` when you need a hold + later capture. It takes the same parameters as `sale` except the trailing `transactionType` (Practice Mode is sale-only), the Flow emits the same events, and `response.transaction?.transactionId` is the ID you’ll pass into capture/refund APIs later. ### Completing a partial approval When an authorization is only partially approved (the issuer approved less than the requested amount), call `sdk.completePartialAuth(...)` to authorize the remaining amount using the original card data — no new tap is required for the card itself, but because it drives the reader session it returns a `Flow` just like `sale`/`preauth`: ```kotlin suspend fun completePartialAuth( activity: Activity, transactionId: String, amount: Int, breakdown: PaymentBreakdown? = null, buttonProperties: List? = null, currency: String = "USD", eventId: String? = null, tapTimeoutMs: Long? = null ): Flow ``` ### Warming up the reader `sdk.prepare()` is a **latency optimization, not a precondition for tapping.** Visa marks the pre-transaction warm-up API **Implementation: Optional** (KiC integration guide §3.4.7, "Preparing the SDK for Point-of-Sale Transactions"), and `startTransaction` (§3.4.11) lists no warm-up among its preconditions. What warming up buys you is timing: §3.4.7 describes the session-establishment and device-attestation work as happening "before startTransaction" either way, and frames the API as a fix for "the delay seen if the startTransaction is called for the first time after app restart". Calling it moves that cost off the critical user path. The one precondition §3.4.7 does state is that **the device must already be enrolled** before you warm up, so call `prepare()` after `enrollDevice()` has succeeded, not before. Call it early (for example in `Application.onCreate`, once enrollment is confirmed). It returns a `Flow` that emits progress (`Called` → `AuthenticationInProgress` → `AttestationInProgress` → `GettingConfigurations` → `ParsingConfigurations` → `Done`); key off the `Done` emission and treat any error variant as "retry on next user action". Nothing else in your flow needs to block on it — a fire-and-forget collect on a worker scope is a valid usage pattern, since the only effect is the warm-up. ```kotlin // collect() is a suspend function — call it from a coroutine on a worker scope. applicationScope.launch(Dispatchers.IO) { sdk.prepare().collect { response -> if (response.status is KoardPrepareStatus.Done) { Log.i("Checkout", "Reader is warmed up") } } } ``` ## Post-Reader Operations All backend-only operations are suspend functions that return a `Result`, so they never emit reader events and can run from any worker thread. The payload varies by call: `capture`, `reverse`, `refund`, `adjust`, and `getTransaction` return `Result`; `getTransactions` returns `Result` (which carries the paging fields); and `sendReceipt` returns `Result`. `cancelTransaction()` and `resetKernelService()` also return a `Result` (`Result`), but they are **not** backend calls — they talk to the Tap to Pay Ready app over IPC to abort a tap or release the service connection. See [Canceling the reader session](#canceling-the-reader-session). ### Capture ```kotlin suspend fun captureTransaction(transactionId: String, amountOverride: Int? = null) { withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() sdk.capture(transactionId = transactionId, amount = amountOverride) .onSuccess { println("Capture successful: ${it.transactionId}") } .onFailure { println("Capture failed: ${it.message}") } } } ``` ### Incremental authorization ```kotlin suspend fun incrementalAuth(transactionId: String, additionalAmountCents: Int) { withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() sdk.incrementalAuth(transactionId, additionalAmountCents) .onSuccess { println("Incremental auth approved") } .onFailure { println("Incremental auth failed: ${it.message}") } } } ``` ### Reverse / void ```kotlin suspend fun reverseTransaction(transactionId: String, amountCents: Int? = null) { withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() sdk.reverse(transactionId, amountCents) .onSuccess { println("Reverse successful: ${it.transactionId}") } .onFailure { println("Reverse failed: ${it.message}") } } } ``` ### Refund (backend-only) `sdk.refund(...)` is a plain backend call — **no card is presented and no reader events are emitted**. Use it to refund against a transaction you already hold an ID for: ```kotlin suspend fun refundTransaction(transactionId: String, amount: Int? = null) { withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() sdk.refund( transactionId = transactionId, amount = amount, // null refunds the full amount eventId = UUID.randomUUID().toString() ).onSuccess { println("Refund successful: ${it.transactionId}") }.onFailure { println("Refund failed: ${it.message}") } } } ``` `sdk.refundTransaction(transactionId, amount, eventId)` is an older alias with the same signature and behavior; prefer `refund(...)` in new code. ### EMV refund (tap required) When the customer must present their card to receive the refund, use `sdk.refundEmv(...)`. This is the **only** refund entry point that drives the thin client, so it returns a `Flow` and emits the same reader events as `sale`: ```kotlin suspend fun refundEmv( activity: Activity, transactionId: String, amount: Int, // amount in cents, required breakdown: PaymentBreakdown? = null, buttonProperties: List? = null, currency: String = "USD", eventId: String? = null ): Flow ``` ```kotlin lifecycleScope.launch(Dispatchers.IO) { sdk.refundEmv( activity = activity, transactionId = transactionId, amount = amountInCents, eventId = UUID.randomUUID().toString() ).collect { response -> handleTransactionEvent(response) // same handler as sale } } ``` Like `sale()`, `refundEmv()` throws a `KoardErrorType.NotReady.*` `KoardException` before returning a Flow when the SDK is not ready, and it has no `tapTimeoutMs` parameter. ### Tip adjustments ```kotlin suspend fun adjustTip(transactionId: String, newTipCents: Int) { withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() sdk.adjust( transactionId = transactionId, type = AmountType.FIXED, amount = newTipCents ).onSuccess { println("Tip adjusted. New total: ${it.totalAmount}") }.onFailure { println("Tip adjustment failed: ${it.message}") } } } ``` ### Confirming a surcharge When a completion event carries a transaction with `status == KoardTransactionStatus.SURCHARGE_PENDING`, prompt the customer and submit their decision with `sdk.confirm(...)`: ```kotlin suspend fun confirmSurcharge(transactionId: String, accepted: Boolean) { withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() sdk.confirm(transactionId = transactionId, confirm = accepted) .onSuccess { println("Surcharge ${if (accepted) "accepted" else "declined"}: ${it.totalAmount}") } .onFailure { println("Confirm failed: ${it.message}") } } } ``` ### Canceling the reader session There are two teardown paths. They do different things — one aborts a tap in flight, the other releases the IPC connection to the Tap to Pay Ready app. | Call | Use for | Effect | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sdk.cancelTransaction()` | Programmatic cancel, tap timeouts | Calls KiC `cancelTransaction()`, which aborts the current tap and emits a cancel terminal through the active flow, then clears the SDK's transaction guard. It does **not** call `unbindKernelService()`, so the SDK-connector's binding to the Tap to Pay Ready app is left in place | | `sdk.resetKernelService()` | Navigating away from the transaction screen (including after a completed transaction, before the receipt screen), and recovery: the kernel app died, an unrecoverable error, or "another tap is already active" | Unbinds the IPC connection to the Tap to Pay Ready app and clears the SDK's transaction state. Enrollment and the kernel app itself are untouched. The next tap re-initializes lazily; call `prepare()` afterward only to avoid paying that latency on the critical path | ```kotlin suspend fun cancelTap() { withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() sdk.cancelTransaction() // abort the tap; leaves the IPC binding in place .onSuccess { println("Tap cancelled") } .onFailure { println("Cancel failed: ${it.message}") } } } suspend fun releaseReaderSession() { withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() sdk.resetKernelService() // unbind IPC; safe after a completed transaction .onSuccess { println("Reader session reset") } .onFailure { println("Reset failed: ${it.message}") } } } ``` **The underlying KiC `cancelTransaction()` API is deprecated.** Visa's 25.06.10 release notes list "Deprecated cancelTransaction() Api" alongside the change that makes the Tap to Pay Ready app call cancel itself when the user presses its cancel button (it now emits `CancelTransactionInitiated` instead of asking your app to cancel). The KiC integration guide has no §3.4 section specifying `cancelTransaction()`'s behavior; it appears only in §3.4.10 as one of the calls that returns a `TransactionResponseMessage`.\ \ Koard's `cancelTransaction()` still calls it, and it is the path the SDK's own tap-timeout timer uses. Treat customer-pressed cancel as handled by the Tap to Pay Ready app, and reach for this wrapper when _your_ code needs to abort a tap. If a future kernel release removes the API, this wrapper is the surface that breaks — pin your integration tests to it. `resetKernelService()` wraps KiC `unbindKernelService()` (KiC integration guide §3.4.12, "Resetting the Service Connection State", implementation _optional_). Visa's own sample calls it once a transaction completes, before navigating to the receipt screen — so it is routine lifecycle hygiene, not just an emergency lever. It resets the connection between the SDK-connector and the Tap to Pay Ready app; it does **not** unenroll the device, uninstall the kernel app, or discard credentials.\ \ It always clears the SDK's internal state, even when the underlying unbind throws (for example because the kernel app is already dead) — the returned `Result` reports the unbind failure, but the next tap flow can still start. Use `sdk.hasActiveTapTransaction()` to check whether a tap is in flight before tearing anything down. ### Account, location, and terminal details Use these suspend lookups to read merchant, location, and terminal configuration (including surcharge and tax details) when building your checkout UI: ```kotlin sdk.getMerchantAccount() // Result sdk.getLocation(locationId) // Result sdk.getTerminal(terminalId) // Result ``` ### Transaction history and receipts ```kotlin // Offset paging over the active location's transactions. KoardTransactionDetails // carries limit/offset/page/total, so advance offset and compare offset + loaded // against total to implement "load more". sdk.getTransactions(limit = 50, offset = 0) // Result sdk.getTransaction(transactionId) // Result // Email, SMS, or both — pass null for the channel you don't want. sdk.sendReceipt( transactionId = transactionId, email = "customer@example.com", phoneNumber = null ) // Result ``` ## Next steps * Review the [Installing the SDK](/docs/setting-up-the-android-sdk/installing-the-sdk) guide for initialization, location selection, and enrollment * Explore the [Demo App](/docs/setting-up-the-android-sdk/demo) to see `MainScreenViewModel` in action * See [SDK Response Codes](/docs/setting-up-the-android-sdk/sdk-response-codes) for the full reference of final statuses, display messages, and error scenarios * Consult `KoardPaymentModels.kt` for `PaymentBreakdown`, `Surcharge`, and other metadata helpers * Check [Supported Devices & NFC Tap Location](/docs/setting-up-the-android-sdk/supported-devices) for device compatibility and where customers should tap ## Best Practices * **Idempotency everywhere**: Pass a stable `eventId` into `sale`, `preauth`, refunds, reversals, and adjustments so Koard can safely dedupe retried requests. * **UI threading**: Collect the payment Flow on `Dispatchers.IO`, but dispatch UI updates (Compose state, views, dialogs) back to the main thread to avoid lifecycle crashes. * **Surface readiness early**: Bind `readinessState` to visible UI (like the demo Settings screen) so operators see why the reader is blocked (developer mode, missing tap-to-pay dependency, no active location). * **Telemetry hooks**: Each `KoardTransactionResponse` carries `statusCode`, `readerStatus`, and `finalStatus`. Log or export these fields for device fleet monitoring and support. * **Lifecycle hygiene**: Cancel the Flow when the hosting Activity/Fragment stops, and call `sdk.cancelTransaction()` for programmatic cancels and timeouts — it aborts the tap without unbinding. Call `sdk.resetKernelService()` when navigating away from the transaction screen — including after a completed transaction, per KiC §3.4.12 — to release the IPC connection. * **NFC registration**: Keep `registerActivityForNfc()` / `unregisterActivityForNfc()` wired into `onResume()` / `onPause()` on every activity that can host a tap. Visa marks these **optional** (KiC §3.3.15.1–2), but they claim NFC foreground dispatch so another `TECH_DISCOVERED` app cannot jump in front of your tap (§3.4.14). # Demo ## Setup To run the demo app, you need to get the project and configure your API credentials: **1. Get the Demo Project** * Clone the Git repository: `https://github.com/koardlabs/koard-android.git` * Or [download the ZIP file](https://github.com/koardlabs/koard-android/archive/refs/heads/main.zip) **2. Open the Project in Android Studio** * Open Android Studio * Select **File > Open** * Navigate to the `koard-android` folder and open it * Wait for Gradle sync to complete The `koard-android` repository is fully standalone—no parent project or linked modules required. The demo app is the root Gradle module, and the SDK itself resolves from Maven Central as `com.koard`. **3. Configure Your Credentials** Open `build.gradle.kts` and set your **API key** for each build flavor via the `API_KEY` `buildConfigField`. The flavor's `ENVIRONMENT` field selects the matching `KoardEnvironment` at startup: ```kotlin android { flavorDimensions += "environment" productFlavors { create("uat") { dimension = "environment" isDefault = true buildConfigField("String", "ENVIRONMENT", "\"UAT\"") buildConfigField("String", "API_KEY", "\"YOUR_API_KEY\"") applicationIdSuffix = ".uat" } create("prod") { dimension = "environment" buildConfigField("String", "ENVIRONMENT", "\"PROD\"") buildConfigField("String", "API_KEY", "\"YOUR_API_KEY\"") } } } ``` Replace `YOUR_API_KEY` with your Koard API key. The merchant **code** and **PIN** are not build config values — you enter them on the demo's login screen at runtime, where they are passed to `sdk.login(merchantCode, merchantPin)`. The active location is selected in-app after login. **Important**: For production use, externalize these credentials using secure storage or environment variables. Never commit actual credentials to version control. **4. Select Build Flavor** The demo app has two build flavors: | Flavor | API Environment | Application ID Suffix | | ------ | --------------------- | --------------------- | | uat | UAT/Testing (default) | `.uat` | | prod | Production | (none) | In Android Studio: * Go to **Build > Select Build Variant** * Choose your desired flavor (e.g., `uatDebug` for testing) There is no `dev` flavor. To point the demo at a development backend, switch the SDK to `KoardEnvironment.Custom(...)` in `DemoApplication`. **5. Build the Project** Build the project using one of these methods: **Via Android Studio:** * Click **Build > Make Project** (⌘+F9 / Ctrl+F9) **Via Command Line:** ```bash # Build UAT flavor ./gradlew assembleUatDebug # Build production flavor ./gradlew assembleProdRelease ``` **6. Install on Physical Device** **Physical Device Required**: Tap to Pay on Android requires a physical device with NFC hardware. The emulator does not support NFC payments. **Prerequisites:** * Physical Android device with NFC support (Android 12+) * Tap-to-pay kernel app installed on the device (see the Running Payments guide) * USB debugging enabled on the device **Install the app:** **Via Android Studio:** * Connect your device via USB * Click **Run > Run 'app'** (Shift+F10) **Via Command Line:** ```bash # Install UAT build ./gradlew installUatDebug # Install production build ./gradlew installProdRelease ``` ## Running the Demo Launch the App Open the Koard Demo app on your device. You should see the main screen. Authenticate Merchant Enter your merchant **code** and **PIN** on the login screen — these are entered at runtime, not baked into the build — and submit. The demo calls `sdk.login(merchantCode, merchantPin)`. You will know authentication was successful if you see: * The app navigates past the login screen * The location selector and enrollment panel become available Select Location Tap the location selector in the top bar and choose your active location. The demo calls `sdk.setActiveLocation(location.id)` with your choice. The selected location is used for all transactions, and **enrollment cannot proceed without it**. Enroll the Device **Enrollment is not automatic.** Nothing enrolls as a side effect of logging in or picking a location — you must press the button. Until enrollment completes, the demo's home screen shows the enrollment panel instead of the transaction form, and any tap API would throw `KoardErrorType.NotReady.NotEnrolled`. While `readinessState.enrollmentState` is anything other than `Enrolled`, the demo's home screen renders an enrollment panel in place of the transaction form. Press **Enroll Device** to run `sdk.enrollDevice()`. Under the hood the demo: 1. Checks the tap-to-pay kernel app is installed via `sdk.isKernelAppInstalled()`, and surfaces "Visa Kernel app is not installed." if it isn't 2. Calls `sdk.enrollDevice()`, which generates device certificates and enrolls the device with Koard's payment services 3. Treats the returned `String` as an error message if it contains "failed" or "error", and catches `KoardException` for typed failures You will know enrollment was successful if you see: * The enrollment panel is replaced by the transaction form * `readinessState.enrollmentState` becomes `Enrolled` and the reader status banner clears * NFC transactions can be initiated Enrollment is rejected outright if developer mode is on, if no active location is set, or if the device is already enrolled. To re-enroll, use **Unenroll Device** on the Settings screen (which calls `sdk.unenrollDevice()`) first. Disable Developer Mode **IMPORTANT: Turn Off Developer Mode** Before processing any tap to pay transactions, you MUST disable developer mode on your Android device: 1. Go to **Settings > System > Developer Options** 2. Toggle **Developer Mode** to OFF 3. Restart your device (recommended) Tap to Pay transactions will fail if developer mode is enabled. This is a security requirement from the payment processor. **Developer Workflow:** * **Enable Developer Mode** → Install/update app → **Disable Developer Mode** → Run transactions Process Sample Transaction Test a contactless payment: 1. Tap **Process Transaction** or **New Payment** 2. Enter a sample dollar amount (e.g., $10.00) 3. Select transaction type (Sale or Preauth) 4. Tap **Start** or **Continue** Complete Tap to Pay Transaction Complete the payment flow: 1. Prompt the customer to tap their card or phone 2. Hold the card/phone against the device's NFC reader 3. Wait for the transaction to process 4. View the transaction result (approved/declined) Transaction details will include: * Transaction ID * Authorization code * Card type and last 4 digits * Amount charged * Transaction status View Transaction History Navigate to **Transaction History** to: * View all completed transactions * Filter by date or status * View detailed transaction information * Send receipts via email or SMS ## Demo App Features The demo app showcases the following SDK capabilities: ### Merchant Authentication * Login with merchant code and PIN * Session management * Automatic token refresh ### Device Enrollment * NFC configuration with Koard's tap-to-pay services * Certificate management * Device provisioning status ### Location Management * Retrieve all merchant locations * Select active location * Location-based transaction processing ### Transaction Processing * Sale transactions (immediate capture) * Preauthorization (auth hold) * Capture (settle preauth) * Refund processing * Transaction reversal * Tip adjustment ### Transaction History * List all transactions * View transaction details * Filter and search * Transaction status tracking ### Digital Receipts * Send receipts via email * Send receipts via SMS * Receipt customization ## Project Structure The demo app structure: ```plaintext koard-android/ ├── src/ │ └── main/ │ ├── java/com/koard/android/ │ │ ├── DemoApplication.kt # SDK initialization │ │ ├── MainActivity.kt # NFC register/unregister in onResume/onPause │ │ ├── navigation/ # Tab navigation (History, Home, Settings) │ │ └── ui/ │ │ ├── LoginScreen.kt / LoginViewModel.kt │ │ ├── MainScreen.kt / MainScreenViewModel.kt │ │ ├── SettingsScreen.kt │ │ ├── TransactionDetailsScreen.kt │ │ └── TransactionHistoryScreen.kt │ ├── res/ │ │ └── xml/nfc_tech_filter.xml # IsoDep tech filter for TECH_DISCOVERED │ └── AndroidManifest.xml ├── build.gradle.kts # Build configuration + SDK coordinate ├── settings.gradle.kts # google() + mavenCentral() only └── README.md ``` ### SDK Integration The demo resolves the SDK straight from Maven Central — there is no bundled AAR and no local repository to configure: ```kotlin // build.gradle.kts implementation("com.koard:koard-android-sdk:1.0.6") ``` The published artifact is a **shaded fat AAR**: it bundles the tap-to-pay engine (KiC) and relocates OkHttp, Okio, Retrofit, kotlinx.serialization, and Ktor under `com.koardlabs.*`, so those never clash with your app's own versions. It also means they are **not** transitive dependencies — see [Installing the SDK](/docs/guides/android-sdk/details/installing-sdk) for what you must declare yourself. The environment (UAT vs. PROD) is chosen at runtime from the flavor's `ENVIRONMENT` `buildConfigField`, not by swapping artifacts: * **UAT builds** → `KoardEnvironment.UAT` * **Prod builds** → `KoardEnvironment.PROD` ### How the Demo Initializes the SDK `DemoApplication.onCreate()` maps `BuildConfig.ENVIRONMENT` to a `KoardEnvironment` and initializes the SDK on a worker thread, passing the API key from `BuildConfig`: ```kotlin val environment = when (BuildConfig.ENVIRONMENT) { "PROD" -> KoardEnvironment.PROD "UAT" -> KoardEnvironment.UAT else -> KoardEnvironment.UAT } KoardMerchantSdk.initialize( application = this@DemoApplication, apiKey = BuildConfig.API_KEY, environment = environment, timeoutSeconds = 30L ) ``` Merchant credentials are supplied later, on the login screen, via `sdk.login(merchantCode, merchantPin)`. To shave first-tap latency, you can warm the reader up after init by collecting `sdk.prepare()` in the background and keying off the `KoardPrepareStatus.Done` emission. See [Running Payments](/docs/guides/android-sdk/details/running-payments) for details. ## Build Commands Reference ```bash # Clean build ./gradlew clean # Build all flavors ./gradlew build # Build specific flavor ./gradlew assembleUatDebug ./gradlew assembleProdRelease # Install on device ./gradlew installUatDebug ./gradlew installProdRelease # Uninstall from device ./gradlew uninstallUatDebug ./gradlew uninstallProd # Run tests ./gradlew test # Generate APK ./gradlew assembleDebug ./gradlew assembleRelease ``` ## Troubleshooting ### Common Issues **App crashes on startup** **Solution**: * Verify credentials are configured correctly in `build.gradle.kts` * Check that you're not using placeholder values (YOUR\_API\_KEY\_HERE) * Review Logcat for specific error messages **"Device not provisioned" error** **Solution**: * Ensure the tap-to-pay kernel app is installed (see the Running Payments guide for setup steps) * Confirm you selected a location and then pressed **Enroll Device** — enrollment never runs on its own * Check NFC hardware is functioning * Verify the kernel app is up to date **NFC not working** **Solution**: * Confirm device has NFC hardware * Enable NFC in device settings * Verify device enrollment was successful * Test with a physical payment card * **IMPORTANT**: Ensure developer mode is DISABLED on the device **"Transaction failed" or "NFC error" during tap to pay** **Solution**: * **Disable developer mode** - This is the most common cause of transaction failures * Go to **Settings > System > Developer Options** and toggle OFF * Restart your device after disabling developer mode * Ensure the tap-to-pay kernel app is running and up to date * Check that NFC is enabled in device settings **Build errors** **Solution**: * Verify JDK 21 is configured * Check minimum SDK is set to 31 * Clean and rebuild: `./gradlew clean build` * Invalidate caches: **File > Invalidate Caches / Restart** **Network errors** **Solution**: * Check device has internet connection * Verify API key and credentials are correct * Ensure you're using the correct build flavor for your environment (uat/prod) ### Debugging Tips **Enable verbose logging:** SDK log verbosity is set **once**, at initialization — there is no runtime setter. Pass a `KoardLogLevel` in `DemoApplication.onCreate()`: ```kotlin import com.koardlabs.merchant.sdk.domain.KoardLogLevel KoardMerchantSdk.initialize( application = this@DemoApplication, apiKey = BuildConfig.API_KEY, environment = environment, timeoutSeconds = 30L, logLevel = KoardLogLevel.VERBOSE ) ``` Levels are `VERBOSE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, and `NONE`. The default is `DEBUG` in debug builds and `NONE` in release, and the level applies in release builds too if you opt in. Secrets — API key and token headers, device certificates, and enrollment key material — are never logged at any level. **Check Logcat:** The SDK logs under the fixed tag `KoardSDK`: ```bash adb logcat -s KoardSDK ``` **Verify SDK version:** Check which SDK version the demo is using: ```bash ./gradlew dependencies | grep koard ``` ## Security Notes * Never commit actual credentials to version control * Use environment variables or secure credential management for production * The `build.gradle.kts` credentials are for demo purposes only * Consider using Android Keystore for sensitive data in production apps * Keep the demo app on devices used exclusively for testing ## Updating the SDK Updating the SDK is a one-line change — bump the Maven coordinate in `build.gradle.kts` and rebuild: ```kotlin implementation("com.koard:koard-android-sdk:") ``` There is no AAR to download or copy: the artifact resolves from `mavenCentral()`, which `settings.gradle.kts` already declares. 1.0.6 changed bytecode signatures (`initialize`, `getTransactions`, and `sale` gained defaulted parameters) and replaced the leaked Visa `ButtonProperties` type with `KoardButtonProperties`. Always do a clean rebuild after bumping the version rather than dropping a new artifact onto pre-compiled call sites. ## Next Steps After running the demo app successfully: * [Install SDK in Your App](/docs/setting-up-the-android-sdk/installing-the-sdk) - Integrate SDK into your own application, including login, location selection, and enrollment * [Running Payments](/docs/setting-up-the-android-sdk/running-payments) - Drive tap-to-pay sessions and post-reader operations * [SDK Response Codes](/docs/setting-up-the-android-sdk/sdk-response-codes) - Understand transaction outcomes, error codes, and error handling * [Payment Lifecycle](/docs/payments/payment-lifecycle) - Understand payment flows * [Supported Devices & NFC Tap Location](/docs/setting-up-the-android-sdk/supported-devices) - Device compatibility and where to tap * [Troubleshooting](/docs/setting-up-the-android-sdk/troubleshooting) - Fix enrollment failures and taps that cancel instantly ## Support For technical support or questions: * Email: * Documentation: * GitHub Issues: --- title: Surcharging --- # Surcharging Surcharging adds a fee to credit card transactions to offset processing costs. Koard supports both **automatic surcharging** (processor-calculated) and **custom surcharging** (merchant-calculated, e.g., BIN-based). > **Debit cards:** Surcharges must not be applied to debit card transactions. Koard automatically excludes debit cards from surcharging for US-based transactions. The SDK will not trigger a `surchargePending` status for debit cards. > **Partner responsibility:** It is the partner's responsibility to ensure that merchants configure the correct surcharge rates, disclosure text, and comply with applicable card brand rules and state/local regulations. Koard provides the surcharge infrastructure, but legal compliance—including rate caps, signage, and receipt requirements—is the merchant's obligation. > **Disclosure requirement:** Most card brand rules and state laws require that the surcharge is disclosed to the cardholder *before* the transaction is completed. Koard's SDK handles this via the surcharge confirmation flow, but the partner must ensure the disclosure content is accurate and legally compliant. ## How Automatic Surcharging Works 1. The merchant initiates a [sale](sale.md). 2. The processor evaluates the card—only **credit cards** in eligible regions are surcharged. Debit cards are automatically excluded. 3. If eligible, the transaction returns with status `surchargePending`. 4. The SDK surfaces the surcharge amount and disclosure text. 5. The merchant app presents the disclosure to the customer. 6. The merchant calls `confirm()` with the customer's decision. 7. If confirmed, the transaction finalizes with the surcharge included. > **Note:** Surcharge pending only applies to **sale** transactions. Preauth transactions do not trigger `surchargePending`—use the [custom BIN surcharge flow](#flow-2-custom-bin-surcharge-via-preauth) instead. ## Surcharge Calculation Basis The surcharge percentage is applied to the **full transaction amount** (subtotal + tax + tip), not just the subtotal: ``` surcharge = (subtotal + taxAmount + tipAmount) * surchargeRate ``` For example, with a 3.5% surcharge on a $100 subtotal + $8.75 tax + $20 tip: ``` surcharge = (10000 + 875 + 2000) * 0.035 = 12875 * 0.035 = $4.51 (451 cents) ``` ## Rate Hierarchy Surcharge rates are resolved in priority order: | Priority | Source | Description | |----------|--------|-------------| | 1 (highest) | `PaymentBreakdown.surcharge` | Per-transaction override passed in the SDK call | | 2 | Terminal configuration | Rate set on the terminal | | 3 | Location configuration | Rate set on the location | | 4 | Account configuration | Default rate on the merchant account | | 5 (lowest) | Processor default | Fallback rate from the payment processor | ## Surcharge Settings (Account / Location / Terminal) Beyond the per-transaction override, surcharge behavior is configured on the **account**, **location**, and **terminal** records. Each level can set: | Field | Meaning | |---|---| | `surcharge_rate` | Surcharge percentage applied to eligible credit-card transactions. | | `surcharge_basis` | What the surcharge is calculated on (e.g. subtotal vs. subtotal + tax + tip). | | `surcharge_confirmation_required` | Whether the cardholder must confirm the surcharge before the sale completes. | **Resolution — most specific wins.** Koard uses the value on the **terminal** if present, else the **location**, else the **account** (a `null` at a more specific level means "inherit"). This is the same account → location → terminal hierarchy used for [tax](/payments/tax-and-tip-handling). ### Surcharge Confirmation When `surcharge_confirmation_required` resolves to `true`, the surcharge must be **confirmed by the cardholder** before the sale completes — the SDK surfaces the surcharge and disclosure (`surchargePending`) and requires acknowledgement via `confirm()` (see [How Automatic Surcharging Works](#how-automatic-surcharging-works)). When it resolves to `false`, the surcharge is applied without a separate confirmation step. Note that card-brand rules and many state laws **require** disclosure before completion regardless of this flag — see the **Disclosure requirement** above. ## The `Surcharge` Object Both SDKs use a nested `Surcharge` object inside `PaymentBreakdown`: ::::scalar-tabs :::scalar-tab{ title="iOS" } ```swift PaymentBreakdown.Surcharge( amount: Int?, // fixed surcharge in cents percentage: Double?, // surcharge rate as decimal (0.035 = 3.5%) bypass: Bool // skip automatic surcharge (default: false) ) ``` ::: :::scalar-tab{ title="Android" } ```kotlin Surcharge( amount: Int?, // fixed surcharge in cents percentage: Double?, // surcharge rate as decimal bypass: Boolean // skip automatic surcharge (default: false) ) ``` ::: :::: ### Usage in PaymentBreakdown ::::scalar-tabs :::scalar-tab{ title="iOS" } ```swift let breakdown = PaymentBreakdown( subtotal: 10000, taxRate: 8.75, taxAmount: 875, tipAmount: 2000, tipType: .fixed, surcharge: PaymentBreakdown.Surcharge( amount: 451, // surcharge on (10000 + 875 + 2000) at 3.5% percentage: 0.035 ) ) ``` ::: :::scalar-tab{ title="Android" } ```kotlin val breakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipAmount = 2000, tipType = "fixed", surcharge = Surcharge( amount = 451, // surcharge on (10000 + 875 + 2000) at 3.5% percentage = 0.035 ) ) ``` ::: :::: --- ## API Flows ### Flow 1: Automatic Surcharge (Sale) This is the standard flow where the processor automatically determines surcharge eligibility. Surcharge pending **only** triggers on sale transactions. #### Step 1 — Initiate Sale via SDK The sale is initiated through the Koard SDK on the device. The SDK handles card reading, encryption, and communication with Koard's servers. ::::scalar-tabs :::scalar-tab{ title="iOS" } ```swift let breakdown = PaymentBreakdown( subtotal: 10000, taxRate: 8.75, taxAmount: 875, tipAmount: 2000, tipType: .fixed ) let result = try await koard.createSale(amount: 12875, breakdown: breakdown) // result.status may be "surcharge_pending" for eligible credit cards ``` ::: :::scalar-tab{ title="Android" } ```kotlin val breakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipAmount = 2000, tipType = TipType.FIXED ) val result = koard.createSale(amount = 12875, breakdown = breakdown) // result.status may be "surcharge_pending" for eligible credit cards ``` ::: :::: #### Step 2 — Handle `surchargePending` Response If the card is a credit card and surcharge rules apply: ```json { "transaction_id": "txn_abc123", "status": "surcharge_pending", "total_amount": 13326, "subtotal": 10000, "tax_amount": 875, "tip_amount": 2000, "surcharge_applied": true, "surcharge_amount": 451, "surcharge_rate": 0.035, "card_brand": "visa", "card_type": "credit", "payment_method": "contactless" } ``` > If the card is debit, the response will be `captured` immediately with no surcharge. No confirm step is needed. #### Step 3 — Confirm or Decline Surcharge Present the surcharge disclosure to the customer, then confirm: ```bash POST /v1/payments/{transaction_id}/confirm X-Koard-apikey: {api_key} { "confirm": true, "event_id": "evt_confirm_id" } ``` **Response (confirmed):** ```json { "transaction_id": "txn_abc123", "status": "captured", "total_amount": 13326, "subtotal": 10000, "tax_amount": 875, "tip_amount": 2000, "surcharge_applied": true, "surcharge_amount": 451, "surcharge_rate": 0.035 } ``` **Response (declined — `confirm: false`):** ```json { "transaction_id": "txn_abc123", "status": "cancelled", "total_amount": 0, "surcharge_applied": false, "surcharge_amount": 0 } ``` --- ### Flow 2: Custom BIN Surcharge via Preauth For merchants who calculate surcharges based on the card's BIN. Use preauth with `bypass: true` to skip automatic surcharge, then add the surcharge via incremental auth. #### Step 1 — Preauth with Bypassed Surcharge via SDK Initiate a preauth through the SDK with `bypass: true` to skip automatic surcharge calculation: ::::scalar-tabs :::scalar-tab{ title="iOS" } ```swift let breakdown = PaymentBreakdown( subtotal: 10000, taxRate: 8.75, taxAmount: 875, tipAmount: 2000, tipType: .fixed, surcharge: PaymentBreakdown.Surcharge(bypass: true) ) let result = try await koard.createPreauth(amount: 12875, breakdown: breakdown) ``` ::: :::scalar-tab{ title="Android" } ```kotlin val breakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipAmount = 2000, tipType = TipType.FIXED, surcharge = Surcharge(bypass = true) ) val result = koard.createPreauth(amount = 12875, breakdown = breakdown) ``` ::: :::: **Response:** ```json { "transaction_id": "txn_preauth_123", "status": "authorized", "total_amount": 12875, "subtotal": 10000, "tax_amount": 875, "tip_amount": 2000, "surcharge_applied": false, "surcharge_amount": 0, "card_brand": "visa", "card_type": "credit" } ``` #### Step 2 — BIN Lookup and Calculate Surcharge Use the card BIN from the response to determine surcharge eligibility: ```javascript const bin = response.transaction.bin; const isDebit = await checkIsDebitCard(bin); if (isDebit) { // Debit card — skip surcharge, capture at original amount await capture(transactionId, 12875); return; } // Credit card — calculate surcharge const baseAmount = 12875; const surchargeRate = lookupSurchargeRate(bin); // e.g., 0.035 const surchargeAmount = Math.round(baseAmount * surchargeRate); // 451 ``` #### Step 3 — Incremental Auth for Surcharge Amount Add the surcharge as an incremental authorization on the existing preauth: ```bash POST /v3/payments/{transaction_id}/auth X-Koard-apikey: {api_key} { "amount": 451, "breakdown": { "subtotal": 0, "surcharge": { "amount": 451, "percentage": 0.035 } }, "event_id": "evt_inc_auth_id" } ``` **Response:** ```json { "transaction_id": "txn_preauth_123", "status": "authorized", "total_amount": 13326, "subtotal": 10000, "tax_amount": 875, "tip_amount": 2000, "surcharge_applied": true, "surcharge_amount": 451, "surcharge_rate": 0.035 } ``` #### Step 4 — Capture the Full Amount ```bash POST /v4/payments/{transaction_id}/capture X-Koard-apikey: {api_key} { "amount": 13326, "breakdown": { "subtotal": 10000, "taxRate": 8.75, "taxAmount": 875, "tipAmount": 2000, "tipType": "fixed", "surcharge": { "amount": 451, "percentage": 0.035 } }, "event_id": "evt_capture_id" } ``` **Response:** ```json { "transaction_id": "txn_preauth_123", "status": "captured", "total_amount": 13326, "subtotal": 10000, "tax_amount": 875, "tip_amount": 2000, "surcharge_applied": true, "surcharge_amount": 451, "surcharge_rate": 0.035, "batch_id": "batch_456" } ``` --- ### Flow 3: Preauth with Surcharge, Then Remove on Capture For cases where you preauth with surcharge included initially, but then discover the surcharge can't be applied (e.g., BIN lookup reveals a debit card). Capture at the lower amount with an updated breakdown. #### Step 1 — Preauth with Surcharge Included via SDK Initiate a preauth with the surcharge pre-calculated and included in the total: ::::scalar-tabs :::scalar-tab{ title="iOS" } ```swift let breakdown = PaymentBreakdown( subtotal: 10000, taxRate: 8.75, taxAmount: 875, tipAmount: 2000, tipType: .fixed, surcharge: PaymentBreakdown.Surcharge(amount: 451, percentage: 0.035) ) // Total = 10000 + 875 + 2000 + 451 = 13326 let result = try await koard.createPreauth(amount: 13326, breakdown: breakdown) ``` ::: :::scalar-tab{ title="Android" } ```kotlin val breakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipAmount = 2000, tipType = TipType.FIXED, surcharge = Surcharge(amount = 451, percentage = 0.035) ) // Total = 10000 + 875 + 2000 + 451 = 13326 val result = koard.createPreauth(amount = 13326, breakdown = breakdown) ``` ::: :::: **Response:** ```json { "transaction_id": "txn_preauth_456", "status": "authorized", "total_amount": 13326, "subtotal": 10000, "tax_amount": 875, "tip_amount": 2000, "surcharge_applied": true, "surcharge_amount": 451, "surcharge_rate": 0.035 } ``` #### Step 2 — BIN Lookup Reveals Debit Card Your BIN lookup returns `debit: true`. Surcharges cannot be applied to debit cards. #### Step 3 — Capture Without Surcharge (Lower Amount) Capture at the original amount without surcharge. The processor releases the unused hold automatically: ```bash POST /v4/payments/{transaction_id}/capture X-Koard-apikey: {api_key} { "amount": 12875, "breakdown": { "subtotal": 10000, "taxRate": 8.75, "taxAmount": 875, "tipAmount": 2000, "tipType": "fixed", "surcharge": { "bypass": true } }, "event_id": "evt_capture_no_surcharge" } ``` **Response:** ```json { "transaction_id": "txn_preauth_456", "status": "captured", "total_amount": 12875, "subtotal": 10000, "tax_amount": 875, "tip_amount": 2000, "surcharge_applied": false, "surcharge_amount": 0, "surcharge_rate": 0, "batch_id": "batch_789" } ``` > The difference between the authorized amount ($133.26) and the captured amount ($128.75) is automatically released back to the cardholder. --- ## Bypassing Automatic Surcharge Set `bypass: true` to skip the processor's automatic surcharge calculation. Required for custom surcharge workflows: ::::scalar-tabs :::scalar-tab{ title="iOS" } ```swift let breakdown = PaymentBreakdown( subtotal: 10000, taxRate: 8.75, taxAmount: 875, tipType: .fixed, surcharge: PaymentBreakdown.Surcharge(bypass: true) ) ``` ::: :::scalar-tab{ title="Android" } ```kotlin val breakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipType = "fixed", surcharge = Surcharge(bypass = true) ) ``` ::: :::: ## Transaction Response — Surcharge Fields | Field | Type | Description | |-------|------|-------------| | `surcharge_applied` | boolean | Whether a surcharge was applied to this transaction | | `surcharge_amount` | integer | Surcharge amount in cents | | `surcharge_rate` | float | Surcharge rate as decimal (0.035 = 3.5%) | ## Surcharging on Other Operations | Operation | Surcharge Behavior | |-----------|-------------------| | [Sale](sale.md) | Surcharge calculated automatically; triggers `surchargePending` for confirmation | | [Preauth](preauth.md) | No automatic surcharge pending. Use `bypass: true` + incremental auth for custom surcharge | | [Capture](capture.md) | Include surcharge in breakdown for accurate settlement. Can capture less to remove surcharge | | [Incremental Auth](incremental-auth.md) | Used to add custom surcharge amounts to existing preauth | | [Refund](refund.md) | Surcharge prorated automatically—no breakdown needed | | [Reverse](reverse.md) | Full surcharge released automatically—no breakdown needed | | [Tip Adjust](tip-adjust.md) | Surcharge preserved—not recalculated on tip change | ## See Also - [Sale](sale.md) — One-step payment with automatic surcharge - [Preauth](preauth.md) — Hold with surcharge bypass option - [Incremental Auth](incremental-auth.md) — Add custom surcharge to existing auth - [Payment Lifecycle](payment-lifecycle.md) — End-to-end payment flow --- title: Incremental Auth --- # Incremental Auth Incremental authorization increases the hold amount on an existing [preauth](preauth.md). Common uses: - Adding a custom surcharge after BIN lookup - Increasing the hold for additional items or services - Adjusting the authorization before [capture](capture.md) ## Basic Incremental Auth **iOS:** ```swift let response = try await KoardMerchantSDK.shared.auth( transactionId: transactionId, amount: 2000 // increase hold by $20 ) ``` **Android:** ```kotlin sdk.incrementalAuth( transactionId = transactionId, amount = 2000 ) ``` ## Incremental Auth with Surcharge Breakdown When adding a custom surcharge via incremental auth, include the surcharge details in the breakdown. The surcharge percentage is applied to the **full transaction amount** (subtotal + tax + tip): **iOS:** ```swift let baseAmount = 10000 + 875 + 2000 // subtotal + tax + tip = 12875 let surchargeRate = 0.035 let surchargeAmount = Int(Double(baseAmount) * surchargeRate) // 451 let surchargeBreakdown = PaymentBreakdown( subtotal: 0, taxAmount: 0, tipType: .fixed, surcharge: PaymentBreakdown.Surcharge( amount: surchargeAmount, percentage: surchargeRate ) ) let response = try await KoardMerchantSDK.shared.auth( transactionId: transactionId, amount: surchargeAmount, breakdown: surchargeBreakdown ) ``` **Android:** ```kotlin val baseAmount = 10000 + 875 + 2000 // 12875 val surchargeRate = 0.035 val surchargeAmount = (baseAmount * surchargeRate).toInt() // 451 val surchargeBreakdown = PaymentBreakdown( subtotal = 0, taxAmount = 0, tipType = "fixed", surcharge = Surcharge( amount = surchargeAmount, percentage = surchargeRate ) ) sdk.incrementalAuth( transactionId = transactionId, amount = surchargeAmount, breakdown = surchargeBreakdown ) ``` ## Custom Surcharging via BIN Lookup The most common use of incremental auth is the [BIN-based surcharging workflow](surcharging.md#bin-based-custom-surcharge): 1. **Preauth** with `surcharge: Surcharge(bypass: true)` to get the card BIN 2. **BIN lookup** to determine if the card is credit (surchargeable) or debit (not surchargeable) 3. **Incremental auth** to add the calculated surcharge amount 4. **Capture** at the full amount with the complete breakdown > **Reminder:** Surcharges must not be applied to debit cards. Always verify the card type from the BIN before adding a surcharge via incremental auth. ## Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `transactionId` | `String` | Yes | Existing preauth to increase | | `amount` | `Int` | Yes | Additional amount to authorize (in minor units) | | `breakdown` | `PaymentBreakdown?` | No | Breakdown for the incremental amount | > **Note:** The iOS SDK method is `auth()` while the Android SDK method is `incrementalAuth()`. ## See Also - [Preauth](preauth.md) — Initial authorization hold - [Capture](capture.md) — Finalize after incrementing - [Surcharging](surcharging.md) — BIN-based custom surcharge workflow # Payment Lifecycle Understand how Koard transactions progress from initial authorization through capture, adjustment, reversal, and refund. **What You Learn** * How sale and preauthorization flows differ * Which follow-up operations are available and when to use them * iOS SDK entry points and their matching REST endpoints * How to monitor state transitions and handle errors ## Before You Begin * Review the transaction-specific guides: [Sale](/docs/payments/methods/sale), [Preauth](/docs/payments/methods/preauth), [Capture](/docs/payments/methods/capture), [Incremental Auth](/docs/payments/methods/incremental-auth), [Tip Adjust](/docs/payments/methods/tip-adjust), [Reverse](/docs/payments/methods/reverse), and [Refund](/docs/payments/methods/refund). * For implementation details in Swift, start with the [Running Payments](/docs/setting-up-the-ios-sdk/running-payments) guide. * Make sure you have a dedicated test iPhone with your Sandbox Apple Account signed in so you can exercise Tap to Pay flows end-to-end. ## Transaction Categories ### Tap-Initiated Transactions (card present) | Transaction | Description | iOS SDK Method | API Entry Point | | ----------- | ----------------------- | ----------------------------------- | ------------------ | | Sale | One-step auth + capture | `KoardMerchantSDK.shared.sale()` | `POST /v4/payment` | | Preauth | Authorization hold | `KoardMerchantSDK.shared.preauth()` | `POST /v4/preauth` | These operations **require** card data from Tap to Pay or another compliant reader. ### Follow-Up Operations (card-not-present) | Transaction | Purpose | iOS SDK | REST Endpoint | | ---------------- | --------------------------------- | ----------- | -------------------------------- | | Capture | Settle an authorized amount | `capture()` | `POST /v3/payments/{id}/capture` | | Incremental Auth | Increase an existing hold | `auth()` | `POST /v3/payments/{id}/auth` | | Tip Adjust | Update gratuity before settlement | `adjust()` | `POST /v1/payments/{id}/adjust` | | Reverse | Release held funds | `reverse()` | `POST /v1/payments/{id}/reverse` | | Refund | Return captured funds | `refund()` | `POST /v1/payments/{id}/refund` | **REST vs SDK**: After a successful tap, you can perform every follow-up operation via the REST API, the iOS SDK, or both—choose the channel that fits your workflow. ## Lifecycle Flows ### Sale Flow ```plaintext Tap → Sale (status: captured) → [Optional] Refund → Complete ``` Sales capture funds immediately. Refunds return money after settlement. ### Preauth Flow ```plaintext Tap → Preauth (status: authorized) ├─ Incremental Auth (optional, status stays authorized) ├─ Capture (status: captured) → Refund (optional) └─ Reverse (status: reversed) ``` Preauths require an explicit capture to collect funds. If plans change, reverse the authorization instead of refunding. **Successive auths**: If a follow-up authorization is declined, Koard automatically reverts to the last successfully authorized amount. ## Swift SDK Reference ```swift // Sale (tap required) KoardMerchantSDK.shared.sale( amount: Int, breakdown: PaymentBreakdown? = nil, currency: CurrencyCode, transactionId: String? = nil, type: PaymentType = .sale ) async throws -> TransactionResponse // Preauthorization (tap required) KoardMerchantSDK.shared.preauth( amount: Int, currency: CurrencyCode, transactionId: String? = nil, breakdown: PaymentBreakdown? = nil ) async throws -> TransactionResponse // Follow-up operations KoardMerchantSDK.shared.capture(transactionId: String, amount: Int? = nil, breakdown: PaymentBreakdown? = nil) KoardMerchantSDK.shared.auth(transactionId: String, amount: Int, breakdown: PaymentBreakdown? = nil) KoardMerchantSDK.shared.adjust(transactionId: String, type: AdjustmentType, amount: Int? = nil, percentage: Double? = nil) KoardMerchantSDK.shared.reverse(transactionId: String, amount: Int? = nil) KoardMerchantSDK.shared.refund(transactionId: String, amount: Int? = nil) ``` ### Updated Breakdown Example ```swift let breakdown = PaymentBreakdown( subtotal: 2500, taxRate: 8.75, // 8.75% as a percent value taxAmount: 219, tipAmount: 500, tipType: .fixed, surcharge: PaymentBreakdown.Surcharge( amount: 75, // $0.75 surcharge in cents percentage: 0.03, // 3% surcharge rate bypass: false ) ) ``` `taxRate` is a floating-point percent value (e.g., `8.75` for 8.75%). Surcharge details are passed as a nested `Surcharge` object with `amount`, `percentage`, and `bypass` fields. ## REST Quick Reference | Operation | Endpoint | Notes | | ---------------- | -------------------------------------------- | --------------------------------------------- | | Sale | `POST /v4/payment` | Requires encrypted card data from Tap to Pay | | Preauth | `POST /v4/preauth` | Returns `transaction_id` for follow-ups | | Capture | `POST /v3/payments/{transaction_id}/capture` | Include breakdown to reconcile tips/surcharge | | Incremental Auth | `POST /v3/payments/{transaction_id}/auth` | Amount is the incremental delta | | Tip Adjust | `POST /v1/payments/{transaction_id}/adjust` | `percentage` is a decimal (e.g., `0.18`) | | Reverse | `POST /v1/payments/{transaction_id}/reverse` | Releases uncaptured funds | | Refund | `POST /v1/payments/{transaction_id}/refund` | Works on captured transactions | See the individual transaction guides for full payload examples. ## Transaction States | State | Description | Transitions | | ------------ | ------------------ | ------------------------------------------------ | | `pending` | Request accepted | → `processing`, `failed` | | `processing` | Gateway evaluating | → `authorized`, `captured`, `declined`, `failed` | | `authorized` | Funds on hold | → `captured`, `reversed`, `cancelled` | | `captured` | Funds collected | → `refunded`, `cancelled` | | `declined` | Processor rejected | Terminal | | `failed` | Processing error | Terminal | | `reversed` | Hold released | Terminal | | `refunded` | Funds returned | Terminal | | `cancelled` | Flow cancelled | Terminal | ### Visual Flow ```plaintext Sale: pending → processing → captured → [refunded | cancelled] Preauth: pending → processing → authorized ├─ capture → captured → [refunded | cancelled] └─ reverse → reversed ``` ## Monitoring State Changes * **Webhooks**: Subscribe to `transaction.*` events (created, authorized, captured, adjusted, reversed, refunded, settled). See [Available Events](/docs/webhooks/available-events). * **iOS SDK**: Inspect `TransactionResponse.transaction.status` to update UI immediately. ```swift switch transaction.status { case .approved, .captured: // Success case .declined: // Inform user case .error: // Retry or escalate default: // Handle intermediate states } ``` ## Error Handling & Retries ```swift func processPaymentWithRetry(maxRetries: Int = 3) async throws { var attempts = 0 while attempts < maxRetries { do { let response = try await KoardMerchantSDK.shared.sale(...) // Success return } catch { attempts += 1 if attempts >= maxRetries { throw error } try await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(attempts))) * 1_000_000_000) } } } ``` ## Best Practices * **Choose the right entry point**: Use sales for immediate capture; use preauth when totals may change. * **Store transaction IDs**: Needed for every follow-up operation and webhook reconciliation. * **Keep breakdowns accurate**: Supply tax, tip, and surcharge data with the latest values to keep reports aligned. * **Use idempotency keys**: Provide `transaction_id` or `event_id` to guard against duplicate requests. * **Monitor via webhooks**: Use asynchronous events to update order states reliably. ## Troubleshooting Checklist * **Transaction not found**: Confirm the transaction belongs to your Koard account and that the ID is spelled correctly. * **Invalid state transition**: Verify the current state (`authorized`, `captured`, etc.) before calling a new operation. * **Amount validation errors**: Capture/Refund amounts cannot exceed the available balances; partial operations require explicit amounts. * **Tap to Pay issues**: Ensure the device has Developer Mode enabled, an active Sandbox Apple Account, and that `prepare()` was called. * **SDK errors**: Authenticate with `login()`, set an active location, and handle `KoardMerchantSDKError` cases explicitly. ## See Also * [Sale](/docs/payments/methods/sale) * [Preauth](/docs/payments/methods/preauth) * [Capture](/docs/payments/methods/capture) * [Incremental Auth](/docs/payments/methods/incremental-auth) * [Tip Adjust](/docs/payments/methods/tip-adjust) * [Reverse](/docs/payments/methods/reverse) * [Refund](/docs/payments/methods/refund) * [Running Payments](/docs/setting-up-the-ios-sdk/running-payments) * [Webhooks – Available Events](/docs/webhooks/available-events) # Setting up Webhooks Learn how to configure and use webhooks to receive real-time updates from Koard's payment platform. [Get started with Koard](/docs/getting-started-with-koard/introduction) Koard allows you to add URLs that receive POST requests when specific events occur in your payment system. Each endpoint can be configured to receive a specific set of events, enabling real-time integration with your applications. **What you learn** In this guide, you'll learn: * How to set up webhook endpoints in Koard * How to configure event subscriptions and filters * How to test and verify webhook functionality * How to monitor webhook delivery and troubleshoot issues * Best practices for webhook security and reliability ## Before you begin This guide covers webhook configuration and management in Koard. For a better understanding of available events, see our [Available Events guide](/docs/webhooks/available-events). If you're ready to start processing payments, see our [Running Payments guide](/docs/setting-up-the-ios-sdk/running-payments). ## What are Webhooks? Webhooks are HTTP callbacks that Koard sends to your application when specific events occur. This allows you to receive real-time notifications about payment events, transaction updates, and other important changes without constantly polling our APIs. **Key benefits:** * **Real-time Updates**: Get instant notifications about payment events * **Automated Processing**: Trigger automated workflows based on events * **Reduced Polling**: Eliminate the need to constantly poll for updates * **Better User Experience**: Provide immediate feedback to users ## Setting Up Webhook Endpoints ### Access the Developer Portal To set up a new webhook endpoint, navigate to one of the following URLs based on your environment: | Environment | URL | | -------------- | ------------------------------------- | | **UAT** | | | **Production** | | ![webhooks-1](/webhooks-1.png) _Koard Developer Portal - Webhook Configuration_ ### Create a New Endpoint 1. **Navigate to Add Endpoint**: Click "Add Endpoint" on the right side of the developer portal page 2. **Configure HTTPS Endpoint**: Enter your HTTPS endpoint URL that will receive POST requests 3. **Select Events**: Choose which events you want to subscribe to 4. **Save Configuration**: Complete the setup process ![webhooks-2](/webhooks-2.png) _Adding a new webhook endpoint in the Koard Developer Portal_ **Important**: Currently, webhooks can only be managed via the Portal, but API management is on the roadmap for creating and managing endpoints and event filters. ### Webhook Endpoint Requirements Your webhook endpoint must meet these requirements: * **HTTPS Only**: All webhook endpoints must use HTTPS * **POST Method**: Endpoints must accept POST requests * **JSON Payload**: Events are sent as JSON in the request body * **Quick Response**: Return a 2xx status code quickly (within 30 seconds) ## Webhook Configuration ### Event Selection Choose which events you want to receive based on your integration needs. If you don't specify any event types, by default your endpoint will receive all events, regardless of type. This can be helpful for getting started and testing, but we recommend selecting specific events for production. | Event Category | Description | Common Events | | ---------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Transaction Events** | Transaction lifecycle events | `transaction.create`, `transaction.authorize`, `transaction.sale`, `transaction.capture`, `transaction.increment`, `transaction.tip_adjust`, `transaction.reverse`, `transaction.cancel`, `transaction.refund` | | **Account Events** | Account management events | `account.created`, `account.updated`, `account.blocked`, `account.unblocked`, `account.deleted` | | **Terminal Events** | Terminal configuration events | `terminal.created`, `terminal.updated`, `terminal.blocked`, `terminal.unblocked`, `terminal.deleted` | | **Location Events** | Location management events | `location.created`, `location.updated`, `location.blocked`, `location.unblocked`, `location.deleted` | | **Batch Events** | Batch processing and settlement | `batch.opened`, `batch.submitted`, `batch.accepted`, `batch.partially_accepted`, `batch.rejected`, `batch.edited` | | **API Key Events** | API key management events | `api_key.created`, `api_key.revoked`, `api_key.reinstated`, `api_key.deleted` | | **Credential Events** | Merchant credential management events | `credential.created`, `credential.blocked`, `credential.unblocked`, `credential.deleted` | For a complete list of available events with schemas and examples, see our [Available Events guide](/docs/webhooks/available-events). ### Webhook Headers Koard uses industry-standard webhook headers powered by Svix for maximum compatibility: ```plaintext Content-Type: application/json svix-id: msg_p5jXN8AQM9LWM0D4loKWxJek svix-timestamp: 1614265330 svix-signature: v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE= User-Agent: Svix-Webhooks/1.0 ``` **Header Descriptions:** * `svix-id`: Unique message identifier for idempotency * `svix-timestamp`: Unix timestamp when the webhook was sent (for replay attack prevention) * `svix-signature`: HMAC signature for verifying webhook authenticity * `Content-Type`: Always `application/json` ### Retry Policy Koard implements a robust retry policy with exponential backoff for failed webhook deliveries: | Retry Attempt | Delay | Time from First Attempt | | ------------- | ---------- | ----------------------- | | 1st | Immediate | 0 seconds | | 2nd | 5 seconds | 5 seconds | | 3rd | 5 minutes | \~5 minutes | | 4th | 30 minutes | \~35 minutes | | 5th | 2 hours | \~2 hours 35 minutes | | 6th | 5 hours | \~7 hours 35 minutes | | 7th | 10 hours | \~17 hours 35 minutes | | 8th | 10 hours | \~27 hours 35 minutes | **Key Points:** * **Response Timeout**: 15 seconds per attempt * **Success Criteria**: 2xx status code (200-299) indicates success * **Failure Criteria**: Any other status code or timeout triggers a retry * **Endpoint Disabling**: After 5 days of consecutive failures, endpoints are automatically disabled * **Manual Recovery**: Use the dashboard to recover or resend failed messages **Note**: When responding to webhooks, return a 2xx status code quickly. Process complex workflows asynchronously to avoid timeouts. ## Testing Your Webhook ### Create a Test Endpoint To test your webhook submission, create a webhook that can accept all transaction events and use the Koard SDK to process some transactions: ```javascript // Express.js example webhook endpoint const express = require('express'); const app = express(); // Use express.raw so req.body is the exact bytes Svix signed. Do NOT JSON-parse // and re-stringify the body — that can change the bytes and fail verification. app.post('/webhooks/koard', express.raw({ type: 'application/json' }), (req, res) => { const payload = req.body; // raw Buffer, exactly as received const headers = { 'svix-id': req.headers['svix-id'], 'svix-timestamp': req.headers['svix-timestamp'], 'svix-signature': req.headers['svix-signature'] }; try { // Verify webhook signature using Svix library const wh = new Webhook(process.env.KOARD_WEBHOOK_SECRET); const verifiedPayload = wh.verify(payload, headers); // Process webhook after verification processWebhook(verifiedPayload); res.status(200).send('OK'); plaintext } catch (err) { console.error('Webhook verification failed:', err); res.status(400).send('Invalid signature'); } }); function processWebhook(payload) { // The body IS the resource object (flat) — there is no { event, data } envelope, // and the event type is not in the body. Subscribe each endpoint to specific // event types (developer portal), and/or branch on payload fields. For a // transaction endpoint, route on transaction_type and read status. const txn = payload; switch (txn.transaction_type) { case 'sale': handleSale(txn); break; case 'capture': handleCapture(txn); break; // Add more: 'auth', 'refund', 'reverse', 'tip_adjust', 'incremental_auth' default: console.log('Unhandled transaction type:', txn.transaction_type); } } app.listen(3000, () => { console.log('Webhook endpoint listening on port 3000'); }); ``` ### Test with Koard SDK Use the Koard SDK to process transactions and trigger webhook events: ```swift // iOS SDK example import KoardMerchantSDK // Process a test transaction let paymentRequest = PaymentRequest( amount: 1000, // $10.00 currency: .USD, description: "Test webhook transaction" ) KoardMerchantSDK.shared.processPayment(paymentRequest) { result in switch result { case .success(let response): print("Payment successful: (response.transactionId)") // This will trigger webhook events case .failure(let error): print("Payment failed: (error)") } } ``` ![webhooks-4](/webhooks-4.png) _Testing webhook functionality with Koard SDK transactions_ ## Webhook Security ### Signature Verification **Why Verify Webhooks?** Webhook signatures let you verify that webhook messages are actually sent by Koard and not a malicious actor. This prevents: * **Spoofing Attacks**: Malicious actors sending fake webhooks * **Replay Attacks**: Old webhooks being resent * **Man-in-the-Middle Attacks**: Webhooks being intercepted and modified For a detailed explanation, see [why you should verify webhooks](https://docs.svix.com/receiving/verifying-payloads/why). ### Using Svix Libraries (Recommended) Koard uses [Svix](https://www.svix.com/) for webhook delivery, which provides official libraries for easy verification: ```javascript Node.js const { Webhook } = require("svix"); const secret = process.env.KOARD_WEBHOOK_SECRET; // Get from Koard dashboard // Use express.raw so req.body is the exact bytes Svix signed. Do NOT JSON-parse // and re-stringify the body — that can change the bytes and fail verification. app.post('/webhooks/koard', express.raw({ type: 'application/json' }), (req, res) => { const payload = req.body; // raw Buffer, exactly as received const headers = { 'svix-id': req.headers['svix-id'], 'svix-timestamp': req.headers['svix-timestamp'], 'svix-signature': req.headers['svix-signature'] }; try { const wh = new Webhook(secret); const verifiedPayload = wh.verify(payload, headers); // Webhook is verified - process it processWebhook(verifiedPayload); res.status(200).json({ success: true }); } catch (err) { console.error('Webhook verification failed:', err.message); res.status(400).json({ error: 'Invalid signature' }); } }); ``` ```python Python from svix.webhooks import Webhook import os webhook_secret = os.environ['KOARD_WEBHOOK_SECRET'] @app.route('/webhooks/koard', methods=['POST']) def webhook_handler(): payload = request.get_data() headers = { 'svix-id': request.headers.get('svix-id'), 'svix-timestamp': request.headers.get('svix-timestamp'), 'svix-signature': request.headers.get('svix-signature') } try: wh = Webhook(webhook_secret) msg = wh.verify(payload, headers) # Webhook is verified - process it process_webhook(msg) return jsonify({'success': True}), 200 except Exception as e: print(f'Webhook verification failed: {e}') return jsonify({'error': 'Invalid signature'}), 400 ``` ```go Go import ( "encoding/json" svix "github.com/svix/svix-webhooks/go" ) func webhookHandler(w http.ResponseWriter, r *http.Request) { webhookSecret := os.Getenv("KOARD_WEBHOOK_SECRET") payload, _ := ioutil.ReadAll(r.Body) headers := http.Header{} headers.Set("svix-id", r.Header.Get("svix-id")) headers.Set("svix-timestamp", r.Header.Get("svix-timestamp")) headers.Set("svix-signature", r.Header.Get("svix-signature")) wh, _ := svix.NewWebhook(webhookSecret) err := wh.Verify(payload, headers) if err != nil { w.WriteHeader(http.StatusBadRequest) return } // Verified — decode the flat body before routing on its fields var txn map[string]interface{} if err := json.Unmarshal(payload, &txn); err != nil { w.WriteHeader(http.StatusBadRequest) return } processWebhook(txn) w.WriteHeader(http.StatusOK) } ``` For more examples in other languages (Ruby, PHP, Java, Rust, Kotlin, C#), see the [Svix webhook verification documentation](https://docs.svix.com/receiving/verifying-payloads/how). ### Manual Verification (Advanced) If you prefer to verify signatures manually without using the Svix library: ```javascript const crypto = require('crypto'); function verifyWebhookSignature(payload, headers, secret) { const timestamp = headers['svix-timestamp']; const signature = headers['svix-signature']; const msgId = headers['svix-id']; // Check timestamp to prevent replay attacks (optional but recommended) const timestampSeconds = parseInt(timestamp); const now = Math.floor(Date.now() / 1000); if (Math.abs(now - timestampSeconds) > 300) { // 5 minute tolerance throw new Error('Webhook timestamp too old'); } // Create the signed content const signedContent = ${msgId}.${timestamp}.${payload}; // Compute expected signature const expectedSignature = crypto .createHmac('sha256', secret) .update(signedContent, 'utf8') .digest('base64'); // Extract signature from header (format: "v1,signature") const signatureParts = signature.split(','); const headerSignature = signatureParts[1]; // Compare signatures return crypto.timingSafeEqual( Buffer.from(headerSignature), Buffer.from(expectedSignature) ); } ``` **Important**: Use the **raw payload body** for verification, not the parsed JSON. Different JSON parsers may produce different string representations. ### HTTPS Requirements * **HTTPS Only**: Webhook endpoints must use HTTPS * **Valid SSL Certificates**: SSL certificates must be valid and trusted * **No Self-Signed Certificates**: Self-signed certificates are not allowed * **TLS 1.2+**: Minimum TLS version 1.2 required ### Security Best Practices * **Verify Signatures**: Always verify webhook signatures * **Use HTTPS**: Only accept webhooks over HTTPS * **Validate Payloads**: Validate webhook payloads before processing * **Rate Limiting**: Implement rate limiting to prevent abuse * **Logging**: Log all webhook events for debugging and security ## Monitoring and Logging ### Webhook Logs All webhooks come with detailed logging around deliverability, attempts, and activity history: 1. **Access Logs**: Go to the Logs tab in the developer portal 2. **Filter Events**: Filter by event type and message content 3. **View Details**: Click on individual events to see delivery details 4. **Monitor Status**: Track delivery status and retry attempts ![webhooks-3](/webhooks-3.png) _Webhook logs and delivery monitoring in the Koard Developer Portal_ ### Delivery Status Monitor webhook delivery status: | Status | Description | | ------------- | --------------------------------------------------- | | **Delivered** | Webhook successfully delivered and acknowledged | | **Pending** | Webhook delivery in progress or scheduled for retry | | **Failed** | Webhook delivery failed after all retry attempts | ### Troubleshooting Common webhook issues and solutions: **Not Using the Raw Payload Body** This is the most common issue. When generating the signed content, Koard uses the raw string body of the message payload. If you convert JSON payloads into strings using methods like `JSON.stringify()`, different implementations may produce different string representations, leading to verification failures. **Solution:** Use the raw request body exactly as received. In Express.js: use `express.raw()` or access `req.body` before JSON parsing. **Missing or Wrong Secret Key** Using an incorrect or outdated webhook secret will cause all verifications to fail. **Solution:** Get your webhook secret from the Koard Developer Portal. Remember that secrets are unique to each endpoint. **Timestamp Too Old** Webhooks with timestamps older than 5 minutes are rejected to prevent replay attacks. **Solution:** Ensure your server's system time is synchronized (use NTP). **Sending Wrong Response Codes** When Koard receives a 2xx status code (200-299), it's interpreted as successful delivery, even if your response payload indicates a failure. **Solution:** Return appropriate status codes: * `200` for successful processing * `400-499` for client errors (will not retry) * `500-599` for server errors (will retry) **Response Timeouts** Webhooks that don't respond within 15 seconds are considered failed and will be retried. **Solution:** Respond immediately with `200 OK` and process webhooks asynchronously: ```javascript app.post('/webhooks/koard', async (req, res) => { // Verify signature const wh = new Webhook(secret); const payload = wh.verify(req.body, req.headers); // Respond immediately res.status(200).send('OK'); // Process asynchronously queue.add('process-webhook', payload); }); ``` ### Failure Recovery **Re-enable a Disabled Endpoint** If all attempts to a specific endpoint fail for 5 days, the endpoint will be automatically disabled. **To re-enable:** 1. Go to the Koard Developer Portal 2. Navigate to Webhooks 3. Find the disabled endpoint 4. Click "Enable Endpoint" **Recovering Failed Messages** **Single Message Recovery:** 1. Find the message in the Developer Portal 2. Click the options menu next to the attempt 3. Click "Resend" to retry delivery **Bulk Message Recovery:** 1. Go to the endpoint details page 2. Click "Options" → "Recover Failed Messages" 3. Choose a time window to recover from 4. All failed messages in that window will be resent **Recovery from Specific Timestamp:** 1. Find any message near your desired recovery point 2. Click the options menu on that message 3. Select "Replay all failed messages since this time" ## Best Practices ### Endpoint Design * **Quick Response**: Return 2xx status codes quickly (within 15 seconds) * **Respond First, Process Later**: Acknowledge receipt immediately, then process asynchronously * **Disable CSRF Protection**: Disable CSRF checks for webhook endpoints * **Use Raw Body**: Access raw request body for signature verification * **Error Handling**: Implement proper error handling and logging ### Event Processing - Idempotency **Why Idempotency Matters:** Webhooks may be delivered more than once due to network issues, retries, or recovery operations. Your endpoint must handle duplicate events gracefully. **Implementation:** Use the `svix-id` header (message ID) to track processed events: ```javascript const processedEvents = new Set(); // In production, use a database app.post('/webhooks/koard', (req, res) => { const messageId = req.headers['svix-id']; // Check if we've already processed this event if (processedEvents.has(messageId)) { console.log('Duplicate event ignored:', messageId); return res.status(200).send('OK'); // Return success for duplicates } // Verify and process webhook const wh = new Webhook(secret); const payload = wh.verify(req.body, req.headers); // Mark as processed BEFORE processing to prevent race conditions processedEvents.add(messageId); // Process the webhook processWebhook(payload); res.status(200).send('OK'); }); ``` **Best Practices:** * Store message IDs in a database (Redis, PostgreSQL, etc.) * Set expiration on stored IDs (e.g., 7 days) to prevent infinite growth * Use database transactions to ensure idempotency * Return `200 OK` for duplicate events (they're already processed) ### Event Ordering **Important:** Delivery is best-effort ordered — events may arrive out of sequence due to network conditions, retries, or processing delays. **Order by the payload's `created_at`.** Koard is event-sourced: a transaction's lifecycle (`authorize` → `incremental_auth` → `capture` → `refund` → …) is a series of events that **share one `transaction_id`**, each with its own `event_id` and its own `created_at`. That `created_at` is assigned as `max(now, previous_event + 1)` in **milliseconds**, so within a `transaction_id` it is **strictly increasing** — a reliable per-event ordering key. Don't use the `svix-timestamp` header for ordering — it's the delivery-_attempt_ time and changes on retries. **Persist atomically** so out-of-order and concurrent deliveries can't regress state: upsert and only advance when the incoming `created_at` is greater than the stored watermark (a single statement, no read-then-write race). Deduplicate exact retries on `svix-id` (see the Idempotency section above). ```javascript app.post('/webhooks/koard', express.raw({ type: 'application/json' }), async (req, res) => { const wh = new Webhook(secret); const txn = wh.verify(req.body, { 'svix-id': req.headers['svix-id'], 'svix-timestamp': req.headers['svix-timestamp'], 'svix-signature': req.headers['svix-signature'], }); // created_at is a per-event, strictly increasing (ms) watermark for a // transaction_id. Upsert atomically and only advance when this event is // newer, so out-of-order or concurrent deliveries can't regress the state. await db.query( INSERT INTO transactions (transaction_id, status, payload, last_created_at) VALUES ($1, $2, $3, $4) ON CONFLICT (transaction_id) DO UPDATE SET status = EXCLUDED.status, payload = EXCLUDED.payload, last_created_at = EXCLUDED.last_created_at WHERE EXCLUDED.last_created_at > transactions.last_created_at, [txn.transaction_id, txn.status, txn, txn.created_at], ); res.status(200).send('OK'); }); ``` If you need strict, guaranteed ordering, use [Svix FIFO endpoints](https://docs.svix.com/advanced-endpoints/fifo-endpoints). Alternatively, treat the webhook as a signal and re-fetch the authoritative transaction from the Koard API before acting. ### Security * **Always Verify Signatures**: Never skip signature verification in production * **Use Svix Libraries**: Use official Svix libraries for proper verification * **HTTPS Only**: Use HTTPS for all webhook endpoints (required by Koard) * **Validate Timestamps**: Reject webhooks with old timestamps (>5 minutes) * **Secret Management**: Securely store webhook secrets (use environment variables) * **Rotate Secrets**: Periodically rotate webhook secrets * **Monitor Failures**: Alert on repeated verification failures (potential attack) ### Monitoring and Alerting * **Track Delivery**: Monitor webhook delivery success rates * **Set Up Alerts**: Alert on repeated failures or timeouts * **Log Everything**: Log all webhook events for debugging * **Monitor Processing Time**: Ensure webhooks process within timeout * **Dashboard Review**: Regularly review webhook logs in Koard Developer Portal ## See also This wraps up the webhook setup guide. See the links below for related information: * [Available Events](/docs/webhooks/available-events) - Complete list of webhook events * [Running Payments](/docs/setting-up-the-ios-sdk/running-payments) - Payment processing fundamentals * [Batch and Settlements](/docs/batch-and-settlements/overview) - Batch processing overview * [Getting Started](/docs/introduction) - Koard platform introduction # Apple Best Practices and Guidelines Comprehensive best practices for developing Tap to Pay on iPhone applications with Apple's ProximityReader framework and Koard SDK. ## Overview Building Tap to Pay on iPhone applications requires adherence to Apple's strict guidelines and best practices to ensure security, usability, and App Store approval. This guide covers essential practices for UI/UX design, security implementation, and the dual review process required for Tap to Pay applications. ## Apple's Dual Review Process Apple requires two distinct review processes for Tap to Pay on iPhone applications: ### 1. Tap to Pay Review The Tap to Pay review is a comprehensive security and compliance assessment focused on: * **Security Implementation**: Ensuring safe and secure payment processing * **Merchant Safety**: Verifying that merchants can safely accept payments on their devices * **Branding and Messaging**: Reviewing UI/UX workflows specifically around Tap to Pay functionality * **Payment Flow Design**: Evaluating the complete payment experience from initiation to completion * **Error Handling**: Assessing how payment errors and edge cases are managed * **Data Protection**: Verifying compliance with Apple's data handling requirements ### 2. App Store Review The standard App Store review process covers: * **General App Functionality**: Core app features and user experience * **TestFlight Distribution**: Ability to share the app via TestFlight for testing * **App Store Submission**: Final approval for public distribution * **Guideline Compliance**: Adherence to App Store Review Guidelines **Important**: Both reviews must be passed successfully before your app can be distributed through the App Store. ## User Experience Best Practices ### Feedback and User Actions Apple emphasizes providing clear feedback when users take explicit actions. This is crucial for Tap to Pay applications: #### Provide Immediate Feedback ```swift // Launch Tap to Pay screen with clear visual feedback func presentTapToPayScreen() { // Show loading indicator showProgressIndicator() // Launch Tap to Pay interface Task { do { let reader = try await ProximityReader.readerIdentifier // Present Tap to Pay UI presentTapToPayInterface(reader: reader) } catch { // Handle error appropriately handleTapToPayError(error) } } } ``` #### Progress Indicators During prepare() Calls ```swift func preparePaymentReader() { // Show progress indicator while prepare() completes showProgressIndicator(message: "Preparing payment reader...") Task { do { // This is a long-running operation let reader = try await ProximityReader.readerIdentifier await prepareReader(reader) // Hide progress indicator hideProgressIndicator() } catch { hideProgressIndicator() handlePreparationError(error) } } } ``` ### Error Handling Best Practices #### Avoid Modal Alerts for Background Operations **❌ Incorrect Approach:** ```swift // Never show modal alerts during background prepare() calls func prepareReaderInBackground() { Task { do { let reader = try await ProximityReader.readerIdentifier await prepareReader(reader) } catch { // DON'T: Show modal alert during app launch DispatchQueue.main.async { let alert = UIAlertController(title: "Error", message: "Failed to prepare reader", preferredStyle: .alert) self.present(alert, animated: true) } } } } ``` **✅ Correct Approach:** ```swift // Use non-modal feedback for background operations func prepareReaderInBackground() { Task { do { let reader = try await ProximityReader.readerIdentifier await prepareReader(reader) } catch { // Use banner notification or other non-modal method DispatchQueue.main.async { self.showBannerNotification( message: "Payment reader preparation failed. Please try again.", type: .error ) } } } } ``` #### Appropriate Error Display Methods ```swift enum ErrorDisplayMethod { case bannerNotification // For background operations case modalAlert // For user-initiated actions case inlineMessage // For form validation case toastNotification // For non-critical errors } func displayError(_ error: Error, method: ErrorDisplayMethod) { switch method { case .bannerNotification: showBannerNotification(message: error.localizedDescription) case .modalAlert: showModalAlert(title: "Error", message: error.localizedDescription) case .inlineMessage: showInlineError(message: error.localizedDescription) case .toastNotification: showToast(message: error.localizedDescription) } } ``` ## ProximityReader Framework Best Practices ### Reader Management Based on Apple's [ProximityReader documentation](https://developer.apple.com/documentation/proximityreader), proper reader management is essential: ```swift import ProximityReader class TapToPayManager: ObservableObject { @Published var isReaderAvailable = false @Published var readerIdentifier: String? func checkReaderAvailability() async { do { let identifier = try await ProximityReader.readerIdentifier await MainActor.run { self.readerIdentifier = identifier self.isReaderAvailable = true } } catch { await MainActor.run { self.isReaderAvailable = false self.readerIdentifier = nil } // Handle error appropriately handleReaderError(error) } } } ``` ### Secure Payment Processing ```swift class SecurePaymentProcessor { func processPayment(amount: Decimal, currency: String) async throws -> PaymentResult { // Verify reader availability before processing guard try await ProximityReader.readerIdentifier != nil else { throw PaymentError.readerNotAvailable } // Process payment securely let paymentData = try await capturePaymentData(amount: amount, currency: currency) // Send to secure backend return try await sendPaymentToBackend(paymentData) } private func capturePaymentData(amount: Decimal, currency: String) async throws -> PaymentData { // Implementation for capturing payment data securely // This should follow Apple's security guidelines } } ``` ## Security Best Practices ### Data Protection and Privacy ```swift class PaymentDataManager { // Never store sensitive payment data private let keychain = Keychain(service: "com.koard.payments") func storeNonSensitiveData(_ data: PaymentMetadata) { // Only store non-sensitive metadata keychain["payment_id"] = data.paymentId keychain["merchant_id"] = data.merchantId // Never store card numbers, CVV, or other sensitive data } func processPaymentSecurely(_ paymentData: PaymentData) async throws { // All sensitive processing should happen on secure backend let encryptedData = try encryptPaymentData(paymentData) try await sendToSecureBackend(encryptedData) } } ``` ### Entitlement Verification ```swift class EntitlementManager { func verifyTapToPayEntitlement() async -> Bool { do { _ = try await ProximityReader.readerIdentifier return true } catch { // Handle entitlement errors logEntitlementError(error) return false } } private func logEntitlementError(_ error: Error) { // Log error for debugging but don't expose sensitive information print("Entitlement verification failed: \(error.localizedDescription)") } } ``` ## UI/UX Design Guidelines ### Payment Flow Design ```swift class PaymentFlowViewController: UIViewController { @IBOutlet weak var amountLabel: UILabel! @IBOutlet weak var tapToPayButton: UIButton! @IBOutlet weak var progressIndicator: UIActivityIndicatorView! override func viewDidLoad() { super.viewDidLoad() setupAccessibility() configurePaymentFlow() } private func setupAccessibility() { // VoiceOver support tapToPayButton.accessibilityLabel = "Pay with Tap to Pay" tapToPayButton.accessibilityHint = "Double tap to initiate payment" amountLabel.accessibilityLabel = "Total amount: $\(formattedAmount)" } private func configurePaymentFlow() { // Clear payment intent amountLabel.text = formattedAmount amountLabel.font = UIFont.preferredFont(forTextStyle: .headline) amountLabel.adjustsFontForContentSizeCategory = true // Minimal steps - single tap to pay tapToPayButton.setTitle("Tap to Pay", for: .normal) } } ``` ### Progress and Loading States ```swift class PaymentProgressManager { func showProgress(for operation: PaymentOperation) { switch operation { case .preparingReader: showProgressIndicator(message: "Preparing payment reader...") case .processingPayment: showProgressIndicator(message: "Processing payment...") case .completingTransaction: showProgressIndicator(message: "Completing transaction...") } } func hideProgress() { hideProgressIndicator() } } ``` ## Testing and Validation ### Comprehensive Testing Strategy ```swift class TapToPayTests: XCTestCase { func testReaderAvailability() async { let manager = TapToPayManager() await manager.checkReaderAvailability() // Test on device with Tap to Pay capability XCTAssertTrue(manager.isReaderAvailable) } func testPaymentFlow() async throws { let processor = SecurePaymentProcessor() let result = try await processor.processPayment(amount: 10.00, currency: "USD") XCTAssertNotNil(result) XCTAssertEqual(result.status, .success) } func testErrorHandling() { // Test various error scenarios let errorHandler = PaymentErrorHandler() let networkError = PaymentError.networkError let userMessage = errorHandler.getUserFriendlyMessage(for: networkError) XCTAssertFalse(userMessage.isEmpty) XCTAssertFalse(userMessage.contains("technical")) } } ``` ### TestFlight Preparation ```swift // Prepare for TestFlight distribution class TestFlightManager { func prepareForTestFlight() { // Ensure all test scenarios are covered validatePaymentFlows() testErrorScenarios() verifyAccessibilityCompliance() checkSecurityImplementation() } private func validatePaymentFlows() { // Test all payment scenarios // Verify UI/UX workflows // Ensure proper error handling } } ``` ## App Store Review Preparation ### Documentation Requirements 1. **Payment Flow Documentation**: Complete walkthrough of payment process 2. **Security Implementation**: Details of security measures and data protection 3. **Error Handling**: Documentation of all error scenarios and user feedback 4. **Accessibility Compliance**: VoiceOver and Dynamic Type support verification 5. **Test Account Credentials**: Sandbox accounts for review team testing ### Review Checklist * [ ] Tap to Pay entitlement properly configured * [ ] Reader availability checked before payment initiation * [ ] Proper error handling for all scenarios * [ ] No modal alerts during background operations * [ ] Clear user feedback for all actions * [ ] Accessibility compliance verified * [ ] Security best practices implemented * [ ] Test accounts provided for review * [ ] Complete payment flow documented ## Performance Optimization ### Memory Management ```swift class OptimizedPaymentManager { weak var delegate: PaymentManagerDelegate? private var readerSession: ProximityReader.Session? func startPaymentSession() { // Use weak references to avoid retain cycles readerSession = ProximityReader.Session() readerSession?.delegate = self } deinit { // Clean up resources readerSession?.invalidate() } } ``` ### Network Optimization ```swift class NetworkOptimizer { private let session: URLSession init() { let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 30 config.timeoutIntervalForResource = 60 self.session = URLSession(configuration: config) } func processPayment(_ data: PaymentData) async throws -> PaymentResult { // Implement retry logic and timeout handling return try await withRetry(maxAttempts: 3) { try await sendPaymentRequest(data) } } } ``` ## Resources and References ### Apple Documentation * [ProximityReader Framework](https://developer.apple.com/documentation/proximityreader) - Core framework for Tap to Pay functionality * [Apple Pay Developer Guide](https://developer.apple.com/apple-pay/) - Payment processing guidelines * [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/) - UI/UX design principles * [App Store Review Guidelines](https://developer.apple.com/app-store/review/guidelines/) - App Store submission requirements ### Koard Resources * [Setting Up the Entitlement](/docs/setting-up-the-ios-sdk/setting-up-the-entitlement-for-tap-to-pay-on-iphone) - Koard SDK integration guide * [Payment Lifecycle Guide](/docs/guides/payments/details/payment-lifecycle.md) - Complete payment flow documentation * [Test Cards Reference](/docs/appendix/resources#resources__test-cards) - Testing with test card numbers * [Security Guidelines](/docs/appendix/developing-with-apple) - Security best practices ### Additional Support * [Apple Developer Forums](https://developer.apple.com/forums/) - Community support and discussions * [WWDC Sessions](https://developer.apple.com/videos/) - Latest Tap to Pay and payment processing sessions * [Koard Developer Support](mailto:developers@koard.com) - Direct support for Koard SDK integration # Koard API Reference Koard API provides unified access to account onboarding, terminal management, payment processing, and reporting across all Koard environments. ## Base URLs | Environment | URL | | ----------- | --------------------------- | | UAT | `https://api.uat.koard.com` | | Production | `https://api.koard.com` | UAT is an isolated sandbox environment — transactions processed there will not appear in card or merchant histories and no money moves. ## API Versioning All Koard API endpoints are versioned with a path prefix (`/v1`, `/v2`, `/v3`, etc.). The version is part of the URL path and is **required** for every request. ```bash # Example: v1 endpoint curl https://api.koard.com/v1/accounts/{account_id} \ -H "x-koard-apikey: YOUR_API_KEY" # Example: v2 endpoint curl https://api.koard.com/v2/terminals \ -H "x-koard-apikey: YOUR_API_KEY" # Example: v3 endpoint curl https://api.koard.com/v3/payments/{transaction_id}/capture \ -H "x-koard-apikey: YOUR_API_KEY" ``` ### Current Versions by Resource | Resource | Version(s) | Path Prefix | Notes | | ------------------- | ---------- | -------------------------------------------- | -------------------------------------------------------------------------------- | | **Accounts** | v1, v2 | `/v1/accounts`, `/v2/accounts` | v2 provisions MMS (Clerk org, SVIX app, API key) on creation | | **Terminals** | v2 | `/v2/terminals` | Paginated listing, search, and full CRUD | | **Locations** | v1 | `/v1/locations` | Create, update, and retrieve locations | | **Transactions** | v1 | `/v1/transactions` | List, search, retrieve, and passthrough-only edit transactions | | **Payments** | v3 | `/v3/payment`, `/v3/preauth` | Card-present payment initiation via Tap to Pay | | **Payment Actions** | v1, v3 | `/v1/payments/{id}/…`, `/v3/payments/{id}/…` | Capture, auth increment, refund, reverse, adjust, confirm | | **Refunds** | v3 | `/v3/refund` | Standalone refund on completed transactions | | **Batches** | v1 | `/v1/batches` | Open, close, and edit settlement batches | | **API Keys** | v5 | `/v5/apikeys` | Scoped-permission keys; see [Authentication](/docs/api-reference/authentication) | The `/v5` surface currently exposes **API-key management only** (`/v5/apikeys`). Other resources such as payments, terminals, and accounts are served by their `/v1`–`/v3` paths above — there is no `/v5/payments`, `/v5/terminals`, etc. ### Version Lifecycle * **Newer versions** may change request/response schemas, add required fields, or alter default behavior. Always use the version shown in the endpoint path. * **Older versions** remain functional but may not include the latest features. We recommend migrating to the latest available version for each resource. * **Breaking changes** are only introduced in new major versions — existing versioned paths remain backward-compatible. > **Note:** The version prefix (e.g. `/v1/`, `/v2/`, `/v3/`) is required in all request paths. Requests without a version prefix will return a `404`. > **Passthrough-only transaction edits:** `PUT /v1/transactions/{transaction_id}` is reserved for PSP passthrough EMV flows where Koard is only forwarding EMV data and later receiving the final transaction outcome back from the PSP. It is not available for TSYS, Elavon, Fiserv, Worldpay, or other directly managed processor flows. Unsupported processor edits return `401`, while both missing and inaccessible transactions return `404`. # Boarding a Merchant with TSYS You can board a TSYS merchant either through the Koard Merchant Management System (MMS) UI or programmatically via the API. Both paths require the same VAR sheet information provided by TSYS. ## Via the MMS After creating the merchant account, click **New Terminal** and select TSYS as the processor. You'll be presented with the **TSYS VAR Sheet Information** form. ![TSYS VAR Sheet Information form](/tsys-var-sheet-form.png) Fill in all required fields (marked with `*`) using the values from the merchant's TSYS VAR sheet: | MMS Label | Required | Notes | | ------------------------------- | -------- | ------------------------------------------------------------------------------------ | | Acquirer BIN | Yes | 6-digit BIN from TSYS | | Merchant Number / MID | Yes | 12-digit TSYS merchant number | | Store Number | Yes | 4 digits — use `0001` for single-location merchants | | Terminal Number | Yes | 4 digits — use `0001` for the first terminal | | Merchant Name | Yes | DBA name as it should appear on cardholder statements | | Merchant Location | Yes | Merchant city | | Merchant State | Yes | Select from dropdown | | Merchant Category Code | Yes | 4-digit MCC | | Industry Code | Yes | See [Industry Codes](#industry-codes) below | | City Code / ZIP | Yes | 5-digit ZIP code | | Language Indicator | Yes | See [Language Indicators](#language-indicators) below | | Time Zone Diff | Yes | See [Time Zone Codes](#time-zone-codes) below | | Acceptor Street Address | Yes | Physical street address | | Acceptor Customer Service Phone | Yes | 10 digits, no formatting | | Acceptor Phone | Yes | 10 digits, no formatting | | Authentication Code | No | UAT only — submit to TSYS to authenticate a terminal and receive a `gen_key` | | Gen Key | No | Pass this if you have previously authenticated a terminal with `authentication_code` | | Currency Code | No | Defaults to `840` (USD) | | Country Code | No | Defaults to `840` (US) | Once saved, assign the terminal to a location and generate merchant credentials as usual. ## Via the API Send `X-Koard-apikey: {API_KEY}` with every request. Use `https://api.uat.koard.com` for sandbox and `https://api.koard.com` for production. ### Create Terminal — `POST /v2/terminals` **Request body** | Field | Required | Description | | ---------------------- | -------- | ---------------------------------------- | | `account_id` | Yes | Merchant account ID | | `processor_config_id` | Yes | TSYS processor configuration ID | | `terminal_name` | Yes | Display name for the terminal | | `terminal_description` | No | Optional description | | `var_sheet` | Yes | TSYS VAR sheet object — see fields below | **Example** ```bash curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_live_tsys_us", "terminal_name": "Front Counter iPhone", "var_sheet": { "acquirer_bin": "123456", "merchant_number": "123456789012", "store_number": "0001", "terminal_number": "0001", "mcc": "5812", "merchant_name": "Bluebird Coffee Roasters", "merchant_location": "San Francisco", "merchant_state": "CA", "city_code": "94105", "acceptor_street_address": "123 Market Street", "industry_code": "R", "acceptor_phone": "4155551234", "acceptor_customer_service_phone": "4155551234" } }' ``` ### Update Terminal — `PUT /v2/terminals/{terminal_id}` To correct or update VAR sheet fields after creation, send a `PUT` with a `var_sheet` object containing only the fields you want to change. All VAR sheet fields are optional on update. **Example** ```bash curl https://api.uat.koard.com/v2/terminals/500600700001 \ -X PUT \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "var_sheet": { "merchant_name": "Bluebird Coffee — Mission", "acceptor_street_address": "456 Valencia Street" } }' ``` ## VAR Sheet Fields ### Required | Field | Format | Description | | --------------------------------- | --------- | ------------------------------------------------------------------------- | | `acquirer_bin` | 6 digits | TSYS acquirer BIN — provided by TSYS for your VAR sheet | | `merchant_number` | 12 digits | TSYS merchant number — unique identifier for the merchant at the acquirer | | `store_number` | 4 digits | Store number — typically `0001` if the merchant has a single location | | `terminal_number` | 4 digits | Terminal number — typically `0001` for the first terminal at a store | | `mcc` | 4 digits | MCC for the merchant's business type (e.g. `5812` for restaurants) | | `merchant_name` | string | Merchant DBA name as it should appear on cardholder statements | | `merchant_location` | string | Merchant city | | `merchant_state` | 2 letters | US state abbreviation (e.g. `CA`) | | `city_code` | 5 digits | ZIP code (e.g. `94105`) | | `acceptor_street_address` | string | Physical street address of the merchant location | | `industry_code` | string | See [Industry Codes](#industry-codes) below | | `acceptor_phone` | 10 digits | Merchant phone number — digits only, no formatting | | `acceptor_customer_service_phone` | 10 digits | Customer-facing service phone number — digits only, no formatting | | `time_zone_diff` | 3 digits | TSYS time zone code — see [Time Zone Codes](#time-zone-codes) below | ### Optional | Field | Default | Description | | --------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `currency_code` | `840` | ISO 4217 numeric currency code — `840` for USD | | `country_code` | `840` | ISO 3166 numeric country code — `840` for US | | `language_indicator` | `00` | 2-digit language indicator — see [Language Indicators](#language-indicators) below | | `authentication_code` | — | Optional. If supplied and the terminal has no `gen_key` on file, Koard sends Transaction Code `TA` (Terminal Authenticate, EIS 1080 §6.223) and TSYS returns response `A1 — ACTIVATED` with the 24-character `gen_key`. If the code is invalid, TSYS returns `A2 — NOT ACTIVATED`. If the terminal already has a `gen_key` and you want to replace it, send `authentication_code` along with `override_gen_key=true` (see below). | | `gen_key` | — | 24-character key returned after a successful `TA` authentication. If your partner already has the `gen_key`, supply it directly and Koard will use it on every subsequent request — `authentication_code` is not needed in that case. | | `override_gen_key` | `false` | Set to `true` only when you also supply a valid `authentication_code` AND want to replace an existing `gen_key`. Koard performs a `TD` (Terminal Deactivate, EIS 1080 §6.223) followed by `TA` so the old key is invalidated before the new one is issued. Rejected if `authentication_code` is absent. | | `surcharge_rate` | — | Surcharge percentage to apply automatically (e.g. `3.5` for 3.5%). Set to `null` to disable automatic surcharge logic. Set to `0` to never surcharge. Configured via `PUT /v2/terminals/{terminal_id}` | ## Industry Codes | Code | Industry Type | | ---- | -------------------------------------- | | `A` | Auto Rental | | `B` | Bank / Financial Institution | | `D` | Direct Marketing | | `H` | Hotel | | `L` | Limited Amount Terminal | | `O` | Oil Company / Automated Fueling System | | `P` | Passenger Transport | | `R` | Retail / Restaurant / Grocery | Use `R` for most mPOS use cases. ## Language Indicators | Indicator | Language | | --------- | -------------------- | | `00` | English | | `01` | Spanish | | `02` | Portuguese | | `03` | Reserved for Irish | | `04` | Reserved for French | | `05` | Reserved for German | | `06` | Reserved for Italian | | `07` | Reserved for Dutch | ## Time Zone Codes | Code | Time Zone | | ----- | -------------- | | `705` | Eastern (EST) | | `706` | Central (CST) | | `707` | Mountain (MST) | | `708` | Pacific (PST) | ## Batch Management Options When creating a TSYS terminal, you choose how batches are managed: ### Option 1: Manual Batch Management (Default) Create the terminal without a `batch_schedule`. You (or your merchant) are responsible for opening and closing batches: ```json POST /v2/terminals { "account_id": "100200300001", "processor_config_id": "prc_live_tsys_us", "terminal_name": "Front Counter iPhone", "var_sheet": { "acquirer_bin": "123456", "merchant_number": "123456789012", "store_number": "0001", "terminal_number": "0001", "mcc": "5812", "merchant_name": "Bluebird Coffee", "merchant_location": "San Francisco", "merchant_state": "CA", "city_code": "94105", "acceptor_street_address": "123 Market Street", "industry_code": "R", "acceptor_phone": "4155551234", "acceptor_customer_service_phone": "4155551234" } } ``` With manual management, you must: * Open a batch before processing transactions (`POST /v1/batches/open`) * Close the batch at the end of the day or period (`POST /v1/batches/{batch_id}/close`) * Open a new batch for the next period ### Option 2: Automated Batch Scheduling After creating the terminal, enable automated scheduling via `PUT /v2/terminals/{terminal_id}`: ```json PUT /v2/terminals/{terminal_id} { "batch_schedule": { "timezone": "US/Eastern", "is_active": true, "schedule": [ { "day": "MON", "times": ["23:00"] }, { "day": "TUE", "times": ["23:00"] }, { "day": "WED", "times": ["23:00"] }, { "day": "THU", "times": ["23:00"] }, { "day": "FRI", "times": ["23:00"] }, { "day": "SAT", "times": ["23:00"] } ] } } ``` With automated scheduling: * Koard automatically closes and reopens batches on your schedule * TSYS batch numbers are auto-managed (001-999, wrapping, no reuse within 5 days) * Failed closes trigger retries and webhook error notifications See [Automated Batch Scheduling](/docs/guides/batch-settlements/details/automated-batch-scheduling) for full configuration details, timezone options, and edge cases. Automated scheduling is available for **TSYS**, **Elavon**, and **Worldpay** terminals only. Fiserv and Payroc terminals must use manual batch management. ## Gotchas * **`merchant_number` must be exactly 12 digits.** TSYS will reject shorter values. Left-pad with zeros if your MID is fewer than 12 digits. * **`store_number` and `terminal_number` must be exactly 4 digits.** Use `0001`, not `1`. * **`city_code` must be exactly 5 digits.** Left-pad with a zero for ZIP codes starting with `0` (e.g. `02101` for Boston). * **Phone numbers must be exactly 10 digits.** No dashes, spaces, or country codes — strip all formatting before submitting. * **`merchant_name` appears on cardholder statements.** Make sure it matches the merchant's registered DBA name — discrepancies can trigger disputes. * **`industry_code` affects transaction routing.** Using the wrong code can cause authorization failures or incorrect interchange rates. * **`acquirer_bin` is VAR sheet-level, not per-merchant.** All merchants under the same TSYS VAR sheet share the same BIN. Do not confuse this with the merchant number. ## Troubleshooting **`400 Bad Request` on create** * Check that all required VAR sheet fields are present. * Verify `merchant_number` is 12 digits, `store_number` / `terminal_number` are 4 digits, `city_code` is 5 digits, and phone numbers are 10 digits. * Confirm `processor_config_id` is a valid TSYS config ID for your environment. **Transactions erroring after boarding** * A VAR sheet field is likely incorrect. The most common culprits are `acquirer_bin`, `merchant_number`, `store_number`, and `terminal_number` — verify each matches exactly what TSYS has on file, including leading zeros. * Check `industry_code` is appropriate for the merchant's transaction type — an incorrect code can cause authorization failures. * If there is no open batch in our system for the terminal, transactions will error. Ensure a batch has been opened before processing payments. **Wrong merchant name on statements** * Use `PUT /v2/terminals/{terminal_id}` to update `merchant_name` in the `var_sheet`. Changes take effect on the next transaction. # Boarding a Merchant with Payroc Payroc merchants require two values from Payroc to board a terminal: a **Processing Terminal ID** and a **Processing MID**. These are provided by Payroc and entered when configuring the terminal in the Koard MMS. ## Via the MMS After creating the merchant account, click **New Terminal** and select Payroc as the processor. Enter the **Processing Terminal ID** and **Processing MID** provided by Payroc. ![Payroc terminal boarding form](/payroc-terminal-board.png) Once saved, assign the terminal to a location and generate merchant credentials. The merchant can then use those credentials to log into the SDK and run payments. Surcharging is not supported for Payroc processor configurations. ## Via the API Send `X-Koard-apikey: {API_KEY}` with every request. Use `https://api.uat.koard.com` for sandbox and `https://api.koard.com` for production. ### Create Terminal — `POST /v2/terminals` | Field | Required | Description | |-------|----------|-------------| | `account_id` | Yes | Merchant account ID | | `processor_config_id` | Yes | Payroc processor configuration ID | | `terminal_name` | Yes | Display name for the terminal | | `terminal_description` | No | Optional description | | `var_sheet.processing_terminal_id` | Yes | Processing Terminal ID provided by Payroc | | `var_sheet.processing_merchant_id` | Yes | Processing MID provided by Payroc | **Example** curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_live_payroc_us", "terminal_name": "Front Counter iPhone", "var_sheet": { "processing_terminal_id": "YOUR_PROCESSING_TERMINAL_ID", "processing_merchant_id": "YOUR_PROCESSING_MID" } }' # Fiserv Cardnet North Cardnet is the **North (CES) front-end** settling to the North (PTS) back-end — terminal capture. Board it like any Fiserv terminal (see the [Fiserv Overview](/boarding-a-merchant/fiserv/fiserv-overview) for the shared shape, SRS, and UMF coverage) using the **Cardnet North** processor config. Note the distinct MID/TID formats. ## Boarding spec | Field | Value / format | Notes | |---|---|---| | **Group ID** | `30001` | Cardnet / North front-end — a **different** front-end from Nashville (`10001`). | | **Merchant ID** (MID) | **12 digits** (`MerchID`) | Cardnet front-end MID, usually the same as the 12-digit Settlement MID. Top-level `mid`. | | **Terminal ID** (TID) | **6 alphanumeric** (`TermID`) | The "Bank TID". Top-level `tid`. | | **Settlement MID** | 12 digits | North Settlement MID. VAR-sheet `settlement_mid` — optional; defaults to a copy of `mid` (which usually equals it). | | **MCC** | 4-digit | Top-level `mcc`. | | **Equipment** (POS Solution) | `CreditCallLTDGTWRC` or `CRDCallResellerRCSS` | VAR-sheet `equipment`. | ## Request shape curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_fiserv_cardnet_north", "terminal_name": "Lane 1", "mid": "445197000368", "tid": "A1B2C3", "mcc": "5045", "var_sheet": { "group_id": "30001", "settlement_mid": "445197000368", "industry": "retail_qsr_grocery", "equipment": "CRDCallResellerRCSS", "merchant_street_address": "102 Woodmont Blvd Ste 125", "merchant_city": "Nashville", "merchant_state": "TN", "merchant_postal_code": "37205", "country_code": "840" } }' ## Capture & settlement **Terminal capture (North PTS).** Same as Nashville North — the gateway holds the batch and submits at cutoff. Boarding must match the host's configured capture mode. ## Gotchas - **Different front-end, different Group ID (`30001`).** Cardnet is *not* the Nashville front-end — it has its own Group ID and its own MID/TID formats. - **12-digit MID, 6-char alphanumeric TID.** These formats differ from the 7-digit Nashville/Omaha values — copy them exactly from the VAR packet. - **⚠️ MID/TID reversal risk.** If the MID is boarded as the TID (or vice-versa) and that reversed combo is live for another merchant, funds route to the wrong account. Confirm deposits before going live. - **`settlement_mid` usually equals the MID** for Cardnet; omit it and Koard copies `mid`. # Installing the SDK Install the Koard Merchant SDK to enable tap-to-pay functionality in your iOS application. If you're ready to start developing, see our [Tap to Pay configuration guide](/docs/setting-up-the-ios-sdk/adding-support-for-tap-to-pay-on-iphone). [Get started with Koard](/docs/getting-started-with-koard/introduction) **What you learn** In this guide, you'll learn: * How to add the Koard SDK with Swift Package Manager * How to install the SDK manually as an XCFramework * How to configure embed settings for proper functionality * How to verify your installation is working correctly **Prerequisites** Before you begin, ensure you have: * **Xcode 16.3 or later** (required for building and distribution) * **iOS 17.4+ deployment target** (minimum supported version) * **Swift 5.9+** (minimum Swift toolchain) * **iPhone XS or greater** (supported hardware for tap-to-pay) * **Valid Koard merchant account** (configured in Koard MMS) * **Sandbox Apple Account signed in on test device** (dedicated iPhone with Developer Mode enabled) **Current version**: The latest published release is **1.0.20**. See the SDK [CHANGELOG](https://github.com/koardlabs/koard-ios/blob/main/CHANGELOG.md) for behavior changes — 1.0.20 changed how `prepare()` and `linkAccountAsync()` report failures, which affects existing integrations. **Test Device**: Use a dedicated test iPhone with your Sandbox Apple Account signed in. Avoid mixing production Apple IDs on the same hardware to prevent authentication conflicts. ## Step 1: Add the SDK We recommend **Swift Package Manager** for most projects. If you cannot use SPM, install the SDK manually as an XCFramework instead. ### Option A: Swift Package Manager (Recommended) 1. **In Xcode, choose File → Add Package Dependencies…** 2. **Enter the package URL**: `https://github.com/koardlabs/koard-ios.git` 3. **Set the dependency rule** to "Up to Next Major Version" starting from `1.0.20` 4. **Add the `KoardSDK` library product** to your app target You can also add it directly to a `Package.swift`: ```swift .package(url: "https://github.com/koardlabs/koard-ios.git", from: "1.0.20") ``` Then list `KoardSDK` as a dependency of your target. ### Option B: Manual XCFramework 1. **Download the latest `KoardSDK.xcframework.zip`** from the [Releases page](https://github.com/koardlabs/koard-ios/releases) or get it directly from the team 2. **Extract the ZIP file** to reveal the `KoardSDK.xcframework` bundle 3. **Drag `KoardSDK.xcframework` into your Xcode project** (or use your target's **General → Frameworks, Libraries, and Embedded Content → "+" → Add Other… → Add Files…**) 4. **Ensure "Copy items if needed" is checked** and the framework is added to your app target **Note**: The XCFramework format ensures compatibility across different architectures (iOS Simulator, iOS Device) and simplifies distribution compared to traditional frameworks. ### Option C: CocoaPods The SDK ships a podspec (`KoardSDK`), which vendors the same `KoardSDK.xcframework`. Add it to your `Podfile`: ```ruby pod 'KoardSDK', '~> 1.0.20' ``` Then run `pod install` and open the generated `.xcworkspace`. ## Step 2: Configure Embed Settings When installing the XCFramework manually, embedding is crucial for the SDK to work properly in your app. (Swift Package Manager handles embedding automatically.) 1. **Open your target's "General" tab under "Frameworks, Libraries, and Embedded Content"** 2. **Find `KoardSDK.xcframework` in the list** 3. **Change the "Embed" setting from "Do Not Embed" to "Embed & Sign"** **Important**: If you skip this step for a manual install, you'll get runtime crashes when trying to use the SDK. The framework must be embedded and signed to function properly. ## Step 3: Verify Installation ### Check Framework Integration 1. **Build your project** (⌘+B) to ensure there are no compilation errors 2. **Verify the package or framework appears** in your project navigator 3. **For a manual install, check the framework is listed** under "Frameworks, Libraries, and Embedded Content" ### Test Basic Import Add this import statement to one of your Swift files to verify the SDK is accessible: ```swift import KoardSDK ``` If the import succeeds without errors, your installation is working correctly. ## Step 4: Initialize the SDK Once the SDK is installed, initialize it early in your app lifecycle with your `KoardOptions` and API key: ```swift import KoardSDK // Configure SDK options let options = KoardOptions( environment: .uat, // .uat | .production | .custom(String) loggingLevel: .debug // .none | .error | .warning | .debug | .verbose ) // Initialize with your API key KoardMerchantSDK.shared.initialize(options: options, apiKey: "your-koard-api-key") ``` For details on retrieving your key, see [Retrieving Your API Key](/docs/setting-up-the-ios-sdk/retrieving-your-api-key). For authentication and payments, see [Running Payments](/docs/setting-up-the-ios-sdk/running-payments). ## Troubleshooting ### Common Installation Issues * **Build Errors**: Ensure you're using Xcode 16.3 or later * **Runtime Crashes**: For a manual XCFramework install, verify the framework is set to "Embed & Sign" * **Import Errors**: Check that the package product (or XCFramework) was added to the correct target, and that you `import KoardSDK` * **Architecture Issues**: The XCFramework automatically handles different architectures ### Verification Checklist * [ ] Xcode 16.3 or later installed * [ ] iOS 17.4+ deployment target set * [ ] `KoardSDK` added via Swift Package Manager, or `KoardSDK.xcframework` added and set to "Embed & Sign" * [ ] Project builds without errors * [ ] `import KoardSDK` works correctly ## Next Steps Once the SDK is installed and configured: * [Create a Sandbox Apple Account](/docs/setting-up-the-ios-sdk/creating-a-sandbox-apple-account) - Ensure your testers are ready * [Configure Tap to Pay](/docs/setting-up-the-ios-sdk/adding-support-for-tap-to-pay-on-iphone) - Enable contactless payment functionality * [Implement Payments](/docs/setting-up-the-ios-sdk/running-payments) - Add payment processing to your app * [Understand Payment Lifecycle](/docs/payments/payment-lifecycle) - Learn about the complete payment flow * [Get Ready for Production](/docs/setting-up-the-ios-sdk/getting-ready-for-production) - Set up schemes and API keys for launch ## See also This wraps up the SDK installation. See the links below for next steps in your integration: * [Creating a Sandbox Apple Account](/docs/setting-up-the-ios-sdk/creating-a-sandbox-apple-account) - Set up dedicated testers * [Adding Tap to Pay](/docs/setting-up-the-ios-sdk/adding-support-for-tap-to-pay-on-iphone) - Enable contactless payments * [Running Payments](/docs/setting-up-the-ios-sdk/running-payments) - Implement payment processing * [Payment Lifecycle](/docs/payments/payment-lifecycle) - Complete payment flow guide * [Apple Best Practices](/docs/appendix/apple-best-practices-and-guidelines) - iOS development guidelines # Payments Learn how to orchestrate Koard payment flows across sale, preauthorization, capture, adjustment, reversal, and refund operations. ## Available Guides * [Idempotency](/docs/payments/idempotency) – Using `event_id` to make payments safely retryable * [Sale](/docs/payments/methods/sale) – One-step auth + capture transactions * [Preauth](/docs/payments/methods/preauth) – Hold funds before finalizing totals * [Capture](/docs/payments/methods/capture) – Settle preauthorized amounts * [Incremental Auth](/docs/payments/methods/incremental-auth) – Increase an existing authorization * [Tip Adjust](/docs/payments/methods/tip-adjust) – Update gratuity before settlement * [Reverse](/docs/payments/methods/reverse) – Release funds from a preauth * [Refund](/docs/payments/methods/refund) – Return captured funds * [Payment Lifecycle](/docs/payments/payment-lifecycle) – End-to-end transaction flow * [Surcharging](/docs/payments/surcharging) - Implementing Surcharging **Looking for SDK usage?** Start with the [iOS Running Payments guide](/docs/guides/ios-sdk/details/running-payments.md) and pair it with these payment references. # SDK Response Codes & Error Handling Understand how the Koard Android SDK surfaces errors and transaction outcomes — including the `KoardException` / `KoardError` types your app catches, transaction response codes, display messages, and error scenarios. **What you learn** * The two error channels: **`KoardException`** (thrown) vs **`KoardTransactionResponse`** (emitted) * The `KoardError` and `KoardErrorType` sealed hierarchy your app inspects for error details * How the SDK wraps all underlying Visa KiC errors into Koard types — you never handle raw KiC exceptions * The final transaction statuses: **Approve**, **Decline**, **Abort**, **Failure**, **AltService**, and **Unknown** * What `statusCode` means and the numeric codes the underlying Visa KiC kernel sends * Display message IDs shown during the tap-to-pay flow * Common abort and error scenarios and how to handle them ## Error Model Overview The SDK surfaces errors through **two channels** depending on context: | Channel | When | How | What to inspect | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | --------------- | ---------------------------------------------------------------------- | | **`KoardException`** (thrown) | Non-transaction operations: enrollment, SDK init, API calls (`capture`, `refund`, `reverse`, `adjust`), validation | `try / catch` | `exception.error.errorType` — a `KoardErrorType` sealed class | | **`KoardTransactionResponse`** (emitted) | During `sale()` / `preauth()` / `completePartialAuth()` / `refundEmv()` tap flows | Callback / Flow | `response.actionStatus`, `response.finalStatus`, `response.statusCode` | **`refund()` is not a tap flow.** `sdk.refund(...)` (and its `refundTransaction(...)` alias) is a backend-only suspend call returning `Result`, so its errors arrive through the `KoardException` channel. The card-present refund is `sdk.refundEmv(...)`, which returns a `Flow` and uses the emitted channel. **You never handle raw KiC exceptions.** The SDK catches every `KiCSdkException` from the Visa Kernel in the Cloud SDK and maps it to a `KoardException` with a typed `KoardErrorType`. Your app only needs to handle Koard types. ## KoardException & KoardError `KoardException` is the main exception thrown by the SDK for all non-transaction-flow errors. It wraps a `KoardError` with a human-readable message and a typed error classification: ```kotlin class KoardException( cause: Throwable? = null, val error: KoardError = KoardError( shortMessage = "Koard Merchant SDK has encountered a fatal error. ...", errorType = KoardErrorType.GeneralError ) ) : Exception(error.shortMessage, cause) data class KoardError( val shortMessage: String, // Human-readable error description val errorType: KoardErrorType // Typed error classification (sealed hierarchy) ) ``` **The underlying engine exception is never attached as `cause`.** When the SDK maps a `KiCSdkException` from the Visa Kernel in the Cloud, it deliberately does _not_ set it as the `KoardException.cause` — this keeps third-party SDK identifiers out of your crash reporters and means you never need a transitive dependency on the engine's exception types. The numeric code and any human-readable detail are folded into `error.shortMessage` and `error.errorType`. ### Catching KoardException ```kotlin try { sdk.capture(transactionId, amount) } catch (e: KoardException) { when (e.error.errorType) { is KoardErrorType.KoardServiceErrorType.HttpError -> { val code = (e.error.errorType as KoardErrorType.KoardServiceErrorType.HttpError).errorCode showError("Server error (HTTP $code): ${e.error.shortMessage}") } is KoardErrorType.KoardServiceErrorType.ConnectionError -> showError("Network error — check your connection") is KoardErrorType.KoardServiceErrorType.Unauthorized -> showError("Session expired — please log in again") is KoardErrorType.VACEnrollmentError -> showError("Enrollment failed — re-enroll the device") else -> showError(e.error.shortMessage) } } ``` ## KoardErrorType Reference `KoardErrorType` is a sealed class hierarchy. Every error the SDK produces maps to one of these types. ### Top-Level Error Types | Error Type | When It Occurs | | --------------------------- | -------------------------------------------------------------------------- | | `GeneralError` | Catch-all for unmapped or unexpected errors | | `CertificateError` | TLS or certificate validation failure | | `BLEError` | Bluetooth/peripheral communication failure | | `MainThreadError` | SDK method called on the main thread (must use a worker thread) | | `NfcTransactionError` | NFC transaction-level failure | | `VACEligibilityError` | Device failed Visa Acceptance Cloud eligibility check (e.g., Android < 12) | | `DeviceNotProvisionedError` | Device has not been provisioned for Tap to Pay | | `VACEnrollmentError` | Enrollment with the Visa Acceptance Cloud failed | ### NotReady — Blocked Before the Tap Starts `sale()`, `preauth()`, `completePartialAuth()`, and `refundEmv()` refresh their readiness checks first and **throw** a `KoardException` whose `error.errorType` is a `KoardErrorType.NotReady.*` value if the SDK cannot transact. The Flow is never returned, so wrap the call itself — not just the collection — in `try / catch`. `KoardSdkReadiness.notReadyReason()` returns the same typed reason without attempting a transaction. | Error Type | Meaning | What to do | | ------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `KernelAppNotInstalled` | The Visa Tap to Pay Ready app isn't installed | Route the user to install it (`installKernelApp(activity)`) | | `DeveloperModeEnabled` | Developer options are on | Prompt the operator to disable developer mode | | `NotAuthenticated` | No merchant session | Call `login(...)` | | `NotEnrolled` | The device has not been enrolled | Set an active location, then call `enrollDevice()` | | `NoActiveLocation` | No active location selected | Call `setActiveLocation(locationId)` | | `ReaderNotStarted` | Enrolled with a location, but the reader connection was never started or was killed | Call `prepare()` to bring the session back up before retrying the tap | | `Preparing` | Enrolling, generating certificates, the payment processor is coming up, or a location switch is in flight | **Transient** — retry shortly | | `CertificateFailed(error)` | Certificate generation failed | Inspect `error`; re-run `refreshDeviceCertificates()` | | `EnrollmentFailed(error)` | Enrollment failed | Inspect `error`; see [Troubleshooting](/docs/guides/android-sdk/details/troubleshooting) | | `PaymentProcessorFailed(error)` | The thin client failed to start | Inspect `error`; `resetKernelService()` to release the IPC connection, then retry (optionally `prepare()` first to re-warm) | `KoardErrorType` and its nested `KoardServiceErrorType` / `KicConnectorError` are `sealed`, and 1.0.6 added the members above plus `KoardServiceErrorType.UnparseableResponse` and `KicConnectorError.KernelAppBusyWithAnotherMerchant`. Any exhaustive `when` over these types will stop compiling until you add the new branches or an `else`. ### KoardServiceErrorType — API / HTTP Errors Thrown when SDK methods call the Koard REST API (`capture`, `refund`, `reverse`, `adjust`, `getTransaction`, etc.): | Error Type | Description | | --------------------------- | --------------------------------------------------------------------------------------------- | | `HttpError(errorCode: Int)` | Server returned an HTTP error — inspect `errorCode` for the status (400, 401, 404, 500, etc.) | | `InvalidRequest` | Request validation failed before sending (e.g., negative amount, missing transaction ID) | | `NotFound` | Resource not found (404) | | `Unauthorized` | Missing or invalid API key / session (401) | | `UnexpectedError` | Unexpected server error or empty response body | | `UnparseableResponse` | The response body could not be deserialized | | `ConnectionError` | Network unreachable, DNS failure, or timeout | ### DeviceIntegrityError — Security Checks Thrown when the device fails security validation during enrollment or transaction preparation: | Error Type | KiC Code | Description | | ------------------------- | -------- | ------------------------------------------------------ | | `EmulatorDetected` | 1000 | Running on an emulator — use a physical device | | `RootDetected` | 1001 | Device is rooted or has superuser binaries | | `TamperDetected` | 1002 | Device tamper detection triggered | | `DeveloperModeEnabled` | 2000 | Developer options must be disabled | | `DebugModeEnabled` | 2001 | USB debugging must be disabled | | `HookDetected` | 2003 | Runtime instrumentation detected (Frida, Xposed, etc.) | | `GenericIntegrityFailure` | -1 | Generic device integrity attestation failure | ### TransactionErrorType — Card & Payment Errors These appear as the `errorType` on a `KoardException` when a transaction-level error is mapped from the KiC thin client. They correspond to EMV-level outcomes: | Error Type | Description | | ---------------------------------- | -------------------------------------------------- | | `TransactionAmountNonPositive` | Amount must be greater than zero | | `RefundMissingParentTransactionId` | Refund requires a parent transaction ID | | `CancelOrEnter` | Cardholder prompted to cancel or confirm | | `CardError` | Unrecoverable card data error | | `NotAuthorisedOrDeclined` | Issuer declined the transaction | | `PinRequired` | PIN entry is required | | `IncorrectPin` | Cardholder entered an incorrect PIN | | `ProcessingError` | Generic processing failure | | `TryAnotherCard` | Card cannot complete — try a different card | | `InsertOrSwipe` | Contactless not supported — use chip or mag-stripe | | `TryAnotherChoice` | Try a different payment method | | `Cancelled` | Transaction was cancelled | | `StrongCvm` | Strong Customer Verification required (SCA) | | `PinBypassed` | PIN entry was bypassed | | `PinNotProvided` | PIN was requested but not provided | | `TransactionNotAllowed` | Transaction type not allowed on this card/terminal | | `NotApplicable` | Status not applicable to this transaction type | | `UnknownStatus` | Unmapped status from the kernel | | `TransactionError` | Generic transaction error | | `EnableReader` | NFC reader needs to be enabled | | `NetworkError` | Network error during transaction processing | | `AuthenticationFailed` | Authentication with the payment backend failed | | `CouldNotAttestError` | Device attestation failed during transaction | | `AsiError` | Visa auth service interface error | | `TcConfigError` | Thin client configuration error | | `VACResponseFailedError` | VAC response indicated failure | | `VACResponseParseError` | Could not parse VAC response | | `TransactionInProgressError` | Another transaction is already in progress | | `DeviceDisabledError` | Device has been disabled for transactions | | `ErrorLoadingConfig` | Could not load transaction configuration | | `VACInternalError` | Internal VAC error | | `TransactionApprovedUploadFailed` | Transaction approved but receipt upload failed | ### KiC Connector Errors Thrown when the SDK cannot communicate with the Visa Tap to Pay Ready kernel app: | Error Type | KiC Code | Description | | ---------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `BindingError` | 92 | Failed to bind to the Visa kernel service | | `ConnectorSendError` | 93 | Sending a message to the kernel failed | | `KernelParseError` | 94 | Kernel response could not be parsed | | `ConnectorParseError` | 95 | Connector-side serialization failed | | `KernelAppNotInstalled` | 96 | Visa kernel app missing — install from Google Play | | `PlayProtectOrVerifyAppDisabled` | 97 | Google Play Protect must be enabled | | `KernelAppBusyWithAnotherMerchant` | 98 | Another merchant app on the device currently holds the lock on the Tap to Pay Ready kernel service (KiC multi-tenancy). Close the other app or wait for it to finish — calling `resetKernelService()` will **not** help because the lock is owned by a different process | ### KiC General Errors | Error Type | KiC Code | Description | | -------------------------------- | -------- | ---------------------------------------- | | `NoNetworkOrTimedOut` | 10 | Network unavailable or request timed out | | `UnsupportedAndroidVersion` | 21 | Device OS below Android 12 | | `TapToPayReadyAppUpdateRequired` | 62 | Visa Tap to Pay Ready app is outdated | ### KiC Eligibility Errors | Error Type | KiC Code | Description | | -------------------------------------------- | -------- | --------------------------------------- | | `UnsupportedOs` | 80 | OS build is unsupported | | `HardwareKeystoreNotPresent` | 81 | No hardware-backed keystore | | `ECEncryptionNotAvailable` | 82 | Elliptic-curve crypto unavailable | | `AESEncryptionNotAvailable` | 83 | AES crypto unavailable | | `DESEncryptionNotAvailable` | 84 | DES crypto unavailable | | `NfcNotAvailable` | 85 | NFC hardware missing or disabled | | `GooglePlayServicesNotAvailableOrOldVersion` | 86 | Google Play Services absent or outdated | | `EligibilityCheckFailed` | -1 | Generic eligibility failure | ### KiC Initialize Errors | Error Type | KiC Code | Description | | ----------------------- | -------- | ------------------------------------------- | | `AlreadyEnrolled` | 1 | Device already enrolled — no action needed | | `DeviceAuthPubKidEmpty` | 3 | Missing device-auth public key — re-enroll | | `VacDeviceIdEmpty` | 4 | VAC device ID not provided | | `XRandomValueEmpty` | 52 | Random nonce required by enrollment missing | | `Failed` | -1 | Generic initialization failure | ### KiC Prepare Errors Pre-transaction secure channel setup failures: | Error Type | KiC Code | Description | | -------------------------------- | -------- | ----------------------------------------------------- | | `AuthenticationFailed` | 7 | VAC authentication failed | | `SdkInitNotDone` | 11 | `init()` not completed before use | | `SdkEnrollNotDone` | 12 | `enrollDevice()` not completed — re-enroll | | `ErrorLoadingConfig` | 17 | Could not load config blobs | | `AsiError` | 18 | Visa auth service interface error | | `HardwareKeystoreNotPresent` | 20 | Hardware keystore missing during key prep | | `AttestationFailed` | 24 | Device attestation failed | | `DoLoginFailed` | 25 | Login exchange with Visa backend failed | | `NullLoginAssertion` | 26 | Login response missing assertion | | `NullLoginCrypto` | 27 | Login response missing crypto payload | | `NullLoginResponse` | 28 | Entire login response was null | | `NullLoginResponseBody` | 29 | Login HTTP body empty | | `NullLoginResponseAuthStatus` | 30 | Login response missing auth status | | `NullSharedSecret` | 31 | Shared secret not derived — re-enroll | | `NullSessionKeys` | 32 | Session keys missing — re-enroll | | `FailedResponseVerification` | 33 | MAC/signature mismatch — possible tampering | | `FailedMacTagVerification` | 34 | MAC tag verification failed | | `FailedAuthStatus` | 36 | Visa backend rejected authorization | | `EmptyAuthStatus` | 37 | Auth status element empty | | `GetSeedListFailure` | 49 | Could not fetch key-rotation seed list | | `CertificatePinningError` | 50 | TLS pinning failed — possible MITM | | `TransactionKeyDerivationFailed` | 51 | Could not derive transaction keys — re-enroll | | `KeyRotationNeeded` | 87 | Kernel requested key rotation (handled automatically) | | `KeyRotationNotNeeded` | 88 | Key rotation not needed (informational) | | `KeyRotationSuccess` | 89 | Key rotation completed (informational) | | `KeyRotationFailure` | 90 | Key rotation failed — re-enroll if transactions fail | | `KeyRotationNullResponse` | 91 | Kernel did not return rotation status | ## Transaction Response Flow Every `KoardTransactionResponse` emitted by `sdk.sale()` or `sdk.preauth()` includes an **action status** that tells your app what stage the transaction is in. Use this to drive your UI: | Action Status | Meaning | What to do | | ------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | | `OnProgress` | Transaction is in flight — the reader is active | Update your UI with the current `displayMessage` and `readerStatus` | | `OnFailure` | A non-recoverable error occurred before completion | Read the `statusCode` to determine the failure reason and display an appropriate error | | `OnComplete` | The transaction has finished — check `finalStatus` for the outcome | Route to your receipt, decline, or error screen based on `finalStatus` | ```kotlin when (response.actionStatus) { KoardTransactionActionStatus.OnProgress -> { showStatus(response.readerStatus.toString(), response.displayMessage) } KoardTransactionActionStatus.OnFailure -> { showError(response.statusCode, response.displayMessage ?: "Transaction failed") } KoardTransactionActionStatus.OnComplete -> { when (response.finalStatus) { KoardTransactionFinalStatus.Approve -> showReceipt(response.transaction!!) KoardTransactionFinalStatus.Decline -> showDeclined(response) KoardTransactionFinalStatus.Abort -> showAborted(response) KoardTransactionFinalStatus.Failure -> showFailure(response) } } } ``` ## Final Transaction Statuses When `actionStatus` is `OnComplete`, the SDK sets `finalStatus` to one of these values: | Final Status | Description | Typical Cause | | ------------------------ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | **`Approve`** | Transaction was authorized by the issuer | Successful payment — display receipt with approval code and transaction details | | **`Decline`** | Transaction was explicitly declined | Issuer denied the authorization, card restricted, insufficient funds, or Strong CVM required (SCA interface switch) | | **`Abort`** | Transaction was terminated before completion | User cancelled, PIN entry cancelled, device security issue, NFC read failure, timeout, or network loss | | **`Failure`** | An internal or system-level error prevented the transaction | SDK/kernel error, device misconfiguration, or unexpected processing failure | | **`AltService`** | Card requested an alternative service | The card network indicated that an alternative acceptance method should be used | | **`Unknown(rawStatus)`** | The kernel returned a status the SDK could not map | Inspect `rawStatus` for the original value; treat as a non-approval | The SDK consolidates the underlying processor response into these statuses so your app does not need to interpret raw processor-level codes. The original acquirer `responseCode` (ISO 8583 field 39) is still available in the transaction receipt for logging and support purposes. ## Persisted Transaction Status The reader `finalStatus` above describes the outcome of a single tap. The persisted transaction itself (`KoardTransaction.status`, also returned by `getTransaction`/`getTransactions` and post-reader operations) uses the `KoardTransactionStatus` enum: | Status | Description | | ------------------- | ------------------------------------------------------------------------------------------- | | `PENDING` | Transaction created but not yet finalized | | `AUTHORIZED` | Funds authorized (preauth/hold) | | `CAPTURED` | Authorization captured/settled | | `SETTLED` | Settled with the processor | | `DECLINED` | Declined by the issuer | | `REFUNDED` | Fully or partially refunded | | `REVERSED` | Reversed/voided | | `CANCELED` | Canceled before completion | | `ERROR` | Errored out | | `SURCHARGE_PENDING` | Awaiting customer surcharge confirmation — call `sdk.confirm(transactionId, confirm = ...)` | | `UNKNOWN` | Unmapped status | A transaction is refundable when its status is `AUTHORIZED`, `CAPTURED`, `SETTLED`, or `PENDING` (exposed as `KoardTransactionStatus.isRefundable`). ## Acquirer Authorization Statuses Behind the scenes, the acquirer returns a more granular `authStatus` in the authorization response. The SDK maps these to the final statuses above, but they are available in the transaction details for advanced use cases: | Auth Status | Description | Maps to Final Status | | ------------------ | ------------------------------------------------------------------------ | ------------------------------------------- | | `Approve` | Issuer approved the transaction | `Approve` | | `Decline` | Issuer declined the transaction (also used for internal acquirer errors) | `Decline` | | `PartialApproval` | Issuer approved a lesser amount than requested | `Approve` (with reduced `authorizedAmount`) | | `InvalidPIN` | The PIN entered by the cardholder was incorrect | `Decline` | | `UnableToGoOnline` | The terminal could not connect to the acquirer for online authorization | `Decline` or `Abort` | | `AdditionalInfo` | Acquirer returned supplementary information (e.g., referral) | Varies | ## Transaction Response Details When a transaction completes (regardless of outcome), the `KoardTransactionResponse` contains the following fields: | Field | Type | Description | | ----------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------- | | `transactionId` | `String` | Unique identifier for the transaction | | `finalStatus` | `KoardTransactionFinalStatus` | Terminal outcome: `Approve`, `Decline`, `Abort`, `Failure`, or `AltService` | | `actionStatus` | `KoardTransactionActionStatus` | Current action phase: `OnProgress`, `OnFailure`, or `OnComplete` | | `readerStatus` | `KoardReaderStatus` | Reader state: `preparing`, `readyForTap`, `cardDetected`, `processing`, `complete`, etc. | | `displayMessage` | `String?` | Human-readable message from the reader/kernel | | `statusCode` | `Int?` | Numeric status code from the Visa KiC kernel — see [Status Code Reference](#status-code-reference) below | | `statusCodeDescription` | `String?` | Human-readable description of the status code (auto-generated from the code) | | `transaction` | `KoardTransaction?` | Full transaction object (populated on completion) | ## Status Code Reference The `statusCode` field on `KoardTransactionResponse` is a numeric integer forwarded from the underlying **Visa Kernel in the Cloud (KiC)** SDK. These codes are passed through on the transaction response for troubleshooting and logging. **These same codes drive the `KoardErrorType` mapping.** When the SDK catches a `KiCSdkException` with one of these codes, it maps it to the corresponding `KoardErrorType` documented in the [KoardErrorType Reference](#koarderrortype-reference) above. You don't need to handle numeric codes directly — use `KoardErrorType` pattern matching instead. For most apps, routing on `actionStatus` + `finalStatus` is sufficient. The `statusCode` is useful for **debugging**, **logging**, and handling edge cases like re-enrollment (`12`) or developer mode (`2000`). The status codes fall into several categories based on what layer of the KiC stack generated them: ### Connector Status (92–97) — Service Binding Failures These indicate problems communicating between the Koard SDK and the Visa Tap to Pay Ready app installed on the device. | Code | Description | What to do | | ---- | --------------------------------------------------- | ---------------------------------------------------------------- | | `92` | Failed to bind to the Visa kernel service | Ensure the Visa Tap to Pay Ready app is installed and up to date | | `93` | Sending a message to the kernel service failed | Retry the operation; if persistent, restart both apps | | `94` | Kernel response payload could not be parsed | Update the Visa Tap to Pay Ready app | | `95` | Connector-side serialization/deserialization failed | Update the Koard SDK to the latest version | | `96` | Visa kernel service app missing on device | Install the Visa Tap to Pay Ready app from Google Play | | `97` | Google Play Protect / Verify Apps is disabled | Enable Play Protect in Google Play settings | ### General Status (10, 21, 62) — Environment Readiness | Code | Description | What to do | | ---- | -------------------------------------------- | ------------------------------------------------ | | `10` | Network unavailable or SDK request timed out | Check network connectivity and retry | | `21` | Device OS level not supported by Tap to Pay | Device must run Android 12 (API 31) or later | | `62` | Visa Tap to Pay Ready app is outdated | Update the Tap to Pay Ready app from Google Play | ### Eligibility Status (80–86) — Device Capability Checks Returned when `checkKiCEligibility()` detects a device hardware or software limitation. | Code | Description | What to do | | ---- | ---------------------------------------------- | ------------------------------------------------------------ | | `80` | OS flavor/build is unsupported | Device uses an incompatible Android build (e.g., custom ROM) | | `81` | Device lacks a hardware-backed keystore | Device does not meet security requirements | | `82` | Elliptic-curve crypto APIs missing or disabled | Device crypto hardware insufficient | | `83` | AES crypto acceleration unavailable | Device crypto hardware insufficient | | `84` | DES crypto unavailable | Device crypto hardware insufficient | | `85` | NFC hardware missing or disabled | Enable NFC in device settings, or device has no NFC | | `86` | Google Play Services absent or out of date | Install or update Google Play Services | ### Initialize Status (1, 3, 4, 52) — Enrollment & Bootstrap | Code | Description | What to do | | ---- | ---------------------------------------------- | ---------------------------------------------------------- | | `1` | Device already enrolled for Tap to Pay | No action needed — the device is already set up | | `3` | Missing device-auth public key identifier | Re-run the enrollment flow | | `4` | Merchant/VAC device ID not provided | Ensure the SDK is configured with a valid merchant profile | | `52` | Random nonce required by enrollment is missing | Re-run the enrollment flow | ### Security Status (1000–2003) — Device Integrity | Code | Description | What to do | | ------ | ------------------------------------------- | ----------------------------------------------------------- | | `1000` | Emulator detected | Tap to Pay cannot run on emulators — use a physical device | | `1001` | Device rooted or superuser binaries present | Device must not be rooted | | `1002` | Device tamper detection triggered | Device has been modified and is not trusted | | `2000` | Developer options must be disabled | Disable developer mode before running transactions | | `2001` | USB debugging/logging must be disabled | Turn off USB debugging in developer options | | `2003` | Runtime hook/instrumentation detected | Remove any instrumentation frameworks (Frida, Xposed, etc.) | ### Prepare Status (7–91) — Pre-Transaction Secure Channel These codes occur during `startUpSdk()` or when the SDK prepares for a transaction. They relate to the secure channel between the device and the Visa backend. | Code | Description | What to do | | ---- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `7` | VAC authentication call to Visa failed | Check API credentials and network connectivity | | `11` | `init()` not completed before use | Complete SDK initialization before starting transactions | | `12` | `enrollDevice()` not completed | Device needs enrollment — show the enrollment UI and re-enroll. This also occurs if the Tap to Pay Ready app was reinstalled | | `17` | Could not load enrollment/transaction config blobs | Re-initialize the SDK | | `18` | ASI (Visa auth service interface) returned error | Transient backend issue — retry | | `20` | Hardware keystore missing when preparing keys | Device does not meet security requirements | | `24` | Device attestation failed or invalid | Re-enroll the device; ensure Play Protect is enabled | | `25` | Login exchange with Visa backend failed | Check network; retry | | `26` | Login response missing assertion blob | Transient backend issue — retry | | `27` | Login response missing crypto payload | Transient backend issue — retry | | `28` | Entire login response was null | Transient backend issue — retry | | `29` | Login HTTP body empty | Transient backend issue — retry | | `30` | Login response missing auth status | Transient backend issue — retry | | `31` | Shared secret not derived | Re-enroll the device | | `32` | Session keys missing | Re-enroll the device | | `33` | MAC/signature mismatch in response | Possible tampering — re-enroll the device | | `34` | MAC tag verification failed | Possible tampering — re-enroll the device | | `36` | Visa backend explicitly rejected authorization | Check merchant configuration with Koard support | | `37` | Auth status element empty | Transient backend issue — retry | | `49` | Could not fetch key-rotation seed list | Check network connectivity | | `50` | TLS pinning check failed | Possible man-in-the-middle — check network security | | `51` | Could not derive transaction keys | Re-enroll the device | ### Key Rotation Status (87–91) | Code | Description | What to do | | ---- | ------------------------------------------- | ------------------------------------------------- | | `87` | Kernel requested key rotation | SDK handles this automatically — no action needed | | `88` | Key rotation already satisfied (not needed) | Informational — no action needed | | `89` | Key rotation completed successfully | Informational — no action needed | | `90` | Key rotation failed | Re-enroll the device if transactions fail | | `91` | Kernel did not return key rotation status | Re-enroll the device if transactions fail | ### Prepare Progress (70–75) — `prepare()` Warm-Up The `sdk.prepare()` warm-up flow emits `KoardPrepareResponse` objects whose `status` is a `KoardPrepareStatus`. Codes 70–75 are the normal lifecycle; anything in the 80–199 range is surfaced as `KoardPrepareStatus.Error(code)`, and an unrecognized code becomes `KoardPrepareStatus.Unknown(code)`. These progress codes are informational — wait for `Done` (or an `Error`) rather than handling each one. | Code | `KoardPrepareStatus` | Meaning | | ---------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `70` | `Called` | KiC accepted the `prepare()` invocation | | `71` | `AuthenticationInProgress` | Authenticating the device against the backend | | `72` | `AttestationInProgress` | Performing Play Integrity / device attestation | | `73` | `GettingConfigurations` | Downloading transaction configurations | | `74` | `ParsingConfigurations` | Parsing the downloaded configurations | | `75` | `Done` | Prepare completed — SDK is ready for transactions | | `80`–`199` | `Error(code)` | Prepare did not reach `Done` (auth/attestation failure, key rotation, network error, etc.) — retry on the next user action | ### Transaction Status — In-Progress Codes These codes appear during an active transaction and are reflected in the `readerStatus` field: | Code | Reader Status | Description | | ----- | --------------- | ----------------------------------------------------------- | | `109` | `readyForTap` | POS state started — reader is waiting for a card tap | | `112` | `preparing` | POS state message — reader is preparing for the transaction | | `106` | `readCompleted` | Card read completed successfully | ### Generic Failure (-1) A status code of `-1` indicates a generic failure. The SDK uses the `statusCodeDescription` field to provide more context: | Description Contains | Meaning | | -------------------------------- | -------------------------------------------- | | `"eligibility"` | Generic eligibility evaluation failure | | `"initialise"` or `"enrol"` | Generic initialization or enrollment failure | | `"integrity"` or `"attestation"` | Device integrity attestation failed | **Status code 12 (enrollment not done)** deserves special handling. The demo app treats `statusCode == 12` during `OnFailure` as retryable (alongside `42`, `53`, and a null status code) — it prompts "Please tap again" rather than surfacing an error. If the code persists, the Tap to Pay Ready app was most likely reinstalled or had its data cleared; the merchant app must then clear its stored enrollment data (`clearEnrollmentState()`) and re-run `enrollDevice()`. Re-enrollment is never automatic. ## Display Message IDs During the tap-to-pay flow, the SDK emits display messages via `displayMessage` on each `OnProgress` event. These correspond to standard EMV message identifiers from the kernel: | Message | ID | Description | | --------------------------------------- | ---- | -------------------------------------------------------------------- | | Approved | `03` | Authorization obtained — transaction approved | | Cancel or Enter | `05` | Prompt to cancel or confirm | | Card Error | `06` | Unrecoverable card data error | | Not Authorized / Declined | `07` | Transaction was declined by the issuer | | Please remove card | `10` | Card not yet removed from the reader field | | Please try again | `13` | Recoverable error — retry the tap | | Welcome | `14` | Idle state — reader is ready | | Present card | `15` | Prompt the cardholder to tap | | Processing | `16` | Transaction is being processed | | Card read OK / Remove card | `17` | Card was read successfully — may be removed | | Please insert or swipe card | `18` | Contactless not supported — try contact/mag-stripe | | Please present one card only | `19` | Card collision detected — present only one card | | Approved. Please Sign | `1A` | Approved; signature required | | Authorizing. Please Wait | `1B` | Online authorization in progress | | Insert, swipe, or try another card | `1C` | Contactless failed — use another interface or card | | Please insert card | `1D` | Chip card should be inserted into the slot | | _(Empty string)_ | `1E` | Clear the display | | See Phone for instructions | `20` | Mobile device CVM required (Touch ID, Face ID, etc.) | | Present card again | `21` | Recoverable error — present the card again | | Practice Mode | `40` | Successful test/practice transaction | | Partial Approval | `43` | Issuer approved a lesser amount | | Cancelled for Device Security | `46` | Transaction cancelled due to a device security issue | | Cancelled | `47` | Generic transaction cancellation | | Try another card - No contact interface | `48` | Card returned GPO error (SW 6984) — transaction aborted | | Strong CVM | `49` | SCA issuer response requires interface switch — transaction declined | ## Abort and Error Scenarios The SDK returns an `Abort` or `Failure` final status in several well-defined situations. Understanding these helps you build robust error handling: ### Transaction Abort Scenarios | Scenario | What Happens | Message ID | | ------------------------------- | ------------------------------------------------------------------------------- | ---------- | | **User cancels PIN entry** | User selects "Cancel Transaction" on the PIN keypad | — | | **PIN session timeout** | 1 minute of inactivity on the PIN keypad | — | | **PIN keypad interrupted** | Another app covers the PIN screen | — | | **Network loss during PIN** | Network drops before the PIN event is sent | — | | **Split screen mode** | Device enters split screen while on PIN screen — sends `CVEntrySecurity` cancel | — | | **Device security issue** | Security configuration problem detected | `46` | | **Generic cancellation** | User or system cancelled the transaction | `47` | | **Card NFC failure (GPO 6984)** | Card cannot complete contactless — abort with `MACompletion` indicator | `48` | | **Strong CVM / SCA switch** | Issuer requires contact interface (not supported) — decline with `MACompletion` | `49` | | **Developer mode enabled** | Developer options are on — reader blocks the transaction | — | ### OnFailure Status Codes When `actionStatus` is `OnFailure`, check `statusCode` for the specific reason: | Status Code Constant | Description | | ---------------------------------- | ------------------------------------------------------------------ | | `TRANSACTION_WINDOW_FOCUS_CHANGED` | The transaction window lost focus (another app came to foreground) | | `CAMERA_IS_ACTIVE` | Device camera is active — conflicts with the secure NFC session | | `DEVELOPER_MODE_ENABLED` | Developer options are enabled on the device | | `NFC_NOT_AVAILABLE` | Device NFC is disabled or unavailable | | `DEVICE_NOT_ENROLLED` | Device has not completed enrollment | | `SESSION_TIMEOUT` | The transaction session timed out | **Developer Mode**: The most common cause of unexpected transaction failures during development. Always disable developer mode before running transactions. Follow the workflow: **Enable dev mode → Install app → Disable dev mode → Run transactions**. ### Completion Indicators The receipt field `emv.tx.tm.CompletionIndicator` tells you how the transaction concluded at the kernel level: | Value | Meaning | | ---------------- | ----------------------------------------------------------------------------------------------------------------- | | `FullCompletion` | Transaction completed normally through the full authorization flow | | `MACompletion` | Transaction was terminated by the kernel (Merchant Application completion) — typically an abort or forced decline | ## Handling Responses in Practice The SDK uses two error channels. Here is a complete pattern for handling both: ### Channel 1: Transaction Flow (KoardTransactionResponse) For `sale()`, `preauth()`, `completePartialAuth()`, and the tap-based `refundEmv()` — errors come via the response callback: ```kotlin private fun handleTransactionEvent(response: KoardTransactionResponse) { when (response.actionStatus) { KoardTransactionActionStatus.OnProgress -> { // Update UI with reader status and display message updateUI( status = response.readerStatus.toString(), message = response.displayMessage ?: "Processing..." ) } KoardTransactionActionStatus.OnFailure -> { // Check for re-enrollment scenario if (response.statusCode == 12) { // Tap to Pay Ready app was reinstalled or cleared data // Clear stored enrollment info and re-run enrollment triggerReEnrollment() return } // Transaction could not proceed — show the reason val reason = buildString { append("Transaction Failed") response.displayMessage?.let { append("\n\n$it") } response.statusCodeDescription?.let { append("\n\n$it") } response.statusCode?.let { append("\n\nStatus Code: $it") } } showError(reason) } KoardTransactionActionStatus.OnComplete -> { when (response.finalStatus) { KoardTransactionFinalStatus.Approve -> { showReceipt(response.transaction!!) } KoardTransactionFinalStatus.Decline -> { showDeclined(response.displayMessage ?: "Transaction declined") } KoardTransactionFinalStatus.Abort -> { showAborted(response.displayMessage ?: "Transaction aborted") } KoardTransactionFinalStatus.Failure -> { showError(response.displayMessage ?: "Transaction failed") } is KoardTransactionFinalStatus.Unknown -> { showError("Unexpected status: ${response.finalStatus}") } } } else -> Unit } } ``` ### Channel 2: API & SDK Operations (KoardException) For `capture()`, `reverse()`, `refund()`, `adjust()`, `enrollDevice()`, `setActiveLocation()`, and other non-tap operations — errors are thrown as `KoardException`: ```kotlin private suspend fun capturePayment(transactionId: String, amount: Int) { try { val result = sdk.capture(transactionId, amount) showReceipt(result) } catch (e: KoardException) { when (val errorType = e.error.errorType) { // HTTP errors from the Koard API is KoardErrorType.KoardServiceErrorType.HttpError -> showError("Server error (HTTP ${errorType.errorCode}): ${e.error.shortMessage}") is KoardErrorType.KoardServiceErrorType.ConnectionError -> showError("Network error — check your connection and retry") is KoardErrorType.KoardServiceErrorType.Unauthorized -> showError("Session expired — please log in again") is KoardErrorType.KoardServiceErrorType.NotFound -> showError("Transaction not found") is KoardErrorType.KoardServiceErrorType.InvalidRequest -> showError("Invalid request: ${e.error.shortMessage}") // Device integrity failures is KoardErrorType.DeviceIntegrityError.DeveloperModeEnabled -> showError("Disable developer mode before processing payments") is KoardErrorType.DeviceIntegrityError -> showError("Device security check failed: ${e.error.shortMessage}") // Enrollment issues is KoardErrorType.VACEnrollmentError -> promptReEnrollment(e.error.shortMessage) is KoardErrorType.VACEligibilityError -> showError("Device not eligible for Tap to Pay: ${e.error.shortMessage}") // KiC connector/kernel errors is KoardErrorType.KicConnectorError.KernelAppNotInstalled -> showError("Install the Visa Tap to Pay Ready app from Google Play") is KoardErrorType.KicConnectorError -> showError("Kernel communication error: ${e.error.shortMessage}") is KoardErrorType.KicPrepareError.SdkEnrollNotDone -> promptReEnrollment("Device needs re-enrollment") // Fallback else -> showError(e.error.shortMessage) } } } ``` ## Next Steps * Review the [Running Payments](/docs/setting-up-the-android-sdk/running-payments) guide for the complete payment flow implementation * See the [Demo App](/docs/setting-up-the-android-sdk/demo) for a working example of response handling in `MainScreenViewModel` * Consult the [API Response Codes](/docs/api-reference/response-codes) for HTTP-level status codes from the Koard REST API # Running Payments Process payments with the Koard Merchant SDK to enable tap-to-pay functionality in your iOS application. If you're ready to start developing, see our [SDK installation guide](/docs/setting-up-the-ios-sdk/installing-the-sdk). [Get started with Koard](/docs/getting-started-with-koard/introduction) **What you learn** In this guide, you'll learn: * How to initialize and authenticate with the Koard Merchant SDK * How to set up location management for multi-location merchants * How to prepare card reader sessions for tap-to-pay * How to process different types of transactions (sale, preauth, refund) * How to handle transaction responses and error scenarios ## Before you begin This comprehensive guide covers everything you need to know about integrating and using the KoardMerchantSDK in your iOS application. For payment concepts and API payloads, explore the [Payments guides](/docs/guides/payments/overview.md)—including [Sale](/docs/payments/methods/sale) and [Preauth](/docs/payments/methods/preauth). To understand the complete flow, see the [Payment Lifecycle guide](/docs/guides/payments/details/payment-lifecycle.md). **Test on Real Hardware**: Keep a dedicated test iPhone with your Sandbox Apple Account signed in. Simulator builds cannot exercise Tap to Pay, and production Apple IDs won't work in the Sandbox environment. ## Key Concepts ### 1. Authentication Tokens The SDK manages several types of tokens automatically: * **API Key**: Your API key for Koard services * **Card Reader Token**: Apple's ProximityReader token for Tap to Pay functionality ### 2. Card Reader Sessions The SDK handles Apple's ProximityReader lifecycle: * **Preparation**: Refreshes tokens and prepares the reader for transactions * **Transaction Processing**: Manages card reading and data collection * **Session Management**: Handles background/foreground transitions automatically ### 3. Location Management Multi-location merchants must set an active location before processing payments: * **Retrieve available locations** after login * **Set the active location** for all subsequent transactions * **Location data is persisted** across app sessions ## Initialize the SDK Initialize the SDK early in your app lifecycle (typically in `AppDelegate` or `SceneDelegate`): ```swift import KoardSDK class AppDelegate: UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Configure SDK options let options = KoardOptions( environment: .uat, // .uat | .production | .custom(String) loggingLevel: .debug // .none | .error | .warning | .debug | .verbose ) // Initialize with your API key KoardMerchantSDK.shared.initialize( options: options, apiKey: "your-koard-api-key" ) return true } } ``` ## Authenticate the Merchant Before processing any payments, authenticate the merchant. The login function returns a JWT token that is then passed in the Bearer token of all successive requests. ```swift import KoardSDK private func authenticateMerchant() async throws { do { // Login with merchant credentials try await KoardMerchantSDK.shared.login( code: "your-merchant-code", pin: "your-merchant-pin" ) print("Merchant authenticated successfully") // After login, set up location try await setupLocation() } catch { print("Authentication failed: \(error)") throw error } } ``` If you have already resolved the merchant identity into a single opaque string (for example via a QR scan, SSO callback, or server-issued provisioning token), you can log in with an alias instead of a code and PIN. It produces the same session token: ```swift try await KoardMerchantSDK.shared.login(alias: "your-merchant-alias") ``` **Session-token auth**: Login is session-token only. The SDK persists the session token and never stores the merchant code, PIN, or alias. When the session expires, call `login(...)` again to re-authenticate. ## Set Location Retrieve and set the active location. Locations are attached to terminals which determines the MID, TID and Processor Configuration to be used for the payments API. This determines whether the merchant is leveraging TSYS, Payroc, Fiserv, or Elavon payment processing rails. ```swift private func setupLocation() async throws { do { // Get available locations let locations = try await KoardMerchantSDK.shared.locations() guard !locations.isEmpty else { throw PaymentError.noLocationsAvailable } // For single location merchants, use the first location let activeLocation = locations.first! // For multi-location merchants, let user select // let activeLocation = userSelectedLocation // Set the active location KoardMerchantSDK.shared.setActiveLocationID(activeLocation.id) print("Active location set: \(activeLocation.name)") } catch { print("Location setup failed: \(error)") throw error } } ``` ## Prepare a Card Reader Session Before accepting payments, prepare the card reader. The reader preparation leverages a PaymentCardReader.Token associated with the merchant session. ```swift import KoardSDK private func prepareCardReader() async throws { do { // Check if account is linked (required for Tap to Pay) let isLinked = try await KoardMerchantSDK.shared.isAccountLinked() if !isLinked { // Link the merchant account to Apple Pay try KoardMerchantSDK.shared.linkAccount() // Wait for linking to complete // This typically requires user interaction return } // Prepare the card reader session try await KoardMerchantSDK.shared.prepare() print("Card reader prepared and ready") // Optional: Monitor reader status monitorReaderStatus() } catch { print("Card reader preparation failed: \(error)") throw error } } private func monitorReaderStatus() { Task { // Monitor reader events for await event in KoardMerchantSDK.shared.readerEvents { DispatchQueue.main.async { self.handleReaderEvent(event) } } } } private func handleReaderEvent(_ event: Event) { switch event { case .readyForTap: print("Ready for tap") case .cardDetected: print("Card detected") case .readCompleted: print("Card read completed") case .readCancelled: print("Card read cancelled") default: print("Reader event: \(event.description)") } } ``` ## Process Sale Transactions Sale transactions are single-step auth + capture that immediately capture funds. Use sales when the final amount is known at payment time. For more details on when to use Sale vs Preauth, compare the [Sale](/docs/payments/methods/sale) and [Preauth](/docs/payments/methods/preauth) guides. ```swift private func processSale() async throws { // Create payment breakdown (optional) let breakdown = PaymentBreakdown( subtotal: 1000, // $10.00 in cents taxRate: 8.75, // 8.75% as a percent value (not a 0-1 decimal) taxAmount: 88, // $0.88 in cents tipAmount: 200, // $2.00 in cents tipType: .fixed // or .percentage ) // Create currency let currency = CurrencyCode(currencyCode: "USD", displayName: "US Dollar") do { // Process the sale let response = try await KoardMerchantSDK.shared.sale( amount: 1288, // Total amount in cents breakdown: breakdown, // Optional breakdown currency: currency, eventId: UUID().uuidString, // Optional idempotency/tracking key (UUID4) type: .sale // Defaults to .sale ) // Handle the response try await handleTransactionResponse(response) } catch { print("Sale failed: \(error)") throw error } } ``` **Tracking and idempotency**: `sale` and `preauth` take an optional `eventId` (UUID4), not a `transactionId`. Koard generates the transaction ID and returns it on `response.transactionId`. The reader is driven internally, so a Tap to Pay sheet is presented during these calls. ## Process Preauthorization Transactions Preauthorization transactions authorize funds without capturing them. They can be incrementally authorized, captured, or reversed. Use preauth when the final amount is uncertain (e.g., restaurant with tip) or when you need to verify funds availability. To complete a preauth, capture the payment using the transaction ID. For the complete flow, see [Preauth](/docs/payments/methods/preauth) and [Capture](/docs/payments/methods/capture). ```swift private func processPreauth() async throws { let currency = CurrencyCode(currencyCode: "USD", displayName: "US Dollar") do { // Process preauthorization (breakdown is optional — pass nil) let response = try await KoardMerchantSDK.shared.preauth( amount: 1000, // Amount to preauthorize in cents breakdown: nil, // Optional breakdown currency: currency, eventId: UUID().uuidString // Optional idempotency/tracking key (UUID4) ) print("Preauth successful: \(response.transactionId ?? "Unknown")") // Store transaction ID for later capture/reverse UserDefaults.standard.set(response.transactionId, forKey: "lastPreauthId") } catch { print("Preauth failed: \(error)") throw error } } ``` ## Handle Transaction Responses Payment methods return a `TransactionResponse`. Read the rich domain object from `response.transaction` (a `KoardTransaction?`). Its `status` is the public `KoardTransaction.Status` enum, which includes `pending`, `authorized`, `captured`, `surchargePending`, `surchargeApplied`, `approved`, `declined`, `refunded`, `reversed`, `pickupCard`, `timedOut`, `canceled`, `cancelled`, `error`, `settled`, and `unknown`. ```swift private func handleTransactionResponse(_ response: TransactionResponse) async throws { guard let transaction = response.transaction else { throw PaymentError.invalidResponse } switch transaction.status { case .approved: print("Transaction approved!") print("Transaction ID: \(transaction.transactionId)") print("Amount: $\(Double(transaction.totalAmount) / 100.0)") case .surchargePending: print("Surcharge pending - customer approval required") // Show surcharge disclosure to customer if let disclosure = transaction.surchargeDisclosure { let approved = try await showSurchargeDisclosure(disclosure) // Confirm or deny the surcharge let confirmedTransaction = try await KoardMerchantSDK.shared.confirm( transaction: transaction.transactionId, confirm: approved ) print("Final transaction status: \(confirmedTransaction.status)") } case .declined: print("Transaction declined: \(transaction.statusReason ?? "Unknown reason")") case .error: print("Transaction error: \(transaction.statusReason ?? "Unknown error")") default: print("Transaction status: \(transaction.status.string)") } } private func showSurchargeDisclosure(_ disclosure: String) async throws -> Bool { // Show disclosure to customer and get their approval // This should be implemented based on your UI requirements return await withCheckedContinuation { continuation in DispatchQueue.main.async { let alert = UIAlertController( title: "Surcharge Notice", message: disclosure, preferredStyle: .alert ) alert.addAction(UIAlertAction(title: "Accept", style: .default) { _ in continuation.resume(returning: true) }) alert.addAction(UIAlertAction(title: "Decline", style: .cancel) { _ in continuation.resume(returning: false) }) // Present alert (you'll need to implement this based on your view hierarchy) // self.present(alert, animated: true) } } } ``` ## Handle Partial Approvals When the issuer authorizes less than the requested amount, the transaction comes back with a `statusReason` of `partial_approval` (the `StatusReason.partialApproval` case). The top-level `status` still reads as `approved`/`captured`. Use the `partialAuthApproval` method to accept the partial amount as final, or reject it to release the hold: ```swift private func handlePartialApproval(_ transaction: KoardTransaction) async throws { guard transaction.isPartialApproval else { return } // remainingAmount is the amount still owed after the partial authorization if let remaining = transaction.remainingAmount { print("Partial approval — remaining balance: \(remaining) cents") } // Accept the partial amount as final (or pass approve: false to release the hold) let settled = try await KoardMerchantSDK.shared.partialAuthApproval( transactionId: transaction.transactionId, approve: true, eventId: UUID().uuidString // Optional UUID4 for idempotency ) print("Partial-auth settled status: \(settled.status.string)") // Optionally run a fresh sale for the remaining balance on another card. } ``` To collect the remaining balance on a second card, pass the original transaction's id as `partialAuthTransactionId` on the follow-up `sale` (or `preauth`). This links the two authorizations so they settle together: ```swift let remainder = try await KoardMerchantSDK.shared.sale( amount: remainingAmount, breakdown: nil, currency: CurrencyCode(currencyCode: "USD", displayName: "US Dollar"), eventId: UUID().uuidString, partialAuthTransactionId: transaction.transactionId ) ``` ## Transaction Management ### Refund a Transaction ```swift import KoardSDK private func processRefund(transactionId: String, amount: Int? = nil) async throws { do { let response = try await KoardMerchantSDK.shared.refund( transactionId: transactionId, amount: amount, // nil for full refund eventId: UUID().uuidString // Optional UUID4 for idempotency ) print("Refund successful: \(response.transactionId ?? "Unknown")") } catch { print("Refund failed: \(error)") throw error } } ``` To run a card-present refund (presenting the Tap to Pay sheet and capturing card data with the refund), pass `withTap: true`. When `withTap` is set, an `amount` is required: ```swift let response = try await KoardMerchantSDK.shared.refund( transactionId: transactionId, amount: 1288, // Required when withTap is true withTap: true ) ``` ### Reverse a Preauthorization ```swift private func reversePreauth(transactionId: String, amount: Int? = nil) async throws { do { let response = try await KoardMerchantSDK.shared.reverse( transactionId: transactionId, amount: amount // nil for full reversal ) print("Reversal successful: \(response.transactionId ?? "Unknown")") } catch { print("Reversal failed: \(error)") throw error } } ``` **Note**: Transactions can be partially reversed and refunded. When an authorization is reversed to 0 or a capture is refunded to 0, the transaction status becomes "cancelled". For more details, see our [Payment Lifecycle guide](/docs/guides/payments/details/payment-lifecycle.md). ### Incremental Authorization Authorize additional amounts on an existing preauth transaction. This is useful for adding incidental charges (e.g., hotel mini bar, additional restaurant items): ```swift private func incrementalAuth(transactionId: String, additionalAmount: Int) async throws { // Optional: Add breakdown for the additional amount let breakdown = PaymentBreakdown( subtotal: additionalAmount, taxRate: 8.75, // 8.75% as a percent value (not a 0-1 decimal) taxAmount: Int(Double(additionalAmount) * 0.0875), tipAmount: 0, tipType: .fixed ) do { let response = try await KoardMerchantSDK.shared.auth( transactionId: transactionId, amount: additionalAmount, breakdown: breakdown // Optional ) print("Incremental auth successful: \(response.transactionId ?? "Unknown")") } catch { print("Incremental auth failed: \(error)") throw error } } ``` ### Capture a Transaction Capture a previously authorized preauth transaction. You can capture the full authorized amount or a partial amount (e.g., adjust for final tip): ```swift private func captureTransaction(transactionId: String, finalAmount: Int? = nil) async throws { // Optional: Update breakdown with final tip amount let finalBreakdown = PaymentBreakdown( subtotal: 1000, // $10.00 taxRate: 8.75, // 8.75% as a percent value (not a 0-1 decimal) taxAmount: 88, // $0.88 tipAmount: 300, // $3.00 final tip tipType: .fixed ) do { let response = try await KoardMerchantSDK.shared.capture( transactionId: transactionId, amount: finalAmount, // nil to capture full authorized amount breakdown: finalBreakdown // Optional: updated breakdown with final tip ) print("Capture successful: \(response.transactionId ?? "Unknown")") } catch { print("Capture failed: \(error)") throw error } } ``` ### Adjust the Tip Adjust the tip on a completed transaction (for example, after a customer finalizes a tip in table service): ```swift private func adjustTip(transactionId: String, newTipTotal: Int) async throws { let response = try await KoardMerchantSDK.shared.tipAdjust( transactionId: transactionId, amount: newTipTotal, // New tip amount in cents tipType: .fixed, // .fixed or .percentage (PaymentBreakdown.TipType) eventId: UUID().uuidString // Optional UUID4 ) print("Tip adjusted: \(response.transactionId ?? "Unknown")") } ``` ### Send a Receipt Deliver a receipt for a completed transaction by email, SMS, or both. Pass at least one of `email` / `phoneNumber`: ```swift private func sendReceipt(transactionId: String) async throws { let response = try await KoardMerchantSDK.shared.sendReceipts( transactionId: transactionId, email: "customer@example.com", phoneNumber: "+15551234567" ) print("Receipt delivery: \(response)") } ``` ### Create a Fallback Payment Link When a tap cannot complete — an unsupported card, a reader problem, or a customer who would rather pay on their own device — generate a hosted payment link for the same amount: ```swift private func createFallback(amount: Int, breakdown: PaymentBreakdown?) async throws { let fallback = try await KoardMerchantSDK.shared.createFallbackLink( amount: amount, breakdown: breakdown ) print("Fallback link: \(fallback)") // Share the link with the customer (SMS, email, or QR code) } ``` ## Transaction History The SDK provides methods to retrieve and filter transaction history: ```swift import KoardSDK private func getTransactionHistory() async throws { do { // Get recent transactions let history = try await KoardMerchantSDK.shared.transactionHistory() print("Found \(history.transactions.count) transactions") // Filter by status let approvedTransactions = try await KoardMerchantSDK.shared.transactionsByStatus("approved") // Search transactions let searchResults = try await KoardMerchantSDK.shared.searchTransactions("card_number_here") // Advanced filtering let filteredTransactions = try await KoardMerchantSDK.shared.searchTransactionsAdvanced( startDate: Date().addingTimeInterval(-86400 * 7), // Last 7 days endDate: Date(), statuses: ["approved", "declined"], types: ["sale", "refund"], minAmount: 100, // $1.00 maxAmount: 10000, // $100.00 limit: 50 ) } catch { print("Transaction history failed: \(error)") throw error } } ``` **Note**: For webhook-based transaction monitoring, see our [Available Events guide](/docs/webhooks/available-events). ## Error Handling SDK calls throw `KoardMerchantSDKError`. Use its `errorDescription` property for a user-facing message. Match the cases you care about: ```swift private func handleSDKError(_ error: Error) { if let koardError = error as? KoardMerchantSDKError { switch koardError { case .missingLocationID: print("No active location set") // Prompt user to select location case .accountNotLinked: print("Tap to Pay account is not linked") // As of 1.0.20, prepare() no longer links the account for you. // Drive linking explicitly, then call prepare() again. try KoardMerchantSDK.shared.linkAccount() case .readerTokenInvalid(let message): print("Reader token invalid: \(message ?? "retry preparation")") // Usually transient — retrying prepare() recovers case .unauthorized: print("Not authenticated or session expired") // Redirect to login case .blockedAccount: print("This merchant account is blocked") case .rateLimited(let message): print("Rate limited: \(message ?? "slow down and retry")") // Back off and retry later case .network(let description, _): print("Network error: \(description)") case .server(let message): print("Server error: \(message ?? "try again later")") case .TTPPaymentFailed(.canceled): print("Customer canceled at the Tap to Pay sheet") // Benign outcome — treat as canceled, not a failure case .TTPPaymentFailed(let ttpError): print("Tap to Pay error: \(ttpError)") // Handle other reader errors case .invalidParameters(let message): print("Invalid parameters: \(message)") default: print("Koard SDK error: \(koardError.errorDescription)") } } else { print("General error: \(error)") } } ``` **Error type**: The SDK's error type is `KoardMerchantSDKError`. Earlier releases added `.rateLimited(message:)` for HTTP 429 and `TTPPaymentError.canceled` for Tap to Pay sheet cancellations. Network/transport failures are thrown as `.network(description:underlying:)` rather than raw `URLError`, and a missing session on a payment/refund/pre-auth throws `.unauthorized`. **Upgrading to 1.0.20**: Two behavior changes require code updates.\ \ **1. `prepare()` no longer auto-links the Tap to Pay account.** It now throws `KoardMerchantSDKError.accountNotLinked` instead. Guard for that case and drive linking explicitly with `linkAccount()` (or `linkAccountAsync()`), then call `prepare()` again.\ \ **2. `linkAccountAsync()` now throws** when linking fails or is declined — previously it never threw. Wrap it in `try`/`catch` and keep showing your "link account" prompt on failure.\ \ `prepare()` can also throw the new `.readerTokenInvalid`; a retry typically recovers. ## Session Management ```swift private func handleAppLifecycle() { // The SDK automatically handles background/foreground transitions // But you can monitor the status if needed NotificationCenter.default.addObserver( forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main ) { _ in Task { // Check if card reader needs re-preparation if !KoardMerchantSDK.shared.status.isReady { try? await self.prepareCardReader() } } } } ``` ## Logout and Cleanup ```swift private func logout() { // Clear all session data KoardMerchantSDK.shared.logout() print("Logged out successfully") // Redirect to login screen } ``` ## Best Practices ### SDK Management * **Token Management**: The SDK handles all token refresh automatically * **Error Handling**: Always wrap SDK calls in try-catch blocks * **Background Handling**: The SDK manages background transitions automatically * **Session Preparation**: Call `prepare()` before each payment session ### Payment Processing * **Amount Formatting**: Always use base currency units (e.g., 1050 cents for $10.50) * **Include Breakdowns**: Provide detailed breakdowns for accurate tax and tip reporting * **Location Setting**: Set active location before any payment operations * **Store Transaction IDs**: Save transaction IDs for all follow-up operations ### User Experience * **Monitor Reader Events**: Track reader events for better UX feedback * **Handle All States**: Implement handlers for all transaction states * **Provide Clear Feedback**: Show clear messages for declined or failed transactions ### Gateway Considerations * **Know Your Gateway**: Different gateways (TSYS, Payroc) have different features * **Batch Management**: Understand your gateway's batch requirements * **Response Codes**: Response codes vary by gateway For more best practices, see: * [Payment Lifecycle guide](/docs/guides/payments/details/payment-lifecycle.md) * [Sale](/docs/payments/methods/sale) and [Preauth](/docs/payments/methods/preauth) * [Capture](/docs/payments/methods/capture) and [Refund](/docs/payments/methods/refund) ## Troubleshooting ### Common Issues **Account Linking Issues** * Ensure device has iCloud account configured * Verify device has passcode enabled * Check that device supports Apple Tap to Pay on iPhone **Token Expiration** * SDK automatically refreshes tokens * Check network connectivity * Verify API key is valid **Card Reader Not Ready** * Call `prepare()` before processing payments * Ensure merchant is authenticated with `login()` * Check that account is linked with `isAccountLinked()` **Missing Location** * Verify location is set with `setActiveLocationID()` * Ensure location has valid terminal configuration * Check that location belongs to authenticated merchant **Transaction Errors** * Check transaction state before performing operations * Verify amounts are within valid ranges * Review gateway response for detailed error information For more troubleshooting help, see: * [Payment Lifecycle guide](/docs/guides/payments/details/payment-lifecycle.md) * [Incremental Auth](/docs/payments/methods/incremental-auth) and [Tip Adjust](/docs/payments/methods/tip-adjust) * [Reverse](/docs/payments/methods/reverse) and [Refund](/docs/payments/methods/refund) ## Requirements * **iOS 17.4+** * **Xcode 16.3+** * **Swift 5.9+** ## See also This wraps up payment processing with the iOS SDK. See the links below for next steps in your integration: * [Payment Lifecycle](/docs/payments/payment-lifecycle) - Complete payment flow guide * [Adding Tap to Pay](/docs/setting-up-the-ios-sdk/adding-support-for-tap-to-pay-on-iphone) - Enable contactless payments * [Installing the SDK](/docs/setting-up-the-ios-sdk/installing-the-sdk) - SDK installation guide * [Creating a Sandbox Apple Account](/docs/setting-up-the-ios-sdk/creating-a-sandbox-apple-account) - Maintain test identities * [Getting Ready for Production](/docs/setting-up-the-ios-sdk/getting-ready-for-production) - Switch schemes and API keys * [Apple Best Practices](/docs/appendix/apple-best-practices-and-guidelines) - iOS development guidelines --- title: Sale --- # Sale A sale authorizes and captures a payment in a single step—use it when the final amount is known at checkout. ## Prerequisites - Authenticated merchant with `login()` - Active location set via `setActiveLocationID()` - Card reader prepared with `prepare()` (iOS) or device enrolled (Android) ## Basic Sale **iOS:** ```swift let breakdown = PaymentBreakdown( subtotal: 10000, // $100.00 in cents taxRate: 8.75, // 8.75% as a percent value taxAmount: 875, // $8.75 in cents tipAmount: 2000, // $20.00 tip tipType: .fixed ) let currency = CurrencyCode(currencyCode: "USD", displayName: "US Dollar") let response = try await KoardMerchantSDK.shared.sale( amount: 12875, // subtotal + tax + tip breakdown: breakdown, currency: currency, eventId: UUID().uuidString ) ``` **Android:** ```kotlin val breakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipAmount = 2000, tipType = "fixed" ) sdk.sale( activity = this, amount = 12875, breakdown = breakdown, eventId = UUID.randomUUID().toString() ).collect { event -> when (event.actionStatus) { ActionStatus.OnComplete -> { val txn = event.response?.transaction println("Sale complete: ${txn?.transactionId}") } ActionStatus.OnFailure -> { println("Sale failed: ${event.response?.message}") } else -> { /* reader progress */ } } } ``` ## Sale with Surcharge The surcharge percentage is applied to the **full transaction amount** (subtotal + tax + tip): ``` surcharge = (subtotal + taxAmount + tipAmount) × surchargeRate = (10000 + 875 + 2000) × 0.035 = 451 cents ($4.51) ``` **iOS:** ```swift let breakdown = PaymentBreakdown( subtotal: 10000, taxRate: 8.75, taxAmount: 875, tipAmount: 2000, tipType: .fixed, surcharge: PaymentBreakdown.Surcharge( amount: 451, // surcharge on full amount percentage: 0.035 ) ) let response = try await KoardMerchantSDK.shared.sale( amount: 13326, // 12875 + 451 surcharge breakdown: breakdown, currency: currency, eventId: UUID().uuidString ) ``` **Android:** ```kotlin val breakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipAmount = 2000, tipType = "fixed", surcharge = Surcharge( amount = 451, percentage = 0.035 ) ) sdk.sale( activity = this, amount = 13326, breakdown = breakdown, eventId = UUID.randomUUID().toString() ).collect { event -> when (event.actionStatus) { ActionStatus.OnComplete -> { println("Sale complete: ${event.response?.transaction?.transactionId}") } ActionStatus.OnConfirmSurcharge -> { val txn = event.response?.transaction sdk.confirm( transactionId = txn?.transactionId ?: "", confirm = true ) } ActionStatus.OnFailure -> { println("Sale failed: ${event.response?.message}") } else -> { /* reader progress */ } } } ``` ## Surcharge Confirmation If the terminal has surcharging enabled and the card is eligible (credit only—debit cards are automatically excluded), the transaction returns `surchargePending`. You **must** present the disclosure and confirm. **iOS:** ```swift if response.transaction?.status == .surchargePending { let disclosure = response.transaction?.surchargeDisclosure ?? "" let approved = await showSurchargeDisclosure(disclosure) let confirmed = try await KoardMerchantSDK.shared.confirm( transaction: response.transactionId ?? "", confirm: approved, amount: nil, breakdown: nil, eventId: nil ) } ``` **Android:** ```kotlin ActionStatus.OnConfirmSurcharge -> { val txn = event.response?.transaction // Present disclosure to customer, then: sdk.confirm( transactionId = txn?.transactionId ?: "", confirm = true ) } ``` ## Bypassing Automatic Surcharge Set `bypass: true` to skip the processor's automatic surcharge. Useful for [custom BIN-based surcharging](surcharging.md#bin-based-custom-surcharge): **iOS:** ```swift let breakdown = PaymentBreakdown( subtotal: 10000, taxRate: 8.75, taxAmount: 875, tipType: .fixed, surcharge: PaymentBreakdown.Surcharge(bypass: true) ) ``` **Android:** ```kotlin val breakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipType = "fixed", surcharge = Surcharge(bypass = true) ) ``` ## Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `amount` | `Int` | Yes | Total amount in minor units (cents) | | `breakdown` | `PaymentBreakdown?` | No | Itemized breakdown (see below) | | `currency` | `CurrencyCode` | Yes (iOS) | Currency for the transaction | | `eventId` | `String?` | No | Idempotency key (UUID recommended) | | `activity` | `Activity` | Yes (Android) | Android activity for NFC access | ### PaymentBreakdown | Field | Type | Description | |-------|------|-------------| | `subtotal` | `Int` | Base amount in minor units | | `taxRate` | `Double?` | Tax rate as a percent value (`8.75` = 8.75%) | | `taxAmount` | `Int` | Calculated tax in minor units | | `tipAmount` | `Int?` | Tip in minor units | | `tipRate` | `Double?` | Tip rate as decimal (alternative to fixed tip) | | `tipType` | `TipType` | `.fixed` / `.percentage` (iOS) or `"fixed"` / `"percentage"` (Android) | | `surcharge` | `Surcharge?` | Nested surcharge object | ### Surcharge | Field | Type | Default | Description | |-------|------|---------|-------------| | `amount` | `Int?` | `nil` | Fixed surcharge in minor units | | `percentage` | `Double?` | `nil` | Surcharge rate as decimal (`0.035` = 3.5%). Applied to subtotal + tax + tip. | | `bypass` | `Bool` | `false` | Skip automatic surcharge calculation | ## See Also - [Preauth](preauth.md) — Hold funds now, capture later - [Surcharging](surcharging.md) — Automatic and custom surcharge workflows - [Payment Lifecycle](payment-lifecycle.md) — End-to-end payment flow # Batch and Settlements Learn how to manage batch processing and settlement operations with Koard's payment platform. [Get started with Koard](/docs/getting-started-with-koard/introduction) Koard provides comprehensive batch processing and settlement management for transactions processed through our system. Whether you're an enterprise ISV, PSP, or building a payment platform, Koard offers flexible solutions to meet your settlement needs. **What you learn** In this guide, you'll learn: * How Koard's batch processing system works * The two main workflows for different business types * How to retrieve batch information and settlement data * Best practices for enterprise vs. platform implementations * How to integrate with Koard's settlement APIs ## Before you begin This guide covers batch processing and settlement management in Koard. For a better understanding of how to process payments, see our [Running Payments guide](/docs/setting-up-the-ios-sdk/running-payments). If you're ready to start accepting payments, see our [Getting Started guide](/docs/getting-started-with-koard/introduction). ## Batch Processing Overview Koard can run batches and settlements for transactions processed through our system. Our platform handles all scheduling and retries automatically, providing you with: * **Batch Lists**: Retrieve comprehensive batch information * **Batch Components**: Detailed breakdown of batch contents * **Settlement Breakdown**: Success and failure analysis * **Automated Scheduling**: Koard handles all timing and retries * **Per-Merchant Batching**: Batches are created per unique MID and TID combination ## Two Main Workflows Koard supports two distinct workflows based on your business model and technical requirements. ### Enterprise ISVs and PSPs **Recommended approach**: Use your own batching solution For enterprise ISVs and PSPs with existing batching infrastructure, we recommend maintaining your own batch processing and settlement management: * **Append to Existing Batches**: Add Koard transactions to your already open batches * **Trigger Batch Closure**: Close batches using your existing system * **Inform Koard**: Update Koard's systems when settlements occur * **Avoid Duplicate Issues**: Prevents duplicate batch IDs and synchronization problems **Important**: For TSYS, Fiserv, and Elavon processors, there's a high risk of duplicate batch IDs, out-of-order synchronization, and missing/invalid tags that may fail batches when using Koard's batching system. **Exception**: If you've created specific MID and TID combinations for Tap to Pay through Koard, you can safely use Koard's batching system without the above concerns. ### Everyone Else **Recommended approach**: Use Koard's settlement system If Koard is your sole authorization layer and you don't have existing batching infrastructure: * **Full Settlement Management**: Let Koard handle all batch processing * **Automated Scheduling**: Koard manages timing and retries * **Simplified Integration**: Single API for all settlement operations * **Comprehensive Reporting**: Built-in analytics and monitoring ## Key Features | Feature | Description | Enterprise ISVs | Platform Users | | ------------------------- | ------------------------------------- | ------------------------- | --------------------------- | | **Batch Creation** | Automatic batch creation per MID/TID | Use your own system | Koard handles automatically | | **Settlement Scheduling** | Automated timing and retries | Your existing schedule | Koard manages timing | | **Error Handling** | Robust retry logic and error recovery | Your error handling | Koard handles retries | | **Reporting** | Batch and settlement analytics | Your reporting system | Koard provides reports | | **API Access** | RESTful APIs for batch management | Limited to status updates | Full API access | ## Getting Started 1. **Determine Your Workflow**: Choose between enterprise or platform approach 2. **Set Up Integration**: Configure your chosen workflow 3. **Test Batch Processing**: Verify your implementation 4. **Monitor Operations**: Track batch and settlement status ## Best Practices ### For Enterprise ISVs and PSPs * **Maintain Existing Batches**: Don't create separate batches for Koard transactions * **Sync Settlement Data**: Keep Koard informed of settlement status * **Handle Edge Cases**: Implement proper error handling for processor-specific issues * **Monitor for Duplicates**: Watch for duplicate batch IDs across systems ### For Platform Users * **Use Koard APIs**: Leverage Koard's comprehensive batch management * **Monitor Settlement Status**: Track batch processing through Koard's dashboard * **Implement Webhooks**: Set up real-time notifications for settlement events * **Regular Reconciliation**: Verify settlement data against your records ## Integration Points * **API Integration**: RESTful APIs for batch management and status updates * **Webhook Notifications**: Real-time batch and settlement status updates * **Dashboard Monitoring**: Visual tracking of batch processing and settlements * **Reporting Tools**: Comprehensive analytics and settlement reports ## See also This wraps up the batch and settlements overview. See the links below for detailed implementation guides: * [Running Batches](/docs/batch-and-settlements/running-batches) - Step-by-step batch implementation * [Running Payments](/docs/setting-up-the-ios-sdk/running-payments) - Payment processing fundamentals * [Setting up Webhooks](/docs/webhooks/setting-up-webhooks) - Configure webhooks to receive real-time batch and settlement event notifications --- title: Tip Adjust --- # Tip Adjust Tip adjust updates the tip amount on a transaction that has already been authorized but not yet settled. Use it when the customer adds or changes a tip after the initial payment. ## Basic Tip Adjust **iOS:** ```swift let response = try await KoardMerchantSDK.shared.tipAdjust( transactionId: transactionId, tipAmount: 3000 // $30.00 tip ) ``` **Android:** ```kotlin sdk.adjust( transactionId = transactionId, tipAmount = 3000 ) ``` > **Note:** The iOS SDK method is `tipAdjust()` while the Android SDK method is `adjust()`. ## Surcharge Behavior When a tip is adjusted on a surcharged transaction, the surcharge is **not recalculated**. The original surcharge amount remains unchanged. If you need to recalculate the surcharge based on the new total (subtotal + tax + new tip), you should handle that in your capture flow instead. ## Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `transactionId` | `String` | Yes | Transaction to adjust | | `tipAmount` | `Int` | Yes | New tip amount in minor units | ## See Also - [Sale](sale.md) — One-step payment - [Preauth](preauth.md) — Hold with tip added later - [Capture](capture.md) — Finalize with updated tip # Authentication All Koard API requests must be authenticated using an API key passed in the `x-koard-apikey` header. curl https://api.koard.com/v1/accounts/YOUR_ACCOUNT_ID \ -H "x-koard-apikey: YOUR_API_KEY" \ -H "Accept: application/json" ## Headers | Header | Required | Value | |--------|----------|-------| | `x-koard-apikey` | Always | Your Koard API key | | `Accept` | Always | `application/json` | | `Content-Type` | When sending a body | `application/json` | The header name is case-insensitive — `x-koard-apikey` and `X-Koard-apikey` are equivalent. ## API Keys API keys are provisioned per account. A partner-level key can manage merchants and terminals under it. A merchant-level key (a key bound to a `merchant` account) is limited to that merchant's own operations. Retrieve or rotate your API key from the [Koard MMS](https://app.koard.com) under your account settings, or manage keys programmatically via the [API Keys endpoints](/docs/api-reference/apikeys). Keep your API key secret. Never expose it in client-side code or public repositories. ### Scoped Permissions Each v5 key carries an explicit list of permissions. A permission is a string in the form `resource:action` (for example `payments:read`, `terminals:create`) or `resource:action:subtype` (for example `accounts:create:merchant`). A key can do **only** what it has been granted — there are no implicit grants. In particular, holding a write permission does **not** imply the matching read permission. Two macro permissions exist: `all` grants every concrete permission (for trusted server-side backends), and `legacy_all` is a grandfathered bucket set only on migrated pre-v5 keys (it cannot be granted to new keys). | Resource | Actions | |----------|---------| | `payments` | `tap-ios`, `tap-android`, `read`, `refund`, `tipadjust`, `capture`, `incremental-auth`, `void`, `confirm` | | `batches` | `read`, `open`, `edit`, `close` | | `terminals` | `read`, `create`, `edit`, `delete` | | `locations` | `read`, `create`, `edit`, `delete` | | `accounts` | `read`, `create:partner`, `create:merchant`, `edit`, `delete` | | `credentials` | `read`, `create`, `edit`, `delete` | | `apikeys` | `read`, `create`, `edit`, `delete` (and the `:sub` variants below) | | `webhooks` | `read`, `create`, `edit`, `delete` (Koard PSP only) | #### Own account vs. sub-accounts API-key management permissions are split by surface. The base form (`apikeys:read`, `apikeys:create`, `apikeys:edit`, `apikeys:delete`) governs keys on **your own** account. The `:sub` variants (`apikeys:read:sub`, `apikeys:create:sub`, `apikeys:edit:sub`, `apikeys:delete:sub`) govern keys on **descendant** (sub-) accounts. These are distinct grants — holding the self permission never satisfies a sub-account operation, and vice versa. #### Privilege-escalation guard When you create a key, you may only grant permissions you yourself hold. Requesting a permission outside your own grant is rejected with `403`. The `webhooks:*` permissions can only be granted by Koard PSP accounts. ### Visibility: 401 vs 404 When a key lacks a permission, the response distinguishes between "denied" and "invisible": - If your key holds **some** permission on the target resource type but not the specific operation, you get `401`. - If your key holds **zero** permissions on the target resource type (the resource is invisible to you), you get `404` — out-of-scope resources are hidden, never advertised. ### Key Lifecycle | Action | Effect | |--------|--------| | **Create** (`POST`) | Issues the key and returns the plaintext once. Defaults to a long expiry; pass `expires_at` to set your own. | | **Revoke** (`PUT` with `status: "revoked"`) | Disables the key for authentication. **Recoverable** — reinstate with `PUT status: "active"`. | | **Reinstate** (`PUT` with `status: "active"`) | Re-enables a revoked key. Does not work on a deleted key. | | **Delete** (`DELETE`) | Permanent soft-delete. The key can never be reinstated. Idempotent — repeating the call returns the same deleted key. | | **Expire** (`expires_at` passes) | The key stops authenticating automatically. | Permissions are **immutable** after creation — a `PUT` may change `name`, `expires_at`, and `status` only. To change a key's permissions, create a new key and delete the old one. Revoking or deleting a key takes effect immediately: in-flight sessions authenticated with that key are locked out. # Payment Configurations Please email support@koard.com for more details on customized payment configurations. At the moment, the only payment configurations that can be set are handled by Bleu on the backend. All partners working directly with Apple will have their own Apple Terminal Profile configurations set up and will need to share the Terminal Profile configurations with the Bleu team. # Available Events Complete list of webhook events Koard can deliver, with the **exact payload your endpoint receives**. ## Payload shape Almost every webhook request body is the **resource object itself, as flat JSON** — for example a `batch.accepted` body is a Batch, a `transaction.sale` body is a Transaction. There is **no `{"event": ..., "data": ...}` envelope**; the fields shown below are top-level. The exception is the **`*.deleted` events** (`account.deleted`, `terminal.deleted`, `location.deleted`), which carry a sparse **id-only** payload — just the resource id (and `account_id` where applicable), not the full object, since the resource no longer exists. Each event's exact body is shown below. **The event type is not in the request body.** Koard delivers webhooks through Svix, so the body is the raw payload and the delivery metadata rides in headers (`svix-id`, `svix-timestamp`, `svix-signature`). To know which event an endpoint received, **subscribe that endpoint to specific event types** in the developer portal (one endpoint per event or per category). Transactions and batches also carry discriminators in the body — branch on `transaction_type` / `status` for transactions and on `status` for batches. Conventions used in every payload: * **Money is in minor units** (cents) — `5000` means $50.00. * **Timestamps:** transaction `created_at` is a Unix timestamp in **milliseconds**; every other resource (`batch`, `account`, `terminal`, `location`, `api_key`, `credential`) uses **ISO-8601** strings (`opened_at`, `created_at`, …). * **Secrets are masked:** the card is truncated (`4113********4242`), an API key is `"****" + last4`, and a credential PIN is `"****"`. Full secrets and hash material are never sent. * Unset optional fields are present with a `null` value. ## Transaction outcomes Transaction webhooks are named after the **operation that was attempted** — not its result. A sale attempt always fires `transaction.sale`, an authorization always fires `transaction.authorize`, and so on — whether the attempt was **approved, declined, or errored**. The **outcome** lives in the payload: * **`status`** — the resulting state (e.g. `authorized`, `captured`, `declined`, `error`). * **`status_reason`** — a machine-readable reason for that status (e.g. `approved`, `insufficient_funds`, `payment_failed`). So your handler should **branch on `status`** to tell success from failure. **Failed and declined attempts fire the same event as a success.** A declined or errored **auth** is still delivered as `transaction.authorize`; a failed **sale** as `transaction.sale`. There is no separate `transaction.failed` or `transaction.declined` event — read `status` / `status_reason` to handle the result. ### Transaction statuses `status` reflects the resulting state of the transaction: | `status` | Meaning | Outcome | | ------------------- | ------------------------------------------------------------------ | --------------- | | `authorized` | Funds held, not yet captured | ✅ Success | | `captured` | Funds captured (sale or capture) | ✅ Success | | `settled` | Funds finalized in a settled batch | ✅ Success | | `refunded` | Refund completed | ✅ Success | | `reversed` | Authorization reversed / voided | ✅ Success | | `pending` | Awaiting external input | ⏳ In progress | | `surcharge_pending` | Awaiting surcharge confirmation | ⏳ In progress | | `declined` | Declined by the issuer | ❌ Failure | | `error` | The attempt could not be completed (host / format / network error) | ❌ Failure | | `canceled` | Canceled by the user or system | ⛔ Not completed | ### Status reasons `status_reason` explains **why** a transaction ended in its `status`. Common values: | `status_reason` | Meaning | | --------------------------------------------------- | ------------------------------------------------------------------------- | | `approved` | The operation was approved | | `declined` | The issuer declined the transaction | | `insufficient_funds` | Declined — the account had insufficient funds | | `invalid_card` | The card was invalid or expired | | `payment_failed` | The host / gateway returned an unclassified failure (not a clean decline) | | `processor_timeout` / `timed_out` | No response from the processor in time | | `network_connectivity_error` | Could not reach the processor | | `invalid_gateway_response` / `format_error` | The processor response could not be parsed | | `invalid_terminal` | The terminal / BIN is not boarded or is misconfigured | | `partial_approval` | Approved for less than the requested amount | | `incremental_auth_declined` | An incremental authorization was declined | | `invalid_tip_adjustment` | A tip adjustment is not allowed for this transaction | | `exceeds_original_auth` | A capture / adjustment exceeds the original authorization | | `invalid_capture_sequence` | A capture was attempted in an invalid state | | `surcharge_declined_by_payer` / `surcharge_expired` | The cardholder declined the surcharge, or the prompt expired | | `user_canceled` / `merchant_canceled` | Canceled by the cardholder or the merchant | | `duplicate_event` | A duplicate event ID was received | ### Parsing a transaction webhook Verify the Svix signature first (see [Setting up Webhooks](/docs/webhooks/setting-up-webhooks)), then read the **flat** transaction body. This endpoint is subscribed to transaction events, so the body is a Transaction; route on `transaction_type` and branch on `status`. ```python Python # POST https://your-app.com/webhooks/koard (subscribed to transaction.* events) from flask import request, jsonify SUCCESS_STATUSES = {"authorized", "captured", "settled", "refunded", "reversed"} IN_PROGRESS_STATUSES = {"pending", "surcharge_pending"} @app.route("/webhooks/koard", methods=["POST"]) def koard_webhook(): txn = request.get_json() # the body IS the transaction (flat, no envelope) operation = txn["transaction_type"] # sale / auth / capture / refund / reverse / ... status = txn.get("status") if status in SUCCESS_STATUSES: handle_success(operation, txn) elif status in IN_PROGRESS_STATUSES: handle_in_progress(operation, status, txn) # awaiting input; not final yet elif status == "canceled": handle_canceled(operation, txn) # not completed else: # only declined / error land here — status_reason explains why handle_failure(operation, status, txn.get("status_reason"), txn) return jsonify(received=True), 200 ``` ```javascript Node.js // POST https://your-app.com/webhooks/koard (subscribed to transaction.* events) const SUCCESS_STATUSES = new Set([ "authorized", "captured", "settled", "refunded", "reversed", ]); const IN_PROGRESS_STATUSES = new Set(["pending", "surcharge_pending"]); app.post("/webhooks/koard", (req, res) => { const txn = req.body; // the body IS the transaction (flat, no envelope) const { transaction_type: operation, status, status_reason } = txn; if (SUCCESS_STATUSES.has(status)) { handleSuccess(operation, txn); } else if (IN_PROGRESS_STATUSES.has(status)) { handleInProgress(operation, status, txn); // awaiting input; not final yet } else if (status === "canceled") { handleCanceled(operation, txn); // not completed } else { // only declined / error land here — `status_reason` explains why handleFailure(operation, status, status_reason, txn); } res.status(200).json({ received: true }); }); ``` ```ruby Ruby # POST https://your-app.com/webhooks/koard (subscribed to transaction.* events) SUCCESS_STATUSES = %w[authorized captured settled refunded reversed].freeze IN_PROGRESS_STATUSES = %w[pending surcharge_pending].freeze post "/webhooks/koard" do txn = JSON.parse(request.body.read) # the body IS the transaction (flat) operation = txn["transaction_type"] status = txn["status"] if SUCCESS_STATUSES.include?(status) handle_success(operation, txn) elsif IN_PROGRESS_STATUSES.include?(status) handle_in_progress(operation, status, txn) # awaiting input; not final yet elsif status == "canceled" handle_canceled(operation, txn) # not completed else # only declined / error land here handle_failure(operation, status, txn["status_reason"], txn) end status 200 { received: true }.to_json end ``` ```java Java // POST https://your-app.com/webhooks/koard (subscribed to transaction.* events) private static final Set SUCCESS_STATUSES = Set.of("authorized", "captured", "settled", "refunded", "reversed"); private static final Set IN_PROGRESS_STATUSES = Set.of("pending", "surcharge_pending"); @PostMapping("/webhooks/koard") public ResponseEntity koardWebhook(@RequestBody Map txn) { String operation = (String) txn.get("transaction_type"); String status = (String) txn.get("status"); if (SUCCESS_STATUSES.contains(status)) { handleSuccess(operation, txn); } else if (IN_PROGRESS_STATUSES.contains(status)) { handleInProgress(operation, status, txn); // awaiting input; not final yet } else if ("canceled".equals(status)) { handleCanceled(operation, txn); // not completed } else { // only declined / error land here handleFailure(operation, status, (String) txn.get("status_reason"), txn); } return ResponseEntity.ok().build(); } ``` ```php PHP true]); ``` The examples below show the exact body delivered for each event. ## Transaction Events ### transaction.authorize Card authorized — funds held, not yet captured. ```json { "transaction_id": "6f6b6d2e-1c3a-4a9e-9d21-2f0e7b8c9a10", "event_id": "6F6B6D2E-1C3A-4A9E-9D21-2F0E7B8C9A10", "mid": "445566778899", "tid": "TERM0001", "processor_mid": "5289003", "processor_tid": "5289003", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "device_id": "dev_5Hj3Kl8Mn2Pq6Rt", "processor": "tsys", "gateway": "sierra", "currency": "USD", "location_id": "loc_7Bd4Cf9Gh2Jk5Lm", "gateway_transaction_id": "BG6RJZHK8Q", "subtotal": 4200, "tip_amount": 800, "tip_type": "amount", "tip_capture_mode": null, "tax_amount": 0, "tax_rate": 0, "tax_source": null, "tax_basis": null, "total_amount": 5000, "surcharge_applied": false, "surcharge_amount": 0, "surcharge_rate": 0, "surcharge_source": null, "surcharge_basis": null, "surcharge_confirmation_required": null, "refunded": 0, "reversed": 0, "created_at": 1768470600000, "status": "authorized", "status_reason": "approved", "payment_method": "contactlessIcc", "card_type": "credit", "card_brand": "visa", "card": "4113********4242", "additional_details": {}, "gateway_transaction_response": { "responseCode": "A", "responseMessage": "APPROVAL", "approvalCode": "522841", "processorResponseCode": "00" }, "processor_response_code": "00", "processor_response_message": "Approved", "transaction_type": "auth", "apple_transaction_id": "", "reader_identifier": "rdr_8Wq2Er5Ty7Ui", "owner_id": "acct_2Nk9Lm4Pq7Rs1Tv", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "merchant_name": "Blue Bottle Coffee", "psp": null, "batch_id": "batch_6Yh3Uj8Ik2Ol", "device_type": null, "terminal_details": null, "history": [] } ``` ### transaction.sale Sale — card authorized and captured in one step. ```json { "transaction_id": "6f6b6d2e-1c3a-4a9e-9d21-2f0e7b8c9a10", "event_id": "6F6B6D2E-1C3A-4A9E-9D21-2F0E7B8C9A10", "mid": "445566778899", "tid": "TERM0001", "processor_mid": "5289003", "processor_tid": "5289003", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "device_id": "dev_5Hj3Kl8Mn2Pq6Rt", "processor": "tsys", "gateway": "sierra", "currency": "USD", "location_id": "loc_7Bd4Cf9Gh2Jk5Lm", "gateway_transaction_id": "BG6RJZHK8Q", "subtotal": 4200, "tip_amount": 800, "tip_type": "amount", "tip_capture_mode": null, "tax_amount": 0, "tax_rate": 0, "tax_source": null, "tax_basis": null, "total_amount": 5000, "surcharge_applied": false, "surcharge_amount": 0, "surcharge_rate": 0, "surcharge_source": null, "surcharge_basis": null, "surcharge_confirmation_required": null, "refunded": 0, "reversed": 0, "created_at": 1768470600000, "status": "captured", "status_reason": "approved", "payment_method": "contactlessIcc", "card_type": "credit", "card_brand": "visa", "card": "4113********4242", "additional_details": {}, "gateway_transaction_response": { "responseCode": "A", "responseMessage": "APPROVAL", "approvalCode": "522841", "processorResponseCode": "00" }, "processor_response_code": "00", "processor_response_message": "Approved", "transaction_type": "sale", "apple_transaction_id": "", "reader_identifier": "rdr_8Wq2Er5Ty7Ui", "owner_id": "acct_2Nk9Lm4Pq7Rs1Tv", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "merchant_name": "Blue Bottle Coffee", "psp": null, "batch_id": "batch_6Yh3Uj8Ik2Ol", "device_type": null, "terminal_details": null, "history": [] } ``` ### transaction.capture A prior authorization was captured for settlement. ```json { "transaction_id": "6f6b6d2e-1c3a-4a9e-9d21-2f0e7b8c9a10", "event_id": "6F6B6D2E-1C3A-4A9E-9D21-2F0E7B8C9A10", "mid": "445566778899", "tid": "TERM0001", "processor_mid": "5289003", "processor_tid": "5289003", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "device_id": "dev_5Hj3Kl8Mn2Pq6Rt", "processor": "tsys", "gateway": "sierra", "currency": "USD", "location_id": "loc_7Bd4Cf9Gh2Jk5Lm", "gateway_transaction_id": "BG6RJZHK8Q", "subtotal": 4200, "tip_amount": 800, "tip_type": "amount", "tip_capture_mode": null, "tax_amount": 0, "tax_rate": 0, "tax_source": null, "tax_basis": null, "total_amount": 5000, "surcharge_applied": false, "surcharge_amount": 0, "surcharge_rate": 0, "surcharge_source": null, "surcharge_basis": null, "surcharge_confirmation_required": null, "refunded": 0, "reversed": 0, "created_at": 1768470600000, "status": "captured", "status_reason": "approved", "payment_method": "contactlessIcc", "card_type": "credit", "card_brand": "visa", "card": "4113********4242", "additional_details": {}, "gateway_transaction_response": { "responseCode": "A", "responseMessage": "APPROVAL", "approvalCode": "522841", "processorResponseCode": "00" }, "processor_response_code": "00", "processor_response_message": "Approved", "transaction_type": "capture", "apple_transaction_id": "", "reader_identifier": "rdr_8Wq2Er5Ty7Ui", "owner_id": "acct_2Nk9Lm4Pq7Rs1Tv", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "merchant_name": "Blue Bottle Coffee", "psp": null, "batch_id": "batch_6Yh3Uj8Ik2Ol", "device_type": null, "terminal_details": null, "history": [] } ``` ### transaction.cancel Transaction cancelled before settlement. ```json { "transaction_id": "6f6b6d2e-1c3a-4a9e-9d21-2f0e7b8c9a10", "event_id": "6F6B6D2E-1C3A-4A9E-9D21-2F0E7B8C9A10", "mid": "445566778899", "tid": "TERM0001", "processor_mid": "5289003", "processor_tid": "5289003", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "device_id": "dev_5Hj3Kl8Mn2Pq6Rt", "processor": "tsys", "gateway": "sierra", "currency": "USD", "location_id": "loc_7Bd4Cf9Gh2Jk5Lm", "gateway_transaction_id": "BG6RJZHK8Q", "subtotal": 4200, "tip_amount": 800, "tip_type": "amount", "tip_capture_mode": null, "tax_amount": 0, "tax_rate": 0, "tax_source": null, "tax_basis": null, "total_amount": 5000, "surcharge_applied": false, "surcharge_amount": 0, "surcharge_rate": 0, "surcharge_source": null, "surcharge_basis": null, "surcharge_confirmation_required": null, "refunded": 0, "reversed": 0, "created_at": 1768470600000, "status": "canceled", "status_reason": "user_canceled", "payment_method": "contactlessIcc", "card_type": "credit", "card_brand": "visa", "card": "4113********4242", "additional_details": {}, "gateway_transaction_response": { "responseCode": "A", "responseMessage": "APPROVAL", "approvalCode": "522841", "processorResponseCode": "00" }, "processor_response_code": "00", "processor_response_message": "Approved", "transaction_type": "sale", "apple_transaction_id": "", "reader_identifier": "rdr_8Wq2Er5Ty7Ui", "owner_id": "acct_2Nk9Lm4Pq7Rs1Tv", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "merchant_name": "Blue Bottle Coffee", "psp": null, "batch_id": "batch_6Yh3Uj8Ik2Ol", "device_type": null, "terminal_details": null, "history": [] } ``` ### transaction.create A transaction record was created. ```json { "transaction_id": "6f6b6d2e-1c3a-4a9e-9d21-2f0e7b8c9a10", "event_id": "6F6B6D2E-1C3A-4A9E-9D21-2F0E7B8C9A10", "mid": "445566778899", "tid": "TERM0001", "processor_mid": "5289003", "processor_tid": "5289003", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "device_id": "dev_5Hj3Kl8Mn2Pq6Rt", "processor": "tsys", "gateway": "sierra", "currency": "USD", "location_id": "loc_7Bd4Cf9Gh2Jk5Lm", "gateway_transaction_id": "BG6RJZHK8Q", "subtotal": 4200, "tip_amount": 800, "tip_type": "amount", "tip_capture_mode": null, "tax_amount": 0, "tax_rate": 0, "tax_source": null, "tax_basis": null, "total_amount": 5000, "surcharge_applied": false, "surcharge_amount": 0, "surcharge_rate": 0, "surcharge_source": null, "surcharge_basis": null, "surcharge_confirmation_required": null, "refunded": 0, "reversed": 0, "created_at": 1768470600000, "status": "pending", "status_reason": "pending", "payment_method": "contactlessIcc", "card_type": "credit", "card_brand": "visa", "card": "4113********4242", "additional_details": {}, "gateway_transaction_response": { "responseCode": "A", "responseMessage": "APPROVAL", "approvalCode": "522841", "processorResponseCode": "00" }, "processor_response_code": "00", "processor_response_message": "Approved", "transaction_type": "sale", "apple_transaction_id": "", "reader_identifier": "rdr_8Wq2Er5Ty7Ui", "owner_id": "acct_2Nk9Lm4Pq7Rs1Tv", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "merchant_name": "Blue Bottle Coffee", "psp": null, "batch_id": null, "device_type": null, "terminal_details": null, "history": [] } ``` ### transaction.reverse Authorization voided/undone after auth, before settlement. ```json { "transaction_id": "6f6b6d2e-1c3a-4a9e-9d21-2f0e7b8c9a10", "event_id": "6F6B6D2E-1C3A-4A9E-9D21-2F0E7B8C9A10", "mid": "445566778899", "tid": "TERM0001", "processor_mid": "5289003", "processor_tid": "5289003", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "device_id": "dev_5Hj3Kl8Mn2Pq6Rt", "processor": "tsys", "gateway": "sierra", "currency": "USD", "location_id": "loc_7Bd4Cf9Gh2Jk5Lm", "gateway_transaction_id": "BG6RJZHK8Q", "subtotal": 4200, "tip_amount": 800, "tip_type": "amount", "tip_capture_mode": null, "tax_amount": 0, "tax_rate": 0, "tax_source": null, "tax_basis": null, "total_amount": 5000, "surcharge_applied": false, "surcharge_amount": 0, "surcharge_rate": 0, "surcharge_source": null, "surcharge_basis": null, "surcharge_confirmation_required": null, "refunded": 0, "reversed": 5000, "created_at": 1768470600000, "status": "reversed", "status_reason": "approved", "payment_method": "contactlessIcc", "card_type": "credit", "card_brand": "visa", "card": "4113********4242", "additional_details": {}, "gateway_transaction_response": { "responseCode": "A", "responseMessage": "APPROVAL", "approvalCode": "522841", "processorResponseCode": "00" }, "processor_response_code": "00", "processor_response_message": "Approved", "transaction_type": "reverse", "apple_transaction_id": "", "reader_identifier": "rdr_8Wq2Er5Ty7Ui", "owner_id": "acct_2Nk9Lm4Pq7Rs1Tv", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "merchant_name": "Blue Bottle Coffee", "psp": null, "batch_id": "batch_6Yh3Uj8Ik2Ol", "device_type": null, "terminal_details": null, "history": [] } ``` ### transaction.refund Funds refunded to the cardholder after settlement. ```json { "transaction_id": "6f6b6d2e-1c3a-4a9e-9d21-2f0e7b8c9a10", "event_id": "6F6B6D2E-1C3A-4A9E-9D21-2F0E7B8C9A10", "mid": "445566778899", "tid": "TERM0001", "processor_mid": "5289003", "processor_tid": "5289003", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "device_id": "dev_5Hj3Kl8Mn2Pq6Rt", "processor": "tsys", "gateway": "sierra", "currency": "USD", "location_id": "loc_7Bd4Cf9Gh2Jk5Lm", "gateway_transaction_id": "BG6RJZHK8Q", "subtotal": 4200, "tip_amount": 800, "tip_type": "amount", "tip_capture_mode": null, "tax_amount": 0, "tax_rate": 0, "tax_source": null, "tax_basis": null, "total_amount": 5000, "surcharge_applied": false, "surcharge_amount": 0, "surcharge_rate": 0, "surcharge_source": null, "surcharge_basis": null, "surcharge_confirmation_required": null, "refunded": 5000, "reversed": 0, "created_at": 1768470600000, "status": "refunded", "status_reason": "approved", "payment_method": "contactlessIcc", "card_type": "credit", "card_brand": "visa", "card": "4113********4242", "additional_details": {}, "gateway_transaction_response": { "responseCode": "A", "responseMessage": "APPROVAL", "approvalCode": "522841", "processorResponseCode": "00" }, "processor_response_code": "00", "processor_response_message": "Approved", "transaction_type": "refund", "apple_transaction_id": "", "reader_identifier": "rdr_8Wq2Er5Ty7Ui", "owner_id": "acct_2Nk9Lm4Pq7Rs1Tv", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "merchant_name": "Blue Bottle Coffee", "psp": null, "batch_id": "batch_6Yh3Uj8Ik2Ol", "device_type": null, "terminal_details": null, "history": [] } ``` ### transaction.increment An existing authorization amount was increased (incremental auth). ```json { "transaction_id": "6f6b6d2e-1c3a-4a9e-9d21-2f0e7b8c9a10", "event_id": "6F6B6D2E-1C3A-4A9E-9D21-2F0E7B8C9A10", "mid": "445566778899", "tid": "TERM0001", "processor_mid": "5289003", "processor_tid": "5289003", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "device_id": "dev_5Hj3Kl8Mn2Pq6Rt", "processor": "tsys", "gateway": "sierra", "currency": "USD", "location_id": "loc_7Bd4Cf9Gh2Jk5Lm", "gateway_transaction_id": "BG6RJZHK8Q", "subtotal": 6200, "tip_amount": 800, "tip_type": "amount", "tip_capture_mode": null, "tax_amount": 0, "tax_rate": 0, "tax_source": null, "tax_basis": null, "total_amount": 7000, "surcharge_applied": false, "surcharge_amount": 0, "surcharge_rate": 0, "surcharge_source": null, "surcharge_basis": null, "surcharge_confirmation_required": null, "refunded": 0, "reversed": 0, "created_at": 1768470600000, "status": "authorized", "status_reason": "approved", "payment_method": "contactlessIcc", "card_type": "credit", "card_brand": "visa", "card": "4113********4242", "additional_details": {}, "gateway_transaction_response": { "responseCode": "A", "responseMessage": "APPROVAL", "approvalCode": "522841", "processorResponseCode": "00" }, "processor_response_code": "00", "processor_response_message": "Approved", "transaction_type": "incremental_auth", "apple_transaction_id": "", "reader_identifier": "rdr_8Wq2Er5Ty7Ui", "owner_id": "acct_2Nk9Lm4Pq7Rs1Tv", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "merchant_name": "Blue Bottle Coffee", "psp": null, "batch_id": "batch_6Yh3Uj8Ik2Ol", "device_type": null, "terminal_details": null, "history": [] } ``` ### transaction.tip\_adjust A tip was added or changed on a captured transaction. ```json { "transaction_id": "6f6b6d2e-1c3a-4a9e-9d21-2f0e7b8c9a10", "event_id": "6F6B6D2E-1C3A-4A9E-9D21-2F0E7B8C9A10", "mid": "445566778899", "tid": "TERM0001", "processor_mid": "5289003", "processor_tid": "5289003", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "device_id": "dev_5Hj3Kl8Mn2Pq6Rt", "processor": "tsys", "gateway": "sierra", "currency": "USD", "location_id": "loc_7Bd4Cf9Gh2Jk5Lm", "gateway_transaction_id": "BG6RJZHK8Q", "subtotal": 4200, "tip_amount": 1200, "tip_type": "amount", "tip_capture_mode": null, "tax_amount": 0, "tax_rate": 0, "tax_source": null, "tax_basis": null, "total_amount": 5400, "surcharge_applied": false, "surcharge_amount": 0, "surcharge_rate": 0, "surcharge_source": null, "surcharge_basis": null, "surcharge_confirmation_required": null, "refunded": 0, "reversed": 0, "created_at": 1768470600000, "status": "captured", "status_reason": "approved", "payment_method": "contactlessIcc", "card_type": "credit", "card_brand": "visa", "card": "4113********4242", "additional_details": {}, "gateway_transaction_response": { "responseCode": "A", "responseMessage": "APPROVAL", "approvalCode": "522841", "processorResponseCode": "00" }, "processor_response_code": "00", "processor_response_message": "Approved", "transaction_type": "tip_adjust", "apple_transaction_id": "", "reader_identifier": "rdr_8Wq2Er5Ty7Ui", "owner_id": "acct_2Nk9Lm4Pq7Rs1Tv", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "merchant_name": "Blue Bottle Coffee", "psp": null, "batch_id": "batch_6Yh3Uj8Ik2Ol", "device_type": null, "terminal_details": null, "history": [] } ``` ## Batch Events ### batch.opened A settlement batch was opened. ```json { "id": "batch_6Yh3Uj8Ik2Ol", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "processor_name": "tsys", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "status": "open", "captured_amount": 0, "refunded_amount": 0, "transaction_count": 0, "processor_batch_id": null, "opened_at": "2026-01-15T08:00:00Z", "closed_at": null, "response": null, "batch_metadata": {} } ``` ### batch.rejected The processor rejected the batch. ```json { "id": "batch_6Yh3Uj8Ik2Ol", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "processor_name": "tsys", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "status": "rejected", "captured_amount": 128400, "refunded_amount": 5000, "transaction_count": 37, "processor_batch_id": "TSYS-BATCH-000481", "opened_at": "2026-01-15T08:00:00Z", "closed_at": null, "response": { "rejection_reason": "Host unavailable \u2014 retry" }, "batch_metadata": {} } ``` ### batch.accepted The processor accepted the batch for settlement. ```json { "id": "batch_6Yh3Uj8Ik2Ol", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "processor_name": "tsys", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "status": "accepted", "captured_amount": 128400, "refunded_amount": 5000, "transaction_count": 37, "processor_batch_id": "TSYS-BATCH-000481", "opened_at": "2026-01-15T08:00:00Z", "closed_at": "2026-01-15T20:00:00Z", "response": null, "batch_metadata": {} } ``` ### batch.partially\_accepted The processor accepted the batch with some rejected records. ```json { "id": "batch_6Yh3Uj8Ik2Ol", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "processor_name": "tsys", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "status": "partially_accepted", "captured_amount": 128400, "refunded_amount": 5000, "transaction_count": 37, "processor_batch_id": "TSYS-BATCH-000481", "opened_at": "2026-01-15T08:00:00Z", "closed_at": "2026-01-15T20:00:00Z", "response": { "accepted": 35, "rejected": 2 }, "batch_metadata": {} } ``` ### batch.edited A batch's transactions were edited before close. ```json { "id": "batch_6Yh3Uj8Ik2Ol", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "processor_name": "tsys", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "status": "open", "captured_amount": 128400, "refunded_amount": 5000, "transaction_count": 36, "processor_batch_id": "TSYS-BATCH-000481", "opened_at": "2026-01-15T08:00:00Z", "closed_at": null, "response": null, "batch_metadata": {} } ``` ### batch.submitted A batch was submitted to the processor for settlement. ```json { "id": "batch_6Yh3Uj8Ik2Ol", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "processor_name": "tsys", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "status": "submitted", "captured_amount": 128400, "refunded_amount": 5000, "transaction_count": 37, "processor_batch_id": "TSYS-BATCH-000481", "opened_at": "2026-01-15T08:00:00Z", "closed_at": null, "response": null, "batch_metadata": {} } ``` ## Account Events ### account.created A new account was created. ```json { "id": "acct_2Nk9Lm4Pq7Rs1Tv", "organization_id": null, "has_access_to_mms": false, "has_access_to_apple_config": false, "available_processor_configs": [], "tax_id": null, "mcc": "5812", "type": "merchant", "status": "active", "name": "Blue Bottle Coffee", "description": "Specialty coffee roaster", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "address": { "street_line1": "1500 Market St", "street_line2": "Suite 200", "city": "San Francisco", "state": "CA", "zip": "94103", "country": "US" }, "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "created_at": "2026-01-15T09:30:00Z", "deleted_at": null, "updated_at": null } ``` ### account.updated An account's details were updated. ```json { "id": "acct_2Nk9Lm4Pq7Rs1Tv", "organization_id": null, "has_access_to_mms": false, "has_access_to_apple_config": false, "available_processor_configs": [], "tax_id": null, "mcc": "5812", "type": "merchant", "status": "active", "name": "Blue Bottle Coffee Co.", "description": "Specialty coffee roaster", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "address": { "street_line1": "1500 Market St", "street_line2": "Suite 200", "city": "San Francisco", "state": "CA", "zip": "94103", "country": "US" }, "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "created_at": "2026-01-15T09:30:00Z", "deleted_at": null, "updated_at": null } ``` ### account.blocked An account was blocked (deactivated). ```json { "id": "acct_2Nk9Lm4Pq7Rs1Tv", "organization_id": null, "has_access_to_mms": false, "has_access_to_apple_config": false, "available_processor_configs": [], "tax_id": null, "mcc": "5812", "type": "merchant", "status": "blocked", "name": "Blue Bottle Coffee", "description": "Specialty coffee roaster", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "address": { "street_line1": "1500 Market St", "street_line2": "Suite 200", "city": "San Francisco", "state": "CA", "zip": "94103", "country": "US" }, "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "created_at": "2026-01-15T09:30:00Z", "deleted_at": null, "updated_at": null } ``` ### account.unblocked A blocked account was reactivated. ```json { "id": "acct_2Nk9Lm4Pq7Rs1Tv", "organization_id": null, "has_access_to_mms": false, "has_access_to_apple_config": false, "available_processor_configs": [], "tax_id": null, "mcc": "5812", "type": "merchant", "status": "active", "name": "Blue Bottle Coffee", "description": "Specialty coffee roaster", "parent_account_ids": [ "acct_0Pp1Qq2Rr3Ss4Tt" ], "address": { "street_line1": "1500 Market St", "street_line2": "Suite 200", "city": "San Francisco", "state": "CA", "zip": "94103", "country": "US" }, "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "created_at": "2026-01-15T09:30:00Z", "deleted_at": null, "updated_at": null } ``` ### account.deleted An account was deleted (soft-deleted). ```json { "id": "acct_2Nk9Lm4Pq7Rs1Tv" } ``` ## Terminal Events ### terminal.created A new terminal was created. ```json { "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "name": "Front Counter \u2014 Register 1", "description": "Android EMV COTS device", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "mid": "445566778899", "tid": "TERM0001", "store_number": "SN-COTS-00481", "bin": "412345", "vid": null, "agent_bank_number": null, "merchant_category_code": "5812", "terminal_capability": "5", "currency_code": "USD", "country_code": "US", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "status": "active", "created_at": "2026-01-15T09:30:00Z", "deleted_at": null, "updated_at": null, "merchant_name": "Blue Bottle Coffee", "transactions": [], "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "var_sheet": null, "batching": null } ``` ### terminal.updated A terminal's configuration was updated. ```json { "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "name": "Front Counter \u2014 Register 2", "description": "Android EMV COTS device", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "mid": "445566778899", "tid": "TERM0001", "store_number": "SN-COTS-00481", "bin": "412345", "vid": null, "agent_bank_number": null, "merchant_category_code": "5812", "terminal_capability": "5", "currency_code": "USD", "country_code": "US", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "status": "active", "created_at": "2026-01-15T09:30:00Z", "deleted_at": null, "updated_at": null, "merchant_name": "Blue Bottle Coffee", "transactions": [], "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "var_sheet": null, "batching": null } ``` ### terminal.blocked A terminal was blocked (deactivated). ```json { "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "name": "Front Counter \u2014 Register 1", "description": "Android EMV COTS device", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "mid": "445566778899", "tid": "TERM0001", "store_number": "SN-COTS-00481", "bin": "412345", "vid": null, "agent_bank_number": null, "merchant_category_code": "5812", "terminal_capability": "5", "currency_code": "USD", "country_code": "US", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "status": "blocked", "created_at": "2026-01-15T09:30:00Z", "deleted_at": null, "updated_at": null, "merchant_name": "Blue Bottle Coffee", "transactions": [], "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "var_sheet": null, "batching": null } ``` ### terminal.unblocked A blocked terminal was reactivated. ```json { "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "name": "Front Counter \u2014 Register 1", "description": "Android EMV COTS device", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "mid": "445566778899", "tid": "TERM0001", "store_number": "SN-COTS-00481", "bin": "412345", "vid": null, "agent_bank_number": null, "merchant_category_code": "5812", "terminal_capability": "5", "currency_code": "USD", "country_code": "US", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "status": "active", "created_at": "2026-01-15T09:30:00Z", "deleted_at": null, "updated_at": null, "merchant_name": "Blue Bottle Coffee", "transactions": [], "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "var_sheet": null, "batching": null } ``` ### terminal.deleted A terminal was deleted (soft-deleted). ```json { "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv" } ``` ## Location Events ### location.created A new location was created. ```json { "id": "loc_7Bd4Cf9Gh2Jk5Lm", "name": "Ferry Building", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "address": { "street_line1": "1500 Market St", "street_line2": "Suite 200", "city": "San Francisco", "state": "CA", "zip": "94103", "country": "US" }, "phone": "+14155551234", "email": "ferry@bluebottle.example", "status": "active", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "country_code": "US", "currency": "USD", "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "metadata": null, "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "created_at": null, "updated_at": null, "deleted_at": null } ``` ### location.updated A location's details were updated. ```json { "id": "loc_7Bd4Cf9Gh2Jk5Lm", "name": "Ferry Building Plaza", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "address": { "street_line1": "1500 Market St", "street_line2": "Suite 200", "city": "San Francisco", "state": "CA", "zip": "94103", "country": "US" }, "phone": "+14155551234", "email": "ferry@bluebottle.example", "status": "active", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "country_code": "US", "currency": "USD", "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "metadata": null, "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "created_at": null, "updated_at": null, "deleted_at": null } ``` ### location.blocked A location was blocked (deactivated). ```json { "id": "loc_7Bd4Cf9Gh2Jk5Lm", "name": "Ferry Building", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "address": { "street_line1": "1500 Market St", "street_line2": "Suite 200", "city": "San Francisco", "state": "CA", "zip": "94103", "country": "US" }, "phone": "+14155551234", "email": "ferry@bluebottle.example", "status": "blocked", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "country_code": "US", "currency": "USD", "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "metadata": null, "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "created_at": null, "updated_at": null, "deleted_at": null } ``` ### location.unblocked A blocked location was reactivated. ```json { "id": "loc_7Bd4Cf9Gh2Jk5Lm", "name": "Ferry Building", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "address": { "street_line1": "1500 Market St", "street_line2": "Suite 200", "city": "San Francisco", "state": "CA", "zip": "94103", "country": "US" }, "phone": "+14155551234", "email": "ferry@bluebottle.example", "status": "active", "processor_config_id": "pcfg_4Df7Gh1Jk8Lm", "country_code": "US", "currency": "USD", "terminal_id": "term_5Hj3Kl8Mn2Pq6Rt", "metadata": null, "surcharge_rate": null, "surcharge_confirmation_required": null, "surcharge_basis": null, "tax_rate": null, "tax_basis": null, "created_at": null, "updated_at": null, "deleted_at": null } ``` ### location.deleted A location was deleted (soft-deleted). ```json { "id": "loc_7Bd4Cf9Gh2Jk5Lm", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv" } ``` ## API Key Events ### api\_key.created A new API key was issued. ```json { "id": "key_1Ab2Cd3Ef4Gh5Ij", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "name": "Production integration", "status": "active", "key": "****4x9Q", "key_last4": "4x9Q", "expires_at": null, "created_at": "2026-01-15T09:30:00Z" } ``` ### api\_key.revoked An API key was revoked (denied at auth until reinstated). ```json { "id": "key_1Ab2Cd3Ef4Gh5Ij", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "name": "Production integration", "status": "revoked", "key": "****4x9Q", "key_last4": "4x9Q", "expires_at": null, "created_at": "2026-01-15T09:30:00Z" } ``` ### api\_key.reinstated A previously revoked API key was reinstated. ```json { "id": "key_1Ab2Cd3Ef4Gh5Ij", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "name": "Production integration", "status": "active", "key": "****4x9Q", "key_last4": "4x9Q", "expires_at": null, "created_at": "2026-01-15T09:30:00Z" } ``` ### api\_key.deleted An API key was deleted. ```json { "id": "key_1Ab2Cd3Ef4Gh5Ij", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "name": "Production integration", "status": "deleted", "key": "****4x9Q", "key_last4": "4x9Q", "expires_at": null, "created_at": "2026-01-15T09:30:00Z" } ``` ## Credential Events ### credential.created A new merchant credential was created. ```json { "id": "cred_7Kl8Mn9Op0Qr1St", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "code": "bluebottle-ferry", "pin": "****", "is_active": true, "created_at": "2026-01-15T09:30:00Z" } ``` ### credential.blocked A merchant credential was blocked (deactivated). ```json { "id": "cred_7Kl8Mn9Op0Qr1St", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "code": "bluebottle-ferry", "pin": "****", "is_active": false, "created_at": "2026-01-15T09:30:00Z" } ``` ### credential.unblocked A blocked merchant credential was reactivated. ```json { "id": "cred_7Kl8Mn9Op0Qr1St", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "code": "bluebottle-ferry", "pin": "****", "is_active": true, "created_at": "2026-01-15T09:30:00Z" } ``` ### credential.deleted A merchant credential was deleted. ```json { "id": "cred_7Kl8Mn9Op0Qr1St", "account_id": "acct_2Nk9Lm4Pq7Rs1Tv", "code": "bluebottle-ferry", "pin": "****", "is_active": false, "created_at": "2026-01-15T09:30:00Z" } ``` ## Event Processing ### Identifying the event Because the event type is **not** in the body, don't `switch` on a body field. Instead: 1. **Recommended — subscribe per event type.** Point one endpoint at each event type (or category) you care about in the developer portal. The endpoint's URL then tells you what it received. 2. **In-body discriminators.** Transactions carry `transaction_type` + `status`; batches carry `status`. Lifecycle events for accounts, terminals, locations, API keys, and credentials do **not** carry the verb in the body (`terminal.created` and `terminal.updated` have identical shapes), so rely on the endpoint subscription for those. ### Idempotency Delivery is **at-least-once** — the same message may arrive more than once (e.g. on retry). **Deduplicate on the `svix-id` header**: it is the canonical delivery id and stays _constant across every retry_ of a message, so it's the key to track processed events (store it, e.g. in Redis with a 24-hour expiry, and skip anything you've already seen). The transaction `event_id` field is a separate, application-level correlation id for the underlying transaction event — useful for tying a webhook back to a transaction, but **not** the delivery-dedup key. ### Event ordering Events are generally delivered in the order they occurred, but retries can cause out-of-order delivery. Use the timestamps in the payload / `svix-timestamp` header if you need strict ordering. ### Event filtering Configure filters per endpoint in the developer portal: * **Event type** — subscribe to specific event types (recommended for production). * **Transaction type** — branch on `transaction_type` (`sale`, `auth`, `capture`, `refund`, `reverse`, `tip_adjust`, `incremental_auth`). * **Status** — branch on `status` (`authorized`, `captured`, `declined`, `refunded`, `reversed`, …). * **Processor** — branch on `processor` (`tsys`, `payroc`, …). ## Complete Event List | Event | Description | Category | | -------------------------- | ------------------------------------------------------------------ | ----------- | | `transaction.authorize` | Card authorized — funds held, not yet captured. | transaction | | `transaction.sale` | Sale — card authorized and captured in one step. | transaction | | `transaction.capture` | A prior authorization was captured for settlement. | transaction | | `transaction.cancel` | Transaction cancelled before settlement. | transaction | | `transaction.create` | A transaction record was created. | transaction | | `transaction.reverse` | Authorization voided/undone after auth, before settlement. | transaction | | `transaction.refund` | Funds refunded to the cardholder after settlement. | transaction | | `transaction.increment` | An existing authorization amount was increased (incremental auth). | transaction | | `transaction.tip_adjust` | A tip was added or changed on a captured transaction. | transaction | | `batch.opened` | A settlement batch was opened. | batch | | `batch.rejected` | The processor rejected the batch. | batch | | `batch.accepted` | The processor accepted the batch for settlement. | batch | | `batch.partially_accepted` | The processor accepted the batch with some rejected records. | batch | | `batch.edited` | A batch's transactions were edited before close. | batch | | `batch.submitted` | A batch was submitted to the processor for settlement. | batch | | `account.created` | A new account was created. | account | | `account.updated` | An account's details were updated. | account | | `account.blocked` | An account was blocked (deactivated). | account | | `account.unblocked` | A blocked account was reactivated. | account | | `account.deleted` | An account was deleted (soft-deleted). | account | | `terminal.created` | A new terminal was created. | terminal | | `terminal.updated` | A terminal's configuration was updated. | terminal | | `terminal.blocked` | A terminal was blocked (deactivated). | terminal | | `terminal.unblocked` | A blocked terminal was reactivated. | terminal | | `terminal.deleted` | A terminal was deleted (soft-deleted). | terminal | | `location.created` | A new location was created. | location | | `location.updated` | A location's details were updated. | location | | `location.blocked` | A location was blocked (deactivated). | location | | `location.unblocked` | A blocked location was reactivated. | location | | `location.deleted` | A location was deleted (soft-deleted). | location | | `api_key.created` | A new API key was issued. | api\_key | | `api_key.revoked` | An API key was revoked (denied at auth until reinstated). | api\_key | | `api_key.reinstated` | A previously revoked API key was reinstated. | api\_key | | `api_key.deleted` | An API key was deleted. | api\_key | | `credential.created` | A new merchant credential was created. | credential | | `credential.blocked` | A merchant credential was blocked (deactivated). | credential | | `credential.unblocked` | A blocked merchant credential was reactivated. | credential | | `credential.deleted` | A merchant credential was deleted. | credential | ## See also * [Setting up Webhooks](/docs/webhooks/setting-up-webhooks) — configuring endpoints, event subscriptions, and signature verification # Fiserv Omaha Omaha (FDR — First Data Resources) is a **hybrid-host** front-end: First Data holds the transaction detail (host-style) but the gateway initiates settlement so both sides reconcile. Board it like any Fiserv terminal (see the [Fiserv Overview](/boarding-a-merchant/fiserv/fiserv-overview) for the shared shape, SRS, and UMF coverage) using the **Omaha** processor config. ## Boarding spec | Field | Value / format | Notes | |---|---|---| | **Group ID** | `40001` | Omaha front-end. | | **Merchant ID** (MID) | 7 digits (`MerchID`) | Omaha front-end MID. Top-level `mid`. | | **Terminal ID** (TID) | 7 digits (`TermID`) | The Bank TID. Top-level `tid`. | | **Settlement MID** | — | Not used — Omaha is hybrid-host, not North terminal capture. | | **MCC** | 4-digit | Top-level `mcc`. | | **Equipment** (POS Solution) | `CREDITCALLHCRC` | VAR-sheet `equipment`. | ## Request shape curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_fiserv_omaha", "terminal_name": "Register 1", "mid": "9446123", "tid": "9259801", "mcc": "5045", "var_sheet": { "group_id": "40001", "industry": "retail_qsr_grocery", "equipment": "CREDITCALLHCRC", "merchant_street_address": "102 Woodmont Blvd Ste 125", "merchant_city": "Nashville", "merchant_state": "TN", "merchant_postal_code": "37205", "country_code": "840" } }' ## Capture & settlement **Hybrid-host capture.** First Data holds the detail, but the gateway initiates settlement by sending batch totals so both sides reconcile. As with the other flavors, the MMS batch panel is read-only. ## Gotchas - **Group ID `40001`.** This is the Omaha front-end. (Historically a `40001` default has been used as a sandbox/test group elsewhere — always use the value on the merchant's VAR packet.) - **No `settlement_mid`.** Omaha is hybrid-host, not North terminal capture — don't send a settlement MID. - **`tid` is zero-padded to 8** as the Datawire `AuthKey2`; supply the raw 7-digit value. - **Hybrid-host means settlement is initiated by the gateway** — funding is FDC/back-office driven; confirm the merchant's Omaha funding relationship before boarding. # Getting Ready for Production Prepare your Koard integration to move beyond the Sandbox by configuring Xcode schemes, build configurations, and API keys for both development and production. **What You Learn** In this guide, you'll learn how to: * Create separate build configurations and schemes in Xcode * Inject environment-specific Koard API keys and endpoints * Automate build-time switching between Sandbox and production settings * Validate your production readiness checklist before submitting to the App Store **Prerequisites** Before you begin, ensure you have: * **Koard API keys** for both Sandbox and production * **Info.plist or `.xcconfig` access** to store environment values * **Xcode project admin access** to edit schemes and build settings * **Dedicated test devices** with Sandbox Apple Accounts for final verification ## 1. Duplicate Build Configurations 1. In Xcode, select your project in the Project Navigator. 2. Under **PROJECT → Info**, duplicate your existing `Debug` and `Release` configurations. Name the copies `Debug-Prod` and `Release-Prod`. 3. Point the production build configurations to `.xcconfig` files (optional but recommended) such as `Koard-Dev.xcconfig` and `Koard-Prod.xcconfig`. ```text Koard-Dev.xcconfig KOARD_API_BASE_URL = https://sandbox-api.koard.com KOARD_API_KEY = ${KoardSandboxAPIKey} Koard-Prod.xcconfig KOARD_API_BASE_URL = https://api.koard.com KOARD_API_KEY = ${KoardProductionAPIKey} ``` Store sensitive values in your CI/CD environment or use Xcode build setting macros rather than hardcoding secrets in source control. ## 2. Customize Schemes for Each Environment Following Apple’s [Customizing the Build Schemes](https://developer.apple.com/documentation/xcode/customizing-the-build-schemes-for-a-project) guidance, create two top-level schemes: * `KoardApp-Dev` mapped to the `Debug`/`Release` configurations * `KoardApp-Prod` mapped to the `Debug-Prod`/`Release-Prod` configurations 1. Open **Product → Scheme → Manage Schemes**. 2. Duplicate your primary scheme and rename it `KoardApp-Prod`. 3. Assign the correct build configuration for each action (Build, Run, Archive, etc.). 4. Uncheck **Shared** while iterating, then re-enable sharing once the configuration is stable so teammates receive the new scheme in source control. **Tip**: Keep the production scheme archived with **Release-Prod** to ensure App Store submissions always use production endpoints and credentials. ## 3. Switch API Keys at Build Time Expose the Koard API key and environment to your app using Info.plist substitutions or Swift build flags. ### Option A: Info.plist placeholders 1. Add keys like `KOARD_API_BASE_URL` and `KOARD_API_KEY` to your Info.plist. 2. Reference them using `${KOARD_API_BASE_URL}` placeholders. 3. Resolve them in code at runtime: ```swift struct KoardAppConfig { static let baseURL: URL = { guard let urlString = Bundle.main.object(forInfoDictionaryKey: "KOARD_API_BASE_URL") as? String, let url = URL(string: urlString) else { fatalError("Missing or invalid KOARD_API_BASE_URL") } return url }() static let apiKey: String = { guard let key = Bundle.main.object(forInfoDictionaryKey: "KOARD_API_KEY") as? String else { fatalError("Missing KOARD_API_KEY") } return key }() } ``` ### Option B: Swift compilation conditions 1. Add custom flags in **Build Settings → Swift Compiler - Custom Flags** (e.g., `-DKOARD_ENV_SANDBOX` and `-DKOARD_ENV_PRODUCTION`). 2. Use those flags to branch logic: ```swift #if KOARD_ENV_PRODUCTION let environment: KoardEnvironment = .production #else let environment: KoardEnvironment = .uat #endif let options = KoardOptions( environment: environment, loggingLevel: environment == .production ? .error : .debug ) KoardMerchantSDK.shared.initialize(options: options, apiKey: apiKey) ``` Use this approach when you prefer compile-time enforcement of environment differences, such as disabling test-specific UI in production builds. **Environment and logging**: `KoardOptions.environment` accepts `.uat`, `.production`, or `.custom(String)`. Lower the `loggingLevel` for production builds (for example `.error` or `.none`) so debug logs don't ship to release users. ## 4. Verify Device and Account Setup * Install the **Dev** build on a dedicated test iPhone that remains signed in with your Sandbox Apple Account. * Install the **Prod** build on a separate device or reset the test device before signing in with the production Apple ID. * Run smoke tests for Tap to Pay, refunds, reversals, and settlement cutovers in each environment. **Deploy Deliberately**: Never archive or submit to App Store Connect using a Sandbox scheme. Require production code reviews to confirm the `KoardApp-Prod` scheme was used for the final archive. **Authentication and Keychain**: Login is session-token only — the SDK persists the session token and never stores the merchant code, PIN, or alias. `logout()` clears only the SDK's own Keychain entries (it no longer performs a blanket delete of the host app's Keychain items), so signing out will not disturb other credentials your app stores. ## 5. Pre-launch Checklist * [ ] Xcode schemes map to the correct build configurations. * [ ] Production API keys and endpoints live outside of source control. * [ ] Secrets are injected via CI/CD, `.xcconfig`, or secure build settings. * [ ] Sandbox and production devices are authenticated with the appropriate Apple IDs. * [ ] A rollback plan is documented in case production rollout needs to be paused. ## Next Steps * [Creating a Sandbox Apple Account](/docs/setting-up-the-ios-sdk/creating-a-sandbox-apple-account) to keep development builds isolated. * [Running Payments](/docs/setting-up-the-ios-sdk/running-payments) to validate production behavior before launch. * Coordinate with your Koard partner manager to schedule final Apple certification checks. # Supported Devices & NFC Tap Location A guide to compatible Android devices for Tap to Pay, device requirements, and where customers should tap their card on each device. **What you learn** In this guide, you'll learn: * Which Android devices are compatible with Tap to Pay * Minimum device requirements for NFC contactless payments * Where the NFC antenna is located on popular devices * How to instruct customers to tap their card correctly ## Device Requirements To accept Tap to Pay on Android, the merchant's device must meet **all** of the following requirements: | Requirement | Details | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | | **Android version** | Android 12 (API level 31) or later | | **NFC hardware** | Built-in NFC chip (virtually all modern flagship and mid-range Android phones) | | **Hardware keystore** | TEE or StrongBox-backed keystore for secure key storage | | **Google Play Services** | Google Mobile Services (GMS) must be installed and up to date | | **Google Play Protect** | Must be enabled for device integrity verification | | **Developer mode** | Must be **disabled** during live transactions | | **Device integrity** | Passes Play Integrity: release-signed APK, locked bootloader, no root/Magisk. No custom ROMs | | **Visa Kernel app** | The Visa Tap to Pay Ready (TTPR) kernel app must be installed (from the Google Play Store, or via `installKernelApp()`) | **Developer Mode**: Tap to Pay transactions will fail if developer mode is enabled. Always disable developer mode before processing payments. See the [Installing the SDK](/docs/guides/android-sdk/details/installing-sdk) guide for the recommended workflow. ### Play Integrity Gating In addition to the hardware/software requirements above, the underlying contactless engine enforces **Play Integrity** at enrollment time. The device must be running a **release-signed APK on a locked bootloader**, with **no root/Magisk** and **Developer Options turned off**. Debug-signed builds installed via Run ▶ from Android Studio are rejected during enrollment, even on otherwise-eligible hardware. ### Eligibility Check The SDK provides a built-in eligibility check that verifies device requirements before attempting a transaction. Call `checkKiCEligibility()` on a worker thread; it returns a `KoardKiCEligibility` with an `isEligible` flag and the set of `failureCodes` when the device is not eligible: ```kotlin withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() val eligibility = sdk.checkKiCEligibility() if (eligibility.isEligible) { // Device is eligible for Tap to Pay } else { // Handle ineligibility — inspect failure codes eligibility.failureCodes.forEach { code -> Log.w("TapToPay", "Eligibility failure code: $code") } } } ``` The numeric `failureCodes` map to the eligibility error codes documented in [SDK Response Codes](/docs/guides/android-sdk/details/sdk-response-codes#kic-eligibility-errors). ### Visa Kernel App Tap to Pay requires the Visa Tap to Pay Ready (TTPR) kernel app on the device. Check for it and trigger an in-app install through the SDK: ```kotlin withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() if (!sdk.isKernelAppInstalled()) { sdk.installKernelApp(activity) // launches the install flow } } ``` ## Tested & Certified Devices The following devices have been tested and certified by Visa for use with the Kernel in the Cloud (KiC) Tap to Pay Ready application: | Device | Android Version | Form Factor | | ------------------ | --------------- | ----------- | | Google Pixel 3a | Android 12 | Phone | | Google Pixel 4 | Android 13 | Phone | | Google Pixel 6 | Android 15 | Phone | | Google Pixel 8 | Android 16 | Phone | | Google Pixel 9 | Android 15 | Phone | | Samsung Galaxy S22 | Android 13 | Phone | | Samsung Galaxy S23 | Android 14 | Phone | | Oona Tablet | — | Tablet | **Not limited to this list**: These are the devices Visa has explicitly tested and certified. In practice, **any Android device** that meets all the requirements listed above (Android 12+, NFC, hardware keystore, GMS) should work with Tap to Pay. The SDK's `checkKiCEligibility()` check will confirm compatibility at runtime. ### Full Supported Device List Beyond the Visa-certified list, Tap to Pay on Android is supported across a wide range of manufacturers and models. The following devices are confirmed compatible (when running Android 12+): | Brand | Supported Models | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Asus** | Zenfone 9, Zenfone 10, Zenfone 11 Ultra, Zenfone 12 Ultra, ROG Phone 6, ROG Phone 7, ROG Phone 8, ROG Phone 9 | | **Google Pixel** | Pixel 6, Pixel 6a, Pixel 7, Pixel 7a, Pixel 8, Pixel 8a, Pixel 9, Pixel 9a, Pixel 10 | | **Nokia** | G22, G42, G60, G400, X30, XR21 | | **Honor** | 70, 70 Lite, 80, 90, 90 Lite, Magic5, Magic6, Magic7, Magic8 | | **Infinix** | Hot 30, Hot 40, Hot 50, Hot 60i, Zero 20, Zero 30, Zero 40 | | **Motorola** | Edge 2023, Edge 2024, Edge 2025, Moto G 2025, Razr 40, Razr 50, Razr 60 | | **OnePlus** | Nord 3, Nord 4, Nord 5, Nord CE3, Nord CE4, Nord CE5, Nord N30, 11, 11R, 12, 12R, 13, 13R | | **Oppo** | A60, A77, A78, A98, Find X5, Find X6, Find X7, Find X8, Find X9, Reno8, Reno9, Reno10, Reno11, Reno12, Reno13, Reno14, Reno15 | | **Samsung Galaxy** | A04s, A05s, A13, A14, A15, A16, A17, A24, A25, A26, A33, A34, A35, A36, A53, A54, A55, A56, A73, S22, S23, S24, S25, S26 Ultra, Z Flip4, Z Flip5, Z Flip6, Z Flip7, Z Fold4, Z Fold5, Z Fold6, Z Fold7 | | **Xiaomi** | 12, 12S, 12T, 13, 13T, 14, 14T, 15, 15T, Redmi 12, Redmi 12C, Redmi 13, Redmi 13C, Redmi 14C, Redmi 15, Redmi Note 12, Redmi Note 13, Redmi Note 14 | **Always verify at runtime**: NFC support can vary by region and carrier variant — especially for mid-range devices. Use the SDK's `checkKiCEligibility()` check at runtime rather than relying solely on a static device list. ## NFC Antenna Location & Tap Guidance The NFC antenna location determines where the customer should hold or tap their contactless card or device. Getting this right is critical for a smooth payment experience. ### General Rule On virtually all Android phones, the **NFC antenna is located on the back of the device, in the upper-center area** (roughly behind the rear camera module). Customers should hold their card flat against the **upper-middle portion of the phone's back**. ### NFC Antenna Location Summary The table below summarizes NFC antenna placement by manufacturer. For per-model details, see the device tables in the [Full Supported Device List](#full-supported-device-list) above. | Manufacturer | NFC Antenna Location | Tap Zone Guidance | | ------------------------------ | ------------------------------------------------------- | -------------------------------------------------- | | **Asus** (Zenfone / ROG Phone) | Center to upper-center back | Hold card against the center-top third of the back | | **Google Pixel** | Upper-center back, near/behind the rear camera bar | Hold card against the top third of the back | | **Nokia** | Upper-center back | Hold card against the top third of the back | | **Honor** | Upper-center back; Magic series slightly above midpoint | Hold card against the top third of the back | | **Infinix** | Upper-center back | Hold card against the top third of the back | | **Motorola** (Edge / Moto G) | Upper-center back, near the camera or Motorola logo | Hold card against the top third of the back | | **Motorola** (Razr foldables) | Upper half of back (when folded) | Tap on the upper portion of the folded device | | **OnePlus** (flagships) | Upper-center back, near camera module | Hold card against the top third of the back | | **OnePlus** (Nord series) | Upper-center back | Hold card against the top third of the back | | **Oppo** (Find X series) | Center back, near camera module | Hold card against the center of the back | | **Oppo** (A / Reno series) | Upper-center back | Hold card against the top third of the back | | **Samsung Galaxy S** series | Center back, slightly above the midpoint | Hold card against the center of the back | | **Samsung Galaxy A** series | Upper-center to center back (varies by tier) | Hold card against the center of the back | | **Samsung Galaxy Z Flip** | Upper half of the back (when folded) | Tap on the upper portion of the folded device | | **Samsung Galaxy Z Fold** | Center of the back panel (when closed) | Tap on the center of the back when folded | | **Xiaomi** (flagships) | Upper-center back, near camera module | Hold card against the top third of the back | | **Xiaomi** (Redmi series) | Upper-center back | Hold card against the top third of the back | | **Tablets** | Varies — typically center back or near one edge | Check manufacturer documentation | ### Visual Tap Guide For the best tap experience, instruct customers to: 1. **Remove the card from any wallet or sleeve** — Other cards or RFID-blocking material can interfere with the NFC signal 2. **Hold the card flat** against the back of the phone — Do not tap at an angle 3. **Position the card** over the NFC antenna zone (upper-center back on most devices) 4. **Hold steady for 1–2 seconds** — Do not pull away until the phone confirms the read 5. **Listen/watch for confirmation** — The device will display a status message and/or vibrate when the card is read successfully ```plaintext ┌─────────────────────┐ │ │ ← Phone (back view) │ ┌───────────┐ │ │ │ 📷 Camera │ │ │ └───────────┘ │ │ ╔═══════════════╗ │ │ ║ NFC ANTENNA ║ │ ← Tap card here │ ║ TAP ZONE ║ │ │ ╚═══════════════╝ │ │ │ │ │ │ │ │ │ └─────────────────────┘ ``` **Phone cases**: Thin phone cases generally do not interfere with NFC reads. However, thick rugged cases, metal cases, or cases with built-in card holders may block or weaken the NFC signal. If customers experience read failures, try removing the case. ### Handling Tap Failures If the card does not read on the first attempt: 1. Reposition the card slightly — move it toward the camera area 2. Ensure the card is flat and not angled 3. Remove any phone case that may be interfering 4. Check that the device screen shows the "Present card" or "Tap card" prompt 5. If the issue persists, the SDK will surface an appropriate [display message](/docs/guides/android-sdk/details/sdk-response-codes#display-message-ids) or an `OnFailure` status code such as `NFC_NOT_AVAILABLE` ## Troubleshooting Device Compatibility | Issue | Possible Cause | Solution | | ------------------------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Eligibility check returns failure codes | Device doesn't meet one or more requirements | Inspect the failure codes from `checkKiCEligibility()` and address each (e.g., enable Google Play Protect, update Google Play Services, disable developer mode) | | NFC transactions fail on a supported device | Developer mode enabled | Disable developer mode in Settings → Developer options | | Card not reading | NFC disabled in device settings | Go to Settings → Connected devices → Connection preferences → NFC and ensure it's toggled on | | Intermittent read failures | Card positioned incorrectly | Guide the customer to tap in the correct NFC zone (see table above) | | "Kernel app not found" error | Visa Tap to Pay Ready app not installed | Install the Visa Kernel app from the Google Play Store | | Transactions fail after app update | Developer mode was re-enabled for deployment | Disable developer mode after installing updates, then restart the device | ## Next Steps * [Installing the SDK](/docs/guides/android-sdk/details/installing-sdk) — Set up your development environment * [Running Payments](/docs/guides/android-sdk/details/running-payments) — Process your first Tap to Pay transaction * [SDK Response Codes](/docs/guides/android-sdk/details/sdk-response-codes) — Understand transaction outcomes and error handling * [Running the Demo App](/docs/guides/android-sdk/details/demo) — Test with the sample application --- title: Preauth --- # Preauth A preauthorization places a hold on the cardholder's funds without capturing. Use it when the final amount may change (e.g., tips, adjustments, custom surcharging). ## Prerequisites - Authenticated merchant with `login()` - Active location set via `setActiveLocationID()` - Card reader prepared with `prepare()` (iOS) or device enrolled (Android) ## Basic Preauth **iOS:** ```swift let breakdown = PaymentBreakdown( subtotal: 10000, taxRate: 8.75, taxAmount: 875, tipType: .fixed ) let currency = CurrencyCode(currencyCode: "USD", displayName: "US Dollar") let response = try await KoardMerchantSDK.shared.preauth( amount: 10875, breakdown: breakdown, currency: currency, eventId: UUID().uuidString ) let transactionId = response.transactionId! ``` **Android:** ```kotlin val breakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipType = "fixed" ) sdk.preauth( activity = this, amount = 10875, breakdown = breakdown, eventId = UUID.randomUUID().toString() ).collect { event -> when (event.actionStatus) { ActionStatus.OnComplete -> { val txn = event.response?.transaction println("Preauth hold placed: ${txn?.transactionId}") } ActionStatus.OnConfirmSurcharge -> { val txn = event.response?.transaction sdk.confirm( transactionId = txn?.transactionId ?: "", confirm = true ) } ActionStatus.OnFailure -> { println("Preauth failed: ${event.response?.message}") } else -> { /* reader progress */ } } } ``` ## Preauth with Surcharge Bypass To calculate surcharges yourself (e.g., BIN-based logic), bypass the processor's automatic surcharge: **iOS:** ```swift let breakdown = PaymentBreakdown( subtotal: 10000, taxRate: 8.75, taxAmount: 875, tipAmount: 2000, tipType: .fixed, surcharge: PaymentBreakdown.Surcharge(bypass: true) ) let response = try await KoardMerchantSDK.shared.preauth( amount: 12875, // subtotal + tax + tip (no surcharge yet) breakdown: breakdown, currency: currency, eventId: UUID().uuidString ) ``` **Android:** ```kotlin val breakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipAmount = 2000, tipType = "fixed", surcharge = Surcharge(bypass = true) ) sdk.preauth( activity = this, amount = 12875, breakdown = breakdown, eventId = UUID.randomUUID().toString() ).collect { /* handle response */ } ``` After the preauth completes, use the BIN from the response to calculate a custom surcharge, then apply it via [incremental auth](incremental-auth.md). See the [BIN-based surcharging workflow](surcharging.md#bin-based-custom-surcharge) for the full flow. ## Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `amount` | `Int` | Yes | Hold amount in minor units (cents) | | `breakdown` | `PaymentBreakdown?` | No | Itemized breakdown | | `currency` | `CurrencyCode` | Yes (iOS) | Currency for the transaction | | `eventId` | `String?` | No | Idempotency key (UUID recommended) | | `activity` | `Activity` | Yes (Android) | Android activity for NFC access | ## After Preauth A preauth hold must be followed by one of: | Action | Description | |--------|-------------| | [Capture](capture.md) | Finalize at the same or lower amount | | [Incremental Auth](incremental-auth.md) | Increase the hold (e.g., add surcharge) | | [Tip Adjust](tip-adjust.md) | Update the tip before capture | | [Reverse](reverse.md) | Void the hold entirely | ## See Also - [Sale](sale.md) — One-step authorize + capture - [Capture](capture.md) — Finalize a preauth - [Surcharging](surcharging.md) — Bypass and custom surcharge workflows # Resources Comprehensive resources for developing Tap to Pay on iPhone applications with Koard and Apple's payment technologies. ## Apple Resources Essential resources from Apple for working with Tap to Pay on iPhone technology. ### ProximityReader Framework The ProximityReader framework provides the core functionality for reading contactless payment cards on iPhone. | Resource | Description | Link | | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | **ProximityReader Framework Documentation** | Complete framework reference including setup steps, API documentation, and implementation guides for integrating Tap to Pay on iPhone | [View Documentation](https://developer.apple.com/documentation/proximityreader) | | **Quick Start Guide** | Step-by-step guide to begin using Tap to Pay on iPhone to read contactless payment cards | [Developer Documentation](https://developer.apple.com/documentation/proximityreader) | | **Framework Reference** | Complete API reference for all ProximityReader classes, methods, and properties | [API Reference](https://developer.apple.com/documentation/proximityreader) | **Key Topics Covered:** * Reader initialization and configuration * Card reading and payment processing * Error handling and recovery * Security and entitlement requirements * Session management and lifecycle ### Apple Business Register Documentation Comprehensive documentation portal for Payment Service Providers (PSPs) working with Tap to Pay on iPhone. | Resource | Description | Link | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- | | **Apple Business Register** | Central hub for PSP-specific Tap to Pay on iPhone documentation, including registration, certification, and integration guides | [Apple Business Register](https://register.apple.com) | | **PSP Integration Guide** | Complete integration guide for Payment Service Providers | \[For PSPs only] | | **Certification Process** | Step-by-step certification requirements and procedures | [Apple Business Register](https://register.apple.com) | **Note**: Apple Business Register documentation is available for Payment Service Providers (PSPs) only. If you're building as a PSP, contact Koard to access these resources. ### Human Interface Guidelines Apple's design guidelines and best practices for Tap to Pay on iPhone applications. | Resource | Description | Link | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | | **Tap to Pay Human Interface Guidelines** | Design principles, UI/UX best practices, and accessibility guidelines specifically for Tap to Pay on iPhone applications | [View Guidelines](https://developer.apple.com/design/human-interface-guidelines/tap-to-pay-on-iphone) | | **Design Patterns** | Recommended UI patterns and components for payment flows | [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/tap-to-pay-on-iphone) | | **Accessibility Guidelines** | VoiceOver, Dynamic Type, and other accessibility requirements | [Accessibility Guide](https://developer.apple.com/design/human-interface-guidelines/tap-to-pay-on-iphone) | **Key Topics Covered:** * User interface design principles * Payment flow UX best practices * Error handling and user feedback * Accessibility compliance * Branding and messaging guidelines ### Merchant Education Educational materials and training resources for PSPs and app developers. | Resource | Description | Link | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- | | **Accept Payments Guide** | Comprehensive guide on how to accept payments using Tap to Pay on iPhone, including merchant setup, training materials, and best practices | [View Guide](https://developer.apple.com/tap-to-pay/how-to-accept-payments/) | | **Merchant Training Materials** | Educational resources for training merchants on using Tap to Pay | [Merchant Education](https://developer.apple.com/tap-to-pay/how-to-accept-payments/) | | **Developer Resources** | Developer-focused educational content and implementation guides | [Developer Resources](https://developer.apple.com/tap-to-pay/how-to-accept-payments/) | **Key Topics Covered:** * Merchant onboarding process * Payment acceptance workflows * Device setup and configuration * Troubleshooting common issues * Training and support materials ### Tap to Pay on iPhone FAQs Frequently asked questions and answers about Tap to Pay on iPhone technology. | Resource | Description | Link | | -------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | | **Tap to Pay FAQs** | Comprehensive FAQ covering common questions about Tap to Pay on iPhone implementation, requirements, and troubleshooting | [View FAQs](https://register.apple.com/tap-to-pay-on-iphone) | | **Technical FAQs** | Technical implementation questions and answers | [Apple Business Register](https://register.apple.com/tap-to-pay-on-iphone) | | **Integration FAQs** | Common integration questions and solutions | [Apple Business Register](https://register.apple.com/tap-to-pay-on-iphone) | **Common Topics:** * Device requirements and compatibility * Entitlement and certification questions * Integration and implementation queries * Security and compliance questions * Troubleshooting and support ### Additional Apple Resources | Resource | Description | Link | | --------------------------- | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | **WWDC Sessions** | Video sessions from Apple's Worldwide Developers Conference covering Tap to Pay and payment technologies | [WWDC Videos](https://developer.apple.com/videos/) | | **Apple Developer Forums** | Community forums for discussing Tap to Pay implementation and troubleshooting | [Developer Forums](https://developer.apple.com/forums/) | | **Apple Developer Support** | Direct support channels for Apple Developer Program members | [Developer Support](https://developer.apple.com/support/) | | **Security Documentation** | Apple's security guidelines and best practices for payment applications | [Security Guide](https://developer.apple.com/documentation/proximityreader) | ## Koard Documentation ### API Documentation | Resource | Description | Link | | ------------------------- | ---------------------------------------------------------------------------------------------- | --------------------------------------------------- | | **REST API Reference** | Complete API documentation with examples, request/response schemas, and authentication details | [API Reference](/api-reference) | | **Webhook Documentation** | Webhook setup guide, event reference, and testing procedures | [Webhook Guide](/docs/webhooks/setting-up-webhooks) | | **API Authentication** | Authentication methods, API keys, and security best practices | [API Reference](/api-reference) | ### SDK Documentation | Resource | Description | Link | | ---------------------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | **iOS SDK Guide** | Complete iOS SDK integration guide with code examples | [Setting Up the Entitlement](/docs/setting-up-the-ios-sdk/setting-up-the-entitlement-for-tap-to-pay-on-iphone) | | **SDK Installation** | Step-by-step installation instructions for iOS SDK | [Installing SDK](/docs/setting-up-the-ios-sdk/installing-the-sdk) | | **Payment Lifecycle** | Complete guide to payment processing and lifecycle management | [Payment Lifecycle](/docs/guides/payments/details/payment-lifecycle.md) | | **Tap to Pay Configuration** | Guide to adding Tap to Pay functionality to your iOS app | [Tap to Pay Guide](/docs/setting-up-the-ios-sdk/adding-support-for-tap-to-pay-on-iphone) | ### Integration Guides | Resource | Description | Link | | ----------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------- | | **Getting Started** | Complete getting started guide for new Koard integrations | [Getting Started](/docs/getting-started-with-koard/introduction) | | **Merchant Setup** | Guide to setting up merchant accounts and configurations | [Merchant Setup](/docs/getting-started-with-koard/setting-up-the-merchant) | | **Batch Settlements** | Guide to batch processing and settlement workflows | [Batch Settlements](/docs/batch-and-settlements/introduction) | | **Webhook Integration** | Complete webhook integration guide with examples | [Webhook Setup](/docs/webhooks/setting-up-webhooks) | ### Developer Resources | Resource | Description | Link | | ----------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------- | | **Code Examples** | Sample implementations in Swift, Objective-C, and other languages | [Code Examples](/docs/setting-up-the-ios-sdk/installing-the-sdk) | | **Sandbox Environment** | Test environment setup and configuration guide | [Getting Started](/docs/getting-started-with-koard/introduction) | | **Best Practices** | Apple best practices and development guidelines | [Apple Best Practices](/docs/appendix/apple-best-practices-and-guidelines) | | **Security Guidelines** | Security best practices and Apple development guidelines | [Security Guide](/docs/appendix/developing-with-apple) | ## SDK Downloads ### iOS SDK ```bash # CocoaPods pod 'KoardSDK' # Swift Package Manager https://github.com/koardlabs/koard-ios ``` ### Android SDK ```gradle // Add to build.gradle implementation 'com.koard:koard-android-sdk:1.0.6' ``` ## Testing Resources ### Test Cards Use these test card numbers to verify your Tap to Pay integration in the Certificate environment. These cards simulate different payment scenarios without processing real transactions. | Card Type | Number | CVV | Expiry | Use Case | | ---------------- | ------------------- | ---- | --------------- | -------------------------- | | Visa | 4242 4242 4242 4242 | 123 | Any future date | Successful payment testing | | Mastercard | 5555 5555 5555 4444 | 123 | Any future date | Successful payment testing | | American Express | 3782 822463 10005 | 1234 | Any future date | Successful payment testing | | Discover | 6011 1111 1111 1117 | 123 | Any future date | Successful payment testing | ### Test Scenarios Test various payment scenarios using these test card numbers: | Scenario | Card Number | Expected Result | | ---------------------- | ------------------------------------------ | ------------------------------------------- | | **Successful Payment** | Use any test card above with valid details | Payment processes successfully | | **Declined Payment** | 4000 0000 0000 0002 | Payment is declined | | **Insufficient Funds** | 4000 0000 0000 9995 | Transaction fails due to insufficient funds | | **Invalid CVV** | Any test card with wrong CVV | CVV validation error | | **Expired Card** | Any test card with past expiry date | Card expiration error | ### Testing Environment | Environment | Description | Use Case | | ---------------------- | ------------------------------------- | ------------------------------------------------- | | **Certificate (CERT)** | Test environment for development | Use for all internal testing and development | | **Production** | Live merchant transaction environment | Use only after thorough testing and certification | **Important**: Always use the Certificate environment for testing. Test cards work only in the Certificate environment and will not process real transactions. ### Testing Checklist Before deploying to production, ensure you've tested: * [ ] Successful payment processing with all card types * [ ] Error handling for declined payments * [ ] Error handling for invalid card details * [ ] Reader availability checking * [ ] Entitlement verification * [ ] Payment flow UI/UX * [ ] Error messaging and user feedback * [ ] Accessibility compliance (VoiceOver, Dynamic Type) * [ ] Network error handling * [ ] Background/foreground transitions ## Support ### Developer Support Get help with technical questions, integration issues, and development challenges. | Support Channel | Description | Link | | ----------------- | -------------------------------------------- | ------------------------------------------------------------------ | | **Documentation** | Comprehensive documentation and guides | [Documentation Hub](/docs/getting-started-with-koard/introduction) | | **GitHub Issues** | Report bugs and request features for iOS SDK | [GitHub Issues](https://github.com/koardlabs/koard-sdk/issues) | ### Business Support Get assistance with merchant accounts, business inquiries, and partnership opportunities. ### Support Resources | Resource | Description | Link | | ------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | **FAQ Section** | Frequently asked questions and answers | [Apple FAQs](https://register.apple.com/tap-to-pay-on-iphone) | | **Troubleshooting Guide** | Common issues and solutions | [Troubleshooting](/docs/getting-started-with-koard/setting-up-the-merchant#setting-up-the-merchant__troubleshooting) | | **Status Page** | Real-time system status and maintenance updates | [Status Page](https://status.koard.com) | ## Community ### Developer Community * **GitHub**: * **Stack Overflow**: Tag questions with `koard` * **Reddit**: r/koard * **Discord**: Koard Developer Discord ### Events and Webinars * **Monthly Webinars**: Product updates and best practices * **Developer Meetups**: Local developer meetups * **Conference Talks**: Industry conference presentations * **Workshops**: Hands-on development workshops ## Tools and Utilities ### Development Tools * **Koard CLI**: Command-line interface for API testing * **Webhook Tester**: Local webhook testing tool * **API Explorer**: Interactive API documentation * **Postman Collection**: Pre-configured API requests ### Monitoring Tools * **Dashboard**: Real-time transaction monitoring * **Analytics**: Payment analytics and reporting * **Alerts**: Custom alert configuration * **Logs**: Detailed transaction logs ## Compliance and Security ### Security Resources * **Security Best Practices**: Comprehensive security guide * **PCI Compliance**: PCI DSS compliance information * **Data Protection**: GDPR and privacy compliance * **Security Audit**: Third-party security audits ### Compliance Documentation * **Terms of Service**: Legal terms and conditions * **Privacy Policy**: Data handling and privacy policy * **Cookie Policy**: Cookie usage and management * **GDPR Compliance**: European data protection compliance ## Training and Certification ### Developer Certification * **Koard Developer Certification**: Official developer certification * **Payment Processing Fundamentals**: Core payment concepts * **API Integration Specialist**: Advanced API integration * **Security Specialist**: Payment security best practices ### Training Materials * **Video Tutorials**: Step-by-step video guides * **Interactive Courses**: Hands-on learning modules * **Documentation**: Comprehensive written guides * **Code Examples**: Real-world implementation examples # Running Batches Learn how to create, manage, and process batches with Koard's payment platform. [Get started with Koard](/docs/getting-started-with-koard/introduction) Koard provides comprehensive batch processing capabilities for managing transactions and settlements. Whether you choose to let Koard handle batches automatically or manage them yourself, this guide covers all the essential operations and best practices. **What you learn** In this guide, you'll learn: * How to choose between Koard-managed and self-managed batch processing * Core batch operations: opening, closing, and editing batches * Required fields for self-managed batch processing * Webhook integration for real-time batch updates * Processor-specific considerations and constraints * Best practices for batch management and error handling ## Before you begin This guide covers batch processing operations in Koard. For a better understanding of batch concepts, see our [Batch and Settlements overview](/docs/batch-and-settlements/introduction). If you're ready to start processing payments, see our [Running Payments guide](/docs/setting-up-the-ios-sdk/running-payments). ## Two Batch Processing Approaches Koard supports two distinct approaches for batch processing based on your technical requirements and business model. ### Via Koard (Recommended) **Best for**: Tap to Pay terminals with specific gateway configurations Koard can handle batches entirely for a MID/TID combination. This approach is recommended if you have specific terminals configured on the gateway for Tap to Pay. **Key benefits:** * **Automatic Management**: Koard handles all batch operations automatically * **Status Tracking**: Automatic transaction status tracking and reconciliation * **Error Handling**: Built-in retry logic and error recovery * **Webhook Integration**: Real-time updates for all batch events ### Self-Managed Batches **Best for**: Enterprise ISVs and PSPs with existing batch infrastructure If you prefer to manage batches yourself, you'll need to handle the core batch operations and maintain the required transaction data. **Key requirements:** * **Required Fields**: Maintain all necessary transaction data * **Webhook Integration**: Process transaction events for batch management * **Sequential Ordering**: Maintain proper batch number sequencing * **Error Handling**: Implement retry logic for failed operations ## Core Batch Operations Koard provides several core functions that you can apply to batches and transactions: | Operation | Description | When to Use | API Endpoint | | ---------------- | ------------------------------------------ | ------------------------------------------ | ----------------------------------- | | **List Batches** | Retrieve paginated list of batches | Viewing batch history, filtering by status | `GET /v1/batches` | | **Get Batch** | Retrieve specific batch details | View batch status and transactions | `GET /v1/batches/{batch_id}` | | **Open Batch** | Create a new batch for transactions | Start of business day or when needed | `POST /v1/batches/open` | | **Close Batch** | Finalize and submit batch for processing | End of business day or when ready | `POST /v1/batches/{batch_id}/close` | | **Edit Batch** | Add or remove transactions from open batch | Before closing the batch | `PUT /v1/batches/{batch_id}/edit` | ### Listing Batches ```bash GET /v1/batches?limit=50&offset=0&statuses=open ``` **Query Parameters:** * `account_id` (optional): Filter by owning account * `terminal_id` (optional): Filter by terminal ID * `statuses` (optional): Filter by batch lifecycle statuses — accepts multiple values (open, closed, submitted, accepted, partially\_accepted, rejected, cancelled) * `processor_config_id` (optional): Filter by processor config ID * `limit` (optional): Maximum items per page (1-500, default: 50) * `offset` (optional): Number of items to skip (default: 0) ### Getting a Batch ```bash GET /v1/batches/{batch_id}?include_transactions=true ``` **Query Parameters:** * `include_transactions` (optional): If true, embed transactions with the batch (default: false) ### Opening a Batch ```bash POST /v1/batches/open { "terminal_id": "tid_987654321", "processor_batch_id": "batch_001" } ``` **Required Fields:** * `terminal_id`: The terminal identifier * `processor_batch_id`: The batch ID for the processor (must be unique per MID/TID combination) ### Closing a Batch ```bash POST /v1/batches/{batch_id}/close ``` No request body is required. The batch must be in an `open` status to be closed. ### Editing an Open Batch Only open batches can be edited. ```bash PUT /v1/batches/{batch_id}/edit { /*"added_transactions": ["17279734-fa7b-4f26-a945-19a8a98b6258"], "removed_transactions": [],*/ "processor_batch_id": "6" } ``` **Body Fields (all optional):** * `processor_batch_id`: The processor batch ID * `added_transactions`: List of transaction IDs to add to the batch * `removed_transactions`: List of transaction IDs to remove from the batch ## Self-Managed Batch Requirements If you choose to run batches yourself, you'll need to maintain these essential fields for each transaction: ### Required Transaction Fields | Field | Description | Example | | -------------------------------- | --------------------------------------- | --------------------------------- | | **Approval Code** | Authorization approval code | `123456` | | **Response Code** | Transaction response code | `00` | | **Transaction Identifier** | Unique transaction ID | `txn_abc123def456` | | **Local Date/Time** | Transaction timestamp | `2024-01-15T14:30:00Z` | | **Amounts** | Auth, settled, tip, surcharge, cashback | `{"auth": 1000, "settled": 1000}` | | **ACI/Void/Reversal Indicators** | Transaction type indicators | `{"aci": "Y", "void": "N"}` | ### Webhook Integration Every transaction processed through Koard triggers a webhook event. Upon delivery, you'll receive: * **Transaction Data**: All required fields for batch processing * **TLV Tags**: General TLV tags from the payment device * **Status Information**: Real-time transaction status updates * **Batch Context**: Information about which batch the transaction belongs to ```json { "event": "transaction.processed", "data": { "transactionId": "txn_abc123def456", "approvalCode": "123456", "responseCode": "00", "amount": { "auth": 1000, "settled": 1000, "tip": 0 }, "timestamp": "2024-01-15T14:30:00Z", "batchId": "batch_xyz789", "tlvTags": { "aci": "Y", "void": "N" } } } ``` ## Batch Lifecycle Management ### Batch Statuses A batch progresses through several statuses during its lifecycle: | Status | Description | Transitions To | | ----------------------- | -------------------------------------------------- | --------------------------------------- | | **open** | Batch is open and accepting transactions | closed | | **closed** | Batch has been closed and submitted for settlement | submitted, rejected | | **submitted** | Batch has been submitted to the processor | accepted, partially\_accepted, rejected | | **accepted** | Batch has been fully accepted by the processor | (terminal state) | | **partially\_accepted** | Some transactions were accepted, others rejected | (terminal state) | | **rejected** | Batch was rejected by the processor | (terminal state) | | **cancelled** | Batch was cancelled before settlement | (terminal state) | ### Automatic Transaction Addition When a batch is open on a terminal: * **Captured Transactions**: Automatically added to the open batch * **Refunds**: Automatically added to the open batch * **Real-time Updates**: Webhook events provide immediate status updates ### Batch Closure Process 1. **Close at Any Time**: Batches can be closed at any point (typically end of business) 2. **Constraint**: You can only close an open batch 3. **Rejected Batches**: If a batch is rejected or partially approved: * Add all rejected transactions to a new open batch * Close the new batch again * Maintain batch number sequential order ### Editing Open Batches You can edit an open batch by: * **Adding Transactions**: Include new transactions before closing * **Removing Transactions**: Remove transactions if needed * **Constraint**: Only open batches can be edited ## Processor-Specific Considerations ### TSYS Constraints TSYS has specific batch ID requirements that affect batch management: | Constraint | Description | Impact | | ------------------ | --------------------------------- | ------------------------------------ | | **Batch ID Range** | Numbered batch IDs from 1-999 | Limited batch ID availability | | **Sliding Window** | 5-day sliding window for reuse | Batch IDs can be reused after 5 days | | **Uniqueness** | Unique per MID/TID combination | Prevents duplicate batch IDs | | **Error Handling** | Reusing batch ID results in error | Requires proper ID management | ### Batch ID Management * **Default Behavior**: Koard automatically configures batch IDs * **Override Option**: You can override batch IDs when running your own batches * **Webhook Integration**: Set batch ID via webhook when managing batches yourself * **Sequential Order**: Maintain proper batch number sequencing ### Splitting Batches **Not Recommended**: Splitting batches is not recommended due to various constraints imposed by processors like TSYS, Fiserv, and Elavon. **Best Practice**: Each MID/TID should have one batch per day to avoid potential duplicate batch ID issues. ## Error Handling and Recovery ### Failed/Rejected Batches When batches fail or are rejected: 1. **Status Information**: Koard returns detailed status information 2. **Failure Details**: Specific information about what failed 3. **Retry Logic**: Automatic retry mechanisms for recoverable errors 4. **Manual Approval**: Rejected batches can be manually approved via the portal on TSYS ### Monitoring and Alerts * **Real-time Status**: Monitor batch processing status in real-time * **Error Notifications**: Receive alerts for batch failures * **Webhook Events**: Subscribe to batch status change events * **Dashboard Monitoring**: Visual tracking of batch operations ## Best Practices ### Batch Timing * **End of Business**: Close batches at the end of each business day * **Peak Hours**: Avoid processing batches during peak transaction times * **Timezone Considerations**: Consider your merchant's timezone for batch windows * **Business Hours**: Process batches during business hours for faster support ### Error Handling * **Retry Logic**: Implement retry logic for failed batch operations * **Partial Failures**: Handle cases where some transactions in a batch fail * **Monitoring**: Set up alerts for batch processing failures * **Recovery Procedures**: Have clear procedures for handling rejected batches ### Performance Optimization * **Batch Size**: Optimize batch sizes for your transaction volume * **Sequential Processing**: Maintain proper batch number sequencing * **Resource Management**: Monitor system resources during batch processing * **Webhook Processing**: Ensure reliable webhook event processing ## See also This wraps up the running batches guide. See the links below for related information: * [Batch and Settlements Overview](/docs/batch-and-settlements/overview) - High-level batch concepts * [Running Payments](/docs/setting-up-the-ios-sdk/running-payments) - Payment processing fundamentals * [Setting up Webhooks](/docs/webhooks/setting-up-webhooks) - Webhook configuration * [Available Events](/docs/webhooks/available-events) - Webhook event reference # Fiserv Nashville (Classic) The Nashville (Envoy) front-end is Fiserv's classic host-capture platform. Board it exactly like any Fiserv terminal — see the [Fiserv Overview](/boarding-a-merchant/fiserv/fiserv-overview) for the shared VAR-sheet shape, SRS onboarding, and UMF field coverage — using the **Nashville** processor config. ## Boarding spec | Field | Value / format | Notes | |---|---|---| | **Group ID** | `10001` | Nashville front-end. Supply the value on your VAR packet. | | **Merchant ID** (MID) | 7 digits (`MerchID`, `an` ..16) | Fiserv-assigned Nashville MID. Sent top-level as `mid`. | | **Terminal ID** (TID) | 7 digits (`TermID`, `an` ..8) | Sent top-level as `tid`. Zero-padded to 8 as Datawire `AuthKey2`. | | **MCC** | 4-digit | Top-level `mcc`. | | **Equipment** (POS Solution) | `CreditCallHCRC` (Retail) or `CreditCallHCECRC` (eCommerce) | VAR-sheet `equipment`. Boarding metadata — the wire identifies the build via `TPPID`. | | **Settlement MID** | — | Not used — Nashville Classic is host capture. | ## Request shape curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_fiserv_nashville", "terminal_name": "Front Counter iPhone", "mid": "9446055", "tid": "9259755", "mcc": "5045", "var_sheet": { "group_id": "10001", "industry": "retail_qsr_grocery", "equipment": "CreditCallHCRC", "merchant_street_address": "102 Woodmont Blvd Ste 125", "merchant_city": "Nashville", "merchant_state": "TN", "merchant_postal_code": "37205", "country_code": "840" } }' Omit `did` to have Koard SRS-register a fresh Datawire ID at boarding time (returned on the created terminal). ## Capture & settlement Nashville Classic is **host capture** — the host holds the open batch and settles on its cutoff. There is no merchant-driven batch open/close; the MMS batch panel is read-only. ## Gotchas - **Group ID `10001`.** Do not reuse a sandbox value like `40001` — that's a different platform. Always use the Group ID on the merchant's VAR packet. - **`tid` is zero-padded to 8** as the Datawire `AuthKey2`; supply the raw 7-digit value — Koard pads it. - **`did` blank ⇒ auto-mint.** SRS registration + activation can take a few seconds; if you see Datawire `Retry` on the first transactions, wait ~30–60s and retry. - **Equipment / TPPID are not the same thing.** `equipment` (e.g. `CreditCallHCRC`) is boarding metadata; the transmitted `TPPID` (`RMY019` today) identifies the certified build and is set by Koard, not the merchant. # Setting Up the Entitlement for Tap to Pay on iPhone Learn how to request and configure the Tap to Pay on iPhone entitlement from Apple, which is required to enable contactless payment processing in your iOS application. [Get started with Koard](/docs/getting-started-with-koard/introduction) ## Overview The Tap to Pay on iPhone entitlement is a managed capability that Apple provides to authorized Payment Service Providers (PSPs) and their partners. This entitlement allows your app to use Apple's ProximityReader framework to accept contactless payments directly on iPhone. This guide covers the entitlement request process, configuration steps, and verification procedures based on [Apple's official documentation](https://developer.apple.com/documentation/proximityreader/setting-up-the-entitlement-for-tap-to-pay-on-iphone). **What you learn** In this guide, you'll learn: * How to request the Tap to Pay entitlement from Apple * How to configure the entitlement in your Apple Developer account * How to add the entitlement to your Xcode project * How to verify the entitlement is properly configured * How to handle entitlement requirements for development and distribution **Prerequisites** Before you begin, ensure you have: * **Apple Developer Account** (organization-level account required) * **Account Holder Access** (entitlement requests must be made by the account holder) * **PSP Partnership** (your organization must be an authorized Payment Service Provider or partner) * **Xcode 16.3 or later** (for project configuration) * **App ID Created** (your app identifier must be registered in Apple Developer) * **Sandbox Apple Account signed in on test device** (dedicated iPhone running Developer Mode for entitlement validation) **Keep a Test iPhone Ready**: Entitlement validation requires running builds on a physical test device signed in with your Sandbox Apple Account. Do not rely on production Apple IDs for these flows. ## Requesting the Entitlement The Tap to Pay on iPhone entitlement must be requested through Apple's developer portal. This process is handled by Apple and requires approval. **1. Access Apple Developer Portal** 1. **Log in to your Apple Developer account** as the account holder 2. **Navigate to Certificates, Identifiers & Profiles** 3. **Select your organization** if you have multiple accounts **Important**: Only the account holder can request the Tap to Pay entitlement. Team members or admins cannot submit this request. **2. Request Tap to Pay Entitlement** 1. **Go to Identifiers** section 2. **Select your App ID** (or create one if needed) 3. **Navigate to Additional Capabilities** 4. **Find "Tap to Pay on iPhone"** in the list of capabilities 5. **Click "Request"** or "Enable" to submit your request **3. Wait for Approval** Apple will review your request and typically respond within **one to two business days**. You'll receive an email notification when the entitlement is approved or if additional information is needed. **Processing Time**: The approval process typically takes 1-2 business days. You must start with the development certificate to access Apple's CERT environment. **4. Verify Entitlement Status** Once approved: 1. **Return to Certificates, Identifiers & Profiles** 2. **Select your App ID** 3. **Check Additional Capabilities** 4. **Verify "Tap to Pay on iPhone"** appears under Managed Capabilities The entitlement will now be available for use in your provisioning profiles. ## Configure Your App ID After receiving approval, configure your App ID to include the entitlement: ### Enable the Capability 1. **Navigate to Certificates, Identifiers & Profiles > Identifiers** 2. **Select your App ID** 3. **Scroll to Additional Capabilities** 4. **Enable "Tap to Pay on iPhone"** 5. **Save your changes** The capability will now be available for your App ID and can be included in provisioning profiles. ## Add Entitlement to Your Xcode Project Once the entitlement is approved and configured in your Apple Developer account, add it to your Xcode project: **1. Create Entitlements File** 1. **Open your project in Xcode** 2. **Select your project** in the Project Navigator 3. **Choose File > New > File** 4. **Select Property List** under Resource 5. **Name the file** `[YourProjectName].entitlements` 6. **Add it to your app target** **2. Configure Build Settings** 1. **Select your project** in the Project Navigator 2. **Select your app target** 3. **Go to Build Settings tab** 4. **Search for "Code Signing Entitlements"** 5. **Set the path** to `[YourProjectName].entitlements` **3. Add Entitlement Key** 1. **Open the `.entitlements` file** in Xcode 2. **Add the following key-value pair**: ```xml com.apple.developer.proximity-reader.payment.acceptance ``` The file should look like this: ```xml com.apple.developer.proximity-reader.payment.acceptance ``` **4. Update Provisioning Profile** 1. **In Xcode, go to Signing & Capabilities** 2. **Select your development team** 3. **Xcode will automatically download** a new provisioning profile that includes the entitlement 4. **Verify the entitlement** appears in the Capabilities section **Note**: If Xcode doesn't automatically update the provisioning profile, you may need to manually regenerate it in the Apple Developer portal. ## Verify Entitlement Configuration After configuring the entitlement, verify it's properly set up: ### Check in Xcode 1. **Select your project** in Xcode 2. **Select your app target** 3. **Go to Signing & Capabilities tab** 4. **Verify "Tap to Pay on iPhone"** appears in the Capabilities section ### Verify in Code You can programmatically verify the entitlement is present by checking the `readerIdentifier` property: ```swift import ProximityReader do { let readerIdentifier = try await PaymentCardReader().readerIdentifier print("Entitlement verified: (readerIdentifier)") // Entitlement is present and valid } catch { print("Entitlement error: (error)") // Handle notAllowed error if entitlement is missing } ``` If the entitlement is missing, `readerIdentifier` will throw a `notAllowed` error. ## Development vs Distribution Entitlements The Tap to Pay entitlement has different requirements for development and distribution: ### Development Entitlement * **Purpose**: Internal testing and development * **Provisioning**: Development provisioning profiles * **Distribution**: Ad-hoc distribution via .ipa files * **Testing**: Limited to registered test devices ### Distribution Entitlement * **Purpose**: TestFlight and App Store distribution * **Provisioning**: Distribution provisioning profiles * **Distribution**: TestFlight beta testing and App Store submissions * **Testing**: Available to all TestFlight testers and App Store users **Important**: If you've already received the development entitlement and need to distribute via TestFlight or the App Store, you must request the distribution entitlement separately. Respond to the original approval email from Apple to request the distribution entitlement. ## Troubleshooting ### Entitlement Not Appearing If the entitlement doesn't appear in your Apple Developer account: * **Verify account holder status**: Only the account holder can request entitlements * **Check PSP partnership**: Ensure your organization is authorized as a PSP or partner * **Contact Apple**: Reach out to Apple Developer Support if the entitlement is not available ### Entitlement Not Working in Xcode If the entitlement doesn't work in Xcode: * **Verify provisioning profile**: Ensure your provisioning profile includes the entitlement * **Check entitlements file**: Verify the `.entitlements` file contains the correct key * **Update provisioning profile**: Regenerate your provisioning profile in Apple Developer portal * **Clean build folder**: In Xcode, go to Product > Clean Build Folder ### Reader Identifier Returns Error If `readerIdentifier` throws a `notAllowed` error: * **Verify entitlement in Apple Developer**: Check that the entitlement is approved and enabled * **Check provisioning profile**: Ensure the profile includes the Tap to Pay capability * **Verify device registration**: For development, ensure test devices are registered * **Check code signing**: Verify your app is signed with the correct provisioning profile ## Additional Resources * [Apple's Tap to Pay Documentation](https://developer.apple.com/documentation/proximityreader/setting-up-the-entitlement-for-tap-to-pay-on-iphone) * [ProximityReader Framework Reference](https://developer.apple.com/documentation/proximityreader) * [Apple Developer Portal](https://developer.apple.com/account) ## See also * [Adding Support for Tap to Pay on iPhone](/docs/setting-up-the-ios-sdk/adding-support-for-tap-to-pay-on-iphone) - Complete implementation guide * [Installing the SDK](/docs/setting-up-the-ios-sdk/installing-the-sdk) - Set up the Koard iOS SDK * [Creating a Sandbox Apple Account](/docs/setting-up-the-ios-sdk/creating-a-sandbox-apple-account) - Prepare dedicated testers * [Getting Ready for Production](/docs/setting-up-the-ios-sdk/getting-ready-for-production) - Configure schemes and API keys * [Developing with Apple](/docs/appendix/developing-with-apple) - Security and environment guidelines # Installing the SDK Install the Koard Merchant SDK to enable tap-to-pay functionality in your Android application. If you're ready to start developing, see our Android SDK implementation guide. [Get started with Koard](/docs/getting-started-with-koard/introduction) **What you learn** In this guide, you'll learn: * How to add the Koard Merchant SDK to your Android project * How to configure Gradle dependencies * How to initialize the SDK in your application * How to verify your installation is working correctly **Prerequisites** Before you begin, ensure you have: * **Android Studio Hedgehog or newer** (required for modern Gradle support) * **JDK 21** (required for compilation) * **Android SDK 36** (minimum SDK 31 - Android 12) * **Physical Android device with NFC support** (Android 12+) — see [Supported Devices & NFC Tap Location](/docs/guides/android-sdk/details/supported-devices) * **Valid Koard merchant account** (configured in Koard MMS) * **Install the Visa Kernel app** on every NFC-enabled test device before running tap-to-pay flows (download from the Google Play Store) **Physical Device Required**: Tap to Pay on Android requires a physical device with NFC hardware. The Android emulator does not support NFC contactless payments. **Developer Mode Workflow**: When working with Tap to Pay on Android, follow this important workflow: 1. **Enable Developer Mode** - Turn on developer mode to install and test app revisions 2. **Install/Update Your App** - Deploy your application updates 3. **Disable Developer Mode** - You MUST turn off developer mode before running tap to pay transactions 4. **Run Transactions** - Process payments with developer mode disabled Tap to Pay transactions will NOT work if developer mode is enabled. This is a security requirement from the payment processor. ## Step 1: Add the SDK Dependency The Koard Android SDK is published to **Maven Central**. The current release is `com.koard:koard-android-sdk:1.0.6` (packaging `aar`). No credentials and no custom repository are required — you only need `mavenCentral()` in your repositories, which most Android projects already have. ### 1a. Ensure Maven Central is a repository Most Android projects already resolve dependencies from Maven Central. If yours doesn't, add `mavenCentral()` to `dependencyResolutionManagement` in your **`settings.gradle.kts`**: ```kotlin dependencyResolutionManagement { repositories { google() mavenCentral() } } ``` ### 1b. Add the dependency Add the Koard SDK to your **app module's `build.gradle.kts`**: ```kotlin dependencies { implementation("com.koard:koard-android-sdk:1.0.6") } ``` ### 1c. Declare the libraries the SDK does _not_ bring in The 1.0.6 artifact is a **shaded fat AAR**. OkHttp, Okio, Retrofit, kotlinx.serialization, and Ktor are packaged _inside_ the AAR (relocated under `com.koardlabs.*`) and are **deliberately dropped from the published POM** — this isolates the SDK from whatever versions your app uses. The published POM declares only: | Dependency | Version | | ------------------------------------------------ | ------- | | `org.jetbrains.kotlin:kotlin-stdlib` | 2.2.21 | | `org.jetbrains.kotlin:kotlin-parcelize-runtime` | 2.2.21 | | `androidx.security:security-crypto` | 1.1.0 | | `com.google.android.gms:play-services-safetynet` | 18.0.1 | | `com.nimbusds:nimbus-jose-jwt` | 10.0.2 | **Nothing else resolves transitively.** AndroidX, Kotlin coroutines, and any networking or serialization library your own code uses must be declared in your app module. If you previously relied on the SDK to pull OkHttp/Retrofit/kotlinx.serialization in for you (1.0.5 and earlier shipped them as ordinary transitive dependencies), declare them directly now. ```kotlin dependencies { implementation("com.koard:koard-android-sdk:1.0.6") // Declare what YOUR code needs — the SDK no longer supplies these transitively. implementation("androidx.core:core-ktx:1.12.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") } ``` ### Fallback: Manual AAR Installation If you have been supplied the SDK as a raw `.aar` file (for example, an offline or pre-release build), you can side-load it instead of resolving from Maven Central: 1. **Create a `libs` directory** in your app module if it doesn't exist 2. **Copy the AAR file** (e.g., `koard-android-release.aar`) to the `libs/` directory 3. **Add the dependency** in your `build.gradle.kts`: ```kotlin dependencies { implementation(files("libs/koard-android-release.aar")) // A raw file dependency carries no POM, so declare everything the SDK // would otherwise pull in — plus whatever your own code needs. // The five dependencies the published POM declares: implementation("org.jetbrains.kotlin:kotlin-stdlib:2.2.21") implementation("org.jetbrains.kotlin:kotlin-parcelize-runtime:2.2.21") implementation("androidx.security:security-crypto:1.1.0") implementation("com.google.android.gms:play-services-safetynet:18.0.1") implementation("com.nimbusds:nimbus-jose-jwt:10.0.2") // Plus whatever your own code needs: implementation("androidx.core:core-ktx:1.12.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") } ``` Do **not** add OkHttp, Retrofit, Okio, kotlinx.serialization, or Ktor "for the SDK" — they are already shaded inside the AAR under `com.koardlabs.*`. Add them only if _your own_ code uses them; the shaded copies will not conflict with yours. ## Step 2: Configure AndroidManifest.xml Permissions Add the required permissions, the `` tag, and the NFC intent filter to your `AndroidManifest.xml`: ```xml ``` The `` tag declares visibility to the Tap to Pay kernel app (`com.visa.kic.app.kernel`), which is required for the SDK to detect and communicate with the Visa kernel during tap-to-pay flows. **Where each permission comes from.** `INTERNET`, `ACCESS_NETWORK_STATE`, `NFC`, and `WAKE_LOCK` are declared in the SDK's own manifest and merged into your app, but declaring them explicitly keeps your permission surface auditable. `ACCESS_WIFI_STATE` and `REBOOT` are **required by Visa's Kernel-in-Cloud (KiC) integration spec** (§3.3.3, "mandatory minimum permissions") for apps that integrate the Tap to Pay Ready kernel — they are **not** contributed by the Koard SDK, so you must declare them yourself. The SDK also contributes `` and `android.hardware.nfc.hce`, both with `required="false"`. The `` tag and the NFC intent filter are likewise not contributed by the SDK. If your activity sets `android="portrait"`, also declare `` so Google Play does not filter out POS devices (Sunmi D3, iMin Swan 1 Pro, and similar) that do not report that feature. ### Add the NFC Tech Filter Resource Create `res/xml/nfc_tech_filter.xml` referenced by the `meta-data` above: ```xml android.nfc.tech.IsoDep ``` ## Step 3: Configure Build Settings ### Set Minimum SDK Version Ensure your app's minimum SDK is set to Android 12 (API level 31): ```kotlin android { compileSdk = 36 // belongs on `android { }`, NOT inside defaultConfig defaultConfig { minSdk = 31 targetSdk = 36 } } ``` ### Configure Java Compatibility Set Java version to 21: ```kotlin android { compileOptions { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } kotlinOptions { jvmTarget = "21" } } ``` ### Add Build Flavors (Optional) The SDK ships exactly two built-in environments — `KoardEnvironment.UAT` and `KoardEnvironment.PROD` — so mirror the demo app and define one flavor per environment: ```kotlin android { flavorDimensions += "environment" productFlavors { create("uat") { dimension = "environment" isDefault = true buildConfigField("String", "ENVIRONMENT", "\"UAT\"") applicationIdSuffix = ".uat" // UAT: https://api.uat.koard.com } create("prod") { dimension = "environment" buildConfigField("String", "ENVIRONMENT", "\"PROD\"") // Production: https://api.koard.com } } } ``` There is no `dev` environment constant. To target a non-standard backend, use `KoardEnvironment.Custom(koardApiUrl, enrollmentUrl, visaCloudPosUrl, name)` rather than adding a `dev` flavor that has nothing to map onto. ## Step 4: Initialize the SDK ### Create Application Class Initialize the SDK in your `Application.onCreate()` method on a worker thread: ```kotlin import android.app.Application import com.koardlabs.merchant.sdk.KoardMerchantSdk import com.koardlabs.merchant.sdk.domain.KoardEnvironment import com.koardlabs.merchant.sdk.domain.KoardLogLevel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext class MyApplication : Application() { override fun onCreate() { super.onCreate() // Initialize SDK synchronously during app startup runBlocking { withContext(Dispatchers.IO) { try { // The only environments are UAT, PROD, and Custom(...) val environment = KoardEnvironment.PROD KoardMerchantSdk.initialize( application = this@MyApplication, apiKey = "your-api-key", environment = environment, timeoutSeconds = 30L, logLevel = KoardLogLevel.DEBUG ) println("Koard SDK initialized successfully") } catch (e: Exception) { // Handle initialization error println("Failed to initialize SDK: ${e.message}") } } } } } ``` **Important**: * SDK initialization must be performed on a worker thread (not the main UI thread). Always use `Dispatchers.IO` with coroutines. * The `initialize()` method takes `application`, `apiKey`, `environment`, and the optional `timeoutSeconds` (default `30L`) and `logLevel` - NOT merchantCode/merchantPin. * Merchant authentication is done separately using `login(merchantCode, merchantPin)` after initialization. * `logLevel` is the **only** place SDK logging verbosity can be set — there is no runtime setter. It defaults to `KoardLogLevel.DEBUG` in debug builds and `KoardLogLevel.NONE` in release. ### Register Application in Manifest Add your custom Application class to `AndroidManifest.xml`: ```xml ``` ## Step 5: Register Your Activity for NFC Android dispatches `TECH_DISCOVERED` to whatever app claims it, so an unrelated NFC app can jump into the foreground the moment the customer presents a card. `registerActivityForNfc(this)` claims NFC foreground dispatch for your activity so it stays in front for the duration of the tap — including the receipt or animation screens that follow, while the card may still be in the field (KiC integration guide §3.4.14). Call it from `onResume()` and `unregisterActivityForNfc(this)` from `onPause()`. Both are suspend functions that must run on a worker thread. Visa marks both APIs **Implementation: Optional** (§3.3.15.1, §3.3.15.2) — a tap can complete without them — but skipping them leaves the tap exposed to being interrupted by another app, so Koard recommends wiring them up on every activity that can host a tap. ```kotlin class MainActivity : ComponentActivity() { private val nfcMutex = Mutex() override fun onResume() { super.onResume() lifecycleScope.launch(Dispatchers.IO) { nfcMutex.withLock { KoardMerchantSdk.getInstance().registerActivityForNfc(this@MainActivity) } } } override fun onPause() { lifecycleScope.launch(Dispatchers.IO) { nfcMutex.withLock { KoardMerchantSdk.getInstance().unregisterActivityForNfc(this@MainActivity) } } super.onPause() } } ``` **Guard the pair with a mutex** (as the demo app does) so a rapid resume/pause cycle cannot interleave the two calls. Also add the `TECH_DISCOVERED` intent filter and the `@xml/nfc_tech_filter` tech-list to the same activity in your manifest — per KiC §3.4.14 the registration APIs are meant to be used alongside that manifest declaration, and it is not contributed by the Koard SDK. `unregisterActivityForNfc()` is safe to call from `onPause()` even during a tap: the SDK detects an in-flight tap and skips the unregister. This matters because the Visa kernel foregrounds its own secure Online-PIN keypad, which pauses your activity — tearing down the registration there would abort PIN entry and decline the sale. ## Step 6: Authenticate Merchant After SDK initialization, authenticate the merchant using their credentials: ```kotlin import androidx.lifecycle.lifecycleScope import com.koardlabs.merchant.sdk.KoardMerchantSdk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launch(Dispatchers.IO) { try { val sdk = KoardMerchantSdk.getInstance() // Login with merchant credentials val success = sdk.login( merchantCode = "your-merchant-code", merchantPin = "your-merchant-pin" ) if (success) { println("Merchant authenticated successfully") } else { println("Authentication failed") } } catch (e: Exception) { println("Error: ${e.message}") } } } } ``` The `login()` method authenticates the merchant and retrieves an access token. This must be done before processing transactions. `login()` returns `false` rather than throwing when the credentials are rejected. If your integration has already resolved the merchant identity into a single opaque string (a QR scan, an SSO callback, or a server-issued provisioning token), use the `login(alias: String): Boolean` overload instead. It hits the same login route and yields the same session token; the alias itself is never stored. Call `logout()` to end the session. Logout clears the session token and the active location but **preserves enrollment**, so the same merchant can log back in without re-enrolling. Switching the device to a _different_ merchant requires an explicit `unenrollDevice()` first. ### Test Basic Import Add this import statement to verify the SDK is accessible: ```kotlin import com.koardlabs.merchant.sdk.KoardMerchantSdk import com.koardlabs.merchant.sdk.domain.KoardEnvironment import com.koardlabs.merchant.sdk.domain.KoardLocation import com.koardlabs.merchant.sdk.domain.exception.KoardException ``` If the imports succeed without errors, your installation is working correctly. ## Step 7: Select a Location and Enroll the Device **Both of these steps are mandatory.** Enrollment is **not** automatic — nothing happens as a side effect of `login()`. Until the device has an active location _and_ is enrolled, `sale()`, `preauth()`, `completePartialAuth()`, and `refundEmv()` throw `KoardException` with `error.errorType` of `KoardErrorType.NotReady.NoActiveLocation` or `KoardErrorType.NotReady.NotEnrolled`. The order is fixed: authenticate, choose a location, then enroll. ```kotlin import com.koardlabs.merchant.sdk.KoardMerchantSdk import com.koardlabs.merchant.sdk.domain.exception.KoardException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext suspend fun setUpDevice() = withContext(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() // 1. Fetch the merchant's locations and pick one. val locations = sdk.getLocations().getOrElse { error -> println("Could not load locations: ${error.message}") return@withContext } val location = locations.firstOrNull() ?: return@withContext try { // 2. Set the active location. Backend-authoritative: it re-binds the // terminal on the backend and only commits locally if that succeeds. sdk.setActiveLocation(location.id) // 3. Enroll the device. Returns a String status; throws on failure. val status = sdk.enrollDevice() println("Enrollment status: $status") } catch (e: KoardException) { println("Setup failed: ${e.error.shortMessage} (${e.error.errorType})") } } ``` ### `setActiveLocation(locationId)` * Suspend, worker thread only, returns `Unit` and **throws** on failure — it no longer silently no-ops. * On an unenrolled device it simply records the selection and flips readiness to `hasActiveLocation = true`. It does **not** enroll for you; you still have to call `enrollDevice()`. * If the device is **already enrolled** and you switch to a _different_ location, the SDK re-binds the terminal on the backend first and commits the new location locally only if the backend approves. A failed re-bind throws and leaves the previous location intact. It then pushes the new location's terminal/CVM profile down to the kernel. * Switching locations on an enrolled device **throws `InvalidRequest` when developer mode is on** — the Visa kernel will not accept a new profile in that state, so the SDK refuses the switch rather than half-applying it. * During the switch, readiness reports `NotReady.Preparing`, so a tap started mid-switch is rejected instead of running against a terminal that is still moving. Observe the resolved location reactively rather than caching your own copy — a private cache survives logout and goes stale, which blocks the next enrollment with "no active location set": ```kotlin sdk.activeLocation.collect { location -> // StateFlow updateUi(location?.name) } // On a cold start, resolve the persisted id into a full object once: sdk.refreshActiveLocation() // suspend, returns KoardLocation? sdk.activeLocationId // String? — the raw persisted id ``` ### `enrollDevice()` `suspend fun enrollDevice(): String` — worker thread only. It returns a status **String** and throws `KoardException` when it cannot proceed. Before calling it, the SDK refreshes its readiness checks and rejects the call when: * **Developer mode is enabled** — `InvalidRequest`, "Disable developer mode before enrolling device." * **The device is already enrolled** — `InvalidRequest`, "Clear enrollment data before enrolling again." Call `clearEnrollmentState()` (local only) or `unenrollDevice()` (clears the enrollment data held by the Tap to Pay Ready app, then clears local state) first. Note that `unenrollDevice()` does **not** fully deprovision the device on Visa's backend — see [Troubleshooting](/docs/guides/android-sdk/details/troubleshooting#tamperdetected-on-every-sale-after-a-failed-enrollment). * **No active location is set** — enrollment requires one, which is why Step 7 runs in this order. Because it is a user-visible, failure-prone operation, surface it as an explicit action in your UI (the demo app renders an **Enroll Device** button whenever `readinessState.enrollmentState` is not `Enrolled`) rather than firing it silently on login. Track progress through `sdk.readinessState`, whose `enrollmentState` moves `NotEnrolled` → `Enrolling` → `Enrolled` (or `Failed`). Use `readiness.notReadyReason()` to get the typed blocking reason without attempting a transaction. ## Step 8: Transaction Processing With installation complete you can now run Tap to Pay sessions. Keep these guardrails in mind: * `sdk.sale(...)`, `sdk.preauth(...)`, `sdk.completePartialAuth(...)`, `sdk.refundEmv(...)`, and `sdk.prepare()` are the APIs that drive a reader session, so they return a cold `Flow` that drives the reader UI. Other calls reach the thin client too (`isKernelAppInstalled()`, `checkKiCEligibility()`, `getTransactionConfig()`, `cancelTransaction()`, `resetKernelService()`, `unenrollDevice()`, the NFC registration pair), but they are one-shot and emit no reader events. * `sdk.refund(...)` is **backend-only** — it returns `Result` and never involves a tap. Use `sdk.refundEmv(...)` when the customer must present a card. * Every other payment action (capture, reverse, incremental auth, receipts, tip adjustment) is a suspend function that returns a `Result` and never emits reader events. * The SDK enforces readiness (`sdk.readinessState.value.isReadyForTransactions`) and blocks taps if the required tap-to-pay dependency is missing, developer mode is on, the device is not enrolled, or no active location is set. Rather than duplicating code here, the dedicated [Running Payments](/docs/setting-up-the-android-sdk/running-payments) guide shows: * How the demo’s `MainScreenViewModel` builds `PaymentBreakdown`, collects the Flow, and updates UI state * Mapping `KoardReaderStatus` + reader status codes to your own UX * Handling surcharge confirmation, canceling the reader, and orchestrating post-reader calls (capture/refund/reverse/incremental/tip adjust/receipt) Use that guide as the canonical reference when wiring payments into your application. ## Troubleshooting ### Common Installation Issues **SDK Not Initialized Error** ```plaintext IllegalStateException: Instance is null. Did you forget to call initialize? ``` **Solution**: Ensure `KoardMerchantSdk.initialize()` is called in `Application.onCreate()` on a worker thread before accessing `getInstance()`. **Thread Enforcement Error** **Solution**: All SDK operations must be called from a worker thread. Use `Dispatchers.IO`: ```kotlin lifecycleScope.launch(Dispatchers.IO) { // SDK operations here } ``` **Dependency Conflicts** **Solution**: Ensure you're using compatible versions of AndroidX and Kotlin libraries. Check for dependency conflicts with: ```bash ./gradlew app:dependencies ``` **Build Errors** **Solution**: * Verify you're using JDK 21 * Ensure minimum SDK is set to 31 (Android 12) * Clean and rebuild: `./gradlew clean build` **NFC Transactions Failing** **Solution**: * **Disable developer mode before running transactions** - This is required for security * Developer mode blocks tap to pay functionality * Workflow: Enable dev mode → Install app → Disable dev mode → Run transactions * Go to **Settings > System > Developer Options** and toggle OFF * Restart device after disabling developer mode ### Verification Checklist * [ ] Android Studio Hedgehog or newer installed * [ ] JDK 21 configured * [ ] Minimum SDK set to 31 (Android 12) * [ ] `mavenCentral()` present in repositories * [ ] SDK dependency added to build.gradle.kts * [ ] Application class created and registered * [ ] SDK.initialize() called on worker thread * [ ] Required permissions added to manifest (INTERNET, ACCESS\_NETWORK\_STATE, ACCESS\_WIFI\_STATE, REBOOT, NFC, WAKE\_LOCK) * [ ] `` tag added with `com.visa.kic.app.kernel` * [ ] NFC `TECH_DISCOVERED` intent filter and `@xml/nfc_tech_filter` meta-data added to your tap activity * [ ] `registerActivityForNfc()` / `unregisterActivityForNfc()` wired into `onResume()` / `onPause()` * [ ] `setActiveLocation(locationId)` called after login * [ ] `enrollDevice()` exposed as an explicit user action and completed successfully * [ ] Project builds without errors * [ ] Import statements work correctly * [ ] Tap-to-pay kernel dependency installed on test device (Visa Tap to Pay Ready) * [ ] Physical device with NFC available for testing * [ ] Aware of developer mode workflow (enable → install → disable → run transactions) ## Next Steps Once the SDK is installed and configured: * [Run the Demo App](/docs/setting-up-the-android-sdk/demo) - Test SDK functionality with the demo application * [Supported Devices & NFC Tap Location](/docs/setting-up-the-android-sdk/supported-devices) - Compatible devices, requirements, and where to tap * [Running Payments](/docs/setting-up-the-android-sdk/running-payments) - Implement tap-to-pay flows, surcharging, and post-reader actions * [SDK Response Codes](/docs/setting-up-the-android-sdk/sdk-response-codes) - Understand transaction outcomes, display messages, and error handling * [Troubleshooting](/docs/setting-up-the-android-sdk/troubleshooting) - Fix enrollment failures and taps that cancel instantly * [Understand Payment Lifecycle](/docs/payments/payment-lifecycle) - Learn about the complete payment flow ## See also This wraps up the SDK installation. See the links below for next steps in your integration: * [Demo App Setup](/docs/setting-up-the-android-sdk/demo) - Run the demo application * [SDK Response Codes](/docs/setting-up-the-android-sdk/sdk-response-codes) - Transaction outcomes, error codes, and display messages * [Payment Lifecycle](/docs/payments/payment-lifecycle) - Complete payment flow guide * [Supported Devices & NFC Tap Location](/docs/setting-up-the-android-sdk/supported-devices) - Device compatibility and tap guidance * [Troubleshooting](/docs/setting-up-the-android-sdk/troubleshooting) - Device-side fixes for enrollment and tap failures ## Best Practices * **Idempotency**: Generate a unique `eventId` (UUID) for every transaction mutation (sale, refund, adjust) and persist it with your POS records. * **Threading**: Always hop to `Dispatchers.IO` before calling any SDK method; return results to the main thread only after the call completes. * **Environment Flavors**: Mirror the demo’s `uat`/`prod` flavors so each build points at the correct Koard environment and credential set. Use `KoardEnvironment.Custom(...)` for anything else. * **Logging**: The SDK writes to Logcat under the fixed tag **`KoardSDK`**. Set verbosity once via `initialize(..., logLevel = KoardLogLevel.VERBOSE)` — there is no runtime setter, and the SDK no longer depends on Timber. Secrets (API key and token headers, device certificates, enrollment key material) are never logged at any level. * **Device Re-provisioning**: When wiping devices or reinstalling the tap-to-pay kernel app, call `refreshDeviceCertificates()` to regenerate keys and rerun enrollment. # Tax and Tip Handling Koard resolves **tax** settings hierarchically, and handles **tips** either at the time of sale or as a later adjustment. > Surcharge settings (`surcharge_rate`, `surcharge_basis`, `surcharge_confirmation_required`) follow the same account → location → terminal hierarchy described below, but are documented on the [Surcharging](/payments/surcharging) page. ## Hierarchical tax defaults `tax_rate` and `tax_basis` can be configured at **three** levels — the **account**, the **location**, and the **terminal**: | Field | Meaning | | ----------- | ------------------------------ | | `tax_rate` | Tax percentage. | | `tax_basis` | What the tax is calculated on. | **Resolution — most specific wins.** For a given transaction, Koard uses the value set on the **terminal** if present; otherwise it falls back to the **location**, then to the **account**. A `null` at a more specific level means "inherit." Set the broad default once on the account. Only set the field on a location or terminal when it needs to differ from the level above it. `tax_rate` and `tax_basis` here are **configuration defaults** (resolved from the account -> location -> terminal hierarchy). They are separate from the **transaction-breakdown** fields returned on a completed transaction: `taxAmount` (the computed tax, in **cents**) and `taxRate` (the rate actually applied to that transaction; see the transaction breakdown schema for its exact representation). Don't conflate the config input (`tax_rate`, a percentage) with the computed breakdown output (`taxRate` / `taxAmount`). ## Tip handling Tips are captured in one of two modes, tracked on the transaction as `tip_capture_mode`: | Mode | When | Notes | | ------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------- | | **Tip at sale** (`tip_at_sale`) | The tip is included on the original sale. | Any non-tip-adjust transaction that carries a tip. | | **Tip adjust** (`tip_adjust`) | The tip is added/changed **after** the sale (e.g. restaurant tip-on-receipt). | A dedicated `tip_adjust` transaction referencing the original. | Each carries a `tip_amount` and `tip_type` (e.g. `percentage` or a fixed amount). **Tip adjust does not apply surcharge.** A tip adjustment only changes the tip on an existing transaction — it does not recompute or add a surcharge. Surcharge is applied on the **sale**, independently of tips (see [Surcharging](/payments/surcharging)). --- title: Refund --- # Refund A refund returns funds to the cardholder **after** settlement. Use it for post-settlement returns, partial returns, or customer disputes. > For voiding a transaction **before** settlement, see [Reverse](reverse.md). ## Full Refund **iOS:** ```swift let response = try await KoardMerchantSDK.shared.refund( transactionId: transactionId, amount: 12875, eventId: UUID().uuidString ) ``` **Android:** ```kotlin sdk.refund( transactionId = transactionId, amount = 12875, eventId = UUID.randomUUID().toString() ) ``` ## Partial Refund Refund a portion of the original transaction: **iOS:** ```swift let response = try await KoardMerchantSDK.shared.refund( transactionId: transactionId, amount: 5000, // refund $50 of the original eventId: UUID().uuidString ) ``` **Android:** ```kotlin sdk.refund( transactionId = transactionId, amount = 5000, eventId = UUID.randomUUID().toString() ) ``` ## Tap-Based Refund (iOS) For card-present refunds where the customer taps their card: ```swift let response = try await KoardMerchantSDK.shared.refund( transactionId: transactionId, amount: 12875, tapBasedRefund: true, eventId: UUID().uuidString ) ``` > Tap-based refunds require the card reader to be prepared. The customer taps their card to confirm the refund. ## Surcharge Handling When refunding a surcharged transaction, the surcharge is **prorated automatically** by the processor. You do not need to pass a breakdown or calculate the surcharge portion—just pass the refund amount and the processor handles the rest. ## Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `transactionId` | `String` | Yes | Original transaction to refund | | `amount` | `Int` | Yes | Refund amount in minor units | | `eventId` | `String?` | No | Idempotency key | | `tapBasedRefund` | `Bool?` | No | iOS only—require card tap to confirm refund | | `activity` | `Activity` | Yes (Android) | Android activity for tap-based refunds | ## See Also - [Reverse](reverse.md) — Void before settlement - [Sale](sale.md) — Original payment - [Payment Lifecycle](payment-lifecycle.md) — End-to-end flow # Developing with Apple Comprehensive guidelines and best practices for developing Tap to Pay on iPhone applications with Koard and Apple's payment technologies. ## Overview Developing Tap to Pay on iPhone applications requires careful attention to security, environment configuration, and compliance with Apple's requirements. This guide covers essential security measures, environment setup, and testing procedures to ensure a secure and compliant integration. ## Security Considerations ### Cardholder Data Protection Given the sensitive nature of cardholder data processed through Tap to Pay on iPhone, Koard has implemented multiple security measures to ensure secure development and operation: * **Secure SDK Architecture**: All card data is handled securely through Apple's ProximityReader framework * **Encrypted Communication**: All API communications use industry-standard encryption * **Tokenization**: Sensitive payment data is tokenized and never stored in plain text * **PCI DSS Compliance**: Koard maintains PCI DSS Level 1 compliance standards ### Entitlement Verification Before initiating any Tap to Pay functionality, verify that your application has the correct Tap to Pay on iPhone entitlement. **Check Entitlement Status** You can verify the entitlement by accessing the `readerIdentifier` property. If the app is missing the required entitlement, `readerIdentifier` will throw a `notAllowed` error. ```swift import ProximityReader do { let readerIdentifier = try await ProximityReader.readerIdentifier // Entitlement is present and valid } catch { // Handle notAllowed error if entitlement is missing print("Entitlement error: \(error)") } ``` **Important**: Ensure your SDK returns an appropriate error if the entitlement is missing, and handle this error gracefully in your application. ## Environment Configuration ### Production and Certificate Environments Koard provides two distinct environments for development and production use: #### Certificate Environment (CERT) The Certificate environment is designed for development and testing purposes. Use this environment when: * Developing internally with your team * Sharing builds with internal team members without a distribution certificate * Testing payment flows without processing real transactions * Conducting integration testing before production deployment **Best Practice**: Always use the Certificate environment for internal development and testing. This ensures that test transactions remain isolated from production payment processing. #### Production Environment The Production environment is used for live merchant transactions. All devices connect to the Koard production environment by default when configured for production use. **Note**: All customers working directly with Koard will have access to both environments via their API key configuration. ### Environment Selection To switch between environments, configure your SDK initialization: ```swift let options = KoardOptions( environment: .cert, // or .production loggingLevel: .debug ) KoardMerchantSDK.shared.initialize( options: options, apiKey: "your-api-key" ) ``` ## Sandbox Testing ### Sandbox Tester Account Setup If your SDK or API developers need to connect to Apple's Certificate environment through your test environment, you must create a Sandbox Tester Account. This account allows you to test Tap to Pay functionality without processing real transactions. ### Creating a Sandbox Tester Account Follow these steps to create a sandbox tester account: 1. **Sign in to App Store Connect** * Navigate to [App Store Connect](https://appstoreconnect.apple.com) * Sign in with your Apple Developer account credentials 2. **Access Sandbox Testers** * On the homepage, click **Users and Access** * In the top navigation, click **Sandbox** * Click the add button (+) * If this is your first time adding sandbox testers, click **Create Test Accounts** 3. **Complete Tester Information** * Enter a first and last name for your tester * Enter an email address that: * Has not been used as an Apple Account * Has not been used to purchase iTunes or App Store content * Consider creating a dedicated email address for each sandbox tester * Enter a strong password that meets Apple's requirements * Choose an App Store country or region 4. **Email Subaddressing (Optional)** If your email service provider supports email subaddressing with a plus sign (+), you can use subaddresses of a sandbox-specific address for multiple testers. For example: * Base email: `billjames2@icloud.com` * Subaddresses: `billjames2+UK@icloud.com`, `billjames2+US@icloud.com`, `billjames2+JP@icloud.com` All communications sent to the subaddresses are also sent to the base address. 5. **Invite the Tester** * Review all information * Click **Invite** to complete the setup 6. **Configure Testing Devices** * Sign out of your Apple Account on all testing devices * Sign back in with your new sandbox tester account **Important Notes**: * Once you create a tester, you cannot edit the name, email, or password * Each test account is associated with one of 175 App Store storefronts * You can edit a tester's App Store country or region after creation to test on different storefronts using the same Sandbox account ### Additional Resources For more detailed information on creating sandbox tester accounts, see [App Store Connect Help: Create a sandbox tester account](https://help.apple.com/app-store-connect/#/dev8b57d558e). ## Best Practices ### Development Workflow 1. **Use Certificate Environment** for all internal development and testing 2. **Verify Entitlements** before attempting to use Tap to Pay functionality 3. **Implement Error Handling** for missing entitlements and other error conditions 4. **Test Thoroughly** using sandbox tester accounts before production deployment ### Security Guidelines * Never log or store sensitive cardholder data * Implement proper error handling without exposing sensitive information * Use secure communication protocols (HTTPS/TLS) * Follow Apple's security guidelines for payment applications * Regularly update your SDK to the latest version for security patches ## Testing Checklist Before deploying to production, ensure: * [ ] Application has Tap to Pay on iPhone entitlement configured * [ ] Entitlement verification is implemented and tested * [ ] Error handling for missing entitlements is in place * [ ] Sandbox tester accounts are created and configured * [ ] Testing is performed in Certificate environment * [ ] All payment flows are tested with test cards * [ ] Production environment is properly configured before go-live ## Support and Resources ### Apple Documentation * [Apple Pay Developer Guide](https://developer.apple.com/apple-pay/) * [ProximityReader Framework Reference](https://developer.apple.com/documentation/proximityreader) * [App Store Connect Help](https://help.apple.com/app-store-connect/) * [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/) ### Koard Resources * [Setting Up the Entitlement](/docs/setting-up-the-ios-sdk/setting-up-the-entitlement-for-tap-to-pay-on-iphone) * [SDK Installation Guide](/docs/setting-up-the-ios-sdk/installing-the-sdk) * [Payment Lifecycle Guide](/docs/guides/payments/details/payment-lifecycle.md) * [Test Cards Reference](/docs/appendix/resources#resources__test-cards) # KoardMerchantSDK Demo App ## Setup To run the demo app, you need to get the project and configure your API credentials: **1. Get the Demo Project:** * The demo app now lives in its own repository, separate from the SDK. * Clone the Git repository: `https://github.com/koardlabs/demos.git` * Or [download the ZIP file](https://github.com/koardlabs/demos/archive/refs/heads/main.zip) **2. Open the Project in Xcode:** * Open Xcode * Select **File > Open Existing Project** * Navigate to the demo's `KoardDemo.xcodeproj` and open it **3. Copy the template file to \`config.plist\`:** ```bash cp KoardMerchantSDK-Demo/Config.plist.template KoardMerchantSDK-Demo/Config.plist ``` **4. Edit the configuration:** Open `KoardMerchantSDK-Demo/Config.plist` and replace the placeholder values: * `YOUR_API_KEY_HERE` - Your [Koard API key](/docs/setting-up-the-ios-sdk/retrieving-your-api-key) * `YOUR_MERCHANT_CODE_HERE` - Your [merchant code](/docs/getting-started-with-koard/setting-up-the-merchant#setting-up-the-merchant__create-merchant-credentials) * `YOUR_MERCHANT_PIN_HERE` - Your [merchant PIN](/docs/getting-started-with-koard/setting-up-the-merchant#setting-up-the-merchant__create-merchant-credentials) ##### Configuration Format The `Config.plist` file should contain: ```xml apiKey your_api_key merchantCode your_merchant_code merchantPin your_merchant_pin ``` **5. Add to Xcode project:** * Open the Demo project in Xcode * Drag `Config.plist` into the project navigator * Ensure it's added to the app target **Important:** The app will crash on startup if Config.plist is missing or contains template values The demo app automatically reads credentials from Config.plist at startup. If the file is missing or contains placeholder values, the app will display an error message and fail to initialize. ## Running the Demo Build the Project: Build and run the project in Xcode. You should see a display like this: ![Demo Main Screen](/ios8.png) Hit Authenticate Merchant You will know if it was successful if you see this screen: ![Authenticate Merchant Success](/ios10.png) Next, hit Setup Card Reader Answer the prompts and agree to the terms and conditions as you see fit. If everything has been set up correctly, you will see this screen: ![Card Reader Setup Success](/ios11.png) Hit Process Sample Transaction and enter a sample dollar amount like so: ![Transaction Entry](/ios9.png) Complete Tap to Pay Transaction: You will see a simulated Tap to Pay transaction: ![Tap to Pay Transaction](/ios12.png) ![Transaction Complete](/ios13.png) ## Security Notes * `Config.plist` is gitignored to prevent accidentally committing credentials * Never commit the actual `Config.plist` file to version control * Only commit the `Config.plist.template` file for reference * Consider using environment variables or secure credential management in production --- title: Reverse (Void) --- # Reverse (Void) A reverse voids a transaction **before** settlement, releasing the hold on the cardholder's funds immediately. Use it to cancel a sale or preauth that hasn't settled yet. > For returning funds **after** settlement, see [Refund](refund.md). ## Full Reverse **iOS:** ```swift let response = try await KoardMerchantSDK.shared.reverse( transactionId: transactionId, eventId: UUID().uuidString ) ``` **Android:** ```kotlin sdk.reverse( transactionId = transactionId, eventId = UUID.randomUUID().toString() ) ``` ## Partial Reverse Reduce the authorized amount without voiding the entire transaction: **iOS:** ```swift let response = try await KoardMerchantSDK.shared.reverse( transactionId: transactionId, amount: 5000, // reduce hold by $50 eventId: UUID().uuidString ) ``` **Android:** ```kotlin sdk.reverse( transactionId = transactionId, amount = 5000, eventId = UUID.randomUUID().toString() ) ``` ## Surcharge Handling When reversing a surcharged transaction, the **full surcharge is released automatically**. No breakdown is needed—the processor handles the surcharge reversal. ## When to Use Reverse vs. Refund | | Reverse | Refund | |---|---------|--------| | **Timing** | Before settlement | After settlement | | **Speed** | Immediate hold release | 3–5 business days | | **Surcharge** | Full surcharge voided | Surcharge prorated | | **Use case** | Cancel, customer changed mind | Post-settlement return | ## Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `transactionId` | `String` | Yes | Transaction to reverse | | `amount` | `Int?` | No | Partial reverse amount in minor units. Omit for full void. | | `eventId` | `String?` | No | Idempotency key | ## See Also - [Refund](refund.md) — Return funds after settlement - [Sale](sale.md) — Original payment - [Preauth](preauth.md) — Authorization hold # Response Codes Koard uses standard HTTP status codes. | Code | Meaning | |------|---------| | `200` | Success | | `201` | Resource created | | `400` | Validation failure — check the response body for details | | `401` | Missing or invalid API key | | `403` | Insufficient permissions for this operation | | `404` | Resource not found | | `409` | Conflict — resource already exists or state mismatch | | `423` | Locked — merchant account is blocked and cannot perform this operation | | `429` | Rate limited — slow down and retry | | `500` | Unexpected server error | ## Error Body Format Errors return a structured envelope with a machine-readable `error` code, a short `message` category, and a human-readable `details` string. Branch on `error` in client code; `details` is always a string (never a list or object): { "error": "validation_error", "message": "Request validation failed", "details": "query: sort_by: Input should be 'name' or 'volume'" } `500` responses never leak internal exception messages or stack traces — the incident is logged server-side and `details` carries only a generic string. ### Envelope Error Codes When an error is returned as the structured envelope, the `error` field is one of: | `error` | HTTP | `message` | |---------|------|-----------| | `validation_error` | 400 | Request validation failed | | `authentication_required` | 401 | Authentication required | | `permission_denied` | 403 | Permission denied | | `not_found` | 404 | Resource not found | | `conflict` | 409 | Resource state conflict | | `rate_limited` | 429 | Rate limit exceeded | | `upstream_error` | 502 | Upstream processor error | | `internal_error` | 500 | Internal server error | # Setting up the Merchant Learn how to set up merchant accounts in the Koard Merchant Management System (MMS) and configure them for tap-to-pay payments. [![link icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXNxdWFyZS1jb2RlLWljb24gbHVjaWRlLXNxdWFyZS1jb2RlIj48cGF0aCBkPSJtMTAgOS0zIDMgMyAzIi8+PHBhdGggZD0ibTE0IDE1IDMtMy0zLTMiLz48cmVjdCB4PSIzIiB5PSIzIiB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHJ4PSIyIi8+PC9zdmc+) Access UAT MMS ](https://app.uat.koard.com)[ Access Production MMS](https://app.koard.com) For the safety and security of your merchant accounts, complete our [merchant checklist](#setting-up-the-merchant__verification-and-testing) before going live. Immediately after you create a merchant account, you can use it in testing environments. In a _sandbox_ (A sandbox is an isolated test environment that allows you to test Koard functionality without affecting your live integration. Use sandboxes to safely experiment with new features and changes), simulate transactions and use all of Koard's features without moving any money. To accept real payments, you must activate your merchant account to use live mode. **Prerequisites** Before you begin, ensure you have: * **Access to the Koard Merchant Management System** * **Valid business registration documents** * **Tax identification number** for each merchant * **Merchant Category Code (MCC)** for each business * **Processor credentials** (MID, TID, VIN) or Partner ID * **Sandbox Apple Account on dedicated test iPhone** for validating Tap to Pay flows **Dedicated Test Hardware**: Make sure your Sandbox Apple Account is signed in on a separate test iPhone. You'll need it to validate Tap to Pay flows before onboarding merchants in production. ## Access the Merchant Management System ### UAT Environment For testing and development, use the UAT MMS: **** ### Production Environment For live merchant onboarding: **** ## Create a Merchant To create a merchant, fill out the merchant application requesting basic information about the business, processor details, and location information. After creating the merchant, you can immediately start configuring terminals and processing payments. Koard's merchant onboarding requirements ensure compliance with payment processor regulations and Apple's tap-to-pay guidelines. These requirements come from our financial partners and Apple, and are intended to prevent abuse of the payment system. We review the information you provide internally to make sure that it complies with our merchant agreement. After you create a merchant account, you can't change its country. If you need to use Koard in a different country that we support, you must create a new merchant account. Privacy and security are priorities for Koard. Our merchant data handling follows industry standards for payment processing and Apple's security requirements. **1. Navigate to Accounts** 1. Log into the MMS 2. Go to the **Accounts** tab 3. You'll see a list of existing Partners 4. Click [![link icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik01IDEyaDE0Ii8+PHBhdGggZD0iTTEyIDV2MTQiLz48L3N2Zz4=) Add Account](#) ![getting-started-1](/getting-started-1.png) **2. Fill Required Fields** Every merchant requires these mandatory fields: * **Name**: Business name or legal entity name * **Tax ID**: Unique tax identification number (merchants with the same Tax ID will be grouped together) * **MCC Code**: 4-digit Merchant Category Code * **HQ Country**: Headquarters country location ![getting-started-2](/getting-started-2.png) **3. Save Merchant** Click **Save** to create the merchant account. ## Assign a Terminal **1. Create New Terminal** 1. After creating the merchant, click [![link icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIi8+PHBhdGggZD0iTTggMTJoOCIvPjxwYXRoIGQ9Ik0xMiA4djgiLz48L3N2Zz4=) New Terminal](#) 2\. You'll see the terminal configuration view ![getting-started-3](/getting-started-3.png) **2. Configure Terminal Details** For most processors, enter: * **MID**: Merchant ID from your processor * **TID**: Terminal ID from your processor * **VIN**: Vendor ID from your processor For processors like Payroc where merchants have their own ID: * Replace MID, TID, and VIN with the **Partner ID** (or Processing Terminal ID) 1. Configure the terminal details by clicking [![link icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xMSA0SDRhMiAyIDAgMCAwLTIgMnYxNGEyIDIgMCAwIDAgMiAyaDE0YTIgMiAwIDAgMCAyLTJ2LTciLz48cGF0aCBkPSJNMTguNSAyLjVhMi4xMjEgMi4xMjEgMCAwIDEgMyAzTDEyIDE1bC00IDEtMS00IDkuNS05LjV6Ii8+PC9zdmc+) Edit Terminal](#) ![getting-started-4](/getting-started-4.png) **3. Assign Location** 1. Go back to the merchant account to access the **Locations** tab ![locations-1](/locations-1.png) 2. Click [![link icon](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIi8+PHBhdGggZD0iTTggMTJoOCIvPjxwYXRoIGQ9Ik0xMiA4djgiLz48L3N2Zz4=) New Location](#) 3. Assign the terminal to that location 4. This gives the merchant a physical location to accept payments from ![getting-started-5](/getting-started-5.png) ## Create Merchant Credentials Your merchants will use these credentials to authenticate with the Koard SDK and process payments. The credentials are tied to specific locations and terminals, ensuring secure payment processing. **1. Generate Credentials** 1. Navigate to the merchant's credential section 2. Click **Create Merchant Credentials** 3. Generate a unique **Code** and **PIN** for the merchant ![getting-started-6](/getting-started-6.png) **2. SDK Integration** Merchants can use these credentials to log into the SDK: ```swift KoardMerchantSDK.shared.login( "Code": "YOUR_CODE", "PIN": "YOUR_PIN" ) ``` **3. Location Configuration** * Merchants can set their location from the SDK or your mPOS app * Location is required to create a card reader session on the iPhone * This ensures payments are processed at the correct merchant location **Security Note**: Keep merchant credentials confidential. Store them securely on your servers and never share them in client-side code or public repositories. ## Multiple Processor Support Each merchant can be configured with multiple MIDs based on the supported processors you have access to: * **Primary Processor**: Main payment processor for the merchant * **Secondary Processors**: Backup or specialized processors * **Regional Processors**: Location-specific payment processing ## Verification and Testing **1. Verify Configuration** 1. Check that all required fields are completed 2. Verify processor credentials are correct 3. Ensure location is properly assigned **2. Test Integration** 1. Use the UAT environment to test merchant login 2. Verify terminal assignment works correctly 3. Test payment processing with test cards **3. Go Live** 1. Move merchant to production environment 2. Update SDK credentials for production 3. Monitor initial transactions ## Keep your merchant accounts safe After you set up your merchant accounts, you'll want to keep them secure. Here are our recommendations: * **Keep private information private**: Don't share merchant credentials and keep your secret API keys confidential on your own servers. As a reminder, Koard employees will never ask you for your keys. * **Use unique credentials**: Generate unique codes and PINs for each merchant. If you reuse credentials across merchants and one account is compromised, an attacker could access multiple merchant accounts. * **Use team members to provide others with access**: You can invite others (with limited access) to your Koard MMS account so that they can log in and take certain actions without full administrative access. * **Update your computer and browser regularly**: We recommend configuring your computer to automatically download and install updates. This helps protect your system against automated attacks and malware. * **Beware of phishing**: All genuine Koard sites use the `koard.com` domain and HTTPS. If you get an email from us that you don't expect, go directly to our site to log in. Don't enter your password after clicking a link in an email. * **Enable two-factor verification**: When you enable two-factor authentication, you'll need to provide an additional unique code from your mobile device to complete the login process. This means that even if someone steals your username and password, they won't be able to log in. ## Best Practices * **Unique Tax IDs**: Ensure each merchant has a unique tax identification * **Proper MCC Codes**: Use accurate Merchant Category Codes for compliance * **Location Accuracy**: Verify physical locations match processor records * **Credential Security**: Store merchant credentials securely * **Regular Audits**: Periodically review merchant configurations ## Troubleshooting ### Common Issues * **Duplicate Tax IDs**: Merchants with same Tax ID will be grouped together * **Invalid MCC Codes**: Ensure 4-digit codes are valid for your region * **Processor Mismatch**: Verify processor credentials match your configuration * **Location Errors**: Ensure locations are properly assigned to terminals ### Support For technical issues with merchant setup: * Check the [Resources](/docs/appendix/resources) section * Contact Koard support for processor-specific issues * Review [Apple Best Practices](/docs/appendix/apple-best-practices-and-guidelines) for compliance ## See also * [iOS SDK Installation](/docs/setting-up-the-ios-sdk/installing-the-sdk) - Install and integrate the Koard Merchant SDK into your iOS application * [Tap to Pay Configuration](/docs/setting-up-the-ios-sdk/adding-support-for-tap-to-pay-on-iphone) - Configure Tap to Pay on iPhone functionality in your app * [Webhook Setup](/docs/webhooks/setting-up-webhooks) - Configure webhooks to receive real-time payment event notifications * [Payment Testing](/docs/setting-up-the-ios-sdk/running-payments) - Learn how to process and test payments with the Koard SDK * [Apple Best Practices](/docs/appendix/apple-best-practices-and-guidelines) - Follow Apple's guidelines and best practices for Tap to Pay development --- title: Capture --- # Capture Capture finalizes a previously authorized ([preauth](preauth.md)) transaction. You can capture at the original amount, a lower amount (partial capture), or with an updated breakdown that includes tip or surcharge. ## Prerequisites - An existing preauth transaction ID - Transaction must be in an authorized (uncaptured) state ## Full Capture **iOS:** ```swift let response = try await KoardMerchantSDK.shared.capture( transactionId: transactionId, amount: 10875 ) ``` **Android:** ```kotlin sdk.capture( transactionId = transactionId, amount = 10875 ) ``` ## Capture with Updated Breakdown Include the final breakdown when the tip or surcharge changed after the preauth: **iOS:** ```swift let finalBreakdown = PaymentBreakdown( subtotal: 10000, taxRate: 8.75, taxAmount: 875, tipAmount: 3000, // customer added a $30 tip tipType: .fixed, surcharge: PaymentBreakdown.Surcharge( amount: 486, // 3.5% of (10000 + 875 + 3000) = 486 percentage: 0.035 ) ) let response = try await KoardMerchantSDK.shared.capture( transactionId: transactionId, amount: 14361, // 10000 + 875 + 3000 + 486 breakdown: finalBreakdown ) ``` **Android:** ```kotlin val finalBreakdown = PaymentBreakdown( subtotal = 10000, taxRate = 8.75, taxAmount = 875, tipAmount = 3000, tipType = "fixed", surcharge = Surcharge( amount = 486, percentage = 0.035 ) ) sdk.capture( transactionId = transactionId, amount = 14361, breakdown = finalBreakdown ) ``` ## Partial Capture Capture at a lower amount than the original hold: **iOS:** ```swift let response = try await KoardMerchantSDK.shared.capture( transactionId: transactionId, amount: 8000 // capture $80 of a $108.75 hold ) ``` **Android:** ```kotlin sdk.capture( transactionId = transactionId, amount = 8000 ) ``` > The remaining hold amount is automatically released back to the cardholder. ## Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `transactionId` | `String` | Yes | The preauth transaction to capture | | `amount` | `Int?` | No | Capture amount in minor units. Defaults to the original preauth amount. | | `breakdown` | `PaymentBreakdown?` | No | Updated breakdown with final tip/surcharge | | `eventId` | `String?` | No | Idempotency key | ## See Also - [Preauth](preauth.md) — Place a hold before capturing - [Incremental Auth](incremental-auth.md) — Increase the hold before capture - [Tip Adjust](tip-adjust.md) — Update tip before capture - [Surcharging](surcharging.md) — Include surcharge in capture breakdown # Launching on the App Store Everything you need to do with Apple — separate from your Koard integration — before your app can go live to merchants. **What you'll learn** In this guide, you'll learn: * The 7-step launch journey and where most teams get stuck * How to pick your distribution path before you write a line of code * The two entitlements you need (and why TestFlight requires the second one) * In-app experience requirements Apple inspects during review * The `prepare()` call and why it must happen at launch, not at checkout * The three video walkthroughs Apple requires — and how to record them * What to include in your App Store Connect submission notes * The most common rejection reasons and how to avoid every one **This is separate from your Koard integration.** Even after your Koard integration is technically complete, you cannot launch your app to merchants without Apple's approval. That approval is a separate, multi-step process you run directly with Apple. Most customers who reach this stage without a plan lose 2–6 weeks to back-and-forth with Apple Review. Track your progress in **Launch Readiness** inside the Koard MMS — it mirrors this guide step by step. ## Why this matters Four of the most common rejection reasons we see from Apple Review: 1. Submission videos don't show how a merchant accepts Apple's Terms & Conditions or links the merchant account. 2. No in-app merchant education — the developer planned to train merchants 1:1 in person. Apple requires _in-app_ education on top of whatever else you do. 3. The `prepare()` call isn't implemented, which means the first tap-to-pay attempt in front of a real customer takes 30–40 seconds. 4. Button copy mixes "Tap to Pay" with "Tap to Pay on iPhone" — Apple is strict about the full product name. None of that is a Koard problem. All of it is an Apple-side gate, and it routinely costs developers weeks they didn't plan to spend. This guide and the in-MMS checklist exist so you find these things in week one of design — not in week one of submission. ## The launch journey Each step is a distinct gate. You cannot skip any of them. **1. Request the Development Entitlement** Free and near-instant. Lets you build and test on developer-registered devices against the PSP sandbox. [Request at Apple's developer portal](https://developer.apple.com/contact/request/tap-to-pay-on-iphone) **2. Build to spec** Use this guide and the Koard MMS checklist to build the required UX — awareness moment, merchant education, T\&C flow, and checkout experience. Don't skip any section. **3. Record the three walkthrough videos** Apple requires a New User, Existing User, and Checkout walkthrough. These cannot be screen-recorded — use a second device to film the screen. **4. Request the Publishing Entitlement** Reply to the email Apple sent you when they granted the Dev entitlement. Apple reviews your app against the v1.5.1 checklist. Allow approximately 5 business days. **TestFlight requires this entitlement.** You cannot distribute via TestFlight until Publishing is granted. There is no shortcut. **5. Submit to App Store Connect** Separate review by the App Store team on top of the entitlement review. Include your test account credentials, video links, wireframes, and entitlement declaration in the submission notes. **6. Pilot** Apple recommends testing with a small group of representative merchants before GA. Use TestFlight or ship behind a feature flag you control. **7. General Availability** Flip your backend flag or publish your App Store listing with Tap to Pay enabled. Update your product page messaging only after the flag is live. ## Pick your distribution path first Apple has different requirements depending on how your app reaches merchants. Decide before you design any screens — the distribution path determines which checklist items are required for you. | Path | Who it's for | Onboarding | Awareness in app | Merchant education | | --------------------- | ---------------------------------------------- | -------------------------------- | ----------------------------------------- | ------------------ | | **Public App Store** | SMBs, anyone discovering your app via search | Required to be in-app, < 15 min | Required (full-screen modal, push, email) | Required | | **Unlisted App** | Specific BYO-device audience reached via URL | External OK | Recommended | Highly recommended | | **Custom App** | One named enterprise customer, branded version | External OK | Recommended | Highly recommended | | **Enterprise (ADEP)** | MDM-deployed to managed devices | External (no Apple ID on device) | Optional | Highly recommended | **Public App Store triggers the full in-app onboarding requirement.** Customers who pick this path because "we want it discoverable" often don't realize it requires a complete in-app merchant sign-up flow that works in under 15 minutes. **Most underused:** Custom App and Unlisted App, which carry lighter requirements for enterprise deployments. Apps that ship via MDM to managed devices are typically a fit for Unlisted. If you're not sure, the Koard solutions team can talk it through — but pick before you start designing screens. ## Two entitlements, not one | Entitlement | What it lets you do | How to get it | When | | --------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------- | | **Development** | Build, run on developer-registered devices, test against the PSP sandbox | [Request at Apple's developer portal](https://developer.apple.com/contact/request/tap-to-pay-on-iphone) | Day one | | **Publishing** | TestFlight, App Store, internal enterprise distribution | Reply to the email Apple sent you with the Dev entitlement. Apple reviews your app against the v1.5.1 checklist. | Once your build meets requirements | **TestFlight requires the Publishing Entitlement.** If you planned to ship a small pilot via TestFlight first, that already requires you to pass Apple's full review. There is no shortcut. ## The in-app experience Apple requires Apple inspects four moments in your user flow. Each has its own checklist covered in the sections below. Together they form the bulk of the v1.5.1 review. **Digital onboarding (Public distribution only)** A new user who just downloaded your app must be able to apply to become an authorised merchant _inside the app_, on an iPhone, and — for the majority of approvable users — accept their first payment within 15 minutes. Practical interpretation: * An external "contact sales" form does not count as digital onboarding. * A `WKWebView` wrapper around your existing onboarding portal _does_ count. * If your KYC requires manual review, the path must still start in-app and use push/email/SMS to bring the user back. **Awareness & enrollment** At least one **awareness moment** — Apple's strong preference is a full-screen modal — must communicate to eligible users that Tap to Pay on iPhone exists in your app. New merchants see it during onboarding. Existing merchants see it on first login after the feature ships. Required elements: * A launch email (Apple has a template). * A push notification (Apple has a template). * A way to enroll _outside_ the checkout flow (in Settings or similar). * A way to enroll _from_ the checkout flow if the user taps "Tap to Pay" without being enrolled. * Only admin-class users can accept the T\&C. Non-admins must see a "contact your admin" message. **Don't forget existing users.** The awareness moment is required for both new merchants during onboarding and existing merchants on their first login after the feature ships. Apple rejects apps that only show it to new users. **Merchant education** This is the most frequently missed requirement. You must provide in-app education demonstrating how to accept payment with Tap to Pay on iPhone. _External training — 1:1 sessions, newsletters, training videos sent by email — does not satisfy this requirement on its own._ **The Koard SDK handles this for you.** The Koard SDK wraps Apple's [`ProximityReaderDiscovery`](https://developer.apple.com/documentation/ProximityReader/ProximityReaderDiscovery) API and surfaces Apple-designed education screens directly in your app. Wiring up the Koard SDK's merchant-education entry point satisfies Apple's checklist item 4.1\* in full — Apple's own language: _"If you use ProximityReaderDiscovery this will fulfill all of the merchant education requirements."_ No need to design your own screens. Two things you still need to handle yourself: 1. **Make education reachable later.** The post-enrollment moment is covered by the SDK, but Apple also requires that users can find the education screens from Settings or Help (checklist item 4.2). Add a "Tap to Pay on iPhone" row in Settings that re-invokes the SDK's education flow. 2. **Cover region-specific requirements if applicable.** If you deploy in a PIN-required region, your education must demonstrate PIN entry and its accessibility features (4.6). If you deploy in a Fallback-required region, demonstrate the fallback payment method (4.7). If you're targeting iOS earlier than 18, you must build your own education screens using Apple's Marketing Guide and Toolkit assets covering, at minimum: * How to accept a contactless card (landscape position, top of iPhone) * How to accept Apple Pay and other digital wallets * PIN entry + accessibility (region-dependent) * Fallback payment method (region-dependent) **Transaction experience** The "Tap to Pay on iPhone" button at checkout must: * Be in a prominent, no-scroll location. * Use exact copy: `"Tap to Pay on iPhone"` (or `"Tap to Pay"` only if the button is too small; `"Charge"` only if Tap to Pay is your sole acceptance method). * **Never** be greyed out or hidden based on enrollment status — if the user isn't enrolled, tapping starts enrollment. * Use the `wave.3.right.circle` or `wave.3.right.circle.fill` SF Symbol if using an icon. After a successful tap: show a processing screen, then a clear approved/declined/timed-out result, and a digital receipt option (SMS, email, QR, or iOS Share). **Button copy is strictly enforced.** Apple is literal — "Tap to Pay" and "Tap to Pay on iPhone" are not interchangeable. Use the full name unless space genuinely does not allow it. ## The `prepare()` call — don't skip this Koard's SDK exposes this as `KoardMerchantSDK.shared.prepare()`. It warms up the reader. The first call after install takes **30–40 seconds**. Subsequent calls take **5–6 seconds, once every 24 hours**. **Where to call it:** at app launch _and_ every time the app comes to the foreground. **Where not to call it:** at checkout. If you do, the first time your merchant tries to take a payment in front of a real customer, they will stand there for 40 seconds. Apple flags this in reviews. It's checklist item 1.4. Implement it on day one. Apple's reviewers flag this often. ```swift // AppDelegate.swift func applicationDidBecomeActive(_ application: UIApplication) { Task { try? await KoardMerchantSDK.shared.prepare() } } ``` ## Terms & Conditions: two paths There are exactly two ways a merchant can accept Apple's Tap to Pay T\&C. Pick one based on your distribution path. **User-led (default)** In-app, the merchant signs into their Apple Account, taps "Accept", and the device kicks off terminal-profile building. This is the path for Public, Unlisted, and Custom App distribution where merchants have their own Apple ID on the device. **Never cache the T\&C status locally.** Always read T\&C acceptance state from Apple via the Koard SDK. A local flag can fall out of sync with Apple's records and cause unexpected T\&C prompts at the worst possible moment. **Enterprise (Apple Business Connect)** For MDM-managed devices without an Apple ID, an organisation admin accepts terms on behalf of the merchant via **Apple Business Connect**. Koard provides the token your admin uses to do this — talk to your Koard solutions contact to set it up _before_ you deploy devices. If the merchant hasn't linked in ABC, the user will see the T\&C prompt on the device anyway. This is the path most MDM-deployed enterprise apps take. It's documented in Apple's v1.5.1 checklist item 3.8.2. ## The three required videos Apple requires three video walkthroughs as part of the Publishing Entitlement review. **You must record with a second device.** The Tap to Pay on iPhone reader screen cannot be screen-recorded. Use a separate iPhone to film the device under test. **New User Flow** Show each of the following in order — Apple rejects videos that skip or cut any step: 1. Account creation 2. KYC (if applicable) 3. Merchant approval 4. Tap to Pay awareness moment 5. T\&C acceptance 6. Merchant education 7. Terminal profile configuration progress indicator 8. Completed indicator 9. One full transaction **Existing User Flow** Show each of the following in order: 1. Sign in to existing account 2. Tap to Pay button visible before T\&C is accepted 3. Awareness moment for existing users 4. T\&C acceptance 5. Merchant education 6. Progress indicator 7. One full transaction 8. PIN entry (if applicable for your region) 9. Fallback payment method (if applicable for your region) **Checkout Flow** Show each of the following in order: 1. Add items to cart (or enter amount) 2. Payment options screen 3. Tap to Pay button 4. Initiate and complete a Tap to Pay transaction 5. PIN entry (if applicable) 6. Fallback (if applicable) **Common rejection:** the videos exist but skip a step ("we cut to after T\&C acceptance"). Re-record showing every step in sequence. If you need to unlink your Apple Account to re-record T\&C acceptance, Apple has documentation on resetting the T\&C state. ## Submitting to App Store Connect For Public, Unlisted, or Custom App distribution, passing the Publishing Entitlement review is not the end. Your app then goes through standard App Store Review _plus_ a Tap to Pay-specific review. In your App Store Connect submission notes, include: * A declaration that you are using the Tap to Pay on iPhone entitlement. * A description of your use case (e.g. "a point-of-sale app for SMB merchants"). * **A test account that works.** Apple says "a vast majority of rejections are due to test accounts not working properly." Validate on a fresh device before submitting. * A link to your video walkthrough _and_ high-fidelity wireframes of your checkout experience. * If your app is geo-fenced or uses a feature flag for Tap to Pay — declare that explicitly. * If Tap to Pay is your _only_ acceptance method, set `UIRequiredDeviceCapabilities` to include `iphone-ipad-minimum-performance-a12` so incompatible devices cannot download the app. **Enterprise apps: do not mention MDM.** If you're MDM-distributing in the enterprise, do not mention MDM in your App Store Connect app details. It flags a different (incorrect) entitlement review. Route enterprise distribution via ADEP. **Respond on the same thread.** Apple will often respond to a missing piece as a "rejection" with a note in the body saying it's actually a request for information. Reply on the same message thread — don't open a new one. ## Common pitfalls | # | Pitfall | Catch it before submission by… | | -- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | 1 | Submission videos skip T\&C / merchant-linking | Using the Videos checklist in MMS Launch Readiness — it lists every Apple-required step per video | | 2 | No in-app merchant education ("we train in person") | Wiring up the Koard SDK's merchant-education entry point (wraps `ProximityReaderDiscovery`, iOS 18+) — satisfies 4.x in one call | | 3 | `prepare()` not implemented or called at checkout | Implementing on day one at app launch and `applicationWillEnterForeground` | | 4 | Test account credentials don't work on Apple's review device | Validating on a fresh iPhone _before_ submitting | | 5 | Tap to Pay button uses wrong copy or greys out when not enrolled | Apple is literal — use "Tap to Pay on iPhone"; never grey the button | | 6 | Mentioned MDM in App Store Connect notes for an enterprise app | Strip the MDM mention; route enterprise via ADEP, not the App Store | | 7 | Awareness moment present but only for new users | Add an existing-user awareness moment too (full-screen modal on first login post-launch) | | 8 | T\&C local cache out of sync with Apple | Always read T\&C status from Apple via the Koard SDK — never trust a local flag | | 9 | Geo-fencing not declared in App Store Connect notes | Add a line: "App is geo-fenced to US, CA. Test account works in these regions." | | 10 | Marketing channels skipped (launch email, push, hero banner) | Items 6.1, 6.2, 6.3 — required for Public distribution | ## After approval: pilot, then GA Apple strongly recommends piloting with a small group of representative merchants before flipping to general availability. Two options: * **TestFlight** — invite-only, requires the Publishing Entitlement. * **Feature flag** — ship the binary to GA with Tap to Pay hidden behind a backend flag you control. This is the path Apple recommends for existing apps, because most users will already have the updated version when you flip the flag. If you go this route, declare it in your App Store Connect submission, and do not update your App Store product page with Tap to Pay messaging until you flip the flag. Apple ships a pilot questionnaire in the Getting Started PDF (Appendix). Use it. ## Quick reference links * [Apple Tap to Pay on iPhone developer site](https://developer.apple.com/tap-to-pay-on-iphone/) * [Request the entitlement](https://developer.apple.com/contact/request/tap-to-pay-on-iphone/) * [Human Interface Guidelines — Tap to Pay on iPhone](https://developer.apple.com/design/human-interface-guidelines/tap-to-pay-on-iphone) * [Apple Marketing Guidelines](https://developer.apple.com/tap-to-pay/marketing-guidelines/) * [ProximityReaderDiscovery API (iOS 18+)](https://developer.apple.com/documentation/ProximityReader/ProximityReaderDiscovery) * [App Store Review Guidelines](https://developer.apple.com/app-store/review/guidelines/) ## Where to get help * **In-MMS checklist** — `Launch Readiness` in your Koard dashboard mirrors this guide section by section. * **Apple entitlement questions** — reply on your existing `applepayentitlements@apple.com` thread, citing your Case ID. * **PSP-side questions** (T\&C from Apple vs local, ABC tokens for enterprise, `prepare()` semantics) — your Koard solutions contact. ## See also * [Setting Up the Entitlement for Tap to Pay on iPhone](/docs/setting-up-the-ios-sdk/setting-up-the-entitlement-for-tap-to-pay-on-iphone) - Request and configure the Apple development entitlement * [Adding Support for Tap to Pay on iPhone](/docs/setting-up-the-ios-sdk/adding-support-for-tap-to-pay-on-iphone) - Configure Xcode and run your first test transaction * [Getting Ready for Production](/docs/setting-up-the-ios-sdk/getting-ready-for-production) - Set up Xcode schemes and API keys for your production build * [Apple Best Practices and Guidelines](/docs/appendix/apple-best-practices-and-guidelines) - UX patterns, ProximityReader usage, and security practices * [Running Payments](/docs/setting-up-the-ios-sdk/running-payments) - SDK reference for payment flows _This guide reflects Apple's published requirements as of March 2025 (App Review Checklist v1.5.1) and September 2023 (Getting Started v1.2). Apple updates these documents periodically. If you see a discrepancy, the Apple source wins — please flag it to us._ # Creating a Sandbox Apple Account Set up an Apple Sandbox tester account so you can exercise Koard payment flows safely without charging real cards. **What You Learn** In this guide, you'll learn how to: * Create and manage Sandbox tester accounts in App Store Connect * Enable Developer Mode so your test devices can run Sandbox builds * Sign in to a dedicated test iPhone with the Sandbox Apple ID * Reset test data between sessions **Prerequisites** Before you begin, make sure you have: * **Apple Developer role access** (_Account Holder_, _Admin_, _App Manager_, or _Developer_) in App Store Connect * **Unique email addresses** for every Sandbox tester you plan to create * **A dedicated test iPhone** running iOS 17 or later with Developer Mode enabled **Important**: Always sign in to your Sandbox tester account on a dedicated test device. Production Apple IDs cannot make Sandbox purchases, and mixing test and production accounts on the same hardware regularly causes authentication issues. ## Step 1: Enable Developer Mode on Your Test Device 1. Connect the iPhone to your Mac and open Xcode. 2. From the menubar, choose **Window → Devices and Simulators**. 3. Select your device, then click **Enable Developer Mode**. 4. Follow the on-device prompts to reboot and confirm Developer Mode. **Why this matters**: Developer Mode is required before a physical device can run apps signed with a development profile or interact with Sandbox services. ## Step 2: Create a Sandbox Tester in App Store Connect 1. Sign in to [App Store Connect](https://appstoreconnect.apple.com/). 2. Navigate to **Users and Access → Sandbox → Test Accounts**. 3. Click the **Add** button (`+`) and fill in the tester’s first and last name. 4. Provide an email address that has never been used for an Apple ID purchase. Email subaddressing (`tester+us@example.com`) works well when supported by your provider.\ _Apple will send all test purchase receipts and account notices to this address._ 5. Choose a strong password that meets Apple’s complexity requirements. 6. Select the App Store country or region you want to test against. 7. Click **Create** to save the tester. Apple allows up to 10,000 Sandbox testers per team, so create regional variants as needed for localization or tax testing. [Source](https://developer.apple.com/help/app-store-connect/test-in-app-purchases/create-a-sandbox-apple-account/). ## Step 3: Sign In on the Test iPhone 1. On the dedicated test device, open **Settings → App Store**. 2. Scroll to the bottom and tap **Sandbox Account**. 3. Sign in with the newly created Sandbox Apple ID. 4. Confirm the Sandbox indicator appears when making in-app purchases. When prompted inside the Koard demo app or your integration, always use the Sandbox credentials you signed into Settings with—never production Apple IDs. ## Step 4: Reset or Remove Sandbox Testers If you encounter inconsistent billing states or need a clean slate: * In App Store Connect, open the tester record and click **Reset** to clear purchase history. * To delete a tester, select it in the Sandbox list and choose **Delete Account**. You must remove the tester from any Sandbox Test Families first. * After deletion, the associated email can be re-used for a brand-new tester if necessary. ## Troubleshooting Tips * **Purchase dialogs ask for payment details**: Verify you’re signed in with the Sandbox account under **Settings → App Store → Sandbox Account**. * **Device won’t install development build**: Confirm Developer Mode is enabled and your provisioning profile includes the test device UDID. * **Sandbox credential lockouts**: Apple temporarily locks accounts after multiple bad password attempts. Wait 30 minutes before trying again, or delete and recreate the tester. * **Test receipts missing**: Check the tester’s email inbox (including spam) for Sandbox receipts, or reset the tester record and attempt the purchase again. ## Next Steps * [Install the SDK](/docs/setting-up-the-ios-sdk/installing-the-sdk) to start building against the Koard Sandbox environment. * [Run payments](/docs/setting-up-the-ios-sdk/running-payments) using Sandbox credentials to validate flows end-to-end. * Move on to [Getting Ready for Production](/docs/setting-up-the-ios-sdk/getting-ready-for-production) once your Sandbox tests succeed. # Online PIN Validation Koard supports **online PIN only**. When a PIN is required, it is validated in real time against the issuer during the authorization request — Koard does not support offline PIN, where the PIN would be verified locally on the device without issuer involvement. The encrypted PIN block is forwarded to the acquirer and issuer as part of the authorization. The issuer validates the PIN against the cardholder's account and will hard decline the transaction if the PIN is incorrect — this decision is made entirely by the issuer and acquirer, not by Koard. When a cardholder enters a PIN during a Tap to Pay on iPhone transaction, Apple encrypts the PIN data before it leaves the device. Koard handles the full decryption and validation flow online — the encrypted PIN never passes through your application unprotected. ## How It Works 1. The iOS SDK captures the cardholder's PIN and returns encrypted cardholder data, encrypted PIN data, and a transaction ID to Koard. 2. Koard calls Apple's Proximity Payment Service to exchange the encrypted data for single-use decryption keys. 3. Apple returns keys scoped to that transaction. Koard validates and decrypts the data, then forwards the PIN block to the payment processor in the authorization request. ## Supported Scenarios | Scenario | PIN Captured | Notes | |----------|-------------|-------| | Cardholder data only | No | Standard contactless — no PIN required | | Cardholder data + PIN | Yes | PIN collected inline during the tap | | Cardholder data + PIN token | Yes | PIN collected and tokenized | | PIN fallback | Yes | Used when the card requires PIN but cannot use standard flow | # Troubleshooting Real-world fixes for issues merchants commonly hit when enrolling devices or running their first tap. If a device fails the SDK eligibility check, see [Supported Devices & NFC Tap Location](/docs/guides/android-sdk/details/supported-devices). For SDK error codes returned during a transaction, see [SDK Response Codes](/docs/guides/android-sdk/details/sdk-response-codes). **What you learn** In this guide, you'll learn: * How to fix the "tap was immediately cancelled" issue * How to resolve enrollment failures caused by device clock drift * How to recover from a `TamperDetected` error on every sale after a failed enrollment * What to do when the kernel is busy with another merchant app * How to fix the "SDK not initialized" error * Which device settings must be enabled before a merchant's first transaction * A pre-flight checklist to run before contacting Koard support ## Tap is Immediately Cancelled ### Symptom The merchant enrolls successfully and the SDK reports the device as eligible. When they attempt their first sale, the reader shows "Present card", but the moment the customer taps, the transaction is **immediately cancelled** — the SDK emits an `Abort` (or `OnFailure`) before the card is read. This has been observed across several Android devices, with **Samsung Galaxy S21** being the most common — but any Android device that exposes a separate **"NFC and contactless payments"** (or similarly named) toggle can hit it. ### Cause Many Android devices ship with **two distinct NFC settings**: 1. **NFC** — the basic NFC radio toggle (enabled by default on most devices). 2. **NFC and contactless payments** — a separate setting that authorizes the device to use NFC for **payment-related** Host Card Emulation (HCE) and Tap to Pay flows. Different OEMs label this differently (e.g. "NFC and contactless payments", "NFC and contactless transactions", "Contactless payments"). The SDK eligibility check verifies that NFC hardware is present and the radio is on, which is why enrollment and the eligibility check both pass. However, if the secondary **"NFC and contactless payments"** setting is disabled, the Visa Tap to Pay Ready kernel cannot complete the contactless card read — the device's payment subsystem blocks the read and the transaction is cancelled before any card data is exchanged. ### Fix On the merchant's device: 1. Open **Settings** 2. Tap **Connections** (or **Connected devices** depending on the OEM and Android version) 3. Tap **NFC and contactless payments** (sometimes labeled **NFC and contactless transactions** or simply **Contactless payments**) 4. Ensure the master toggle is **On** 5. Optionally set **Contactless payments → Default payment app** to your merchant app, or leave it as the system default if your app does not require the system payment role After enabling the setting, retry the sale. No re-enrollment is required. **Why the eligibility check doesn't catch this**: `checkKiCEligibility()` verifies that NFC hardware exists and the system NFC radio is enabled. The OEM-specific "contactless payments" toggle gates a higher layer (the payment HCE subsystem) and is not visible to the eligibility API. If a merchant sees taps cancelled instantly, always confirm this setting before deeper debugging. If "NFC and contactless payments" is enabled and the tap is still cancelled instantly, check whether a wallet app (Samsung Wallet, Google Wallet, etc.) is set as the default and is intercepting the tap. Temporarily clear the default payment app under **NFC and contactless payments → Contactless payments**, then retry. ## Enrollment Fails or Returns Errors ### Symptom Calls to `enrollDevice()` (or the SDK's enrollment flow) fail with errors such as: * `InvalidRequest` — "Developer mode is enabled", "Device already enrolled", or no active location was set. `enrollDevice()` requires an authenticated session **and** a prior `setActiveLocation(locationId)` call * `VACEnrollmentError` * `AuthenticationFailed` (status code `7`) * `AttestationFailed` (status code `24`) * `CouldNotAttestError` * Generic prepare errors in the `7`–`51` range The merchant sees an "Enrollment failed" screen and cannot proceed to take their first tap. ### Cause Enrollment establishes a secure channel between the device and the Visa Acceptance Cloud (VAC). That handshake includes a **device attestation step** that is sensitive to three things: 1. **Developer mode is enabled** — most enrollment endpoints reject devices with developer options on, because the device cannot produce a trustworthy attestation. 2. **The device clock has drifted** — VAC verifies signed timestamps during the handshake. If the device clock is off by more than a few minutes (a "clock drift" issue), signatures fail to verify and attestation is rejected. This is most common on devices that have been offline for a long time, recently factory-reset, or have automatic time disabled. 3. **Google Play Protect is disabled** — Play Protect provides the integrity signals VAC uses to attest the device. With Play Protect off, the attestation payload is incomplete and enrollment is rejected. ### Fix Walk the merchant through all three checks before retrying enrollment: #### 1. Disable Developer Mode 1. Open **Settings → System → Developer options** 2. Toggle **Developer options** to **Off** 3. If the option is missing entirely, developer mode is already off — proceed to the next step If developer mode was on, restart the device after disabling it. #### 2. Enable Automatic Date & Time 1. Open **Settings → General management → Date and time** (Samsung) or **Settings → System → Date & time** (stock Android) 2. Toggle **Automatic date and time** to **On** 3. Toggle **Automatic time zone** to **On** 4. Wait a few seconds for the device to sync with the network time source This corrects clock drift and is the most common fix when enrollment fails on a device that previously worked. #### 3. Enable Google Play Protect 1. Open the **Google Play Store** app 2. Tap your profile icon (top right) → **Play Protect** 3. Tap the **gear icon** (settings) in the top right of the Play Protect screen 4. Toggle **Scan apps with Play Protect** to **On** Play Protect must be enabled for the device attestation step to succeed. If the merchant cannot enable Play Protect (for example on a device without Google Mobile Services), the device is not eligible for Tap to Pay. After all three checks pass, restart the device and retry enrollment. If the SDK still returns an enrollment error, capture the `statusCode` and `errorType` and consult the [SDK Response Codes](/docs/guides/android-sdk/details/sdk-response-codes#kic-prepare-errors) reference. **Why clock drift matters**: The VAC attestation handshake relies on signed timestamps to prevent replay attacks. The SDK's enrollment flow includes a fresh nonce and a device-side timestamp; if the device clock is more than a few minutes off the server clock, the server rejects the timestamp and the handshake fails — often surfacing as a generic `AuthenticationFailed` (code `7`) or `AttestationFailed` (code `24`). Enabling automatic time syncs the device against a network time source and eliminates the drift. ## `TamperDetected` on Every Sale After a Failed Enrollment ### Symptom `enrollDevice()` (or `enableNfcTransactionsAsync(...)`) appears to finish but reports a non-success status. Afterwards, **every** sale throws `KoardErrorType.DeviceIntegrityError.TamperDetected` (status code `1002`). Clearing the app's data via Android Settings makes the next enrollment succeed — which points at corrupted local state rather than a genuinely tampered device. ### Cause A partial enrollment can leave the SDK's local preferences (certificates, VAC device ID, auth keys, x-via hint, x-random value) half-written and out of sync with the kernel's internal state. The mismatch surfaces as a false `TamperDetected` on the next transaction. ### Fix Call `clearEnrollmentState()` from a worker thread to wipe the local enrollment preferences, then re-attempt enrollment. The active location is intentionally preserved, and this is a local-only operation — it does **not** contact the backend. ```kotlin lifecycleScope.launch(Dispatchers.IO) { val sdk = KoardMerchantSdk.getInstance() sdk.clearEnrollmentState() sdk.enrollDevice() // or enableNfcTransactionsAsync(...) } ``` Use `clearEnrollmentState()` for half-enrolled recovery. If the device is **fully** enrolled, call `unenrollDevice()` instead: it asks the Tap to Pay Ready app to clean up the enrollment data it holds for your app, then clears the SDK's local state. Local state is flushed either way — if the kernel call fails, the returned string reports the failure but the device can still re-enroll.\ \ **This does not fully deprovision the device on Visa's backend.** Per KiC integration guide §3.4.20 (implementation _optional_), `unenrollDevice()` only cleans up enrollment data on the Tap to Pay Ready app; Visa requires the integrator to _also_ call the backend `manageDevice` API to disable the device, otherwise it keeps being billed. Coordinate that step with Koard support. ## Tap to Pay Is Busy with Another Merchant App ### Symptom A sale fails with `KoardErrorType.KicConnectorError.KernelAppBusyWithAnotherMerchant` (status code `98`). ### Cause Introduced with KiC multi-tenancy, this means a **different** merchant app on the device currently holds the lock on the Visa Tap to Pay Ready kernel service. ### Fix Ask the operator to close the other merchant app (or wait for it to finish its transaction), then retry. Calling `resetKernelService()` will **not** resolve this — the lock is owned by a different process, not your app. ## "SDK Not Initialized" Error ### Symptom ```text IllegalStateException: Instance is null. Did you forget to call initialize? ``` is thrown the first time you call `KoardMerchantSdk.getInstance()`. ### Cause `getInstance()` was called before `KoardMerchantSdk.initialize(...)` completed. ### Fix Initialize the SDK in `Application.onCreate()` on a worker thread (`Dispatchers.IO`) before any code accesses `getInstance()`. See the [Installing the SDK](/docs/guides/android-sdk/details/installing-sdk) guide for the recommended startup sequence. ## Pre-Flight Checklist Before contacting Koard support about a device that "won't take a tap" or "fails to enroll", confirm **all** of the following on the merchant's device: * Device is running **Android 12 (API 31) or later** * **NFC** is enabled in Settings * **NFC and contactless payments** is enabled (where present) * **Developer mode** is **off** * **Automatic date and time** is **on** * **Google Play Protect** is enabled * **Google Play Services** is installed and up to date * **Visa Tap to Pay Ready** kernel app is installed from the Play Store * Device is not rooted and is not running a custom ROM * `checkKiCEligibility()` returns no failures If every item is checked and the device still fails, gather the following before contacting support: * Device make, model, and Android version * **Koard SDK version** (the version of `koard-android` / the Koard Merchant SDK your app is built against) * The `statusCode` and `statusCodeDescription` from the failing `KoardTransactionResponse`, or the `KoardErrorType` from the thrown `KoardException` * A log excerpt from the time of the failure. The SDK logs to Logcat under the fixed tag **`KoardSDK`**; capture it with `adb logcat -s KoardSDK`. Verbosity is set once at startup via `KoardMerchantSdk.initialize(..., logLevel = KoardLogLevel.VERBOSE)` — there is no runtime setter, and the default is `NONE` in release builds, so ask the merchant for a build that opts in. The SDK never logs API keys, tokens, device certificates, or enrollment key material at any level. # Boarding a Merchant with Elavon You can board an Elavon merchant either through the Koard Merchant Management System (MMS) UI or programmatically via the API. Elavon assigns the merchant a **Bank Number** and **Terminal Number** out-of-band. Those two values plus the standard top-level fields (`mid`, `tid`, `mcc`) are everything Koard needs from the merchant to board a terminal. ## Before You Start Elavon provisions merchants and terminals on their side; Koard never makes a "create merchant" call. Once provisioning is complete, the merchant's packet contains: | Provided by Elavon | What it is | |---|---| | **Bank Number** | 6 digits — assigned by Elavon per merchant (viaConex v4.090 §11.9, p.144) | | **Terminal Number** | 16 digits — assigned by Elavon per POS device (viaConex v4.090 §11.9, p.144) | That's it. Everything else (Application ID, Vendor ID, Registration Key) is configured by Koard once per environment and never appears on a merchant VAR sheet. ## How `terminal_id` is built Elavon's wire format uses a single 22-digit `Terminal_ID` on every request. The spec is explicit (viaConex v4.090 §11.9 p.144): > Digits 1–6 = Bank Number (6 digits, fixed length, assigned by Elavon) > Digits 7–22 = Terminal Number (16 digits, fixed length, assigned by Elavon) The 22-digit `Terminal_ID` is **pure concatenation** — no padding, no spacing. You can supply either form: | Format | Example | Koard does | |---|---|---| | `bank_number` + `terminal_number` separately | `bank_number="001734"`, `terminal_number="0008025708085490"` | Concatenates to `0017340008025708085490` | | Pre-built 22-digit `tid` | `tid="0017340008025708085490"` | Uses as-is, splits into bank + terminal internally | Pick whichever your merchant's paperwork makes easier. Both end up with identical persisted state. ## Via the MMS After creating the merchant account, click **New Terminal** and select Elavon as the processor. You'll be presented with the **Elavon VAR Sheet Information** form. | MMS Label | Required | Notes | |---|---|---| | Merchant ID | Yes | 12-digit MID from Elavon | | Bank Number | Either this **or** the 22-digit Terminal ID | 6 digits | | Terminal Number | Either this **or** the 22-digit Terminal ID | 16 digits | | 22-digit Terminal ID | Either this **or** Bank Number + Terminal Number | Pre-concatenated single value | | Merchant Category Code | Yes | 4-digit MCC | Optional fields are accepted (see [VAR Sheet Fields → Optional](#optional)) but **not required to onboard**. Address, phone, DBA name and similar fields are only used when the merchant joins Elavon's Dynamic Merchant Data program, which is restricted-access and explicitly approved per merchant (viaConex §3 Block 03, p.22552–22557). ## Via the API Send `X-Koard-apikey: {API_KEY}` with every request. Use `https://api.uat.koard.com` for sandbox and `https://api.koard.com` for production. ### Create Terminal — `POST /v2/terminals` **Request body** | Field | Required | Description | |---|---|---| | `account_id` | Yes | Merchant account ID | | `processor_config_id` | Yes | Elavon processor configuration ID | | `terminal_name` | Yes | Display name for the terminal | | `terminal_description` | No | Optional description | | `mid` | Yes | Elavon-assigned 12-digit Merchant ID | | `tid` | Yes — either the 22-digit form OR omit and use `var_sheet.bank_number` + `var_sheet.terminal_number` | If you supply the 22-digit form, Koard splits it; if you supply Bank + Terminal in `var_sheet`, Koard concatenates them. | | `mcc` | Yes | 4-digit MCC | | `var_sheet` | Conditional | Required only if you split Bank Number from Terminal Number, or want to set any optional fields. | **Example — pre-built 22-digit Terminal ID** curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_live_elavon_us", "terminal_name": "Front Counter iPhone", "mid": "123456789012", "tid": "0017340008025708085490", "mcc": "5812" }' **Example — split Bank + Terminal Number** curl https://api.uat.koard.com/v2/terminals \ -X POST \ -H "X-Koard-apikey: $KOARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "account_id": "100200300001", "processor_config_id": "prc_live_elavon_us", "terminal_name": "Front Counter iPhone", "mid": "123456789012", "mcc": "5812", "var_sheet": { "bank_number": "001734", "terminal_number": "0008025708085490" } }' ### Update Terminal — `PUT /v2/terminals/{terminal_id}` To correct or update VAR sheet fields after creation, send a `PUT` with a `var_sheet` object containing only the fields you want to change. ## VAR Sheet Fields ### Required | Field | Format | Description | |---|---|---| | `bank_number` | 6 digits | First 6 digits of the Elavon `Terminal_ID`. Assigned by Elavon per merchant. Required only if `tid` was not supplied as the full 22-digit form. | | `terminal_number` | 16 digits | Last 16 digits of the Elavon `Terminal_ID`. Assigned by Elavon per POS device. Required only if `tid` was not supplied as the full 22-digit form. | ### Optional | Field | Default | Description | |---|---|---| | `merchant_dba_name` | — | Restricted-access. Only honored if your merchant is enrolled in Elavon's Dynamic Merchant Data program (viaConex v4.090 §3 Block 03). Otherwise ignored at auth; supply at clearing time instead. | | `merchant_city` | — | Same restriction as `merchant_dba_name`. | | `merchant_state` | — | Same restriction. | | `merchant_zip` | — | Same restriction. | | `merchant_country` | `USA` | 3-letter alpha country code per ISO 3166-1 alpha-3. Allowed values include `USA`, `CAN`, `MEX`, `GBR`, etc. — see Elavon's clearing currency/country table. Same restriction at auth; required (mandatory) on the clearing BHR per Elavon Clearing Format §3. | | `acceptor_phone` | — | Restricted-access. Only carried via the optional Merchant Address Addendum (MAA) record on the clearing file. | | `currency_code` | `USD` | 3-letter alpha currency code per ISO 4217 (e.g. `USD`, `CAD`, `EUR`, `GBP`). Auth and clearing paths both expect alpha; only the EMV TLV tag `5F2A` uses ISO 4217 numeric (e.g. `840`). | | `surcharge_rate` | — | Surcharge percentage. Set to `null` to disable, `0` to never surcharge. Configured via `PUT /v2/terminals/{terminal_id}`. | ## Batch Management Elavon merchants in production use **clearing files** (the `.txt` flat-file format defined by Elavon Clearing Format v4.69) for daily settlement. Koard handles this on the merchant's behalf — there is no merchant-driven batch open/close model in the live auth flow for Elavon. If your merchant requires manual or automated batch scheduling via `PUT /v2/terminals/{terminal_id}` with a `batch_schedule`, see [Automated Batch Scheduling](/docs/guides/batch-settlements/details/automated-batch-scheduling). ## Country & Currency — Cheat Sheet | Where used | Format | Examples | |---|---|---| | Auth (viaConex Block 03 Dynamic_Country_Code) | 3-letter alpha | `USA`, `CAN`, `MEX` | | Auth EMV TLV tag 9F1A | 3-digit numeric | `840` (US), `124` (CA), `826` (GB) | | Clearing BHR Merchant Country | 3-letter alpha | `USA`, `CAN` | | Auth currency | 3-letter alpha | `USD`, `CAD`, `EUR`, `GBP` | | EMV TLV tag 5F2A | 3-digit numeric | `840` (USD), `124` (CAD) | The Koard API exposes the alpha forms (`USD`, `USA`) on the VAR sheet — the numeric EMV tags are constructed internally during the auth message build. ## Gotchas - **`mid` and `tid` are top-level fields** on the request, NOT inside `var_sheet`. This matches every other processor on Koard. - **`tid` is the 22-digit Elavon `Terminal_ID`** — not a 3- or 4-digit lane number like TSYS or Worldpay. Build it from Bank Number + Terminal Number per the [How `terminal_id` is built](#how-terminal_id-is-built) section, or paste the pre-concatenated form. - **Merchant DBA name / city / state / zip / phone are optional at auth** and restricted to Elavon's Dynamic Merchant Data program. Don't add them unless your merchant is explicitly enrolled — Elavon silently ignores them otherwise (viaConex §3 Block 03). - **Country code is alpha-3 at the API level** (`USA`, not `840`). The numeric form only appears inside EMV TLV tags, which Koard builds internally. ## Troubleshooting **`400 Bad Request` on create with `bank_number` / `terminal_number` errors** - Verify `bank_number` is exactly 6 digits and `terminal_number` is exactly 16 digits. - Or supply the full 22-digit `tid` directly and omit the `var_sheet` split. **Transactions erroring with `INVALID TERMINAL`** - `bank_number + terminal_number` must match what Elavon has on file character-for-character, including leading zeros. **Wrong merchant name on statements** - Auth-path dynamic merchant fields are restricted. The DBA name on statements is set in the **clearing file** (BHR record), not on the auth. Update the merchant's DBA at Elavon directly or via the clearing pipeline. # Automated Batch Scheduling ## Automated Batch Scheduling Koard supports automated batch close and re-open scheduling per terminal. Instead of manually closing batches at the end of each day, you can configure a schedule and Koard handles it automatically. ### Supported Processors | Processor | Supported | Notes | | ------------ | --------- | --------------------------------------------------- | | **TSYS** | ✅ | Full support. Batch numbers auto-managed (001-999). | | **Elavon** | ✅ | Full support via ViaConex TC 920/921/929. | | **Worldpay** | ✅ | Full support via 610 settlement interface. | | Fiserv | ❌ | Not supported for automated scheduling. | | Payroc | ❌ | Not supported for automated scheduling. | ### How It Works 1. **Configure a schedule** on the terminal via `PUT /v2/terminals/{terminal_id}` 2. **Koard's scheduler** runs every minute, checking for terminals due for batch close 3. When due, Koard **closes the current batch** and **opens a new one** 4. If a close fails, Koard **retries up to 10 times** with backoff 5. On persistent failure, a **webhook error event** is sent ### Setting Up a Schedule Add a `batch_schedule` field to your terminal update: ```json PUT /v2/terminals/{terminal_id} { "batch_schedule": { "timezone": "US/Eastern", "is_active": true, "schedule": [ { "day": "MON", "times": ["23:00"] }, { "day": "TUE", "times": ["23:00"] }, { "day": "WED", "times": ["23:00"] }, { "day": "THU", "times": ["23:00"] }, { "day": "FRI", "times": ["23:00"] }, { "day": "SAT", "times": ["23:00"] } ] } } ``` #### Response The terminal response includes confirmation that the schedule was saved: ```json { "terminal_id": "term-abc123", "name": "Front Counter POS", "mid": "886000001130", "tid": "00000001", "processor_config_id": "cfg-tsys-001", "status": "active", "var_sheet": { "applicationId": "B001" } } ``` ### Schedule Format #### Days Use 3-letter day codes: | Code | Day | | ----- | --------- | | `MON` | Monday | | `TUE` | Tuesday | | `WED` | Wednesday | | `THU` | Thursday | | `FRI` | Friday | | `SAT` | Saturday | | `SUN` | Sunday | #### Times Times are in 24-hour `HH:MM` format. You can set **multiple closes per day**: ```json { "day": "WED", "times": ["12:00", "18:00", "23:00"] } ``` This closes the batch at noon, 6pm, and 11pm on Wednesdays. #### No Close on a Day Simply omit the day from the schedule. If Saturday and Sunday are not listed, no batch close happens on weekends. ### Timezones **DST vs Fixed Timezones**: Choose carefully between DST-aware and fixed-offset timezones. Most merchants want DST-aware timezones so the batch close follows "wall clock" time. #### DST-Aware Timezones (Recommended) These follow daylight saving time transitions. `23:00 US/Eastern` means 11pm EDT in summer and 11pm EST in winter. | Timezone | Description | | ------------- | -------------------------- | | `US/Eastern` | Eastern Time (New York) | | `US/Central` | Central Time (Chicago) | | `US/Mountain` | Mountain Time (Denver) | | `US/Pacific` | Pacific Time (Los Angeles) | | `US/Alaska` | Alaska Time | | `US/Hawaii` | Hawaii Time (no DST) | | `US/Arizona` | Arizona Time (no DST) | You can also use full IANA zone names like `America/New_York`, `America/Chicago`, etc. #### Fixed-Offset Timezones These **never** change for DST. Use only if you want a fixed UTC offset year-round. | Timezone | UTC Offset | Notes | | -------- | ------------- | ------------------------------------ | | `EST` | UTC-5 always | Does **not** switch to EDT in summer | | `MST` | UTC-7 always | Does **not** switch to MDT in summer | | `HST` | UTC-10 always | Same as US/Hawaii | | `UTC` | UTC+0 always | Universal Coordinated Time | #### Example: DST Impact A batch close at `23:00 US/Eastern`: * **Winter (EST)**: Fires at 04:00 UTC * **Summer (EDT)**: Fires at 03:00 UTC A batch close at `23:00 EST`: * **Always**: Fires at 04:00 UTC (even in summer when "wall clock" Eastern time is EDT) ### Managing Schedules #### Update an Existing Schedule ```json PUT /v2/terminals/{terminal_id} { "batch_schedule": { "timezone": "US/Pacific", "schedule": [ { "day": "MON", "times": ["22:00"] }, { "day": "FRI", "times": ["14:00", "22:00"] } ] } } ``` #### Pause Scheduling (Keep Config) Set `is_active` to `false` to temporarily disable without losing your schedule: ```json PUT /v2/terminals/{terminal_id} { "batch_schedule": { "is_active": false } } ``` #### Resume Scheduling ```json PUT /v2/terminals/{terminal_id} { "batch_schedule": { "is_active": true, "schedule": [ { "day": "MON", "times": ["23:00"] }, { "day": "TUE", "times": ["23:00"] } ] } } ``` **Important**: An active schedule must have at least one day with times configured. Setting `is_active: true` with an empty schedule will return an error. #### Remove Schedule Entirely (Back to Manual) Set `batch_schedule` to `null`: ```json PUT /v2/terminals/{terminal_id} { "batch_schedule": null } ``` After removal, the terminal returns to manual batch management. #### Updates Without batch\_schedule Updating other terminal fields (name, MID, var\_sheet, etc.) does **not** affect the schedule: ```json PUT /v2/terminals/{terminal_id} { "name": "New Terminal Name" } ``` The existing batch schedule is preserved. ### Switching Between Auto and Manual #### Switching from Auto to Manual To switch a terminal back to manual batch management, remove the schedule: ```json PUT /v2/terminals/{terminal_id} { "batch_schedule": null } ``` **Important**: When switching to manual, the current open batch stays open. You are now responsible for: * Closing the current batch manually (`POST /v1/batches/{batch_id}/close`) * Opening new batches manually (`POST /v1/batches/open`) * Closing all future batches — they will no longer auto-close #### Switching from Manual to Auto Enable a schedule on an existing terminal: ```json PUT /v2/terminals/{terminal_id} { "batch_schedule": { "timezone": "US/Central", "is_active": true, "schedule": [ { "day": "MON", "times": ["22:00"] }, { "day": "TUE", "times": ["22:00"] }, { "day": "WED", "times": ["22:00"] }, { "day": "THU", "times": ["22:00"] }, { "day": "FRI", "times": ["22:00"] } ] } } ``` If the terminal already has an open batch, the scheduler will close it at the next scheduled time and open a new one automatically. ### Closing a Batch Early You can **always** close a batch early, even when automated scheduling is enabled: ```bash POST /v1/batches/{batch_id}/close ``` **You must open a new batch immediately after an early close.** Transactions cannot be processed without an open batch. Call `POST /v1/batches/open` right after the early close. When the scheduler fires later at its scheduled time: * If it finds an **open batch with transactions**, it closes and reopens normally * If it finds an **open batch with no transactions** (e.g., you just opened it), it cancels the empty batch and opens a fresh one * If it finds **no open batch** (e.g., you closed early and didn't reopen), it opens a new one This means early closes are safe and the scheduler self-heals on the next run. #### Example: Early Close at 3pm, Scheduled Close at 11pm 1. **3:00 PM** — You close the batch early via API 2. **3:01 PM** — You open a new batch via API 3. **3:01 PM – 11:00 PM** — Transactions accumulate in the new batch 4. **11:00 PM** — Scheduler fires, closes the batch (with transactions), opens a new one If you forget to reopen at step 2, the scheduler at 11pm will detect no open batch and open one for you — but any transactions between 3:01 PM and 11:00 PM will have failed because there was no open batch. ### TSYS-Specific Behavior #### Batch Number Auto-Management TSYS batch numbers must be between **001-999** and cannot be reused within **5 consecutive days**. Koard handles this automatically: 1. **On open**: Queries the last closed batch for the terminal and increments 2. **On close**: Verifies no conflicting batch number, auto-increments if needed 3. **On duplicate (QD)**: Automatically retries with the next batch number (up to 10 attempts) #### Batch Number Wrapping When the batch number reaches 999, it wraps around to 001. ### Error Handling | Scenario | Koard's Response | | -------------------------------- | ------------------------------------------ | | Processor unavailable | Retries up to 10 times with backoff | | Duplicate batch number (TSYS QD) | Auto-increments and retries | | All retries exhausted | Sends `batch.rejected` webhook, logs error | | No open batch to close | Skips close, opens a new batch | ### Webhook Events When using automated scheduling, you'll receive the standard batch webhook events: | Event | When | | ----------------- | ----------------------------------------------------------------------- | | `batch.submitted` | Batch sent to processor | | `batch.accepted` | Processor accepted the batch | | `batch.rejected` | Processor rejected the batch, or the scheduler failed after all retries | | `batch.opened` | New batch opened after close | ### Permissions The following roles can view and manage batch schedules: | Role | View | Create/Edit | Delete | | -------- | ---- | ----------- | ------ | | PSP | ✅ | ✅ | ✅ | | Partner | ✅ | ✅ | ✅ | | Merchant | ✅ | ✅ | ✅ | ### See also * [Batch and Settlements Overview](/docs/batch-and-settlements/overview) - Batch concepts * [Running Batches](/docs/batch-and-settlements/running-batches) - Manual batch management * [Setting up Webhooks](/docs/webhooks/setting-up-webhooks) - Webhook configuration # 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](/docs/payments/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_id`s 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_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`: ```http GET /v1/transactions/event/{event_id} X-Koard-apikey: ``` 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](/docs/webhooks/setting-up-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 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_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.