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

# Events & Webhooks

> Receive Migma event notifications in your app, workflow tool, or queue.

## Overview

Use webhooks when your integration needs to react to Migma activity without polling.
Migma creates an event, finds active webhook subscriptions for that event type, and sends an HTTPS `POST` request to your endpoint.

Common uses:

* Continue an automation when API email generation finishes
* Sync contact changes into a CRM or data warehouse
* Alert a team when generation fails
* Track unsubscribe activity outside Migma

<Info>
  Webhooks are outbound notifications from Migma to your system. They are not an inbound custom-events API.
</Info>

<Warning>
  Most webhook events are emitted by API v1 activity. `subscriber.unsubscribed` also fires from real unsubscribe paths, including one-click unsubscribe, mailto unsubscribe, and dashboard status changes.
</Warning>

## Set up a webhook

<Steps>
  <Step title="Create an HTTPS endpoint">
    Your endpoint must accept `POST` requests and return a `2xx` status when the event is accepted.

    ```js theme={null}
    import express from 'express';

    const app = express();

    app.post('/webhooks/migma', express.raw({ type: 'application/json' }), (req, res) => {
      const event = JSON.parse(req.body.toString('utf8'));

      console.log(event.type, event.id);
      res.sendStatus(204);
    });
    ```
  </Step>

  <Step title="Create the webhook in Migma">
    In the dashboard, go to **Settings -> Developers -> Webhooks**, add your endpoint URL, and choose the events to receive.

    You can also create a webhook through the API:

    ```bash theme={null}
    curl https://api.migma.ai/v1/webhooks \
      -H "Authorization: Bearer $MIGMA_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://example.com/webhooks/migma",
        "events": ["email.generation.completed", "subscriber.unsubscribed"],
        "description": "Production automation webhook"
      }'
    ```
  </Step>

  <Step title="Store the signing secret">
    `POST /v1/webhooks` returns the signing `secret` once, in the create response. Store it securely.

    `GET /v1/webhooks/{webhookId}` only returns `secretConfigured`; it does not return the secret again. To rotate a secret, delete and recreate the webhook.
  </Step>

  <Step title="Send a test event">
    Use the dashboard test action or `POST /v1/webhooks/{webhookId}/test` to confirm your endpoint receives a signed event and returns `2xx`.
  </Step>
</Steps>

## Events Migma emits

Use `GET /v1/webhooks/events` to list the event types available to your account.
For production automations, build around these emitted events:

| Event                        | Source                                                      | Payload highlights                                                       |
| ---------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------ |
| `email.generation.started`   | `POST /v1/projects/emails/generate` starts async generation | `conversationId`, `projectId`, `prompt`, `model`, `languages`, `count`   |
| `email.generation.completed` | API generation reaches completed status                     | `conversationId`, `projectId`, `count`, `languages`                      |
| `email.generation.failed`    | API generation fails                                        | `conversationId`, `projectId`, `error`                                   |
| `email.test.sent`            | A test email is sent through the API                        | `conversationId`, `recipient`, `messageId`, `sentAt`, optional `emailId` |
| `project.import.started`     | `POST /v1/projects/import` starts a brand import            | `projectId`, `urls`, `domain`                                            |
| `subscriber.added`           | `POST /v1/contacts` creates a contact                       | `subscriberId`, `projectId`, `email`, `tags`                             |
| `subscriber.updated`         | `PATCH /v1/contacts/{id}` updates a contact                 | `subscriberId`, `projectId`, `email`                                     |
| `subscriber.unsubscribed`    | A contact unsubscribes or is deleted                        | `subscriberId`, `projectId`, `email`, `action`, optional `source`        |
| `subscriber.bulk_imported`   | A contact bulk import completes                             | `projectId`, `success`, `failed`, `updated`, optional `jobId`            |

<Note>
  Brand imports currently emit `project.import.started`. To detect completion, poll `GET /v1/projects/import/{projectId}/status` until the import status is `active` or `error`.
</Note>

<Note>
  Email engagement events such as opens, clicks, bounces, and complaints are not delivered through this webhook system. Use campaign and sending analytics pages for those metrics.
</Note>

## Payload format

Every delivery uses the same envelope:

```json theme={null}
{
  "id": "666f2f7b9a9b6d84f1b2c345",
  "type": "email.generation.completed",
  "timestamp": "2026-06-27T12:34:56.789Z",
  "apiVersion": "v1",
  "data": {
    "conversationId": "665f1f0b8c2a9e4d8f123456",
    "projectId": "665f1e928c2a9e4d8f123123",
    "count": 3,
    "languages": ["en"]
  },
  "metadata": {
    "userId": "665f1d018c2a9e4d8f122222",
    "projectId": "665f1e928c2a9e4d8f123123",
    "conversationId": "665f1f0b8c2a9e4d8f123456"
  }
}
```

| Field        | Meaning                                           |
| ------------ | ------------------------------------------------- |
| `id`         | Unique event ID. Store it for idempotency.        |
| `type`       | Event type, such as `email.generation.completed`. |
| `timestamp`  | Event creation time in ISO 8601 format.           |
| `apiVersion` | Webhook payload version.                          |
| `data`       | Event-specific payload.                           |
| `metadata`   | Account and resource context used for routing.    |

## Delivery headers

