Troubleshooting Developers

Webhook signature verification fails on your side

Why X-DS-Signature does not match on your server: changed raw body, wrong key, timestamp or encoding, and how to test with the ping message and the message log.

Last updated 22 October 2026 · Last verified 22 October 2026

What it means#

Each webhook message we send is signed. Your server recomputes the signature and compares it with the X-DS-Signature header; when they differ, your code rejects the message, for example with a 401. The platform cannot see the comparison. It only sees your HTTP answer, so a mismatch shows up as a failed delivery: the Response column of the message log shows the code (for example 401), and the details window and the link's last-sent line show the error text Your system response 401 (Arabic: «رد نظامك 401»). That text is stored when the message is sent, in the language active at that moment, so older or background messages may appear in the other language; the numeric code is the reliable signal.

Who is affected#

Merchants (platform merchants with the webhooks feature) and company admins, who receive the same signed messages for their links. Each link has its own signing key.

Likely causes#

The signature is sha256= followed by the lowercase hex of HMAC-SHA256, where the message is X-DS-Timestamp, a dot, and the exact raw body, and the key is the whole signing key including whsec_.

Cause What goes wrong Fix
Body changed You parse the JSON and sign a re-encoded copy. The sent body keeps non-ASCII letters (such as Arabic) as they are and does not escape slashes (no \/; only a few control characters are still escaped), and the parcel data often contains Arabic text and a tracking_url full of slashes, so most re-encoders produce different bytes. Body-parsing middleware, proxies or firewalls that minify or re-wrap the body do the same. Sign the raw bytes, read before any parsing
Wrong key The key of another link, an API key (sk_, ck_), the key without whsec_, an old key after Renew, a masked copy with dots, or a trailing space or newline in a variable Copy the key of this link from the panel
Wrong timestamp You sign with your own clock or the created_at field instead of the X-DS-Timestamp header, or you treat it as milliseconds Use the header value as text, exactly
Stale message Your freshness check fails because your clock is off, or you check after queueing. The tolerance in the panel's examples is 300 seconds Check the age on receipt and fix the server time
Format You compare without the sha256= prefix, in base64 (Shopify and WooCommerce use base64, we use hex), or in a different letter case Compare the whole header value with a constant-time function
Reused signature Each attempt has a new timestamp, so a stored signature fails Recompute every time

How to diagnose#

  1. Send a test. In the merchant panel open Webhook and API, then Manage webhooks. In the company panel open Integrations and API, then Manage links and log. Press Send test on the link (it appears only for enabled links). A ping message is sent with the same signing; its data holds the latest parcel in your account, or a short test message if there is none. The result is The test message reached your system (response 200). or Not delivered: …. Through the API, POST /api/v1/webhooks/{id}/test does the same, but its data always holds only a short test message, not parcel data.

  2. Open the message log. Each row shows the response code. The eye button shows what we sent and your answer (the first 2,000 characters), which often contains your own "invalid signature" text. The payload there is shown formatted, and the signature and timestamp are not stored, so it cannot be used to recompute the signature.

  3. Log on your side the raw body, X-DS-Timestamp and X-DS-Signature, then recompute by hand:

    bash
    ( printf '%s.' "$TIMESTAMP"; cat body.raw ) | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET"
    

    The hex result must equal the header after sha256=. If it matches, your verification code is at fault; if not, the key or the body is.

  4. Check your code against the test vector in the signature verification guide.

Resolution steps#

  1. Verify the raw body

    Read the bytes before any JSON parsing and turn off automatic body parsing on this route.

  2. Use the right key

    Open the link's Signing key (the eye button shows it, the copy button copies it). Store the whole whsec_ string with no spaces. After Renew the old key stops working at once, so update your server first.

  3. Fix the time check

    Take the timestamp from the header and use seconds. Keep the server clock synchronised.

  4. Test again, then resend

    Press Send test. Failed messages are retried automatically after 1, 5 and 30 minutes, then 2, 6, 12 and 24 hours, for 8 attempts in all. To avoid waiting, press Resend on a row.

What happens while it fails#

After 40 consecutive failed sends, including tests and retries, the link is suspended and shows its reason. Fix your server, then press Edit, tick Enabled and save; the counter restarts. The log keeps 30 days.

When to contact support#

  • Your recomputed signature matches the header, yet your server still rejects the message.
  • Send test reports Not delivered with an error about the address, although your URL is public HTTPS.
  • Always quote the evt_ id and the time of the message; both are in the log.

API

  • POST /api/v1/webhooks/{id}/test