Skip to main content

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

EventDescription
card.createdNew card synced from MAES
card.activatedCard was activated
card.enabledFuel authorizations enabled
card.disabledFuel authorizations disabled
card.syncedCard data synced from MAES
sync.completedFull sync completed successfully
sync.failedSync operation failed
testTest 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_typeDescriptionIncludes cards
fullFull sync of all cards from MAES✅ Yes
new_cardsSync only newly added cards✅ Yes
push_pendingPush 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

  1. Extract timestamp (t) and signature (v1) from the header
  2. Construct the signed payload: {timestamp}.{raw_body}
  3. Compute HMAC-SHA256 using your webhook secret
  4. Compare computed signature with v1
  5. 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:

AttemptDelay After Failure
1Immediate
21 minute
35 minutes
430 minutes
52 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

  1. Always verify signatures — Don't trust unverified payloads
  2. Process asynchronously — Queue events for processing
  3. Handle duplicates — Events may be delivered more than once
  4. Log everything — Keep records for debugging
  5. Return 200 quickly — Avoid timeouts