Skip to main content

Documentation Index

Fetch the complete documentation index at: https://developers.paxoslabs.com/llms.txt

Use this file to discover all available pages before exploring further.

Every webhook request from Paxos Labs includes a cryptographic signature that lets you confirm the request is authentic and hasn’t been tampered with. Always verify signatures before processing webhook payloads.

Headers

Each webhook POST request includes these headers:
HeaderExampleDescription
X-PAXOS-LABS-TIMESTAMP2026-04-07T18:06:40.000ZRFC 3339 UTC timestamp of event creation. Concatenated with the payload before signing.
X-PAXOS-LABS-SIGNATUREa3f2...9b01HMAC-SHA256 hex digest of the timestamp and payload
Content-Typeapplication/jsonAlways JSON
User-AgentPaxosLabs-Webhooks/1.0Identifies the sender

Signature Scheme

X-PAXOS-LABS-SIGNATURE contains the raw hex-encoded HMAC-SHA256 digest. The timestamp used in the HMAC computation is sent in the separate X-PAXOS-LABS-TIMESTAMP header.

Verification Algorithm

1

Read the timestamp and signature headers

Read X-PAXOS-LABS-TIMESTAMP for the RFC 3339 timestamp and X-PAXOS-LABS-SIGNATURE for the hex signature.
2

Check the timestamp

Reject requests where the timestamp is more than 5 minutes from your server’s current time. This protects against replay attacks.
3

Construct the signed payload

Concatenate the timestamp, a literal period (.), and the raw request body (the exact bytes received — do not parse and re-serialize):
{timestamp}.{raw_body}
4

Compute the expected signature

Calculate an HMAC-SHA256 using your endpoint’s signing secret as the key and the constructed string as the message. Hex-encode the result.
5

Compare signatures

Use a constant-time comparison to check that your computed signature matches the v1 value from the signature header. This prevents timing attacks.

Implementation Examples

import { createHmac, timingSafeEqual } from 'node:crypto'

const TOLERANCE_SECONDS = 300

function verifyWebhookSignature(rawBody, signatureHeader, timestampHeader, secret) {
  const timestamp = new Date(timestampHeader)
  if (Number.isNaN(timestamp.getTime())) {
    throw new Error('Invalid timestamp header')
  }

  if (Math.abs(Date.now() - timestamp.getTime()) / 1000 > TOLERANCE_SECONDS) {
    throw new Error('Timestamp outside tolerance — possible replay attack')
  }

  const signedPayload = `${timestampHeader}.${rawBody}`
  const expected = createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex')

  const received = Buffer.from(signatureHeader, 'hex')
  const expectedBuf = Buffer.from(expected, 'hex')

  if (received.length !== expectedBuf.length ||
      !timingSafeEqual(received, expectedBuf)) {
    throw new Error('Invalid signature')
  }
}
Use timingSafeEqual instead of === to prevent timing side-channel attacks.

Best Practices

Never process a webhook payload without verifying the signature first. An unverified payload could be forged by a malicious actor.
Standard string comparison (===, ==) leaks timing information that attackers can exploit. Always use timingSafeEqual (Node.js), hmac.compare_digest (Python), or hmac.Equal (Go).
Reject events where the timestamp is more than 5 minutes from your server’s clock. This prevents captured requests from being replayed later.
Compute the HMAC over the exact bytes received in the HTTP body. Parsing to JSON and re-serializing can change whitespace or key ordering, producing a different signature.
Keep your signing secret in a secrets manager or encrypted environment variable — never hard-code it or commit it to source control.
Return a 2xx status code within 10 seconds. Move heavy processing to a background queue so the webhook handler returns immediately.
Use the event id field to deduplicate. Store processed event IDs and skip any that you’ve already handled.

Troubleshooting

SymptomCauseFix
Signature mismatchRe-serialized body instead of raw bytesUse the raw HTTP body for HMAC computation
Timestamp rejectionServer clock driftSync your server with NTP; widen tolerance if needed
401 on test eventsWrong secretVerify you’re using the correct pxlwh_ secret for this endpoint
Secret lostSecret was not saved at creationDelete the endpoint and create a new one