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.