Webhooks
Webhooks
Instead of polling, register an endpoint and Pixwel will POST a signed JSON payload whenever something changes. You receive events only for resources your account can already see.
Create an endpoint
Register a destination with POST /api/webhook-endpoints. Provide an HTTPS url and the event names you want — omit enabledEvents or pass ["*"] to receive everything. You can also manage endpoints from Preferences → Webhooks in the app.
POST /api/webhook-endpoints HTTP/1.1
Authorization: Bearer pat_…
Content-Type: application/json
{
"url": "https://example.com/pixwel/webhooks",
"enabledEvents": ["asset.updated", "work_request.completed"],
"description": "production integration"
}{
"_id": "665f…",
"url": "https://example.com/pixwel/webhooks",
"transport": "http",
"description": "production integration",
"enabledEvents": ["asset.updated", "work_request.completed"],
"apiVersion": "2026-07-31",
"status": "enabled",
"createdAt": 1719300000,
"lastSuccessAt": null,
"lastFailureAt": null,
"secret": "whsec_3f9a…" // shown only once — store it now
}The secret is returned once, at creation (and again only when you roll it via POST /api/webhook-endpoints/{id}/roll-secret). Store it securely — you need it to verify deliveries.
Payload
Each delivery is a JSON envelope. data.object is a documented, versioned subset of the affected resource, scoped to what the endpoint owner can see. The endpoint's apiVersion pins that field set. Use the id for idempotency and the type to route.
POST /pixwel/webhooks HTTP/1.1
webhook-id: 665f0a…
Pixwel-Event-Type: asset.updated
webhook-timestamp: 1719300000
webhook-signature: v1,<base64 signature>
Content-Type: application/json
User-Agent: Pixwel-Webhooks/1
{
"id": "evt_665f0a…",
"type": "asset.updated",
"apiVersion": "2026-07-31",
"created": 1719300000,
"test": false,
"data": {
"object": { "_id": "…", "name": "…", "project": "…", "type": "…" }
}
}Verify the signature
Verify deliveries with a Standard Webhooks library. Pass your signing secret, the raw request body, and the webhook-id, webhook-timestamp, and webhook-signature headers to its verifier. Reject the request if verification fails; the library checks both the signature and timestamp freshness.
import crypto from 'node:crypto'
const TOLERANCE_SECONDS = 300
// req.body must be the RAW request body (string/Buffer), not parsed JSON.
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
String(header || '').split(',').map((part) => part.split('=').map((value) => value.trim()))
)
const timestamp = Number(parts.t)
const signature = parts.v1 || ''
if (
!Number.isInteger(timestamp) ||
Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS ||
!/^[0-9a-f]{64}$/i.test(signature)
) {
return false
}
const expected = crypto
.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest()
const provided = Buffer.from(signature, 'hex')
return expected.length === provided.length && crypto.timingSafeEqual(expected, provided)
}Local development
Your machine isn't reachable from our servers, so use the CLI to receive events on localhost. It registers an endpoint that we never POST to — deliveries queue up and the CLI pulls them down and replays them locally, so no tunnel is needed.
brew install Pixwel/tap/pixwel
pixwel login
pixwel listen --forward-to localhost:3000/webhooksReady. Forwarding events to http://localhost:3000/webhooks
Signing secret: whsec_3f9a…
Events: all
10:04:12 asset.updated -> 200 OK
10:04:19 work_request.completed -> 500The signing secret is stable across runs, so you can set it once in your environment. Events are forwarded byte-for-byte with the same webhook-signature header a production delivery carries — the verification code above is exercised for real.
To exercise a handler without waiting for real activity, trigger a test event. It sends a fixture payload marked "test": true and mutates nothing:
# in a second terminal
pixwel trigger asset.updatedRun pixwel trigger with no argument to list every type you can send. Whatever your local receiver returns is reported back, so the delivery log under Preferences → Webhooks reflects what actually happened.
Retries & failures
Respond with a 2xx quickly (do heavy work asynchronously). Non-2xx responses, timeouts and connection errors are retried with backoff over roughly three days. Delivery is at least once, so use the event id to ignore duplicates. An endpoint that keeps failing is automatically disabled; re-enable it from Preferences once your receiver is healthy. Inspect and manually re-send individual attempts via GET /api/webhook-endpoints/{id}/deliveries and POST /api/webhook-deliveries/{id}/retry.
Event types
The full, current list is available at GET /api/webhook-event-types.
Assets
asset.createdasset.updatedasset.note_addedasset.note_updatedasset.workflow_phase_changedFiles
file.createdfile.embargo_expiredComments
comment.createdcomment.updatedcomment.reply_createdcomment.reply_updatedWork requests
work_request.submittedwork_request.incompletework_request.awaiting_materialswork_request.in_progresswork_request.for_reviewwork_request.approvedwork_request.rejectedwork_request.restartedwork_request.completedwork_request.cancelledwork_request.updatedwork_request.translation_updatedwork_request.translation_submittedwork_request.translation_for_reviewAttachment changes are delivered as work_request.updated.