# Linq Webhooks

[Linq](https://docs.linqapp.com/channel/imessage/) is a partner API for iMessage, RCS and SMS lines.
You create webhook subscriptions through the API, and Linq delivers each event as a JSON `POST` signed
with Standard Webhooks headers:
`webhook-id`, `webhook-timestamp` and `webhook-signature`. The event name is the top-level `event_type`
field, and `event_id` is the deduplication key.

## When to Use This Skill

* How do I receive Linq webhooks?
* How do I verify Linq webhook signatures (`webhook-signature`, `whsec_` secret)?
* How do I handle `message.received`, `message.delivered` or `message.failed` events?
* How do I subscribe to `reaction.added` or `phone_number.status_updated`?
* Why is my Linq webhook signature verification failing, or rejecting Hookdeck retries?

## Verification (core)

Signed content is `{webhook-id}.{webhook-timestamp}.{raw body}`. Linq signs it with HMAC-SHA256 using the
base64-decoded secret (the part after `whsec_`), base64-encodes the result, and sends one or more
space-separated `v1,<signature>` entries. Use a Standard Webhooks library (`standardwebhooks` on npm and
PyPI), which compares in constant time. Read the `webhook-*` headers, and when `webhook-id` is absent fall
back to the `svix-*` headers (`svix-id`, `svix-timestamp`, `svix-signature`), as Hookdeck does.

```javascript
const { Webhook } = require('standardwebhooks');

const prefix = req.get('webhook-id') ? 'webhook' : 'svix';
const headers = {
  'webhook-id': req.get(`${prefix}-id`),
  'webhook-timestamp': req.get(`${prefix}-timestamp`),
  'webhook-signature': req.get(`${prefix}-signature`),
};
// rawBody: the raw Buffer/string, never re-serialized JSON
const event = new Webhook(process.env.LINQ_WEBHOOK_SECRET).verify(rawBody, headers);
// throws on a bad signature or a timestamp outside the window; returns the parsed body

```

```python
from standardwebhooks.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["LINQ_WEBHOOK_SECRET"])  # "whsec_..."
event = wh.verify(raw_body, {"webhook-id": id_, "webhook-timestamp": ts, "webhook-signature": sig})

```

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/linq-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/linq-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/linq-webhooks/examples/fastapi/).

The secret is returned once as `signing_secret` when you create a webhook subscription, and it can't be
retrieved later. It starts with `whsec_`. Keep the full value in `LINQ_WEBHOOK_SECRET`. Each subscription
has its own secret. Handlers fail closed: with the variable unset they return 500 instead of accepting
anything. A missing header or a bad signature returns 400.

### Hookdeck retries and replays hit the 5-minute window

The Standard Webhooks libraries always enforce a fixed 5-minute timestamp window, and it can't be
configured. Hookdeck retries and replays resend the **original** `webhook-timestamp`, so the library
rejects any retry or replay that happens more than 5 minutes after the original delivery. If you verify
behind Hookdeck (or replay old events), use the manual verifier in
[references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/linq-webhooks/references/verification.md), where the window can be dropped or widened.
Deduplicate on `event_id` instead of relying on the timestamp.

## Common Event Types

76 events are documented, and the list may not be exhaustive. The name is `event_type` in the JSON body.
The examples handle these:

| Event | Triggered When |
| --- | --- |
| `message.received` | An incoming message arrives on your line |
| `message.sent` | A message was successfully sent from your line |
| `message.delivered` | The message reached the recipient's device (not sent for SMS/MMS) |
| `message.read` | The recipient read the message (not sent for SMS/MMS) |
| `message.failed` | Message delivery failed |
| `message.edited` | A message was edited (`2026-02-03` webhook version only) |
| `reaction.added` | A reaction was added to a message part |
| `reaction.removed` | A reaction was removed |
| `chat.created` | A chat was created |
| `participant.added` | Someone joined a group chat |
| `participant.removed` | Someone left or was removed from a group chat |
| `phone_number.status_updated` | A line's `status` or `reputation` changed |

Linq also sends poll, chat, call, payment, location, contact card and account events (`api_token.*`,
`webhook_subscription.*`, `team_member.*`). See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/linq-webhooks/references/overview.md) for the
full list and the payload envelope.

## Environment Variables

```bash
# Signing secret of the webhook subscription (Standard Webhooks format, starts with whsec_)
LINQ_WEBHOOK_SECRET=whsec_xxxxx

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 linq --path /webhooks/linq

```

Create a webhook subscription whose `target_url` is the tunnel URL, then send a message to one of your
lines to see an event arrive.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/linq-webhooks/references/overview.md) - Linq webhook concepts, events, payload envelope, versions, retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/linq-webhooks/references/setup.md) - Create a subscription through the API, get the secret, manage and rotate it
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/linq-webhooks/references/verification.md) - Standard Webhooks verification details, header fallback, manual verifier, timestamp window