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.

Request
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"
}
Response
{
  "_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.

Delivery
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.

Node.js
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.

Install and listen
brew install Pixwel/tap/pixwel

pixwel login
pixwel listen --forward-to localhost:3000/webhooks
Ready. 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        -> 500

The 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:

Trigger
# in a second terminal
pixwel trigger asset.updated

Run 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_changed

Files

file.createdfile.embargo_expired

Comments

comment.createdcomment.updatedcomment.reply_createdcomment.reply_updated

Work 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_review

Attachment changes are delivered as work_request.updated.