# Webhook signature checks that fail, and why

**Category:** [Webhooks](https://help.totalctrl.app/hc/totalctrl/totalctrl-help-center/en-US/categories/platform-webhooks)
**Updated:** 2026-10-04

## Start with the Deliveries tab

**Developer & API → Webhooks → Deliveries** records every attempt with the status code and the first 2000 characters your endpoint returned. Before changing any code, read what your own server said — most signature problems are visible there in one line.
## Every signature fails, and the payload looks right

Almost always the body was re-serialized before it was signed. Your framework parsed the JSON into an object and you turned that object back into a string; the characters differ from what we sent even though the data is identical, so the hash differs too.

Log both values once and compare them literally:
```
print(repr(request.get_data()))     # what you are hashing
print(request.headers.get('X-TotalCtrl-Signature'))
```

If the body prints with different spacing or key order from what the Deliveries tab shows, that is your answer. Capture the raw bytes first.
## Common causes, in the order they are worth checking
SymptomUsuallyEvery delivery fails the checkThe body was re-serialized, or the `sha256=` prefix was left off the expected value.Worked, then stopped after a deployNew middleware is touching the body — a JSON body-parser, a proxy, or compression added in front of your app.Works locally, fails in productionThe wrong secret for that environment, or a load balancer buffering and rewriting the body.Some subscriptions verify, others do notEach subscription has its OWN secret. Pick it using `X-TotalCtrl-Hook-Id`.Signature is emptyHeader names are case-insensitive but underscores are not dashes — check for `HTTP_X_TOTALCTRL_SIGNATURE` in PHP and CGI-style environments.Deliveries show a timeoutYou are doing the work before responding. Verify, store, return 2xx, then process.
## Test your check without waiting for an event

Compute a signature over a body of your choosing and POST it to yourself. If this is accepted and a real delivery is not, the difference is in how you are reading the body, not in the hashing:
```
SECRET='your-signing-secret'
BODY='{"event":"test","timestamp":"2026-09-02T00:00:00Z","account_id":"x","data":{}}'
SIG="sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')"

curl -i -X POST https://your-server.com/hooks/totalctrl \
  -H "Content-Type: application/json" \
  -H "X-TotalCtrl-Signature: $SIG" \
  -H "X-TotalCtrl-Event: test" \
  --data "$BODY"
```

`printf` rather than `echo` — `echo` adds a trailing newline, which changes the bytes and therefore the signature. That one character is a surprisingly common hour lost.
## If you have lost the secret

It is displayed once, when the subscription is created, and cannot be shown again. Delete that subscription and create a new one with the same URL and events; you will be given a fresh secret. Deliveries already recorded are unaffected.

---

## Related Articles

- [Events you can now subscribe to that you could not before](https://help.totalctrl.app/en-US/articles/webhooks-newly-available-events)
- [Every offered webhook event now actually fires](https://help.totalctrl.app/en-US/articles/webhooks-all-events-live)
- [Events you can now subscribe to that you could not before](https://help.totalctrl.app/en-US/articles/webhooks-newly-available-events-1)
- [Issue events now fire for issues created through the API](https://help.totalctrl.app/en-US/articles/webhooks-issues-api-event-names)
- [Issue events now fire for issues created through the API](https://help.totalctrl.app/en-US/articles/webhooks-issues-api-event-names-1)

---
[← Back to TotalCtrl Help Center](https://help.totalctrl.app/en-US/)