Skip to main content

Webhooks

Register an endpoint to receive real-time, HMAC-signed events when transactions occur. Webhooks are the recommended way to stay in sync — far more efficient than polling.

Webhooks cover transactional data only (withdrawals, returns, exchanges, and training records) — the events devices generate upstream that you can't predict. Organizational data (employees, products, sectors, …) is not delivered as webhooks: you manage it yourself via the REST write endpoints, so you already know when it changes.

Registering an endpoint

In the console, go to Settings ▸ API Integration ▸ Webhooks and create an endpoint with:

  • an HTTPS URL to receive POST deliveries,
  • the event types you want.

A signing secret (whsec_…) is shown once on creation. Store it securely — you need it to verify signatures, and it cannot be retrieved again.

Use the Send test button on the endpoint to have SmartEPI POST a synthetic signed webhook.test delivery to your URL — a quick way to confirm your receiver and signature verification work before real events flow. A test never affects the endpoint's failure tracking.

Event types

EventFires when
withdrawal.createdA withdrawal is recorded by a device.
return.createdA return is recorded.
exchange.createdAn exchange is recorded.
training_record.createdA training record is created.

Transactions are insert-only — a withdrawal/return/exchange is written once with its final status and never modified — so there is no *.updated event.

Subscribe to * to receive all current and future event types.

Delivery format

Each delivery is an HTTP POST with a JSON body:

{
"id": "evt_withdrawal_created_w-123",
"type": "withdrawal.created",
"createdAt": 1750000000,
"data": { "id": "w-123", "withdrawalStatus": "SUCCESS", "...": "..." }
}

data is exactly the DTO the matching GET endpoint returns — no sensitive or internal field is ever included.

Headers

HeaderPurpose
X-SmartEPI-Signaturet=<unix>,v1=<hmac-sha256-hex> — verify this.
X-SmartEPI-Event-IdStable per-delivery id — use it for idempotency.
X-SmartEPI-Event-TypeThe event type (e.g. withdrawal.created).

Verifying the signature

The signature header looks like:

X-SmartEPI-Signature: t=1750000000,v1=5f1e3a…<hex>

To verify a delivery:

  1. Parse t (a UNIX timestamp) and v1 (the signature) from the header.
  2. Build the signed payload: "<t>.<rawRequestBody>" — using the raw bytes of the request body, not a re-serialized copy.
  3. Compute HMAC-SHA256(signingSecret, signedPayload) as lowercase hex.
  4. Compare it to v1 using a constant-time comparison.
  5. Reject the delivery if t is more than 300 seconds from your current time (replay protection).

Node.js

const crypto = require('node:crypto')

function verify(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
const timestamp = Number.parseInt(parts.t, 10)
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false

const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`, 'utf8')
.digest('hex')

const a = Buffer.from(expected)
const b = Buffer.from(parts.v1)
return a.length === b.length && crypto.timingSafeEqual(a, b)
}

Python

import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
ts = int(parts["t"])
if abs(time.time() - ts) > tolerance:
return False
expected = hmac.new(
secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])

Responding & retries

  • Respond with any 2xx status to acknowledge receipt. Respond fast — do the heavy work asynchronously.
  • A non-2xx response, a timeout, or a connection error counts as a failure. The delivery is retried up to 5 times at a fixed interval (roughly every few minutes, via the delivery queue's redrive policy — there is no exponential backoff). After the attempts are exhausted the event is dropped. If you detect a gap, reconcile via the list endpoints.
  • After 100 consecutive failures the endpoint is auto-disabled. Re-enable it from the console (Settings → API Integration → Webhooks) once your receiver is healthy; re-enabling resets the failure counter. The endpoint keeps its existing signing secret (re-enabling does not rotate it).

Idempotency

Deliveries are at-least-once — you may occasionally receive the same event twice (e.g. after a retry). Deduplicate on X-SmartEPI-Event-Id: if you have already processed that id, acknowledge with 200 and do nothing.