Skip to main content

Verify Webhook Signatures

Webhook events should be verified before your application updates an order. Manatee signs webhook requests with HMAC-SHA256 so your backend can reject forged or modified payloads.

Each API key has its own webhook signing secret. Manatee shows it once when the credential is created or rotated; it cannot be revealed again later. Store it only on your server and rotate it if the original value was not saved.

Required headers​

HeaderPurpose
X-Manatee-Event-IdUnique event delivery ID for idempotency.
X-Manatee-Event-TypeEvent type, such as payment.detected or payment.confirmed.
X-Manatee-TimestampUnix timestamp in seconds.
X-Manatee-SignatureHMAC-SHA256 signature of timestamp + "." + raw request body.

For backwards compatibility, Manatee currently also sends the legacy X-Event-ID, X-Event-Type, and X-Signature headers. New integrations should verify the X-Manatee-* headers.

Verification rules​

  • Verify the signature before marking an order as paid.
  • Use the webhook signing secret that belongs to the API key used to create the payment.
  • Use the raw request body, not a parsed JSON object.
  • Reject requests where X-Manatee-Timestamp is too old or too far in the future. A five-minute tolerance is a good default.
  • Compare signatures with a timing-safe comparison.
  • Store processed X-Manatee-Event-Id values so repeated deliveries cannot double-credit an order.

Endpoint reachability​

Your webhook endpoint must accept server-to-server HTTP requests from Manatee. If the endpoint is protected by Cloudflare, a WAF, bot protection, captchas, browser challenges, or login redirects, bypass those protections for the webhook path and verify authenticity with the X-Manatee-Signature HMAC header instead.

Scope any bypass narrowly to the webhook route, for example /webhooks/manatee, rather than disabling protection for your whole domain.

Node.js example​

const crypto = require('crypto');

function verifySignature(rawBody, signatureHeader, timestampHeader, secret) {
if (!signatureHeader?.startsWith('sha256=')) {
return false;
}

const timestamp = Number(timestampHeader);
if (!Number.isFinite(timestamp)) {
return false;
}

const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > 300) {
return false;
}

const actual = Buffer.from(signatureHeader.slice('sha256='.length), 'hex');
const expected = crypto
.createHmac('sha256', secret)
.update(String(timestamp))
.update('.')
.update(rawBody)
.digest();

if (actual.length !== expected.length) {
return false;
}

return crypto.timingSafeEqual(actual, expected);
}

Processing recommendation​

Return a 2xx response only after your application has accepted the event. If your endpoint fails or returns a non-2xx response, Manatee retries delivery according to the retry schedule documented in the Quickstart.

For the full delivery model, including payload shape, retry timing, and failure cases, see Webhook Delivery in a Crypto Payment API.