Webhook security guide

Verify every payment webhook before fulfillment.

A webhook is an instruction to your backend, so authenticity and replay handling matter. Verify the exact request bytes with the endpoint secret, claim the event ID once, and acknowledge duplicate deliveries without repeating fulfillment.

Maintained by Payclave Product and Engineering. Updated .

Read the raw body first

Signature verification must use the bytes Payclave sent. Parsing and reserializing JSON can change those bytes.

Verify timestamp and HMAC

The server SDK validates the t and v1 values in X-Payclave-Signature with a five-minute default tolerance.

Deduplicate before side effects

Claim event.id under a unique database constraint before queuing fulfillment, email, inventory, or accounting work.

Use the endpoint secret, not an API key

Each webhook endpoint has its own signing secret. Store that secret in server-only configuration and rotate it if it is exposed. API keys authenticate your calls to Payclave; webhook secrets authenticate calls from Payclave to your endpoint. They are not interchangeable.

Payclave sends X-Payclave-Signature, X-Payclave-Timestamp, X-Payclave-Delivery, and X-Payclave-Event headers. The signature header contains a Unix timestamp and one or more v1 signatures.

Verify the exact request body

Read the request as text or bytes before a JSON body parser runs. The signed message combines the timestamp, a period, and the raw payload. The server SDK calculates HMAC SHA-256 and compares the result in constant time.

constructPayclaveWebhookEvent verifies the signature and then parses the event. Its default timestamp tolerance is 300 seconds. Keep server clocks synchronized and return a non-2xx response when verification fails.

Verify and construct the eventtypescript
import { constructPayclaveWebhookEvent } from "@payclave/sdk-server"

export async function POST(request: Request) {
  const rawBody = await request.text()
  const signature = request.headers.get("X-Payclave-Signature") ?? ""

  const event = constructPayclaveWebhookEvent({
    payload: rawBody,
    signature,
    secret: process.env.PAYCLAVE_WEBHOOK_SECRET!,
  })

  await eventStore.processOnce(event.id, async () => {
    if (event.type === "invoice.paid") {
      await queueFulfillment(event.data)
    }
  })

  return new Response(null, { status: 204 })
}

Handle secret rotation overlap

During secret rotation, Payclave can send both the new and previous v1 signatures in the same header. The header shape is t=<timestamp>,v1=<new>,v1=<old>. The SDK accepts the delivery when any v1 value matches the secret supplied by your endpoint.

Deploy code that can read the new secret, send a test delivery, and end the overlap only after the new path succeeds. Payclave keeps the previous secret valid for no more than 24 hours.

Claim the event before fulfillment

Successful delivery is at least once, not exactly once. Network timeouts, retries, and manual replay can deliver the same event again. Put event.id under a unique constraint and claim it in the same durable workflow that schedules the business action.

If the event already exists, return a 2xx response without repeating the action. Payclave treats any 2xx response as acknowledgement. Failed deliveries retry after 1 minute, 5 minutes, 30 minutes, and 2 hours before automatic retries stop after the fifth failed attempt.

Fulfill only the event you expect

Use invoice.paid for the first verified transition to a fully paid invoice. Treat payment.underpaid, payment.overpaid, invoice.expired, and payment.failed as operational outcomes that need their own order logic.

Check the event type, merchant mode, invoice identifier, external reference, and commercial total before fulfillment. Do not turn every signed event into the same business action.

For the complete request fields, response schemas, error codes, and event types, use the Payclave API reference.

Test the workflow

Create a checkout before changing your production order flow.

Use test keys and the API playground to verify checkout creation, payment status, and signed webhook handling.