Skip to main content
Paymove signs every outgoing webhook request with HMAC-SHA256. This lets you confirm that a notification genuinely came from Paymove, and not from someone who learned your endpoint URL.
Signature verification is mandatory. Without it, anyone who knows your endpoint can send a forged payment notification and have an order fulfilled without paying.

The header

Every request carries a single signature header:

Signing secret

Each webhook has its own secret in the format whsec_<base64>. You receive it in the response to registering the webhook, in the signingSecret field.
Use the entire string including the whsec_ prefix, UTF-8 encoded, as the HMAC key. Do not strip the prefix and do not base64-decode the remainder - this is the single most common cause of failing verification.
If you lose the secret, you can read it again from the GET /api/pay/plugin/webhook/{webhookId} response - there is no need to rotate it for that reason.

Algorithm

  1. Read the X-Paymove-Signature header.
  2. Split the value on the comma into the t=<timestamp> and v1=<signature> parts.
  3. Build the signed content: {timestamp}.{raw_request_body}.
  4. Compute HMAC-SHA256 using the full secret (with prefix) as the key.
  5. Base64-encode the result and prepend v1=.
  6. Compare it with the signature from the header using a timing-safe comparison.
The signature is computed over the raw request body, byte for byte. If your framework parses the JSON and re-serializes it, the result will almost certainly differ. Capture the raw body before parsing - in Express via express.raw({ type: "application/json" }), in Next.js via await request.text().

Code

Node.js

Usage in Express:

Python

Java

Test vector

Check your implementation against the values below - skip the tolerance window, since the timestamp is in the past: If your function returns a different signature, check in order: that you use the full secret including the whsec_ prefix, that you encode to base64 (not hex), and that you sign the raw body without re-serializing it.

Replay protection

The timestamp is covered by the signature, so it cannot be tampered with - but Paymove itself does not reject old requests. Your server decides how long a signature stays valid. The recommended window is 5 minutes; the examples above already apply it. On top of that, fulfil orders idempotently, keyed on externalId. A redelivered notification must never result in shipping the goods twice.

Rotating the secret

The previous secret stops working immediately - there is no period during which both are accepted. Update your own configuration at the same moment, otherwise you will start rejecting genuine notifications.

What’s next

Payment statuses

What the webhook payload contains and how to check a payment’s state.

Webhook configuration

Registering a webhook and assigning it to a product.