Skip to content

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.

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:

Terminal window
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.

contact.unsubscribed

A contact unsubscribed: from a link, one-click in their mail app, through the API, by complaining, or during an import.

Detail fieldTypeAlways presentMeaning
tenantIdstringYesYour tenant ID (a ULID)
occurredAtstringYesWhen it happened (ISO 8601, UTC)
contactIdstringYesContact ID (ct_…)
campaignIdstringNoCampaign ID (cm_…)
viastringYesOne 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 fieldTypeAlways presentMeaning
tenantIdstringYesYour tenant ID (a ULID)
occurredAtstringYesWhen it happened (ISO 8601, UTC)
contactIdstringYesContact ID (ct_…)
campaignIdstringYesCampaign ID (cm_…)
bounceTypestringYesOne 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 fieldTypeAlways presentMeaning
tenantIdstringYesYour tenant ID (a ULID)
occurredAtstringYesWhen it happened (ISO 8601, UTC)
contactIdstringYesContact ID (ct_…)
campaignIdstringYesCampaign 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 fieldTypeAlways presentMeaning
tenantIdstringYesYour tenant ID (a ULID)
occurredAtstringYesWhen it happened (ISO 8601, UTC)
campaignIdstringYesCampaign ID (cm_…)
stageintegerYes
statsCampaignStatsYesSee 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 fieldTypeAlways presentMeaning
tenantIdstringYesYour tenant ID (a ULID)
occurredAtstringYesWhen it happened (ISO 8601, UTC)
campaignIdstringYesCampaign ID (cm_…)
pauseReasonPauseReasonYesSee PauseReason
statsCampaignStatsYesSee 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 fieldTypeAlways presentMeaning
tenantIdstringYesYour tenant ID (a ULID)
occurredAtstringYesWhen it happened (ISO 8601, UTC)
campaignIdstringYesCampaign ID (cm_…)
statsCampaignStatsYesSee 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 fieldTypeAlways presentMeaning
tenantIdstringYesYour tenant ID (a ULID)
occurredAtstringYesWhen it happened (ISO 8601, UTC)
importIdstringYesImport ID (im_…)
summaryImportSummaryYesSee 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 fieldTypeAlways presentMeaning
tenantIdstringYesYour tenant ID (a ULID)
occurredAtstringYesWhen it happened (ISO 8601, UTC)
importIdstringYesImport ID (im_…)
summaryImportSummaryYesSee 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.

FieldType
audienceinteger
enqueuedinteger
sentinteger
skippedinteger
failedinteger
unknowninteger
deliveredinteger
uniqueOpensinteger
machineOpensinteger
uniqueClicksinteger
clicksinteger
botClicksinteger
hardBouncesinteger
softBouncesinteger
undeterminedBouncesinteger
suppressedBouncesinteger
complaintsinteger
unsubscribesinteger
delayedinteger

PauseReason

Carried by the events above as a snapshot at the moment they were published.

FieldType

ImportSummary

Carried by the events above as a snapshot at the moment they were published.

FieldType
readinteger
toImportinteger
suppressedinteger
skippedobject of integer
removedByCleaninginteger
errorsarray of ImportRowError

ImportRowError

Carried by the events above as a snapshot at the moment they were published.

FieldType
rowinteger
messagestring

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.