How the webhook URL works. It is a public address that records whatever is sent to it and
answers 200. An inbox keeps the 50 most recent requests, accepts bodies up to 256 KB, and is
deleted 24 hours after it was created — or sooner if you press Delete URL or our server
restarts, because requests are held in memory and never written to disk.
Anyone who has the URL can read what it received. Use sandbox and test-mode accounts, do not route production traffic or customer data through it, and delete it when you are done. Privacy details.
What is a Linear webhook?
A Linear webhook is an HTTP POST sent to a URL registered in a workspace when data of a subscribed type changes: issues, comments, projects, cycles, labels, reactions and more. A webhook covers one team or all public teams, and you choose the resource types it receives when you create it.
Every payload has the same frame. type is the kind of entity, action is create, update or remove, and data is the entity after the change. On an update, updatedFrom lists the fields that changed with the values they had before, which turns "an issue changed" into "the state moved from this to that" without a second request.
What a Linear webhook request looks like
The request line and headers as they reach your server, followed by the body. Ids, tokens and the signature are placeholders.
POST /your/webhook/path HTTP/1.1
Host: your-server.example
Linear-Signature: 766e1d90a96e2f5ecec342a99c5552999dd95d49250171b902d703fd674f5086
Linear-Event: Issue
Linear-Delivery: 234d1a4e-b617-4388-90fe-adc3633d6b72
Content-Type: application/json; charset=utf-8
User-Agent: Linear-Webhook
{
"action": "update",
"type": "Issue",
"createdAt": "2026-01-01T12:00:00.000Z",
"data": {
"id": "2174add1-f7c8-44e3-bbf3-2d60b5ea8bc9",
"identifier": "ENG-417",
"number": 417,
"title": "Webhook worker drops retries",
"priority": 2,
"teamId": "72b2a2dc-6f4f-4423-9d34-24b5bd10634a",
"stateId": "a1c3e5f7-0b2d-4f6a-8c9e-1d3f5a7b9c0e",
"state": {
"id": "a1c3e5f7-0b2d-4f6a-8c9e-1d3f5a7b9c0e",
"name": "In Progress",
"type": "started"
},
"team": {
"id": "72b2a2dc-6f4f-4423-9d34-24b5bd10634a",
"key": "ENG",
"name": "Engineering"
},
"assigneeId": "c4f2a9d0-e3b1-4c8f-a6d5-e4b3c2a1f0e9",
"labelIds": [],
"createdAt": "2025-12-30T09:14:02.000Z",
"updatedAt": "2026-01-01T12:00:00.000Z"
},
"updatedFrom": {
"stateId": "0e9d8c7b-6a5f-4e3d-2c1b-0a9f8e7d6c5b",
"updatedAt": "2025-12-31T16:40:11.000Z"
},
"url": "https://linear.app/acme/issue/ENG-417/webhook-worker-drops-retries",
"organizationId": "9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
"webhookId": "000042e3-d123-4980-b49f-8e140eef9329",
"webhookTimestamp": 1767225600000
}
updatedFrom contains only the fields that changed, holding their old values; the new values are in data. Here the state id changed, so the issue was moved to "In Progress" from whatever state the old id names. webhookTimestamp is the send time in milliseconds and is there for replay protection. Priority is a number where 0 means none and 1 is the most urgent.
Headers Linear sends
| Header | Example | What it is |
|---|---|---|
Linear-Signature | 766e1d90a96e2f5ecec342a99c5552999dd95d49250… | HMAC-SHA256 of the body, hex, with no prefix. |
Linear-Event | Issue | The entity type. Matches type in the body. |
Linear-Delivery | 234d1a4e-b617-4388-90fe-adc3633d6b72 | Unique id of this delivery. |
Content-Type | application/json; charset=utf-8 | The body is JSON. |
User-Agent | Linear-Webhook | Identifies the sender. |
Header names are case-insensitive. Your framework may show them lower-cased, and HTTP/2 always does.
How Linear signs a webhook
Each webhook has a signing secret, shown when you create it. Linear computes an HMAC-SHA256 of the raw request body with that secret and sends the hex digest, with no prefix, in Linear-Signature.
The timestamp is inside the body rather than in a header, so replay protection is a second step after the HMAC passes: parse the JSON and check that webhookTimestamp is within about a minute of your clock, as Linear's documentation recommends. Compute it over the raw request body exactly as received — before any JSON parsing — and compare with a constant-time function.
const crypto = require('crypto');
// rawBody: string of the body exactly as received
function verifyLinear(rawBody, header, secret, toleranceMs = 60000) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(header || '');
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return false;
const sentAt = JSON.parse(rawBody).webhookTimestamp;
return Math.abs(Date.now() - sentAt) <= toleranceMs;
}
Timeouts, retries and disabled webhooks
Linear expects an HTTP 200 within about five seconds, per its documentation at the time of writing. A slower or failed delivery is retried a small number of times with a delay that grows from minutes to hours. An endpoint that keeps failing can have its webhook disabled, at which point the workspace admins are told and someone has to re-enable it.
Changes made in quick succession produce separate deliveries that may not arrive in order. data.updatedAt tells you which snapshot is newer, and Linear-Delivery identifies a delivery if you need to recognise a repeat.
Catch a real Linear webhook
- Press Create webhook URL at the top of this page and copy the address.
- In Linear, open the workspace settings and find API → Webhooks. This needs admin rights.
- Create a webhook, give it a label, and paste the address as the URL.
- Tick the resource types to receive — Issues is enough to start — choose a team, and save. Note the signing secret.
- Change the status of an issue and read the delivery, including
updatedFrom, in the inbox above.
To run a caught request against your own code, select it and press Open in API Tester: the method, headers and body are loaded into the request console, where you enter your server's public URL and send it. A body you edit will no longer match its signature — that is the signature doing its job.
Things that catch people out
- The signature has no prefix. Unlike GitHub's
sha256=…, the header is the bare hex digest. removeis not always deletion. Archiving and trashing an entity surface as their own state changes; readdatabefore deleting anything on your side.- Related entities arrive as ids. Some relations are expanded, as
stateandteamare here, but most are only…Idfields to resolve through the GraphQL API.
api-tester.com is independent and is not affiliated with Linear. Providers change header names, limits and retry schedules; treat the official Linear documentation as the source of truth.
Related examples and tools
Clerk Webhook Example
Clerk delivers its webhooks through Svix, so the three svix- headers and their signing scheme are shared with every other service that uses the same infrastructure.
Paddle Webhook Example
Paddle Billing notifications look a lot like Stripe's until you parse the signature header, which uses different separators and joins the timestamp to the body differently.
Razorpay Webhook Example
Razorpay wraps each event's entities inside payload under their own names, and signs the body with a secret you choose when creating the webhook.
Twitch Webhook Example
Twitch EventSub sends three kinds of message to the same URL, told apart by a header: a challenge to prove you own the endpoint, the notifications themselves, and a revocation when a subscription ends.
Zoom Webhook Example
Zoom will not save a webhook endpoint until it has answered a validation challenge with a hash made from the app's Secret Token, and it repeats that check from time to time.
GraphQL Tester
Query the Linear GraphQL API to resolve the ids a webhook hands you.
Curl POST JSON
Send a JSON body like this one from a terminal.
Webhook Tester
The inbox on its own page, plus a guide to signatures, retries and idempotent handlers.
All Webhook Examples
30 providers side by side: content type, signature scheme and retry behaviour.
Other webhooks signed with an HMAC of the body
References
- Official Linear documentation — The provider's own guide to the requests described on this page.
- Linear Developers: webhooks in the SDK — Handling webhooks with the Linear SDK.
- Linear Developers: GraphQL API — Getting started with the API a webhook's ids refer to.
- RFC 2104: HMAC — The keyed-hash construction this signature is built on.
- Node.js: crypto.timingSafeEqual() — The constant-time comparison used in the code on this page.
These links leave api-tester.com, which is independent of the companies and projects they point to.
Linear webhook FAQ
What does a Linear webhook payload look like?
A JSON object with action (create, update or remove), type (such as Issue or Comment), data containing the entity, updatedFrom with previous values on updates, plus url, organizationId, webhookId, createdAt and webhookTimestamp.
How do I verify a Linear webhook?
Compute an HMAC-SHA256 of the raw request body with the webhook signing secret, hex-encode it and compare it to the Linear-Signature header in constant time. Then check that webhookTimestamp in the body is recent.
What is updatedFrom in a Linear webhook?
An object present on update actions that lists the fields that changed, each with the value it had before the change. The current values are in data.
Does Linear retry failed webhooks?
Yes. A delivery that fails or takes longer than about five seconds is retried a few times with increasing delays, and a webhook that keeps failing can be disabled, according to Linear's documentation at the time of writing.
Which headers does Linear send with a webhook?
Linear-Signature with the HMAC, Linear-Event with the entity type, Linear-Delivery with a unique delivery id, a JSON Content-Type and a User-Agent of Linear-Webhook.