Wunder Mobility Platform Webhooks deliver real-time event notifications to your endpoint whenever something happens in the platform – a rental starts, an invoice is created, a vehicle changes state, and more. Each event is sent as an HTTP POST request with a JSON payload describing what happened.
Webhooks are configured during onboarding by your Customer Success Manager (CSM), who provides you with the authentication keys and registers your endpoint URL.
Not to be confused with Data Export Webhooks: Data export webhooks deliver scheduled CSV reports and follow the Standard Webhooks specification with HMAC-SHA256 signing and
webhook-*headers. Platform webhooks described here deliver real-time event notifications, useX-Webhook-*headers, and sign with HMAC-SHA1.
Who This Guide Is For: Developers integrating with Wunder Mobility’s real-time event webhook system.
Related Documentation: Platform Webhooks Reference
For a complete list of event types and their payload schemas, see the Platform Webhooks Reference.
During onboarding you will receive two keys:
| Key | Purpose |
|---|---|
| API Key | Sent in the X-Api-Key header on every request. Use it to authenticate that the request comes from Wunder Mobility. |
| Signature Key | Used to compute the X-Webhook-Payload-Hash header (HMAC-SHA1 of the request body). Use it to verify message integrity and authenticity – proving the payload has not been tampered with and was signed by a party that knows the key. |
Both keys should be stored securely and never exposed in client-side code or public repositories.
Every webhook is delivered as an HTTP POST request with a JSON body.
| Header | Example | Description |
|---|---|---|
Content-Type |
application/json |
Always application/json. |
X-Api-Key |
sk_live_abc123 |
Your API Key for request authentication. |
X-Webhook-Payload-Hash |
a1b2c3d4e5... |
HMAC-SHA1 signature of the request body, computed with your Signature Key (see Verifying Requests). |
X-Webhook-Id |
550e8400-e29b-41d4-a716-446655440000 |
Unique identifier for this webhook. Remains the same across retry attempts – use this for deduplication. |
X-Webhook-Dispatch-Attempt-Count |
1 |
Which delivery attempt this is (starts at 1). |
X-Event-Created-At |
2025-03-15 14:30:00 |
Timestamp when the event was created (YYYY-MM-DD HH:MM:SS, UTC). Note: this header uses a space-separated format, while timestamp fields in the JSON body use ISO 8601 (2025-03-03T12:00:00Z). |
X-Event-Created-At-Epoch |
1710513000 |
Same timestamp as a Unix epoch (seconds). |
The request body is a JSON object. Its structure depends on the event type. A typical payload looks like:
{
"type": "RENTAL_STARTED",
"timestamp": "2025-03-03T12:00:00Z",
"data": {
"id": 12345
}
}
For the full list of event types and their payload schemas, see the Platform Webhooks Reference.
We recommend verifying both the API Key and the payload signature on every incoming request.
X-Api-Key header matches the API Key you received during onboarding. Reject requests with a missing or incorrect key.X-Webhook-Payload-Hash header contains an HMAC-SHA1 signature of the raw request body, computed with your Signature Key. Verifying it proves the payload was not tampered with in transit.Important: Compute the HMAC over the raw request body string, not a re-serialized version of the parsed JSON. Re-serializing may change key order or whitespace, causing a mismatch.
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
// Step 1: Verify API Key
String receivedApiKey = request.getHeader("X-Api-Key");
if (receivedApiKey == null || !MessageDigest.isEqual(
apiKey.getBytes(StandardCharsets.UTF_8),
receivedApiKey.getBytes(StandardCharsets.UTF_8))) {
response.setStatus(401);
return;
}
// Step 2: Verify payload signature (read raw bytes to preserve exact body)
byte[] payloadBytes = request.getInputStream().readAllBytes();
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(new SecretKeySpec(signatureKey.getBytes(StandardCharsets.UTF_8), "HmacSHA1"));
String expected = bytesToHex(mac.doFinal(payloadBytes));
String received = request.getHeader("X-Webhook-Payload-Hash");
if (received == null || !MessageDigest.isEqual(
expected.getBytes(StandardCharsets.UTF_8),
received.getBytes(StandardCharsets.UTF_8))) {
response.setStatus(401);
return;
}
// Step 1: Verify API Key
if (!hash_equals($apiKey, $_SERVER['HTTP_X_API_KEY'] ?? '')) {
http_response_code(401);
exit;
}
// Step 2: Verify payload signature
$payload = file_get_contents('php://input');
$expected = hash_hmac('sha1', $payload, $signatureKey);
if (!hash_equals($expected, $_SERVER['HTTP_X_WEBHOOK_PAYLOAD_HASH'] ?? '')) {
http_response_code(401);
exit;
}
import hmac, hashlib
# Step 1: Verify API Key
if not hmac.compare_digest(request.headers.get('X-Api-Key', ''), api_key):
abort(401)
# Step 2: Verify payload signature
payload = request.get_data(as_text=True)
expected = hmac.new(signature_key.encode(), payload.encode(), hashlib.sha1).hexdigest()
if not hmac.compare_digest(expected, request.headers.get('X-Webhook-Payload-Hash', '')):
abort(401)
const crypto = require('crypto');
// Step 1: Verify API Key
const receivedKey = Buffer.from(req.headers['x-api-key'] || '');
const expectedKey = Buffer.from(apiKey);
if (receivedKey.length !== expectedKey.length || !crypto.timingSafeEqual(receivedKey, expectedKey)) {
return res.status(401).end();
}
// Step 2: Verify payload signature (use raw string body, not parsed JSON)
const payload = req.body;
const expected = crypto.createHmac('sha1', signatureKey).update(payload).digest('hex');
const received = req.headers['x-webhook-payload-hash'] || '';
if (expected.length !== received.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
return res.status(401).end();
}
Your endpoint’s HTTP response status code determines what happens next:
| Your Response | Our Behavior | Retry |
|---|---|---|
| 2xx (200, 201, 204, …) | Delivery successful. No further attempts. | — |
| 5xx (500, 502, 503, …) | Treated as a temporary failure. We will retry with backoff. | ✓ |
| 4xx (400, 401, 403, 404, …) | Treated as a permanent failure. We assume your endpoint is misconfigured. | — |
| Timeout / network error | Treated as a temporary failure. We will retry with backoff. | ✓ |
| 1xx, 3xx | Treated as a permanent failure. | — |
Important: Do not return 4xx for transient errors – we will not retry those. Return 5xx if you want us to retry (e.g., your service is temporarily overloaded).
When delivery fails with a 5xx status or a network error, we retry using progressive backoff:
| Attempt | Wait Before Retry | Cumulative Time |
|---|---|---|
| 1 | – (first try) | 0 |
| 2 | Immediate | ~0 seconds |
| 3 | 10 seconds | ~10 seconds |
| 4 | 1 minute | ~1 minute |
| 5 | 10 minutes | ~11 minutes |
| 6-28 | 1 hour each | up to ~24 hours |
After 28 failed attempts (~24 hours), delivery is permanently abandoned.
The X-Webhook-Dispatch-Attempt-Count header tells you which attempt you are receiving.
The system provides at-least-once delivery. This means:
Use the X-Webhook-Id header to deduplicate. This ID is stable across all retry attempts for the same webhook.
webhook_id = request.headers['X-Webhook-Id']
# Use an atomic upsert (e.g. INSERT ... ON CONFLICT DO NOTHING) to avoid race conditions
if not db.webhooks_processed.insert_if_absent(webhook_id):
return Response(status=200) # Already handled, return success
process_webhook(request)
return Response(status=200)
Your webhook endpoint should:
X-Api-Key and the payload signature from X-Webhook-Payload-HashX-Webhook-Id before processing200 OK) to confirm receipt| Problem | Likely Cause | Solution |
|---|---|---|
| Not receiving webhooks | Endpoint not publicly reachable | Ensure your URL is accessible from the internet and not blocked by firewall rules. |
| Signature verification fails | Wrong key or modified body | Verify you’re using the correct Signature Key (not the API Key) and computing HMAC over the raw body string. |
| Receiving duplicate webhooks | At-least-once delivery | Implement deduplication using X-Webhook-Id. |
| Webhooks stop after first failure | Returning 4xx on error | Return 5xx for transient errors so retries are triggered. |
| Webhooks arrive late | Backoff after failures | After repeated failures, retries are spaced up to 1 hour apart. Check your endpoint uptime. |
To configure platform webhooks for your organization:
For questions or changes to your webhook configuration, reach out to your Customer Success Manager.