# Unipile Webhooks

Unipile is a unified API for messaging (LinkedIn, WhatsApp, Instagram, Messenger,
Telegram, X) and email (Google, Microsoft, IMAP). You create a webhook with
`POST /api/v1/webhooks` (or in the Unipile dashboard), choose a `source`, and
Unipile sends an HTTP POST to your `request_url` when something happens.

Things that differ from most providers:

1. **There is no signature.** Unipile doesn't sign the body. No HMAC, no
  timestamp header. Instead you add your own secret to the webhook's `headers`
  array and Unipile sends it on every delivery.
2. **Payloads differ by `source`.** Messaging, email, email tracking and users
  webhooks send a flat object with an `event` field. Account status webhooks
  send `{"AccountStatus": {...}}` with no `event` field.
3. **No JSON content type by default** for webhooks created through the API.
  Add `{"key": "Content-Type", "value": "application/json"}` to `headers`, and
  parse the raw body yourself either way.

## When to Use This Skill

* How do I receive Unipile webhooks?
* How do I verify Unipile webhook signatures? (You can't. Send a shared secret in a header instead.)
* How do I create a Unipile webhook with the API and add an `Unipile-Auth` header?
* How do I handle `message_received`, `message_read` and `mail_received`?
* How do I detect an account that needs reconnecting (`CREDENTIALS`)?
* Why does my Unipile webhook arrive more than once?

## Verification (core): a shared secret header

Create the webhook with a secret in `headers`:

```bash
curl --request POST --url https://{YOUR_DSN}/api/v1/webhooks \
  --header 'X-API-KEY: XXXXXXXX' --header 'content-type: application/json' \
  --data '{
    "request_url": "https://example.com/webhooks/unipile",
    "source": "messaging",
    "headers": [
      { "key": "Content-Type", "value": "application/json" },
      { "key": "Unipile-Auth", "value": "<secret from openssl rand -hex 32>" }
    ]
  }'

```

Then check the header in constant time before you parse the body:

```javascript
const crypto = require('crypto');

// NOT a Unipile signature. This is a secret YOU put in the webhook's headers array.
function verifyUnipileSecret(headerValue) {
  const expected = process.env.UNIPILE_WEBHOOK_SECRET;
  if (!expected) throw new Error('UNIPILE_WEBHOOK_SECRET is not set'); // fail closed
  if (typeof headerValue !== 'string') return false;
  const a = Buffer.from(headerValue);
  const b = Buffer.from(expected);
  // Length check first: timingSafeEqual throws on a length mismatch.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express: verifyUnipileSecret(req.get('Unipile-Auth'))

```

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

Return `401` for a missing or wrong secret and `500` when the secret isn't
configured, so the endpoint never accepts everything by accident. The header name
(`Unipile-Auth`) is your choice, and header names are case-insensitive. See
[references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/unipile-webhooks/references/verification.md).

## Sources and Events

| `source` | Events | Payload shape |
| --- | --- | --- |
| `messaging` | `message_received` (default), `message_read`, `message_reaction`, `message_edited`, `message_deleted`, `message_delivered` | Flat: `account_id`, `account_type`, `event`, `chat_id`, `message_id`, `message`, `sender`, `attendees`, `attachments` |
| `email` | `mail_received` (default), `mail_sent`, `mail_moved` | Flat: `email_id`, `account_id`, `event`, `from_attendee`, `to_attendees`, `subject`, `body`, `origin` |
| `email_tracking` | `mail_opened`, `mail_link_clicked` | Flat: `event`, `event_id`, `tracking_id`, `email_id`, `account_id`, `url` |
| `users` | `new_relation` | Flat, with `event` |
| `account_status` | `message` is `CREATION_SUCCESS`, `RECONNECTED`, `SYNC_SUCCESS`, `OK`, `CREDENTIALS`, `ERROR`, `STOPPED`, `DELETED`, ... | `{"AccountStatus": {"account_id", "account_type", "message"}}` |

Notes:

* Sent messages and emails are delivered too (`message_received` / `mail_received`).
  For messages, compare `account_info.user_id` with `sender.attendee_provider_id`
  to tell your own from others. For emails, `origin` is `unipile` or `external`.
* Account status `message` values include `OK`, `CREDENTIALS`, `ERROR`/`STOPPED`,
  `CONNECTING`, `DELETED`, `CREATION_SUCCESS`, `RECONNECTED` and `SYNC_SUCCESS`.
  When the status isn't `OK`, Unipile stops sending new-message webhooks for that
  account, so surface `CREDENTIALS` to the user and start a reconnect.
* Dispatch on `AccountStatus` first, then on `event`. Acknowledge events you don't
  handle with `200`.

## Delivery, Retries and Idempotency

* Reply with a `200` in under **30 seconds**. Otherwise Unipile makes **five retry
  attempts** with an increasing delay between them.
* There's **no delivery ID header**, so dedupe on payload fields: `message_id`
  (messaging), `email_id` (email), `event_id` (tracking), or `account_id` + status.
* Messages that arrive while an account is disconnected are delivered after it
  reconnects. You won't receive webhooks for old messages when an account is first
  connected.
* Do slow work after responding, not before.

## Environment Variables

```bash
# REQUIRED. The value you put in the webhook's headers array. Generate with: openssl rand -hex 32
UNIPILE_WEBHOOK_SECRET=
# OPTIONAL. The header key (case-insensitive). Default: Unipile-Auth
UNIPILE_WEBHOOK_HEADER=Unipile-Auth

```

## Local Development

```bash
npx hookdeck-cli listen 3000 unipile --path /webhooks/unipile

```

No account required. The CLI creates a guest account on first run and gives you a
public HTTPS URL to use as `request_url`, plus a web UI for inspecting requests.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/unipile-webhooks/references/overview.md) - Sources, events, payload examples, retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/unipile-webhooks/references/setup.md) - Creating webhooks with the API or dashboard, adding headers, testing
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/unipile-webhooks/references/verification.md) - Why there is no signature, the shared-secret header pattern, gotchas