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

# Event stream

> Send every send, delivery, bounce, open, click, unsubscribe and conversion to your own Amazon S3 bucket, Segment or RudderStack, or an HTTPS endpoint, as it happens.

The event stream copies your workspace's email activity to storage you own. Your analysts can then join it with orders and product data in Snowflake, BigQuery, Redshift or Athena, without exporting CSV files.

> [!NOTE]
>
> The event stream needs the **Pro** or **Enterprise** plan. Only people who can change workspace settings can set it up.

## Choose a destination

Go to **Settings → Developers → Event stream** and pick one destination. A workspace has one stream.

| Destination | What arrives | How often |
| - | - | - |
| **Amazon S3** | Gzip NDJSON files under `<prefix>/dt=YYYY-MM-DD/hour=HH/` | Every 5 minutes, or sooner when 10,000 events wait |
| **Segment or RudderStack** | `track` calls such as `Email Opened`, sent in batches to `<data plane URL>/v1/batch` | Within seconds |
| **HTTPS endpoint** | `POST` requests with up to 4 MB of NDJSON each | Within seconds |

### Amazon S3

1. Create a bucket, or pick one, in any AWS region.
2. Create an IAM user whose only permission is `s3:PutObject` on that bucket and prefix.
3. Enter the bucket, region, prefix, access key ID and secret access key, then click **Save**.

The credentials are encrypted and never shown again. To change them, type new ones; leave the fields blank to keep the saved ones.

Each file is named after the first and last event it holds, so a retried upload replaces the same file instead of adding a second one. The `dt=` and `hour=` folders are partitions that Athena, BigQuery and Snowflake external tables read directly.

### Segment or RudderStack

Enter your data plane URL (for example `https://api.segment.io`) and a write key for a source you created for InstantCampaign. Each event arrives as a `track` call:

| Event type | Track call name |
| - | - |
| `email.sent` / `email.failed` / `email.skipped` | Email Sent / Email Failed / Email Skipped |
| `email.delivered` / `email.bounced` / `email.complained` | Email Delivered / Email Bounced / Email Marked as Spam |
| `email.opened` / `email.clicked` | Email Opened / Email Link Clicked |
| `contact.unsubscribed` / `contact.resubscribed` / `contact.status_changed` | Unsubscribed / Resubscribed / Contact Status Changed |
| `event.recorded` / `goal.converted` | Event Recorded / Goal Converted |

> [!WARNING]
>
> Watch your CDP bill. Segment bills by monthly tracked users, and every contact who receives an email can become one (`ic_<contact id>`). RudderStack bills by event volume, and one email produces about 2.4 events (sent, delivered, plus some opens and clicks). A large send can push either plan over its limit. Send only the event types you need (for example bounces, unsubscribes and goals), or use Amazon S3, which costs cents per million emails.

The call's `messageId` is the event's `id`, so Segment and RudderStack drop a repeated delivery. The person is `anonymousId: ic_<contact id>`. When you turn on **Include email addresses**, `context.traits.email` carries the address, so your CDP can merge the events into its own profiles.

### HTTPS endpoint

Enter an `https` URL. Each request has `Content-Type: application/x-ndjson`, one record per line, and these headers:

* `webhook-id`, `webhook-timestamp`: the batch's id and the time it was sent.
* `webhook-signature`: present when you set a signing secret. It is computed like our [webhooks](https://docs.instantcampaign.ai/guides/developers/webhooks.md), so the same verification code works.
* `x-instantcampaign-records`: how many records the request holds.

Answer with any `2xx` once you have stored the batch. Any other answer, or no answer within 30 seconds, means the batch is sent again later.

## Record format

Every record is one JSON object on one line:

```json theme={null}
{"v":1,"id":"se_cm3x…_delivered","type":"email.delivered","occurredAt":"2026-11-08T10:15:02.000Z","workspaceId":"cm1a…","sendEventId":"cm3x…","contactId":"cm2b…","campaignId":"cm9c…","campaignSendId":"cm9d…","subject":"Your November picks","previousStatus":"sent","recipientDomain":"gmail.com"}
```

| Field | In | Meaning |
| - | - | - |
| `v` | every record | Format version, now `1` |
| `id` | every record | Stable per event. Use it to remove duplicates |
| `type` | every record | One of the types in the table above |
| `occurredAt` | every record | When it happened (ISO 8601, UTC) |
| `workspaceId` | every record | Your workspace |
| `contactId` | most records | The contact, when there is one |
| `campaignId`, `campaignSendId`, `journeyId`, `journeyStepId`, `transactionalMessageId` | email records | What sent the email |
| `sendEventId`, `messageId`, `subject`, `recipientDomain` | `email.sent` to `email.complained` | The send, the provider's message id, the subject line, the recipient's domain |
| `previousStatus` | `email.delivered`, `email.bounced`, `email.complained` | The status before this one |
| `reason` | `email.failed`, `email.skipped`, `email.bounced`, `email.complained` | Why, as the provider or the platform said it |
| `analyticsEventId`, `url`, `linkPosition`, `isBot` | `email.opened`, `email.clicked` | The link clicked; `isBot: true` marks a security scanner or other machine |
| `previousStatus`, `newStatus`, `source`, `reason` | `contact.*` | The status change and what caused it |
| `customEventId`, `eventName`, `goalType`, `value`, `currency`, `externalId`, `properties` | `event.recorded`, `goal.converted` | The event or goal as it was recorded |
| `email` | any | Only when **Include email addresses** is on |

New fields may be added to a version. A field is never removed or renamed within a version.

Opens and clicks from machines are sent with `isBot: true`, not left out, so you can count them your own way. Our dashboard counts only `isBot: false`.

## Choose what to send

* **Send every event type** sends all of the types above. Turn it off to pick types one by one.
* **Include email addresses** is off by default. Without it, records carry the contact id and the recipient's domain, but not the address. Turn it on only if your destination is allowed to hold personal data.

## Delivery and failures

Events wait in a queue until your destination accepts them, and are deleted from the queue only then.

* **Nothing is skipped.** Each event is written to the queue in the same database transaction as the send, open or unsubscribe it records.
* **Repeats are possible, but each event keeps its `id`.** If an attempt fails after part of the data arrived, the events are sent again. For S3 the file name stays the same; for Segment and RudderStack the `messageId` stays the same; for HTTPS, remove duplicates by `id`.
* **Failed deliveries are retried.** The wait starts at 30 seconds and grows to 15 minutes.
* **After 24 hours of failures the stream pauses.** Owners and admins get a notification. Events keep collecting while it is paused. Fix the destination, then click **Save and resume**.
* **The queue holds up to 5 million events.** Beyond that, the oldest are dropped and counted under **Dropped**.

The settings page shows how many events are waiting, how old the oldest one is, how many were delivered and dropped, the last delivery, and the last error. **Send a test record** delivers one record of type `stream.test` straight away and reports what your destination answered.

If the workspace moves to a plan without the event stream, the stream stops collecting. Turn it on again after upgrading.

## Removing the stream

**Remove stream** deletes the stream and every event still waiting. Data already delivered to your destination stays there.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.