Skip to main content

Android Semi-Integrated (Bolt)

Overview

CardPointe Bolt is Fiserv's cloud-connected attended-terminal rail: a physical terminal (Clover or Ingenico) keeps a persistent connection to CardPointe's cloud, and your Android POS drives it through the Gateway's card-present API — the app never touches card data. The Gateway connects to the terminal, blocks on the cardholder interaction, and records definitive approved and declined outcomes. Captured sales emit payment.completed; recorded declines emit payment.declined.

All Bolt endpoints live under /api/v1/card-present/bolt/... on the card-present service and are feature-gated server-side by bolt.enabled.

The Android SDK exposes Bolt through a terminal facade with the same developer ergonomics as driving a NexGo device: obtain one terminal-scoped handle, call typed suspend operations on it, and dispatch on a typed outcome. Where NexGo needs a managed on-device runtime (GatewayAndroidApp.withManagedRuntime(...)app.sale(...)), Bolt is cloud-driven — so the handle comes straight from the client (client.bolt.terminal(hsn)terminal.sale(...)) and provisioning is an explicit one-time bindTerminal step instead of managed device enrollment.

A compile-checked Kotlin reference implementation of everything on this page lives in the monorepo at sdks/kotlin/gateway-sdk-android/src/sample/kotlin/com/myriad/gateway/sdk/android/samples/BoltReferenceFlow.kt (built as //sdks/kotlin/gateway-sdk-android:bolt-reference-flow-sample).

1. Token acquisition

Every call carries a Gateway bearer token. Two supported paths:

Provision an OAuth client for the location and request the scopes the app needs. With the Android SDK the client-credentials flow is automatic:

val client = GatewayAndroidClient(
GatewayConfig(
clientId = BuildConfig.GATEWAY_CLIENT_ID,
clientSecret = BuildConfig.GATEWAY_CLIENT_SECRET,
environment = Environment.STAGING,
),
)

Staff session (portal-authenticated users)

A signed-in staff user's bearer token can be passed via GatewayConfig.accessToken. Role-derived permissions apply: a cashier-tier location_user carries devices:read (list terminals, terminal details) but not transactions:write — payment operations from a staff session require location_admin or above. Ship the OAuth API client path for cashier-driven sale flows.

Scope map

Endpoint (/api/v1/card-present/bolt/...)Scope
POST /sale, POST /auth, POST /auth-manual, POST /canceltransactions:write
POST /tip, POST /read-card, POST /read-manual, POST /read-input, POST /read-signature, POST /read-confirmationtransactions:write
POST /display, POST /clear-display, POST /print-receipt, POST /date-time, POST /terminals (bind)devices:write
POST /terminals/{hsn}/suspend, POST /terminals/{hsn}/reactivate, POST /terminals/{hsn}/repoint, DELETE /terminals/{hsn}devices:write
GET /terminals, POST /terminal-details, POST /terminals/{hsn}/ping, GET /terminals/vendordevices:read

2. Bind the terminal

One-time setup (admin-tier, devices:write): register the terminal's hardware serial number (HSN) against your location. The gateway resolves that location's effective CARD_PRESENT credential profile and derives the CardPointe MID; callers do not submit either vendor routing value.

client.bolt.bindTerminal(
hsn = "1800XXXXXXXX",
merchantId = merchantId,
)

Every subsequent operation goes through a terminal-scoped handle and requires this binding to be ACTIVE — an unknown or inactive HSN returns 404:

val terminal = client.bolt.terminal(hsn = "1800XXXXXXXX", merchantId = merchantId)

Hold one handle per lane, like a started NexGo app. client.bolt.listTerminals() returns the merchant's bound terminals (devices:read).

Vendor reconciliation requires a real online seed terminal HSN because the vendor listTerminals operation requires a Bolt session key. The SDK never chooses or caches an arbitrary terminal:

client.bolt.listVendorTerminals(
credentialProfileId = credentialProfileId,
seedHsn = "1800XXXXXXXX",
merchantId = merchantId,
)

3. Run a sale

val tipAmountMinor = terminal.tip(
prompt = "Select tip",
amountMinor = 1250,
tipPercentPresets = listOf(15, 20, 25),
)

val outcome = terminal.sale(
GatewayAndroidBoltPaymentRequest(
amountMinor = 1250, // $12.50 — minor units
tipAmountMinor = tipAmountMinor, // Sent with base amount; recorded separately.
externalReferenceId = "pos-order-000123", // your idempotency key
currency = "USD", // Bolt terminals settle in the MID's currency; USD only
),
)

The call blocks while the cardholder taps/inserts/swipes at the terminal. externalReferenceId is an idempotency key: retrying with the same value replays the recorded outcome without re-prompting the cardholder. amountMinor remains the base amount. When tipAmountMinor is present, the Gateway sends their sum to the terminal, then records base and tip separately. terminal.authorize(request) supports the same field. Do not set it for keyed-entry manual auth because that operation does not forward tips.

4. Handle the three outcome states

GatewayAndroidBoltTransactionOutcome is a sealed three-state contract, so when is exhaustive — the compiler makes you handle all three:

when (outcome) {
is GatewayAndroidBoltTransactionOutcome.Approved -> {
// Recorded in the gateway ledger. outcome.transactionId drives follow-on
// capture/void/refund; outcome.retref is the processor reference.
}
is GatewayAndroidBoltTransactionOutcome.Declined -> {
// Definitive host decline: when persistence succeeds, the Gateway records a
// DECLINED transaction with processedAmount=0 and emits payment.declined.
// outcome.raw.transactionId identifies that ledger row. It is never an
// approval or evidence that money moved; show outcome.reason and let the
// clerk start a NEW sale (new externalReferenceId) if desired.
}
is GatewayAndroidBoltTransactionOutcome.Unknown -> {
// INDETERMINATE — the charge may or may not have completed on the host
// (processor returned a retry/indeterminate status). outcome.guidance
// carries the operator instruction. See the verify-or-reverse rule below.
}
}

The verify-or-reverse rule (Unknown)

Never auto-retry an Unknown outcome. The facade itself never retries in any state — a blind resubmit risks charging the cardholder twice. Instead:

  1. Verify the transaction's true state via the terminal, CardPointe reporting, or your gateway transaction list (search by outcome.retref or your order reference).
  2. Reverse it (void/refund) if it actually completed and the sale should not stand.
  3. Only after verifying it did not complete, re-attempt — reuse the same externalReferenceId so a late-recorded approval replays instead of re-prompting.

The same rule applies when a sale call fails with HTTP 503 and an "outcome is indeterminate" message: the request may have reached the terminal before the failure, so treat it exactly like an Unknown result.

Older SDK builds map any unrecognized future status value to Unknown as well — code that gates ledger-visible behavior on Approved stays fail-closed.

5. Other terminal operations

All on the same terminal handle:

  • terminal.authorize(request) — pre-auth (capture-later); same three-state outcome.
  • terminal.saleManual(request) / terminal.authorizeManual(request) — keyed-entry sale/auth (the clerk keys the card number at the terminal).
  • terminal.cancel() — cancel the in-flight device prompt.
  • terminal.tip(prompt, amountMinor, tipPercentPresets) — prompt for a tip selection; presets remain numeric percentages. Pass the returned minor-unit amount as GatewayAndroidBoltPaymentRequest.tipAmountMinor on sale or authorize.
  • terminal.readCard(amountMinor, includeSignature, confirmAmount) / terminal.readManual(amountMinor, includeSignature, includeExpirationDate, beep) — tokenize a card (CardSecure token) without authorizing.
  • terminal.readInput(format, prompt), terminal.readSignature(prompt), terminal.readConfirmation(prompt) — prompt for user input, a signature, or a yes/no confirmation (vendor limit: prompts are 16 characters or fewer).
  • terminal.captureSignature(retref) — deferred-signature flow: capture a signature at the terminal and attach it to the CardPointe transaction identified by retref.
  • terminal.display(text) / terminal.clearDisplay() — drive the idle display.
  • terminal.printReceipt(orderId) — reprint receipt(s) for a past transaction (include authMerchantId if the original authorization used one).
  • terminal.terminalDetails(), terminal.dateTime("2026-07-14 12:00:00") — hardware details and terminal clock sync (dateTime is not supported on Clover devices).