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 thewebhooks:read/webhooks:writescopes./api/webhooks- the same underlying delivery mechanism, but authenticated by browser session (cookie) rather than an API key, and restricted toadmin/superadminusers. 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.
| Method | Path | Description |
|---|---|---|
GET | /api/webhooks | List endpoints for the org |
POST | /api/webhooks | Create an endpoint |
PATCH | /api/webhooks/:id | Update url, events, or active |
DELETE | /api/webhooks/:id | Remove an endpoint |
POST | /api/webhooks/:id/ping | Send 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:
| Event | Fired when |
|---|---|
offboarding.created | A new offboarding is created |
offboarding.stage_changed | The offboarding advances to a new stage |
offboarding.completed | The offboarding reaches the completed stage |
offboarding.deleted | An offboarding is deleted |
asset.returned | An asset is marked as returned |
checklist.completed | All checklist tasks for an offboarding are done |
document.signed | An employee signs a document |
audit.created | A 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.