Webhooks
Webhooks allow your application to receive real-time notifications when events occur in MAES Platform.
Environment Separation
Important: Webhooks are configured separately for each environment:
- Sandbox Webhook — Receives events for sandbox/test cards only
- Production Webhook — Receives events for production/real cards only
This separation ensures:
- Test data never reaches your production systems
- You can use different endpoints for testing vs production
- Each environment has its own secret for security
Project
├── Sandbox Webhook
│ ├── URL: https://staging.example.com/webhook
│ └── Events: sandbox cards only
│
└── Production Webhook
├── URL: https://api.example.com/webhook
└── Events: production cards only
How Webhooks Work
MAES Platform Your Server
│ │
│ 1. Event occurs │
│ (card enabled) │
│ │
│ 2. POST to your URL │
│ ─────────────────────────► │
│ with signed payload │
│ │
│ 3. 200 OK │
│ ◄───────────────────────── │
│ │
Webhook Events
| Event | Description |
|---|---|
card.created | New card synced from MAES |
card.activated | Card was activated |
card.enabled | Fuel authorizations enabled |
card.disabled | Fuel authorizations disabled |
card.synced | Card data synced from MAES |
sync.completed | Full sync completed successfully |
sync.failed | Sync operation failed |
test | Test event for verification |
Webhook Payloads
Card Events
Card events (card.*) follow this format:
{
"id": "evt_1a2b3c4d5e6f",
"event": "card.enabled",
"created_at": "2025-12-27T16:24:05.712Z",
"data": {
"card_id": "09c9861c-4c4b-411f-be85-c16ed7e26da4",
"card_number": "782521009000153700",
"license_plate": "AB-123-CD",
"driver": "John Doe",
"status": "active",
"auth_gasoline": true,
"auth_diesel": true,
"auth_lpg": true,
"auth_heating_oil": true,
"environment": "production"
}
}
Sync Events
Sync events (sync.*) notify you when synchronization operations complete. Full sync includes ALL cards in the project (not just created/updated) so you can replace your entire local database without additional API calls.
sync.completed
{
"id": "evt_7g8h9i0j1k2l",
"event": "sync.completed",
"created_at": "2025-12-27T16:30:00.000Z",
"data": {
"sync_type": "full",
"cards_created": 5,
"cards_updated": 12,
"duration_ms": 2450,
"cards": [
{
"id": "09c9861c-4c4b-411f-be85-c16ed7e26da4",
"card_number": "782521009000153700",
"maes_card_id": "93228880367",
"license_plate": "AB-123-CD",
"driver": "John Doe",
"status": "active",
"expiration_date": "2028-01-01T00:00:00.000Z",
"auth_gasoline": true,
"auth_diesel": true,
"auth_lpg": true,
"auth_heating_oil": true,
"environment": "production",
"sync_status": "synced",
"last_synced_at": "2025-12-27T16:30:00.000Z",
"notes": null,
"internal_reference": null,
"created_at": "2025-12-01T10:00:00.000Z",
"updated_at": "2025-12-27T16:30:00.000Z"
}
]
}
}
sync_type | Description | Includes cards |
|---|---|---|
full | Full sync of all cards from MAES | ✅ Yes |
new_cards | Sync only newly added cards | ✅ Yes |
push_pending | Push local changes to MAES | ❌ No |
sync.failed
{
"id": "evt_3m4n5o6p7q8r",
"event": "sync.failed",
"created_at": "2025-12-27T16:30:00.000Z",
"data": {
"sync_type": "full",
"error": "Connection timeout to MAES portal",
"duration_ms": 30000
}
}
Signature Verification
All webhook requests include a signature header:
X-Webhook-Signature: t=1703693400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Verification Steps
- Extract timestamp (
t) and signature (v1) from the header - Construct the signed payload:
{timestamp}.{raw_body} - Compute HMAC-SHA256 using your webhook secret
- Compare computed signature with
v1 - Optionally verify timestamp to prevent replay attacks
Node.js Example
const crypto = require('crypto');
function verifyWebhookSignature(payload, signatureHeader, secret) {
const [timestampPart, signaturePart] = signatureHeader.split(',');
const timestamp = timestampPart.replace('t=', '');
const signature = signaturePart.replace('v1=', '');
const signedPayload = `${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex'),
);
}
// Express.js example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const payload = req.body.toString();
if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
console.log('Received event:', event.event);
// Handle the event
switch (event.event) {
case 'card.enabled':
// Handle card enabled
break;
case 'card.disabled':
// Handle card disabled
break;
}
res.status(200).send('OK');
});
Retry Logic
Failed deliveries are retried with exponential backoff:
| Attempt | Delay After Failure |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
After 5 failed attempts, the delivery is marked as permanently failed.
Auto-Disable
Webhooks are automatically disabled after 10 consecutive failures. You can re-enable them from the dashboard.
Requirements
- HTTPS only — Webhook URLs must use HTTPS
- 200-299 response — Return a 2xx status to acknowledge receipt
- Respond quickly — Process asynchronously if needed (< 30s timeout)
Best Practices
- Always verify signatures — Don't trust unverified payloads
- Process asynchronously — Queue events for processing
- Handle duplicates — Events may be delivered more than once
- Log everything — Keep records for debugging
- Return 200 quickly — Avoid timeouts