API Reference
Webhooks

Webhooks

PurpletGo can POST event notifications to any HTTPS endpoint you configure. There are two ways to manage webhook endpoints, and it's worth knowing which one you're using:

  • /api/v1/webhooks - the public API-key surface. Use this if you're integrating from outside PurpletGo. Requires a Bearer API key with the webhooks:read / webhooks:write scopes.
  • /api/webhooks - the same underlying delivery mechanism, but authenticated by browser session (cookie) rather than an API key, and restricted to admin/superadmin users. This is what the PurpletGo dashboard itself calls, and it's the only place you can update an endpoint or send a manual test ping - the API-key surface only supports list, create, and delete.

Both require the webhooks feature on your plan.


Managing webhooks with an API key

List webhooks

GET /api/v1/webhooks

Required scope: webhooks:read

Response 200 OK

[
  {
    "id": "3f1b2c4a-...",
    "url": "https://your-app.com/purpletgo-events",
    "events": ["offboarding.completed", "asset.returned"],
    "active": true,
    "created_at": "2024-03-01T00:00:00Z"
  }
]

Create a webhook

POST /api/v1/webhooks

Required scope: webhooks:write

Request body

{
  "url": "https://your-app.com/purpletgo-events",
  "events": ["offboarding.completed", "offboarding.stage_changed"]
}

url must be HTTPS and resolve to a public host - PurpletGo rejects loopback, private-network, and link-local addresses. events is required and must be a non-empty array of event names (up to 50).

Response 201 Created

{
  "id": "3f1b2c4a-...",
  "url": "https://your-app.com/purpletgo-events",
  "events": ["offboarding.completed", "offboarding.stage_changed"],
  "active": true,
  "created_at": "2024-03-01T00:00:00Z",
  "secret": "whsec_a1b2c3d4..."
}
⚠️

secret is returned only in this response. Store it - you'll need it to verify the X-PurpletGo-Signature header on incoming deliveries.

Delete a webhook

DELETE /api/v1/webhooks/:id

Required scope: webhooks:write

Response 200 OK - { "message": "Webhook deleted" }


Managing webhooks from the dashboard session

These mirror the API-key endpoints above but live under /api/webhooks, use your logged-in session, and require the admin or superadmin role.

MethodPathDescription
GET/api/webhooksList endpoints for the org
POST/api/webhooksCreate an endpoint
PATCH/api/webhooks/:idUpdate url, events, or active
DELETE/api/webhooks/:idRemove an endpoint
POST/api/webhooks/:id/pingSend a test ping event

Ping a webhook

POST /api/webhooks/:id/ping

Sends a signed test payload ({ "event": "ping", "timestamp": "..." }) to the configured URL so you can confirm connectivity and signature verification before relying on real events.

Response 200 OK

{ "success": true, "status": 200 }

If the request to your endpoint fails outright (DNS, timeout, connection refused), you'll get { "success": false, "error": "Webhook delivery failed" } instead of an HTTP error.


Event types

An endpoint's events array is validated against this fixed set - anything else is rejected at creation/update time:

EventFired when
offboarding.createdA new offboarding is created
offboarding.stage_changedThe offboarding advances to a new stage
offboarding.completedThe offboarding reaches the completed stage
offboarding.deletedAn offboarding is deleted
asset.returnedAn asset is marked as returned
checklist.completedAll checklist tasks for an offboarding are done
document.signedAn employee signs a document
audit.createdA new audit log entry is written

Passing events: [] subscribes an endpoint to everything.


Payload

Every delivery is a flat JSON object - event and timestamp plus whatever fields are relevant to that event, merged directly at the top level (there's no separate data wrapper):

{
  "event": "offboarding.stage_changed",
  "timestamp": "2024-06-02T14:30:00.000Z",
  "type": "stage_advanced",
  "title": "Offboarding advanced to IT Revocation",
  "body": "Jordan Lee's offboarding moved to the next stage.",
  "data": { "offboardingId": "..." }
}

The exact set of extra fields depends on the event - treat everything besides event and timestamp as best-effort context rather than a fixed schema.


Verifying signatures

Every request carries an X-PurpletGo-Signature header of the form sha256=<hex digest>, computed as an HMAC-SHA256 of the raw request body using your endpoint's secret. There's also an X-PurpletGo-Event header with the event name and an X-PurpletGo-Delivery header with a unique ID per delivery attempt.

const crypto = require('crypto');
 
function verifyWebhook(rawBody, signatureHeader, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader)
  );
}
 
app.post('/purpletgo-events', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-purpletgo-signature'];
  if (!sig || !verifyWebhook(req.body, sig, process.env.PURPLETGO_WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  const event = JSON.parse(req.body);
  // handle event...
  res.json({ received: true });
});

Retry policy

If a delivery doesn't get a 2xx response (or the request errors out), PurpletGo retries up to 3 times with exponential backoff: 1 second, then 2 seconds, then 4 seconds after the previous attempt. After the third failed attempt, delivery is abandoned and logged server-side - there's currently no delivery log or manual-retry UI, so a persistently failing endpoint just stops receiving that event.


Related

  • API Keys - generating a key with webhooks:read/webhooks:write scopes
  • Audit Log - a durable alternative if you can't guarantee an always-on receiving endpoint