public-api-docs

Platform Webhooks Integration Guide

Overview

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, use X-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


How It Works

  1. An event occurs – a rental starts, an invoice is created, a vehicle changes state, etc.
  2. We send a webhook – an HTTP POST request with a JSON payload is sent to your registered endpoint
  3. You verify the request – check the API Key and HMAC-SHA1 signature to confirm authenticity
  4. You acknowledge receipt – return a 2xx status code to confirm successful delivery
  5. You process the event – handle the payload asynchronously in a background job

For a complete list of event types and their payload schemas, see the Platform Webhooks Reference.


Authentication Keys

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.


HTTP Request Format

Every webhook is delivered as an HTTP POST request with a JSON body.

Headers

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).

Body

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.


Verifying Requests

We recommend verifying both the API Key and the payload signature on every incoming request.

  1. Authenticate the request – check that the X-Api-Key header matches the API Key you received during onboarding. Reject requests with a missing or incorrect key.
  2. Verify message integrity and authenticity – the 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();
}

Response Handling

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).


Retry Behavior

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.


Delivery Guarantees

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.

Deduplication Example

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)

Endpoint Requirements

Your webhook endpoint should:


Troubleshooting

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.

Getting Started

To configure platform webhooks for your organization:

  1. Contact your CSM to discuss which events you need to subscribe to
  2. Provide your endpoint URL – a publicly reachable HTTPS endpoint that accepts POST requests
  3. Receive your keys – your CSM will provide your API Key and Signature Key
  4. Implement verification – validate the API Key and HMAC-SHA1 signature on every request
  5. Test the integration – your CSM can trigger test events to verify everything works end-to-end

For questions or changes to your webhook configuration, reach out to your Customer Success Manager.


Reference