Skip to content

Emitting events

An emit tells us that something happened on your platform for one recipient: a booking was made, an invoice was paid. We check it against its event type’s schema and record it. When the type has a template, we render it with the payload and send it from your domain. The idempotency key makes a retry safe, so a network error never means a guest gets two confirmations.

Emit an event into a workspace:

Terminal window
curl -X POST https://api.sendtruss.com/v1/workspaces/ref:cust_4821/events \
-H "Authorization: Bearer $SENDTRUSS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "booking.created",
"idempotency_key": "booking-7731-created",
"recipient": {"contact": "ref:guest_998"},
"payload": {"guest_name": "Ana", "arrival": "2026-10-12"}
}'
  • type is the event type’s name, as you registered it.
  • idempotency_key is your own unique key for this event, up to 255 printable ASCII characters with no spaces. Something like your record’s id plus what happened to it works well. Keep names and addresses out of it.
  • recipient is either {"contact": "ref:guest_998"}, a contact by your reference or our id, or {"email": "ana@example.com"}. Exactly one of the two.
  • payload is the event’s fields, checked against the type’s schema. Send {} for a type with no fields.
  • attachments, optional, are files for the email (see below).

A new emit answers 202. Nothing has been delivered yet; what happens next comes back as webhooks.

{
"data": {
"id": "01JA2Q8V6R0Y7Z3K5M9N1P4T6W",
"type": "booking.created",
"idempotency_key": "booking-7731-created",
"contact": {"id": "01JA2Q5D3B8C1E6F9G2H4J7K0M", "reference": "guest_998"},
"message": {
"id": "01JA2Q8V7S1A2B3C4D5E6F7G8H",
"state": "queued",
"template_version_id": "01JA2P0X9Y8Z7A6B5C4D3E2F1G",
"fell_back": [],
"simulated": false
},
"created_at": "2026-10-08T14:03:11Z"
}
}
  • message.state is queued, or suppressed when the address is suppressed for all email (it hard-bounced before). A suppressed message is not sent, and your endpoint gets message.suppressed.
  • message is null when the type has no template: the event is recorded and nothing is sent. That’s useful for events you want on record without an email.
  • message.fell_back lists the merge tags that had no value and rendered their fallback. The email still went out.
  • contact is null when you emitted to an address no contact holds.

Send the same key with the same body again and you get the first answer back, with the header Idempotent-Replayed: true, and nothing is sent twice. So when a request times out, retry it as it was.

The same key with a different body is refused with idempotency_key_reused: a key reused for different content is a bug you’ll want to see.

A refused emit records nothing, its key included. Fix the problem and send it again with the same key.

Keys are kept for 7 days. After that, the same key is a new emit.

A contact named by reference must exist in the workspace; one that doesn’t is refused with validation_failed under recipient.contact. An emit never creates a contact. Push contacts with the contacts API first.

A bare address that a contact in the workspace holds is treated as that contact: the email renders with their attributes and locale, and the webhooks name the contact. An address no contact holds still gets the email, with no contact behind it.

Transactional email doesn’t need marketing consent, so a pending or unsubscribed contact still gets it. See Transactional and marketing streams.

Status Code What to do
422 validation_failed Fix the field errors names: the body’s shape, an unknown type, a payload field (payload.guest_name, payload.items.2.price), a payload over 512 KB, or a recipient contact that doesn’t exist.
409 idempotency_key_reused You sent this key before with a different body. Use a new key for new content.
409 workspace_suspended You suspended this workspace. Resume it to send.
409 domain_not_sendable Your sending domain isn’t registered or verified yet. Check the console’s domain screen.
429 recipient_rate_limited This address has had 30 emits in this workspace in the last hour. Wait for Retry-After.
429 rate_limited Your platform’s request limit. Wait for Retry-After.

The per-address limit is there to stop a bug in a loop from flooding one person’s inbox. It counts emits that reach sending: a replay doesn’t count, and neither does an emit of a type with no template.

An emit can carry up to 10 files, up to 10,000,000 bytes in all once decoded, each as a filename and its content in base64:

"attachments": [
{"filename": "invoice-7731.pdf", "content": "JVBERi0xLjcK..."}
]

PDF, PNG, JPEG, GIF, WebP and iCalendar files are accepted. We check the type by the file’s content, and the file name’s extension must match it. Attachments are for transactional email only.

You can emit to our simulator addresses before your sending domain verifies: delivered@, bounce@ and complaint@simulator.sendtruss.com. They send nothing, but they produce the same message states and webhooks a real delivery, bounce or complaint would, each marked simulated. See Testing.