Migma includes these headers with delivery requests:

| Header                | Value                                        |
| --------------------- | -------------------------------------------- |
| `Content-Type`        | `application/json`                           |
| `User-Agent`          | `Migma-Webhooks/1.0`                         |
| `X-Migma-Signature`   | HMAC SHA-256 hex digest of the raw JSON body |
| `X-Migma-Event-Type`  | Event type                                   |
| `X-Migma-Event-Id`    | Event ID                                     |
| `X-Migma-Delivery-Id` | Delivery attempt ID                          |
| `X-Migma-Test`        | Present on test deliveries                   |

Custom headers configured on the webhook are also included.

## Verify signatures

Always verify `X-Migma-Signature` before processing a webhook. Compute the HMAC over the exact raw request body, not a parsed and re-serialized JSON object.

```js theme={null}
import crypto from 'crypto';
import express from 'express';

const app = express();

function verifyMigmaSignature(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  if (!signature || signature.length !== expected.length) {
    return false;
  }

  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

app.post('/webhooks/migma', express.raw({ type: 'application/json' }), async (req, res) => {
  const signature = req.header('X-Migma-Signature');
  const valid = verifyMigmaSignature(req.body, signature, process.env.MIGMA_WEBHOOK_SECRET);

  if (!valid) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body.toString('utf8'));
  await handleMigmaEvent(event);

  res.sendStatus(204);
});
```

```python theme={null}
import hashlib
import hmac
import os
from flask import Flask, request

app = Flask(__name__)

def verify_migma_signature(raw_body, signature, secret):
    expected = hmac.new(
        secret.encode("utf-8"),
        raw_body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature or "", expected)

@app.post("/webhooks/migma")
def migma_webhook():
    raw_body = request.get_data()
    signature = request.headers.get("X-Migma-Signature")

    if not verify_migma_signature(raw_body, signature, os.environ["MIGMA_WEBHOOK_SECRET"]):
        return "Invalid signature", 401

    event = request.get_json()
    handle_migma_event(event)
    return "", 204
```

## Delivery and retries

Webhook delivery is non-blocking. Migma returns API responses without waiting for your webhook endpoint.

Delivery behavior:

* `2xx` response: delivery succeeds.
* Non-`2xx` response or timeout: delivery is retried with backoff.
* Timeout: each attempt times out after 10 seconds.
* Attempts: Migma stops after three total delivery attempts.
* Ordering: do not rely on strict ordering across different event types or webhooks.
* Duplicate handling: delivery is at least once, so process by `id` idempotently.

Keep handlers small. Acknowledge the event quickly, enqueue your own work, then process it asynchronously.

```js theme={null}
const processed = new Set();

async function handleMigmaEvent(event) {
  if (processed.has(event.id)) {
    return;
  }

  switch (event.type) {
    case 'email.generation.completed':
      await queueGeneratedEmailSync(event.data.conversationId);
      break;
    case 'subscriber.unsubscribed':
      await crm.markUnsubscribed(event.data.email);
      break;
    default:
      console.log(`Unhandled Migma event: ${event.type}`);
  }

  processed.add(event.id);
}
```

## Monitor deliveries

Use delivery history when debugging endpoint failures:

```bash theme={null}
curl https://api.migma.ai/v1/webhooks/{webhookId}/deliveries \
  -H "Authorization: Bearer $MIGMA_API_KEY"
```

Useful endpoints:

| Endpoint                                  | Purpose                                                          |
| ----------------------------------------- | ---------------------------------------------------------------- |
| `GET /v1/webhooks`                        | List configured webhooks                                         |
| `POST /v1/webhooks`                       | Create a webhook                                                 |
| `PATCH /v1/webhooks/{webhookId}`          | Update URL, events, active state, description, or custom headers |
| `DELETE /v1/webhooks/{webhookId}`         | Delete a webhook                                                 |
| `GET /v1/webhooks/events`                 | List available event types                                       |
| `POST /v1/webhooks/{webhookId}/test`      | Send a test delivery                                             |
| `GET /v1/webhooks/{webhookId}/deliveries` | Inspect recent delivery attempts                                 |
| `GET /v1/webhooks/stats`                  | Get aggregate webhook delivery stats                             |

Webhook management requires API keys with `webhook:read` and `webhook:write` permissions.

## Troubleshooting

| Issue                          | Check                                                                             |
| ------------------------------ | --------------------------------------------------------------------------------- |
| Signature verification fails   | Use the raw request body and the secret returned when the webhook was created.    |
| Test delivery fails            | Confirm the URL is public HTTPS and responds within 10 seconds.                   |
| Duplicate processing           | Store processed event IDs and make handlers idempotent.                           |
| No generation completion event | Confirm the email was generated through the API async generation flow.            |
| No import completion event     | Poll the import status endpoint; only import start is emitted as a webhook today. |
| Missing unsubscribe event      | Confirm the webhook subscribes to `subscriber.unsubscribed` and is active.        |

<CardGroup cols={2}>
  <Card title="API Reference" icon="book" href="/api-reference/introduction">
    Review webhook endpoints, schemas, and permissions.
  </Card>

  <Card title="n8n" icon="diagram-project" href="/n8n">
    Trigger no-code automations from Migma events.
  </Card>
</CardGroup>
