> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vibefollow.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook event types

> Full payload spec for every event Vibefollow emits.

Every webhook delivery shares an envelope:

```ts theme={null}
interface WebhookEvent {
  type: 'email.opened' | 'email.clicked' | 'email.bounced' | 'email.replied' | 'email.unsubscribed';
  id: string;       // unique per delivery; idempotent
  created: number;  // Unix seconds when Vibefollow signed the payload
  data: { ... };    // type-specific
}
```

<Info>
  `id` is unique per delivery and stable across retries — dedupe on it if you're persisting events.
</Info>

## Event catalog

<AccordionGroup>
  <Accordion title="email.opened" icon="envelope-open">
    Fires when the recipient opens an email Vibefollow sent on your behalf.

    <Note>
      Tracking pixels are stripped if the recipient's mail client blocks them — do not assume a missing open means the email wasn't read.
    </Note>

    **TypeScript shape**

    ```ts theme={null}
    interface EmailOpenedEvent {
      type: 'email.opened';
      id: string;
      created: number;
      data: {
        draftId: string;
        projectId: string;
        externalUserId: string;
        messageId: string;     // Postmark message id
        openedAt: string;      // ISO 8601
      };
    }
    ```

    **Example payload**

    ```json theme={null}
    {
      "type": "email.opened",
      "id": "evt_01HZ8X3F4T5K9N2M6P7R8Q1B2C",
      "created": 1747526400,
      "data": {
        "draftId": "drf_42",
        "projectId": "prj_acme",
        "externalUserId": "usr_42",
        "messageId": "msg_abcdef",
        "openedAt": "2026-05-17T14:00:00.000Z"
      }
    }
    ```
  </Accordion>

  <Accordion title="email.clicked" icon="arrow-pointer">
    Fires when the recipient clicks any tracked link in the email. One event per click — the same recipient clicking three links produces three events.

    **TypeScript shape**

    ```ts theme={null}
    interface EmailClickedEvent {
      type: 'email.clicked';
      id: string;
      created: number;
      data: {
        draftId: string;
        projectId: string;
        externalUserId: string;
        messageId: string;
        url: string;           // the destination URL
        clickedAt: string;     // ISO 8601
      };
    }
    ```

    **Example payload**

    ```json theme={null}
    {
      "type": "email.clicked",
      "id": "evt_01HZ8X4Q2W3E5R6T8Y9U0I1O2P",
      "created": 1747526460,
      "data": {
        "draftId": "drf_42",
        "projectId": "prj_acme",
        "externalUserId": "usr_42",
        "messageId": "msg_abcdef",
        "url": "https://acme.io/onboarding/step-3",
        "clickedAt": "2026-05-17T14:01:00.000Z"
      }
    }
    ```
  </Accordion>

  <Accordion title="email.bounced" icon="triangle-exclamation">
    Fires when Postmark reports a bounce or spam complaint. `bounceType` distinguishes the kind so you can decide whether to suppress the address.

    **TypeScript shape**

    ```ts theme={null}
    interface EmailBouncedEvent {
      type: 'email.bounced';
      id: string;
      created: number;
      data: {
        draftId: string;
        projectId: string;
        externalUserId: string;
        messageId: string;
        bounceType: 'hard' | 'soft' | 'transient' | 'complaint' | 'unknown';
        reason?: string;       // human-readable explanation
        bouncedAt: string;     // ISO 8601
      };
    }
    ```

    **Bounce type guide**

    | `bounceType` | What it means                            | Action                                           |
    | ------------ | ---------------------------------------- | ------------------------------------------------ |
    | `hard`       | Permanent failure — address invalid      | Suppress; don't email again                      |
    | `soft`       | Temporary — mailbox full, server timeout | Vibefollow retries; no action needed             |
    | `transient`  | Routing hiccup                           | Vibefollow retries                               |
    | `complaint`  | Marked as spam                           | Suppress; also indicates content/relevance issue |
    | `unknown`    | Postmark couldn't classify               | Investigate manually                             |
  </Accordion>

  <Accordion title="email.replied" icon="reply">
    Fires when the recipient replies to a Vibefollow-sent email. The full plain-text body is included, along with a tone classification used internally to auto-pause sends to users who reply negatively.

    **TypeScript shape**

    ```ts theme={null}
    interface EmailRepliedEvent {
      type: 'email.replied';
      id: string;
      created: number;
      data: {
        draftId: string;
        projectId: string;
        externalUserId: string;
        messageId: string;
        replyBody: string;     // plain text, no signature stripping
        tone: 'positive' | 'neutral' | 'negative' | 'unsubscribe_request';
        receivedAt: string;    // ISO 8601
      };
    }
    ```

    **Tone classifications**

    | `tone`                | Vibefollow's reaction                                 |
    | --------------------- | ----------------------------------------------------- |
    | `positive`            | Continue lifecycle as normal                          |
    | `neutral`             | Continue lifecycle as normal                          |
    | `negative`            | Auto-pause further sends to this user                 |
    | `unsubscribe_request` | Treat as unsubscribe; also emits `email.unsubscribed` |

    <Note>
      Vibefollow does **not** auto-reply on your behalf in v1. Inbound replies route to your project's reply-to address; the webhook is a copy for your systems to react to.
    </Note>
  </Accordion>

  <Accordion title="email.unsubscribed" icon="ban">
    Fires when the recipient unsubscribes — through the one-click `List-Unsubscribe` header (RFC 8058), through a reply that scored as `unsubscribe_request`, or through a manual flag in the dashboard.

    **TypeScript shape**

    ```ts theme={null}
    interface EmailUnsubscribedEvent {
      type: 'email.unsubscribed';
      id: string;
      created: number;
      data: {
        projectId: string;
        externalUserId: string;
        email: string;
        source: 'one_click' | 'reply_request' | 'manual';
        unsubscribedAt: string; // ISO 8601
      };
    }
    ```

    **Source values**

    | `source`        | Triggered by                                 |
    | --------------- | -------------------------------------------- |
    | `one_click`     | Gmail / Outlook one-click unsubscribe button |
    | `reply_request` | Inbound reply tone-classified as unsubscribe |
    | `manual`        | Founder flagged the user in the dashboard    |

    <Info>
      Unsubscribed users are excluded from every future trigger evaluation. You don't need to do anything — this webhook is informational so your CRM can mirror the state.
    </Info>
  </Accordion>
</AccordionGroup>
