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
POSTdeliveries, - 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
| Event | Fires when |
|---|---|
withdrawal.created | A withdrawal is recorded by a device. |
return.created | A return is recorded. |
exchange.created | An exchange is recorded. |
training_record.created | A 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
| Header | Purpose |
|---|---|
X-SmartEPI-Signature | t=<unix>,v1=<hmac-sha256-hex> — verify this. |
X-SmartEPI-Event-Id | Stable per-delivery id — use it for idempotency. |
X-SmartEPI-Event-Type | The 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:
- Parse
t(a UNIX timestamp) andv1(the signature) from the header. - Build the signed payload:
"<t>.<rawRequestBody>"— using the raw bytes of the request body, not a re-serialized copy. - Compute
HMAC-SHA256(signingSecret, signedPayload)as lowercase hex. - Compare it to
v1using a constant-time comparison. - Reject the delivery if
tis 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.