Maxmove LogoDocs

Die Entwicklerdokumentation ist auf Englisch verfügbar.

Verify webhook signatures

Check the x-signature HMAC on every Maxmove webhook request with your endpoint's signing secret, and reject stale or forged events.

Als Markdown ansehen

Every webhook request is signed with the signing secret (whsec_…) of the endpoint that receives it. Verify the signature before you trust the body.

Headers#

HeaderContent
x-event-idEvent id, same as id in the body.
x-event-typeEvent type.
x-delivery-idDelivery id.
x-event-timestampSend time, RFC 3339.
x-signaturesha256= followed by a hex HMAC-SHA256.

How the signature is computed#

base      = "{x-event-id}.{x-delivery-id}.{x-event-timestamp}.{sha256 hex of the raw body}"
signature = "sha256=" + hex(HMAC-SHA256(signing_secret, base))

To verify a request:

  1. Reject it if x-event-timestamp is more than 5 minutes away from your clock. This blocks replayed requests.
  2. Hash the raw request body with SHA-256, exactly as received and before JSON parsing.
  3. Build the base string from the headers and the body hash, and compute the HMAC with your signing secret.
  4. Compare the result with x-signature in constant time.
import crypto from 'node:crypto';
 
// rawBody: the request body exactly as received (Buffer or string), before JSON parsing.
export function isValidMaxmoveWebhook(headers, rawBody, secret) {
  const timestamp = headers['x-event-timestamp'];
  if (Math.abs(Date.now() - Date.parse(timestamp)) > 5 * 60 * 1000) return false;
 
  const bodyHash = crypto.createHash('sha256').update(rawBody).digest('hex');
  const base = [headers['x-event-id'], headers['x-delivery-id'], timestamp, bodyHash].join('.');
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(base).digest('hex');
 
  const received = String(headers['x-signature'] ?? '');
  return (
    received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))
  );
}

Use the raw body#

Most frameworks parse JSON before your handler runs. Re-serializing the parsed object changes the bytes and breaks the signature, so read the raw body for verification.

import express from 'express';
import { isValidMaxmoveWebhook } from './verify.js';
 
const app = express();
 
app.post('/maxmove/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  if (!isValidMaxmoveWebhook(req.headers, req.body, process.env.MAXMOVE_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(req.body.toString('utf8'));
  res.sendStatus(200);
  // Process the event after answering, for example by putting it on a queue.
  queueEvent(event);
});

queueEvent and queue_event stand for your own background processing. Answer within 5 seconds; see Retries and replay.

Check your verification#

POST /v1/webhooks/{webhookId}/test sends a signed sample event to your endpoint. Use it to confirm that valid requests pass and that a modified body fails.

Rotate the secret#

PATCH /v1/webhooks/{webhookId} with "rotate_secret": true issues a new signing secret and returns it once in the response. From then on, every request to the endpoint is signed with the new secret, including retries of earlier events. Update your configuration right after rotating.

Hat das Ihre Frage beantwortet?