Product events
Record what a contact did in your product and resume the campaign steps waiting for it.
A product event is your app telling SendSets that a contact did something: trial.started, report.opened, plan.upgraded. A wait_for_event step holds a contact until a named event arrives for them (or its timeout passes), so an outbound sequence can follow what the person actually did instead of a calendar.
Events are stored per workspace under the engagement-event retention window and are searchable afterwards.
Send a product event
POST /events
Auth: Scope WRITE_EVENTS · Org permission send_campaigns · Idempotency-Key required
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Dotted lowercase words: [a-z0-9_-]+(\.[a-z0-9_-]+)*, up to 120 characters. |
email | string | one of | The contact's email. |
contact_id | string (UUID) | one of | The contact's id. Exactly one of email or contact_id. |
occurred_at | string (RFC 3339) | no | When it happened. Default: now. A wait only matches events that occurred after the contact entered it. |
data | object | no | Event properties, up to 64 KiB. Becomes .App.<output_key> of the wait step that consumed it. |
{
"name": "trial.started",
"email": "jane@acme.com",
"data": { "plan": "pro" }
}Response: 202 Accepted
{
"event_id": "8b0a...",
"contact_id": "c1d2...",
"name": "trial.started",
"received_at": "2026-09-01T10:00:00Z",
"matched_waits": 1,
"matched_runs": [
{ "run_id": "run_123", "campaign_id": "camp_1", "step_id": "wait_456", "contact_id": "c1d2...", "state": "waiting", "mode": "live" }
]
}matched_runs names every step run the event resumed; an empty list means nothing was waiting. A replay with the same Idempotency-Key returns the stored event and resumes nothing new. An email or id that matches no contact answers 404 event_contact_not_found with a fix.
List product events
GET /events
Auth: Scope READ_CAMPAIGNS · Org permission view_campaigns
| Parameter | In | Type | Description |
|---|---|---|---|
name | query | string | Only this event name. |
email | query | string | Only this contact, by email. |
contact_id | query | string (UUID) | Only this contact, by id. |
since | query | string (RFC 3339) | Only events received after this time. |
limit | query | integer | Page size, 1 to 200. Default 50. |
cursor | query | string | Opaque cursor from pagination.next_cursor. |
{
"data": [
{ "id": "8b0a...", "organization_id": "...", "contact_id": "c1d2...", "name": "trial.started", "data": { "plan": "pro" }, "occurred_at": "...", "received_at": "..." }
],
"pagination": { "next_cursor": "", "has_more": false }
}Get a product event
GET /events/:id
Auth: Scope READ_CAMPAIGNS · Org permission view_campaigns
Returns one event, or 404 event_not_found.
From the CLI
sendsets events send trial.started --email jane@acme.com --data '{"plan":"pro"}'
sendsets events list --name trial.started --since 2026-09-01T00:00:00Z
sendsets events tail --intent CAMPAIGNevents send mints an Idempotency-Key per call; pass --idempotency-key to make a retry a replay.