> ## 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

> Record customer events server-side to attribute conversions and revenue to the emails that earned them.

## Overview

Events let your backend tell Migma when a customer does something that matters, such as completing a purchase, starting a trial, or requesting a refund. Migma stores each event against a contact and uses purchase events to attribute revenue back to the email a recipient clicked.

Common uses:

* Report a purchase so Migma can credit the email that drove it
* Record trial starts, upgrades, or cancellations as customer activity
* Send negative amounts for refunds and cancellations so revenue stays honest
* Feed order and profile context into a contact without changing their subscription

<Info>
  Events are inbound: your system reports them to Migma. This is the opposite direction from [webhooks](/webhooks), which are outbound notifications Migma sends to you.
</Info>

Reporting events requires an API key with the `audience:write` permission.

## Quick start

Report a completed purchase with an order identifier and a value. Migma stores the event and, when the value and order identifier are present, records it as a conversion.

```bash theme={null}
curl https://api.migma.ai/v1/events \
  -H "Authorization: Bearer $MIGMA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-10432" \
  -d '{
    "projectId": "665f1e928c2a9e4d8f123123",
    "name": "purchase.completed",
    "identifiers": {
      "email": "casey@example.com",
      "externalOrderId": "order-10432"
    },
    "properties": {
      "value": 49.99,
      "currency": "USD"
    },
    "occurredAt": "2026-07-03T12:34:56.789Z"
  }'
```

The response is synchronous:

```json theme={null}
{
  "stored": true,
  "eventId": "668f2f7b9a9b6d84f1b2c345",
  "dedupeKey": "order-10432"
}
```

## Event fields

| Field         | Required | Meaning                                                                                              |
| ------------- | -------- | ---------------------------------------------------------------------------------------------------- |
| `projectId`   | Yes      | The brand the event belongs to.                                                                      |
| `name`        | Yes      | The event name, such as `purchase.completed` or `trial.started`.                                     |
| `identifiers` | Yes      | How Migma matches the event to a contact. See below.                                                 |
| `profile`     | No       | Contact metadata to update: `firstName`, `lastName`, `phone`, `country`, `language`, `customFields`. |
| `properties`  | No       | Structured event data. Up to 16KB, 50 top-level keys, and 4 levels deep.                             |
| `occurredAt`  | No       | When the event happened, in ISO 8601 format. Defaults to receipt time.                               |
| `dedupeKey`   | No       | A key to make the event replay-safe. See [Replay safety](#replay-safety).                            |

### Identifiers

Provide at least one way to match the event to a contact:

| Identifier           | Use                                                 |
| -------------------- | --------------------------------------------------- |
| `email`              | Match or create a contact by email address.         |
| `subscriberId`       | Match an existing contact by its Migma ID.          |
| `externalCustomerId` | Your own customer ID, stored for cross-referencing. |
| `externalCheckoutId` | Your checkout or session ID.                        |
| `externalOrderId`    | Your order ID. Required for conversion attribution. |

### Consent stays intact

Events never change subscription status. An event never opts anyone in, resubscribes an unsubscribed contact, or makes a non-sendable contact sendable. When you include `profile`, Migma updates contact metadata only. To manage subscription status, use the Contacts API.

## Replay safety

Networks retry. Migma gives you two ways to make sure a retried report is stored once.

* Single events: send an `Idempotency-Key` header. A retry with the same key returns the original result instead of storing a second event.
* Batches: set a per-event `dedupeKey` on each row. A request-level `Idempotency-Key` header on a batch returns `400`, because one key cannot cover many events.

Raw events are retained for 90 days.

## Batches

Report up to 500 events in one request with `POST /v1/events/batch`. Each row is validated on its own, so a batch can partially succeed: valid rows are stored, invalid rows are reported back, and duplicates are counted.

```bash theme={null}
curl https://api.migma.ai/v1/events/batch \
  -H "Authorization: Bearer $MIGMA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "665f1e928c2a9e4d8f123123",
    "events": [
      {
        "name": "purchase.completed",
        "identifiers": { "email": "casey@example.com", "externalOrderId": "order-10432" },
        "properties": { "value": 49.99, "currency": "USD" },
        "dedupeKey": "order-10432"
      },
      {
        "name": "trial.started",
        "identifiers": { "email": "jordan@example.com" },
        "dedupeKey": "trial-jordan-1"
      }
    ]
  }'
```

The response reports each row:

```json theme={null}
{
  "accepted": 1,
  "duplicates": 1,
  "results": [
    { "stored": true, "eventId": "668f30a19a9b6d84f1b2c400", "dedupeKey": "order-10432" },
    { "stored": false, "duplicate": true, "dedupeKey": "trial-jordan-1" }
  ],
  "invalid": []
}
```

| Field        | Meaning                                                    |
| ------------ | ---------------------------------------------------------- |
| `accepted`   | Rows stored on this request.                               |
| `duplicates` | Rows skipped because their `dedupeKey` was already stored. |
| `results`    | Per-row outcome, in request order.                         |
| `invalid`    | Rows that failed validation, with the reason for each.     |

## Conversion attribution

An event becomes a conversion when it carries both `identifiers.externalOrderId` and a numeric `properties.value` in major units, such as `49.99`. Set `properties.currency` to an ISO code; it defaults to `USD`.

Migma attributes the conversion to the last email the recipient clicked within the previous five days, and credits that email once.

Refunds and cancellations keep the numbers honest. When an event name contains `refund` or `cancelled`, Migma records a negative amount, so returned revenue is subtracted rather than counted.

```bash theme={null}
curl https://api.migma.ai/v1/events \
  -H "Authorization: Bearer $MIGMA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: refund-10432" \
  -d '{
    "projectId": "665f1e928c2a9e4d8f123123",
    "name": "order.refunded",
    "identifiers": {
      "email": "casey@example.com",
      "externalOrderId": "order-10432"
    },
    "properties": {
      "value": 49.99,
      "currency": "USD"
    }
  }'
```

### Revenue in campaign stats

When conversions have been attributed to a campaign, `GET /v1/campaigns/{id}/stats` includes a conversion block, and the same figures appear in campaign analytics in the app when it ships:

```json theme={null}
{
  "conversions": 12,
  "revenue": 584.88,
  "currency": "USD",
  "source": "events"
}
```

The conversion block is omitted entirely when nothing has been reported, so a campaign with no attributed revenue shows no conversion figures rather than fake zeros.

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

  <Card title="Webhooks" icon="bell" href="/webhooks">
    Receive outbound notifications when Migma activity happens.
  </Card>
</CardGroup>
