Webhook payloads and signatures
What a strackt webhook delivery looks like — the request, the JSON body, the signature header and how to check it, the events in each category, and how retries work.
Webhook payloads and signatures
This page is the lookup reference for what arrives at your endpoint. To add, pause, or remove an endpoint, see Webhooks.
The request
Every delivery is a single HTTPS request:
| Part | Value |
|---|---|
| Method | POST |
Content-Type |
application/json |
Signature |
Hex-encoded HMAC-SHA256 of the request body, keyed with your endpoint's signing secret. See Verify the signature. |
| Body | A JSON object with the four fields below |
strackt waits 3 seconds for your endpoint to answer. Each endpoint gets its own delivery, signed with its own secret — if two endpoints subscribe to the same category, both receive the event independently.
The body
Every event uses the same envelope:
| Field | Type | Meaning |
|---|---|---|
event |
string | The event name, such as server.offline or operation.completed. Use this to decide what to do. |
category |
string | The category the event belongs to — one of the six categories below, or test for a test delivery. This is what your endpoint subscribed to. |
timestamp |
string | When strackt prepared the delivery, as ISO 8601 in UTC (for example 2026-09-26T14:03:12+00:00). A retry carries the same value. |
data |
object | Details about what happened. The fields depend on the event. |
An example server.offline delivery:
{
"event": "server.offline",
"category": "server_health",
"timestamp": "2026-09-26T14:03:12+00:00",
"data": {
"server_id": "9d3f6a1e-4b2c-4e8f-9a51-2c7d8e0f1b34",
"server_name": "web-01"
}
}
Values in the examples on this page are illustrative. Read the field names, not the values.
Neither the body nor the headers carry a unique delivery ID. If you need to
recognise a repeated delivery, compare the event, the timestamp and the IDs
inside data.
Operation events
Deploys, rollbacks, configuration updates, server installs and similar work all run as operations, and they arrive as three events:
| Event | Sent when |
|---|---|
operation.started |
The operation begins |
operation.completed |
The operation finished successfully |
operation.failed |
The operation stopped with an error |
The category depends on what kind of operation it is — a deploy arrives
under deployment, a configuration update under configuration, a server
install under adoption. So subscribing to Deployments gets you deploy
starts, completions and failures, without the configuration traffic.
Automatic updates that strackt applies to your servers send no
operation.started or operation.completed event. You get operation.failed
if one fails. A summary of the month's updates arrives by email instead — see
How strackt keeps your servers updated.
All three carry the same data fields:
| Field | Type | Meaning |
|---|---|---|
operation_id |
string | The operation's ID |
operation_type |
string | A stable identifier for the kind of operation, such as ssh-deploy |
operation_label |
string | A readable name for the kind of operation, such as Deploy |
server_id |
string or null | The server it ran on |
server_name |
string or null | That server's name |
operation.failed adds two more:
| Field | Type | Meaning |
|---|---|---|
error_message |
string | What went wrong, in plain language |
error_code |
string or null | A stable code for the kind of failure, when one is known |
{
"event": "operation.failed",
"category": "deployment",
"timestamp": "2026-09-26T14:03:12+00:00",
"data": {
"operation_id": "9d3f6b02-7c41-4d0a-b1e3-5f6a7b8c9d01",
"operation_type": "ssh-deploy",
"operation_label": "Deploy",
"server_id": "9d3f6a1e-4b2c-4e8f-9a51-2c7d8e0f1b34",
"server_name": "web-01",
"error_message": "The build step failed. Check the deploy log, fix the error, and deploy again.",
"error_code": null
}
}
Background housekeeping that doesn't change your site — such as strackt checking which plugins a WordPress site has — is not sent. Server reboots are announced through the maintenance events instead of operation events.
Events by category
When you add an endpoint you choose categories, and you receive every event in
the categories you tick. Event names don't always start with their category
name — server.offline belongs to server_health — so route on event, and
use category only if you need to know why the event reached you.
Besides the operation events above, each category carries:
| Category | Label in settings | Events |
|---|---|---|
deployment |
Deployments | deploy.health_check_failed, deploy.merge_conflict, preview.environment_created, preview.cap_reached, preview.fork_refused |
server_health |
Server Health | server.online, server.offline, server.unhealthy, server.http_unreachable, server.alert_fired, server.alert_resolved |
configuration |
Configuration | configuration.environment_created, configuration.environment_deleted, configuration.wordpress_update_failed, server.configuration_applied, server.configuration_not_applying |
security |
Security | security.ssh_ca_fallback, maintenance.scheduled, maintenance.override_scheduled, maintenance.completed, maintenance.cancelled, maintenance.failed_terminal |
adoption |
Adoption | server.adoption_succeeded, server.adoption_failed |
platform |
Platform | billing.payment_failed, application.storage_warning, application.storage_critical, server.orphaned_dataset_warning, server.orphaned_dataset_destroyed |
Each of these events carries its own data fields — typically the ID and name
of the server, application or environment involved, plus details specific to
the event. Treat any field you don't recognise as optional, and expect new
events and new data fields to appear over time: ignore what you don't handle
rather than rejecting the delivery.
Test deliveries
Send test on an endpoint delivers a test.ping event. It's signed and sent
exactly like a real event, so it's a good way to check your verification code:
{
"event": "test.ping",
"category": "test",
"timestamp": "2026-09-26T14:03:12+00:00",
"data": {
"message": "This is a test webhook from strackt.",
"endpoint_id": "9d3f6c55-0e12-4a7b-8c3d-4e5f6a7b8c9d"
}
}
The test category only ever carries test.ping. You can't subscribe to it;
it's sent only when you ask for it.
Verify the signature
The Signature header is the HMAC-SHA256 of the raw request body, keyed with
your endpoint's signing secret, written as lowercase hex. To check a delivery:
- Read the body exactly as it arrived, before parsing it.
- Compute HMAC-SHA256 of those bytes with your signing secret.
- Compare the result with the
Signatureheader using a constant-time comparison. If they differ, reject the request.
Always sign the raw bytes. Parsing the JSON and re-encoding it can change the
escaping of characters like / and produce a different signature.
Node.js:
import crypto from 'node:crypto';
function isFromStrackt(rawBody, signatureHeader, secret) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const given = Buffer.from(signatureHeader ?? '', 'utf8');
const wanted = Buffer.from(expected, 'utf8');
return given.length === wanted.length && crypto.timingSafeEqual(given, wanted);
}
PHP:
$rawBody = file_get_contents('php://input');
$expected = hash_hmac('sha256', $rawBody, $secret);
if (! hash_equals($expected, $_SERVER['HTTP_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}
The signature covers the body only. There is no separate timestamp header.
When you rotate the secret, new deliveries are signed with the new secret straight away. A delivery that was already on its way — including its retries — keeps the signature it was given with the old secret, so accept both secrets for a few minutes after rotating.
Responses and retries
| Your endpoint | What strackt does |
|---|---|
Answers with any 2xx status within 3 seconds |
The delivery counts as successful. The response body is ignored. |
| Answers with any other status, times out, or can't be reached | The attempt counts as failed, and strackt tries again. |
A delivery gets up to 3 attempts. After the first failure strackt waits about 10 seconds before trying again, and after the second about 100 seconds. If the third attempt also fails, strackt stops trying for that event — it isn't queued for later, and a later event doesn't resend it. While the most recent delivery is failing, the endpoint shows Needs attention in Settings → Connections → Webhooks; it clears once a later delivery succeeds.
strackt also won't deliver to an address that resolves to a private or internal network. Such a delivery fails straight away, without retrying.
Some practical consequences:
- Answer fast, then do the work. Return a
2xxas soon as you've verified the signature and stored the event. Anything that takes longer than 3 seconds counts as a failure, even if your code finishes afterwards. - Expect the occasional duplicate. If your endpoint handled a delivery but
answered too slowly or with a non-
2xxstatus, the same event arrives again. Make your handler safe to run twice. - Don't rely on order. Retries and separate deliveries can arrive in a
different order from the events that caused them. Use
timestampand the current state in strackt, not arrival order, when order matters. - Webhooks are a signal, not a record. A delivery that fails all three attempts is not sent again. If you need a complete history of operations, read it from the REST API.
Was this helpful?