Webhooks

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.

How Webhooks Work

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.

Webhook Delivery Flow

1

Event Occurs

An action triggers an event in Outlign (e.g., a task is created)

2

Subscription Matched

Outlign checks for active webhook subscriptions matching the event type and company

3

Payload Delivered

An HTTP POST request is sent to your endpoint with the event data as JSON

Available Event Types

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 Payload Format

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.

Project Event Payload

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.

Task Event Payload

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.

Phase Event Payload

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" }
}

Milestone Event Payload

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"
}

Managing Subscriptions

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.

The Subscription Object

{
  "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.

List Subscriptions

GET /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"

Create a Subscription

POST /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
  }'

Response

{
  "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"
}

Get a Subscription

GET /api/v1/webhooks/{id}

Returns a single subscription (with its company loaded), or 404 if it does not belong to one of your companies.

Update a Subscription

PUT PATCH /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 }'

Delete a Subscription

DELETE /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"

Verifying deliveries

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.

Best Practices

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.