Events on EventBridge
We publish events to the default EventBridge bus in your own account, in your Region, so your systems can react: sync unsubscribes to your CRM, alert when a campaign pauses, or start a report when one completes. Events carry IDs and counts only, never addresses or field values. Look the contact up through the API if you need more.
Every event has source rhino-relay.email, its type as detail-type, and tenantId and occurredAt in detail. Events are delivered at least once and can arrive out of order, so make your handlers safe to run twice.
Subscribe to events
Section titled “Subscribe to events”Create a rule on your default bus that matches the types you want, with a target of your own: an SQS queue, a Lambda function, a Step Functions state machine, or anything else EventBridge can deliver to. For example, to send completed campaigns and unsubscribes to a queue of yours:
aws events put-rule --name rhino-relay-to-crm \ --event-pattern '{"source":["rhino-relay.email"],"detail-type":["campaign.completed","contact.unsubscribed"]}'
aws events put-targets --rule rhino-relay-to-crm \ --targets 'Id=crm-queue,Arn=arn:aws:sqs:us-east-2:123456789012:crm-updates'The queue’s policy must let events.amazonaws.com send to it for that rule. See EventBridge targets in the AWS docs.
Check it worked: the next unsubscribe or finished campaign shows up in your queue, as in the examples below.
Event reference
Section titled “Event reference”contact.unsubscribed
A contact unsubscribed: from a link, one-click in their mail app, through the API, by complaining, or during an import.
| Detail field | Type | Always present | Meaning |
|---|---|---|---|
tenantId | string | Yes | Your tenant ID (a ULID) |
occurredAt | string | Yes | When it happened (ISO 8601, UTC) |
contactId | string | Yes | Contact ID (ct_…) |
campaignId | string | No | Campaign ID (cm_…) |
via | string | Yes | One of link, one_click, api, complaint, import |
Example contact.unsubscribed event
{ "version": "0", "id": "6f1e0c52-8d0a-4b8e-9d3a-2f5b8c1e7a90", "detail-type": "contact.unsubscribed", "source": "rhino-relay.email", "account": "123456789012", "time": "2026-10-01T09:30:00Z", "region": "us-east-2", "resources": [], "detail": { "tenantId": "01J9Z8X7W6V5T4S3R2Q1P0N9M8", "occurredAt": "2026-10-01T09:30:00.000Z", "contactId": "ct_01J9Z8X7W6V5T4S3R2Q1P0N9M8", "campaignId": "cm_01J9Z8X7W6V5T4S3R2Q1P0N9M8", "via": "link" }}contact.bounced
An email to a contact bounced. bounceType says whether it was hard or soft.
| Detail field | Type | Always present | Meaning |
|---|---|---|---|
tenantId | string | Yes | Your tenant ID (a ULID) |
occurredAt | string | Yes | When it happened (ISO 8601, UTC) |
contactId | string | Yes | Contact ID (ct_…) |
campaignId | string | Yes | Campaign ID (cm_…) |
bounceType | string | Yes | One of hard, soft |
Example contact.bounced event
{ "version": "0", "id": "6f1e0c52-8d0a-4b8e-9d3a-2f5b8c1e7a90", "detail-type": "contact.bounced", "source": "rhino-relay.email", "account": "123456789012", "time": "2026-10-01T09:30:00Z", "region": "us-east-2", "resources": [], "detail": { "tenantId": "01J9Z8X7W6V5T4S3R2Q1P0N9M8", "occurredAt": "2026-10-01T09:30:00.000Z", "contactId": "ct_01J9Z8X7W6V5T4S3R2Q1P0N9M8", "campaignId": "cm_01J9Z8X7W6V5T4S3R2Q1P0N9M8", "bounceType": "hard" }}contact.complained
A contact marked one of your emails as spam.
| Detail field | Type | Always present | Meaning |
|---|---|---|---|
tenantId | string | Yes | Your tenant ID (a ULID) |
occurredAt | string | Yes | When it happened (ISO 8601, UTC) |
contactId | string | Yes | Contact ID (ct_…) |
campaignId | string | Yes | Campaign ID (cm_…) |
Example contact.complained event
{ "version": "0", "id": "6f1e0c52-8d0a-4b8e-9d3a-2f5b8c1e7a90", "detail-type": "contact.complained", "source": "rhino-relay.email", "account": "123456789012", "time": "2026-10-01T09:30:00Z", "region": "us-east-2", "resources": [], "detail": { "tenantId": "01J9Z8X7W6V5T4S3R2Q1P0N9M8", "occurredAt": "2026-10-01T09:30:00.000Z", "contactId": "ct_01J9Z8X7W6V5T4S3R2Q1P0N9M8", "campaignId": "cm_01J9Z8X7W6V5T4S3R2Q1P0N9M8" }}campaign.stage_completed
Every batch of a campaign stage has been queued and sent.
| Detail field | Type | Always present | Meaning |
|---|---|---|---|
tenantId | string | Yes | Your tenant ID (a ULID) |
occurredAt | string | Yes | When it happened (ISO 8601, UTC) |
campaignId | string | Yes | Campaign ID (cm_…) |
stage | integer | Yes | |
stats | CampaignStats | Yes | See CampaignStats |
Example campaign.stage_completed event
{ "version": "0", "id": "6f1e0c52-8d0a-4b8e-9d3a-2f5b8c1e7a90", "detail-type": "campaign.stage_completed", "source": "rhino-relay.email", "account": "123456789012", "time": "2026-10-01T09:30:00Z", "region": "us-east-2", "resources": [], "detail": { "tenantId": "01J9Z8X7W6V5T4S3R2Q1P0N9M8", "occurredAt": "2026-10-01T09:30:00.000Z", "campaignId": "cm_01J9Z8X7W6V5T4S3R2Q1P0N9M8", "stage": 0, "stats": { "audience": 0, "enqueued": 0, "sent": 0, "skipped": 0, "failed": 0, "unknown": 0, "delivered": 0, "uniqueOpens": 0, "machineOpens": 0, "uniqueClicks": 0, "clicks": 0, "botClicks": 0, "hardBounces": 0, "softBounces": 0, "undeterminedBounces": 0, "suppressedBounces": 0, "complaints": 0, "unsubscribes": 0, "delayed": 0 } }}campaign.paused
A campaign paused itself (bounce or complaint rate too high, or SES paused sending) or you paused it.
| Detail field | Type | Always present | Meaning |
|---|---|---|---|
tenantId | string | Yes | Your tenant ID (a ULID) |
occurredAt | string | Yes | When it happened (ISO 8601, UTC) |
campaignId | string | Yes | Campaign ID (cm_…) |
pauseReason | PauseReason | Yes | See PauseReason |
stats | CampaignStats | Yes | See CampaignStats |
Example campaign.paused event
{ "version": "0", "id": "6f1e0c52-8d0a-4b8e-9d3a-2f5b8c1e7a90", "detail-type": "campaign.paused", "source": "rhino-relay.email", "account": "123456789012", "time": "2026-10-01T09:30:00Z", "region": "us-east-2", "resources": [], "detail": { "tenantId": "01J9Z8X7W6V5T4S3R2Q1P0N9M8", "occurredAt": "2026-10-01T09:30:00.000Z", "campaignId": "cm_01J9Z8X7W6V5T4S3R2Q1P0N9M8", "pauseReason": "hard_bounce_rate", "stats": { "audience": 0, "enqueued": 0, "sent": 0, "skipped": 0, "failed": 0, "unknown": 0, "delivered": 0, "uniqueOpens": 0, "machineOpens": 0, "uniqueClicks": 0, "clicks": 0, "botClicks": 0, "hardBounces": 0, "softBounces": 0, "undeterminedBounces": 0, "suppressedBounces": 0, "complaints": 0, "unsubscribes": 0, "delayed": 0 } }}campaign.completed
A campaign finished: nothing is left to send.
| Detail field | Type | Always present | Meaning |
|---|---|---|---|
tenantId | string | Yes | Your tenant ID (a ULID) |
occurredAt | string | Yes | When it happened (ISO 8601, UTC) |
campaignId | string | Yes | Campaign ID (cm_…) |
stats | CampaignStats | Yes | See CampaignStats |
Example campaign.completed event
{ "version": "0", "id": "6f1e0c52-8d0a-4b8e-9d3a-2f5b8c1e7a90", "detail-type": "campaign.completed", "source": "rhino-relay.email", "account": "123456789012", "time": "2026-10-01T09:30:00Z", "region": "us-east-2", "resources": [], "detail": { "tenantId": "01J9Z8X7W6V5T4S3R2Q1P0N9M8", "occurredAt": "2026-10-01T09:30:00.000Z", "campaignId": "cm_01J9Z8X7W6V5T4S3R2Q1P0N9M8", "stats": { "audience": 0, "enqueued": 0, "sent": 0, "skipped": 0, "failed": 0, "unknown": 0, "delivered": 0, "uniqueOpens": 0, "machineOpens": 0, "uniqueClicks": 0, "clicks": 0, "botClicks": 0, "hardBounces": 0, "softBounces": 0, "undeterminedBounces": 0, "suppressedBounces": 0, "complaints": 0, "unsubscribes": 0, "delayed": 0 } }}import.dry_run_ready
An import finished its dry run. Read the summary, then commit it.
| Detail field | Type | Always present | Meaning |
|---|---|---|---|
tenantId | string | Yes | Your tenant ID (a ULID) |
occurredAt | string | Yes | When it happened (ISO 8601, UTC) |
importId | string | Yes | Import ID (im_…) |
summary | ImportSummary | Yes | See ImportSummary |
Example import.dry_run_ready event
{ "version": "0", "id": "6f1e0c52-8d0a-4b8e-9d3a-2f5b8c1e7a90", "detail-type": "import.dry_run_ready", "source": "rhino-relay.email", "account": "123456789012", "time": "2026-10-01T09:30:00Z", "region": "us-east-2", "resources": [], "detail": { "tenantId": "01J9Z8X7W6V5T4S3R2Q1P0N9M8", "occurredAt": "2026-10-01T09:30:00.000Z", "importId": "im_01J9Z8X7W6V5T4S3R2Q1P0N9M8", "summary": { "read": 0, "toImport": 0, "suppressed": 0, "skipped": {}, "removedByCleaning": 0, "errors": [] } }}import.completed
A committed import finished writing your contacts.
| Detail field | Type | Always present | Meaning |
|---|---|---|---|
tenantId | string | Yes | Your tenant ID (a ULID) |
occurredAt | string | Yes | When it happened (ISO 8601, UTC) |
importId | string | Yes | Import ID (im_…) |
summary | ImportSummary | Yes | See ImportSummary |
Example import.completed event
{ "version": "0", "id": "6f1e0c52-8d0a-4b8e-9d3a-2f5b8c1e7a90", "detail-type": "import.completed", "source": "rhino-relay.email", "account": "123456789012", "time": "2026-10-01T09:30:00Z", "region": "us-east-2", "resources": [], "detail": { "tenantId": "01J9Z8X7W6V5T4S3R2Q1P0N9M8", "occurredAt": "2026-10-01T09:30:00.000Z", "importId": "im_01J9Z8X7W6V5T4S3R2Q1P0N9M8", "summary": { "read": 0, "toImport": 0, "suppressed": 0, "skipped": {}, "removedByCleaning": 0, "errors": [] } }}CampaignStats
Carried by the events above as a snapshot at the moment they were published. These are the same counters as a campaign's report, before rates are worked out.
| Field | Type |
|---|---|
audience | integer |
enqueued | integer |
sent | integer |
skipped | integer |
failed | integer |
unknown | integer |
delivered | integer |
uniqueOpens | integer |
machineOpens | integer |
uniqueClicks | integer |
clicks | integer |
botClicks | integer |
hardBounces | integer |
softBounces | integer |
undeterminedBounces | integer |
suppressedBounces | integer |
complaints | integer |
unsubscribes | integer |
delayed | integer |
PauseReason
Carried by the events above as a snapshot at the moment they were published.
| Field | Type |
|---|
ImportSummary
Carried by the events above as a snapshot at the moment they were published.
| Field | Type |
|---|---|
read | integer |
toImport | integer |
suppressed | integer |
skipped | object of integer |
removedByCleaning | integer |
errors | array of ImportRowError |
ImportRowError
Carried by the events above as a snapshot at the moment they were published.
| Field | Type |
|---|---|
row | integer |
message | string |
Other events in your account
Section titled “Other events in your account”These are our events about your contacts and campaigns. The raw SES events for our sends are on your default bus too. Your stack sends them in full to your archive bucket, and forwards them to our event bus, where our rules keep only IDs, timestamps and flags for our service. See What the template creates.