# Aimfox Webhooks

Aimfox (LinkedIn automation) sends a JSON webhook to your endpoint when something happens in a
workspace: a connection is made or accepted, a lead replies, an account is logged out, a campaign
starts or ends. Workspace webhooks are in beta. Webhooks are **not signed**. The only
authentication is the optional **Authentication Header** you add to the webhook (Basic, or a
custom header such as a Bearer token), which Aimfox sends with every request.

## When to Use This Skill

* How do I receive Aimfox webhooks?
* How do I verify the Aimfox webhook Authentication Header?
* Does Aimfox sign webhook payloads? (No: use a shared credential header)
* How do I handle `new_connection`, `accepted`, `new_reply` or `account_logged_out` events?
* Why does my Aimfox webhook endpoint return 401?

## Verification (core)

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

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyAimfoxRequest(headers, secret = process.env.AIMFOX_WEBHOOK_SECRET) {
  if (!secret) return false;                                    // never accept when unconfigured
  const name = (process.env.AIMFOX_WEBHOOK_HEADER || 'Authorization').toLowerCase();
  const received = headers[name];                               // Node lowercases header names
  return Boolean(received) && safeEqual(received, secret);      // whole value, no prefix parsing
}

```

* The header name is whatever you typed into **Authentication Header** in Aimfox. Set
  `AIMFOX_WEBHOOK_HEADER` to that name (default `Authorization`) and `AIMFOX_WEBHOOK_SECRET` to the
  **exact, whole header value**, including any `Bearer ` or `Basic ` prefix. Do not strip or parse
  the prefix.
* Look the header up case-insensitively. Return `401` when it is missing or differs.
* A static shared credential authenticates the sender, not the body. There is no signature and no
  signed timestamp, so nothing stops a captured request being replayed. Use HTTPS and rotate the
  credential if it leaks.
* Read the body raw (`express.raw({ type: '*/*' })`) and authenticate before parsing it. Return
  `400` for a body that isn't a JSON object, `200` once handled.

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

## Event Types

Every payload has `id`, `event_type`, `workspace` and an `event` object whose fields depend on
the type. Aimfox retries failed deliveries up to 6 times with exponential backoff, so handle
duplicates by `id`.

| `event_type` | Sent When |
| --- | --- |
| `account_logged_in` | A workspace LinkedIn account logs in and is authenticated |
| `account_logged_out` | A workspace account is logged out (manually or session expiry) |
| `new_connection` | A new LinkedIn connection is detected on a workspace account |
| `view` | A campaign visits a LinkedIn profile |
| `connect` | A campaign sends a connection request |
| `accepted` | A campaign connection request is accepted |
| `inmail` | A campaign sends an InMail |
| `message` | A campaign sends a message |
| `new_reply` | A reply arrives to any LinkedIn message on a workspace account |
| `reply` | A lead sends their first reply to a campaign message |
| `inmail_reply` | A lead replies to a campaign InMail |
| `lead_label_added` | A label is applied to a lead |
| `campaign_created` | A campaign is created |
| `campaign_started` | A campaign starts for the first time |
| `campaign_ended` | A campaign runs to completion |
| `inbox_event` | Any inbox activity on a workspace account |
| `campaign_reply` | A lead replies to any campaign message |

## Environment Variables

```bash
AIMFOX_WEBHOOK_HEADER=Authorization            # the header name you set in Aimfox
AIMFOX_WEBHOOK_SECRET="Bearer your-secret"     # the exact whole header value

```

## Local Development

Aimfox can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

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

```

Use the printed URL as the webhook URL in Aimfox.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/aimfox-webhooks/references/overview.md) - What Aimfox webhooks are, events and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/aimfox-webhooks/references/setup.md) - Add a webhook and an Authentication Header
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/aimfox-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/aimfox-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests