Webhooks
Real-time notifications, delivered to you.
Register HTTP endpoints, filter by event type, verify HMAC signatures, and build reactive integrations that respond the moment something happens.
Concepts
- Endpoint -- A URL you register to receive event notifications.
- Delivery -- A single attempt to send an event payload to your endpoint.
- Secret -- An HMAC signing key used to verify that a delivery came from wrk!ng.
API endpoints
All webhook endpoints require authentication with a Bearer token.
| Method | Path | Description |
|---|---|---|
POST | /api/webhooks | Create an endpoint (returns signing secret once) |
GET | /api/webhooks | List endpoints for your organization |
GET | /api/webhooks/:id | Get endpoint details with recent deliveries |
PATCH | /api/webhooks/:id | Update URL, event types, or active status |
DELETE | /api/webhooks/:id | Soft-delete an endpoint |
POST | /api/webhooks/:id/test | Fire a test event to the endpoint |
GET | /api/webhooks/:id/deliveries | List deliveries (paginated) |
POST | /api/webhooks/:id/deliveries/:did/retry | Manually retry a failed delivery |
Creating a webhook
Register a new endpoint by posting the target URL and the event types you want to receive:
curl -X POST https://api.wrk.ing/api/webhooks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/wrking-webhook",
"event_types": ["task.post_create", "employee.post_update"],
"description": "Sync tasks and employee changes"
}' const res = await fetch("https://api.wrk.ing/api/webhooks", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://your-server.com/wrking-webhook",
event_types: ["task.post_create", "employee.post_update"],
description: "Sync tasks and employee changes",
}),
});
const webhook = await res.json(); res = requests.post(
"https://api.wrk.ing/api/webhooks",
headers={"Authorization": f"Bearer {token}"},
json={
"url": "https://your-server.com/wrking-webhook",
"event_types": ["task.post_create", "employee.post_update"],
"description": "Sync tasks and employee changes",
},
)
webhook = res.json() Copy your secret immediately
The response includes a secret field that is only returned once. You need it to verify delivery signatures -- store it securely right away.
{
"id": 12,
"url": "https://your-server.com/wrking-webhook",
"secret": "whsec_7f3a9b...",
"event_types": ["task.post_create", "employee.post_update"],
"description": "Sync tasks and employee changes",
"is_active": true
} Event filtering
The event_types array controls which events are delivered to your endpoint. Pass an empty array (or omit the field) to subscribe to all events.
Event types follow the {aggregate_type}.{phase} naming pattern:
task.post_create-- a task was createdtask.post_update-- a task was updatedemployee.post_update-- an employee record was changedworkflow.completed-- a workflow run finishedfeedback.post_create-- new feedback was submittedsurvey.post_create-- a survey was created
See the Events guide for the full list of lifecycle phases and event types.
Delivery format
Each delivery is an HTTP POST to your endpoint URL.
Headers
| Header | Description |
|---|---|
Content-Type | application/json |
X-Wrking-Signature | HMAC-SHA256 hex digest of the raw request body |
X-Wrking-Event | The event type string (e.g. task.post_create) |
X-Wrking-Delivery-Id | Unique identifier for this delivery attempt |
Body
The body is a JSON object containing the full event payload:
{
"id": 8401,
"org_id": 1,
"aggregate_type": "task",
"aggregate_id": 234,
"event_type": "task.post_create",
"payload": {
"org_id": 1,
"actor_id": 42,
"record_id": 234,
"record_type": "task",
"record_name": "Update Q3 roadmap",
"action": "post_create",
"timestamp": "2026-04-12T14:30:00.000Z"
},
"occurred_at": "2026-04-12T14:30:00.000Z"
} Signature verification
Always verify the X-Wrking-Signature header before processing a delivery. This confirms the payload was sent by wrk!ng and has not been tampered with.
The signature is a hex-encoded HMAC-SHA256 digest of the raw request body, using your endpoint's secret as the key.
import crypto from "node:crypto";
function verifySignature(rawBody, secret, signature) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
}
// In your request handler:
const isValid = verifySignature(
req.body, // raw string, not parsed JSON
process.env.WEBHOOK_SECRET,
req.headers["x-wrking-signature"],
);
if (!isValid) {
res.status(401).send("Invalid signature");
return;
} import hashlib
import hmac
def verify_signature(raw_body: bytes, secret: str, signature: str) -> bool:
expected = hmac.new(
secret.encode(),
raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature) package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func verifySignature(body []byte, secret, signature string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
} Retry policy
Automatic retries with backoff
When a delivery fails, wrk!ng retries up to 6 times with exponential backoff. After all attempts are exhausted, you can trigger a manual retry through the API at any time.
| Attempt | Delay after failure |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 12 hours |
Success means your endpoint returned an HTTP 2xx status code within 10 seconds.
Failure means any non-2xx response, a timeout (>10s), or a network error.
Testing
Use the test endpoint to send a webhook.test event to your registered URL without waiting for a real event:
curl -X POST https://api.wrk.ing/api/webhooks/12/test \
-H "Authorization: Bearer $TOKEN" await fetch("https://api.wrk.ing/api/webhooks/12/test", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
}); requests.post(
"https://api.wrk.ing/api/webhooks/12/test",
headers={"Authorization": f"Bearer {token}"},
) This fires a delivery with event_type: "webhook.test" and a sample payload. Check your server logs to confirm it arrived and that signature verification passed.
Best practices
- Respond with 2xx quickly. Do your processing asynchronously. If your handler takes more than 10 seconds, the delivery is marked as failed.
- Verify signatures on every request. Never skip this step, even in development.
- Use the delivery ID for idempotency. The
X-Wrking-Delivery-Idheader uniquely identifies each delivery. Store processed IDs to avoid handling the same event twice on retries. - Return 4xx for permanent failures. If a delivery payload is malformed or your system cannot process that event type, return a 4xx to prevent unnecessary retries.
- Monitor your delivery log. Check the
/api/webhooks/:id/deliveriesendpoint or the admin UI to spot patterns in failed deliveries.