Skip to main content

Card-Present Device Webhook Events

Overview

The gateway emits two card-present (NEXGO / XTMS) device lifecycle events so downstream systems can react to terminal binding completion instead of polling GET /card-present-devices/{provider}/{serialNumber} or the app-push attempt list.

XTMS → Gateway is poll-based (there is no inbound XTMS callback). The gateway owns the single reconcile sweep that detects completion (CardPresentDeviceOrchestrationService.reconcilePendingBindings), so it is the one authority that emits these events — every other consumer subscribes rather than standing up a duplicate poll loop.

Both events fire asynchronously through the standard webhook delivery pipeline (persisted to webhook_events, signed HMAC body, retries with exponential backoff). Subscribe via the Webhooks page in the merchant dashboard.

Delivery semantics: at-least-once. A repeated reconcile sweep over the same terminal may re-publish after the publisher's idempotency window. Consumers should treat (locationId, gatewayDeviceId, serialNumber, status) as the business idempotency key.

The event-type catalogue is asserted by WebhookServiceEventTypeContractTest (see services/online-txn) — adding a new event must update both WebhookEventTypes.kt and that contract test, otherwise CI fails.


Event types

card_present_device.synced

Fires when a pending binding transitions to BOUND on the reconcile sweep — the terminal is bound and its TSYS/gateway app push has been kicked off, so the device is ready.

{
"eventId": "evt_01HX...",
"eventType": "card_present_device.synced",
"timestamp": "2026-06-19T00:00:00Z",
"type": "com.myriad.gateway.webhook.WebhookEvent.CardPresentDeviceSynced",
"organizationId": "org_01HX...",
"locationId": "loc_01HX...",
"provider": "NEXGO",
"serial_number": "SN-1234",
"gateway_device_id": "terminal-SN-1234",
"status": "BOUND",
"synced_at": "2026-06-19T00:00:00Z",
"data": {
"organizationId": "org_01HX...",
"provider": "NEXGO",
"serial_number": "SN-1234",
"gateway_device_id": "terminal-SN-1234",
"status": "BOUND",
"synced_at": "2026-06-19T00:00:00Z"
}
}

card_present_device.bind_failed

Fires when a binding reaches a terminal failure on the reconcile sweep (a non-deferrable XTMS error, or the device is already bound elsewhere). A retryable PENDING_CLOUD_SYNC deferral does not emit — only a terminal FAILED outcome does, so consumers can surface a real failure instead of an indefinite pending state.

{
"eventId": "evt_01HX...",
"eventType": "card_present_device.bind_failed",
"timestamp": "2026-06-19T00:00:00Z",
"type": "com.myriad.gateway.webhook.WebhookEvent.CardPresentDeviceBindFailed",
"organizationId": "org_01HX...",
"locationId": "loc_01HX...",
"provider": "NEXGO",
"serial_number": "SN-1234",
"gateway_device_id": "terminal-SN-1234",
"status": "FAILED",
"synced_at": "2026-06-19T00:00:00Z",
"failure_reason": "Device SN-1234 is already bound to another merchant",
"data": {
"organizationId": "org_01HX...",
"provider": "NEXGO",
"serial_number": "SN-1234",
"gateway_device_id": "terminal-SN-1234",
"status": "FAILED",
"synced_at": "2026-06-19T00:00:00Z",
"failure_reason": "Device SN-1234 is already bound to another merchant"
}
}

The wire payload preserves the existing snake_case device fields. The public SDK models expose camelCase aliases (serialNumber, gatewayDeviceId, syncedAt, and failureReason) while still accepting the delivered snake_case payload.

The pull endpoints (GET .../{serialNumber}, GET .../app-push-attempts) remain a valid backstop and are unchanged.