Last Updated: July 20, 2026
Webhooks let you react to assignment and time-tracking activity in real time. When a resource is scheduled or a time entry is created, ResourcePlanner sends an HTTP POST to your URL.
You can rotate the URL or secret any time. Updates take effect immediately.
| Event | Triggered when… |
| ----- | --------------- |
| `assignment.created` | A new assignment is created via the UI or REST API. |
| `assignment.updated` | An existing assignment changes (dates, hours, note, task details, project/resource reassignment). |
| `assignment.deleted` | An assignment is removed. |
| `time-entry.created` | A completed time entry is added or a running timer is started in the UI. |Assignment events include the full assignment payload. The time-entry.created event includes the normalized time entry. Starting a timer sends the event immediately with endedAt: null and isRunning: true; stopping, updating, or deleting it does not send another time-entry event.
Every webhook POST has the following headers:
| Header | Description |
| ------ | ----------- |
| `Content-Type` | Always `application/json`. |
| `X-ResourcePlanner-Event` | Event name (e.g. `assignment.updated`). |
| `X-ResourcePlanner-Signature` | Hex-encoded HMAC SHA-256 signature (present only when a webhook secret is configured). |An assignment event body looks like this:
{
"event": "assignment.created",
"workspaceId": "64f1088d4f3c2c0012b12345",
"timestamp": "2025-10-15T09:21:34.512Z",
"data": {
"id": "6717a347e1577b0b4e9fd200",
"project": { "id": "6702ac44e1577b0b4e9fd100", "title": "Website Refresh" },
"resource": { "id": "66fa6f3be1577b0b4e9fd001", "name": "Hana Sato" },
"dateFrom": "2025-10-20T00:00:00.000Z",
"dateTo": "2025-10-24T00:00:00.000Z",
"hoursPerDay": 6,
"note": "Workshop preparation",
"title": "Prepare workshop materials",
"description": "<p>Collect slides, examples, and handouts.</p>",
"taskId": "6717a347e1577b0b4e9fd201",
"task": {
"id": "6717a347e1577b0b4e9fd201",
"title": "Prepare workshop materials",
"description": "<p>Collect slides, examples, and handouts.</p>",
"comments": []
}
}
}A completed time-entry event body looks like this:
{
"event": "time-entry.created",
"workspaceId": "64f1088d4f3c2c0012b12345",
"timestamp": "2026-07-20T09:21:34.512Z",
"data": {
"id": "687e0d5ee1577b0b4e9fd300",
"title": "Implementation",
"startedAt": "2026-07-20T08:00:00.000Z",
"endedAt": "2026-07-20T09:00:00.000Z",
"isRunning": false,
"resourceId": "66fa6f3be1577b0b4e9fd001",
"resource": {
"id": "66fa6f3be1577b0b4e9fd001",
"name": "Hana Sato"
},
"projectId": "6702ac44e1577b0b4e9fd100",
"project": {
"id": "6702ac44e1577b0b4e9fd100",
"title": "Website Refresh",
"color": "#2563eb"
},
"taskId": null,
"task": null,
"createdAt": "2026-07-20T09:21:34.500Z",
"updatedAt": "2026-07-20T09:21:34.500Z"
}
}When a secret is configured, compare the X-ResourcePlanner-Signature header with your own HMAC computation:
const crypto = require('crypto');
function verifySignature(secret, payload, signature) {
const expected = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature || '', 'hex'),
Buffer.from(expected, 'hex')
);
}Use a timing-safe comparison to avoid leaking information if the signature fails.
Webhooks fire in the background and do not block the UI or API responses. If your endpoint returns a non-2xx status code or is unreachable, the event is logged but not retried automatically. We recommend making your handler idempotent and returning quickly (e.g. enqueue work for processing).