Webhooks allow your application to receive real-time notifications when events occur in Outlign. When an event fires, Outlign sends an HTTP POST request to your configured URL with the event payload.
Webhook subscriptions are configured per company and can listen for specific event types. When a matching event occurs, Outlign delivers a JSON payload to your endpoint via HTTP POST.
Event Occurs
An action triggers an event in Outlign (e.g., a task is created)
Subscription Matched
Outlign checks for active webhook subscriptions matching the event type and company
Payload Delivered
An HTTP POST request is sent to your endpoint with the event data as JSON
The following webhook event types are available for subscription:
| Event Type | Description |
|---|---|
| project.created | Fires when a new project is created |
| task.created | Fires when a new task is created |
| task.completed | Fires when a task is marked as completed |
| task.due_date_updated | Fires when a task's due date is changed |
| task.tags_updated | Fires when a task's tags change |
| phase.updated | Fires when a phase is updated |
| milestone.created | Fires when a new milestone is created |
| milestone.updated | Fires when a milestone is updated |
Template projects: project.created is never fired for a template.
The task, phase and milestone events, however, fire whenever the underlying record changes —
including when someone edits a template inside Outlign. If your integration only cares about live
client work, check the project in the payload against
GET /api/v1/projects/{id}, which returns 404 for templates.
Webhook payloads are delivered as JSON via HTTP POST. The payload contains the full resource data for the affected entity at the time of the event.
Sent for project.created events.
{
"id": 789,
"title": "Website Redesign",
"code": "ACME-014",
"description": "Complete redesign of the company website",
"is_client": true,
"is_internal": false,
"client_project_type": "process",
"internal_project_type": "todos",
"created_at": "2023-01-01T12:00:00.000000Z",
"updated_at": "2023-01-01T12:00:00.000000Z",
"client": { "id": 101, "title": "Acme Corporation" },
"company": { "id": 202, "title": "Design Studio Inc" }
}
Note that client_project_type and internal_project_type are delivered as the
raw stored values (process / todos), not the
"Process"/"Board" labels the REST API returns.
Sent for task.created, task.completed, task.due_date_updated, and task.tags_updated events.
{
"id": 123,
"title": "Design homepage wireframe",
"completed": false,
"published": true,
"is_approval": false,
"approval_status": null,
"due_date": "2023-02-15T00:00:00.000000Z",
"created_at": "2023-01-01T12:00:00.000000Z",
"updated_at": "2023-01-01T12:00:00.000000Z",
"phase": { "id": 456, "title": "Design Phase", "is_internal": false },
"project": { "id": 789, "title": "Website Redesign" },
"client": { "id": 101, "title": "Acme Corporation" },
"company": { "id": 202, "title": "Design Studio Inc" },
"assignees": [ { "id": 3, "name": "Dave Prince" } ]
}
Some task events include additional context fields alongside the task data:
task.completed adds completed_at;
task.due_date_updated adds previous_due_date and
due_date_updated_at (all ISO 8601). due_date is an empty string
("") when the task has no due date. For approval tasks
(is_approval: true), a task.completed event's
approval_status carries the verdict: approved or
not_approved.
task.tags_updated adds three tag arrays alongside the task data:
tags (the task's full tag list after the change), plus added_tags and
removed_tags for the delta — so a subscriber can react to
“Ready for QA was just added” without diffing state itself. Each entry is a tag
object (id, name, color), exactly as on the
task tag endpoints.
Sent for phase.updated events.
{
"id": 456,
"title": "Design Phase",
"due_date": "2023-03-15T23:59:59.000000Z",
"is_internal": false,
"order": 1,
"created_at": "2023-01-01T12:00:00.000000Z",
"updated_at": "2023-01-15T14:30:00.000000Z",
"project": { "id": 789, "title": "Website Redesign" },
"client": { "id": 101, "title": "Acme Corporation" },
"company": { "id": 202, "title": "Design Studio Inc" }
}
Sent for milestone.created and milestone.updated events.
{
"id": 42,
"title": "Design Review",
"description": "Final design review with stakeholders",
"color": "#9177FB",
"due_date": "2023-03-15T00:00:00.000000Z",
"is_completed": false,
"milestoneable_type": "App\\Models\\Project",
"milestoneable_id": 789,
"project": {
"id": 789,
"title": "Website Redesign"
},
"client": {
"id": 101,
"title": "Acme Corporation"
},
"company": {
"id": 202,
"title": "Design Studio Inc"
},
"created_at": "2023-01-01T12:00:00.000000Z",
"updated_at": "2023-01-01T12:00:00.000000Z"
}
Webhook subscriptions are managed through the API. A subscription binds one event type to one URL for one company. You can list subscriptions for any company you belong to, but creating, updating, and deleting them requires the manager role on that company — the same permission as the rest of company settings.
Response shape: Single-subscription endpoints return the subscription object
directly, not wrapped in data. The list endpoint returns the subscriptions under
data, with next_page_url and prev_page_url for navigation.
{
"id": 12,
"url": "https://example.com/hooks/outlign",
"event": "task.completed",
"company_id": 202,
"active": true,
"created_at": "2023-01-01T12:00:00.000000Z",
"updated_at": "2023-01-01T12:00:00.000000Z"
}
The create response additionally includes a secret — used to verify
the signature on every delivery (see Verifying
deliveries). It is shown once, at creation, and never returned again; store it
securely. If you lose it, delete the subscription and create a new one.
/api/v1/webhooks
Returns the subscriptions for all companies you belong to, each including its company.
Accepts an optional company_id filter (a 403 is returned for a company you do
not have access to) and per_page (max 1000).
curl https://go.outlign.co/api/v1/webhooks?company_id=202 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Accept: application/json"
/api/v1/webhooks
| Parameter | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | The endpoint that will receive the POST payload |
| event | string | Yes | One of the event types listed above |
| company_id | integer | Yes | The company whose events you want to receive. Must be a company you belong to |
Requires the manager role on the company. New subscriptions are created with active: true.
The response is 201 Created with the subscription object, including the signing
secret — the only time it is returned. Subscribing to a company_id you do
not belong to returns 403.
curl -X POST https://go.outlign.co/api/v1/webhooks \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"url": "https://example.com/hooks/outlign",
"event": "task.completed",
"company_id": 202
}'
{
"id": 12,
"url": "https://example.com/hooks/outlign",
"secret": "3f9aC1eK7pRq2sT8uV0wX4yZ6bD1gH5jL9nM3oQ7rS2tU8v",
"event": "task.completed",
"company_id": 202,
"active": true,
"created_at": "2023-01-01T12:00:00.000000Z",
"updated_at": "2023-01-01T12:00:00.000000Z"
}
/api/v1/webhooks/{id}
Returns a single subscription (with its company loaded), or 404 if it does
not belong to one of your companies.
/api/v1/webhooks/{id}
Only url and active can be changed — the event and
company_id of a subscription are fixed. Set active: false to pause deliveries
without deleting the subscription.
curl -X PUT https://go.outlign.co/api/v1/webhooks/12 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{ "active": false }'
/api/v1/webhooks/{id}
Deletes the subscription and stops all deliveries for it. Returns 204 No Content.
Subscriptions are hard-deleted — to pause deliveries temporarily, set active: false
instead.
curl -X DELETE https://go.outlign.co/api/v1/webhooks/12 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Accept: application/json"
Every delivery is signed so you can confirm it genuinely came from Outlign and was not tampered with.
Each request carries a Signature header containing a hex-encoded
HMAC-SHA256 of the raw request body, computed with the subscription's
secret (returned once when you created the subscription).
To verify, recompute the HMAC over the exact raw body you received and compare it to the header using a constant-time comparison. Reject the request if they do not match.
// Node.js (Express, with the raw body available)
const crypto = require("crypto");
const expected = crypto
.createHmac("sha256", SUBSCRIPTION_SECRET)
.update(rawRequestBody) // the exact bytes received, before JSON parsing
.digest("hex");
const received = req.header("Signature");
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
return res.status(400).send("Invalid signature");
}
Sign against the raw body. Compute the HMAC over the exact bytes you received, not over a re-serialized copy of the parsed JSON — re-serialization can reorder keys or change whitespace and break the comparison.
Respond Quickly: Your endpoint should return a 2xx status code within a few seconds. Process webhook data asynchronously if your handling logic is complex.
Handle Duplicates: In rare cases, the same event may be delivered more than once. Use the resource ID and timestamps to detect and handle duplicate deliveries.
Verify the signature: Always check the Signature header before
trusting a payload (see Verifying deliveries). Treat
any request that fails verification as hostile and reject it.