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:
OAuth API client (recommended for POS apps)
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 /cancel | transactions:write |
POST /tip, POST /read-card, POST /read-manual, POST /read-input, POST /read-signature, POST /read-confirmation | transactions: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/vendor | devices: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:
- Verify the transaction's true state via the terminal, CardPointe reporting,
or your gateway transaction list (search by
outcome.retrefor your order reference). - Reverse it (void/refund) if it actually completed and the sale should not stand.
- Only after verifying it did not complete, re-attempt — reuse the same
externalReferenceIdso 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 asGatewayAndroidBoltPaymentRequest.tipAmountMinoronsaleorauthorize.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 byretref.terminal.display(text)/terminal.clearDisplay()— drive the idle display.terminal.printReceipt(orderId)— reprint receipt(s) for a past transaction (includeauthMerchantIdif the original authorization used one).terminal.terminalDetails(),terminal.dateTime("2026-07-14 12:00:00")— hardware details and terminal clock sync (dateTimeis not supported on Clover devices).