Skip to content

Send transactional email for your events

This guide turns one thing that happens on your platform, a booking, into an email your customers’ guests get from your domain. You register the event type with its fields, give it a template, try it on a simulator address, and then emit it from the code that creates the booking. It assumes you have an API key and a workspace; the Quickstart covers both.

Decide what the email needs to say, and make each piece a field. Register an event type:

Terminal window
curl -X PUT https://api.sendtruss.com/v1/event-types/booking.created \
-H "Authorization: Bearer $SENDTRUSS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"fields": [
{"name": "guest_name", "type": "string", "required": true},
{"name": "arrival", "type": "date", "required": true},
{"name": "site_name", "type": "string", "required": false, "fallback": "your site"},
{"name": "total", "type": "money", "required": false},
{"name": "items", "type": "list", "required": false, "fields": [
{"name": "description", "type": "string", "required": true},
{"name": "price", "type": "money", "required": true}
]}
]
}'

Make a field required only when the email makes no sense without it. An optional field with a fallback is more forgiving, and you can always relax a required field later but never tighten an optional one. Put this call in your deploy: sending a schema that hasn’t changed does nothing. See Event types and schemas for the field types and how schemas may change.

Facts about the person that don’t change per booking, like their first name, belong in contact attributes rather than the payload.

An event type sends nothing until it has a template. Your team can build it in the console’s template editor, which previews it as they go. Or save it from code with Save an event type’s default template:

Terminal window
curl -X PUT https://api.sendtruss.com/v1/event-types/booking.created/template \
-H "Authorization: Bearer $SENDTRUSS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subject": "Your booking at {{ workspace_name }} is confirmed",
"from_local_part": "bookings",
"display_name": "{{ workspace_name }}",
"locale": "en",
"composition": {
"editorVersion": 3,
"target": "email",
"document": {
"blocks": [
{"id": "title", "type": "heading", "attrs": {"text": "See you soon, {{ event.guest_name }}"}},
{"id": "intro", "type": "text", "attrs": {"html": "You booked {{ event.site_name }} from {{ event.arrival }}."}}
]
}
}
}'

Every save goes live in every workspace at once. To check it first, preview it with a sample payload:

Terminal window
curl -X POST https://api.sendtruss.com/v1/event-types/booking.created/template/preview \
-H "Authorization: Bearer $SENDTRUSS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"payload": {"guest_name": "Ana", "arrival": "2026-10-12"}}'

The answer’s fallen_back lists the tags that had no value, here event.site_name, which renders its fallback. Templates covers merge tags, versions and translations.

Before your domain verifies, and any time you want to test without sending real email, emit to a simulator address. delivered@simulator.sendtruss.com plays a delivery:

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": "test-booking-1",
"recipient": {"email": "delivered@simulator.sendtruss.com"},
"payload": {"guest_name": "Ana", "arrival": "2026-10-12"}
}'

Nothing is sent, but you get a 202 with message.simulated true, and your webhook endpoint gets message.delivered marked simulated. Use bounce@ and complaint@ to see those outcomes too. See Testing.

Where your platform creates the booking, emit the event for the guest, by your reference for them:

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", "site_name": "Site 14"}
}'

Three things to get right in that code:

  • Build the idempotency key from your own record, such as the booking’s id and what happened to it. Then a retry after a timeout sends the same key with the same body, gets the first answer back with Idempotent-Replayed: true, and the guest gets one email. Never use a random key per attempt.
  • Retry on network errors, 5xx and 429. On a 429, wait for Retry-After. Don’t retry a 422 or 409 unchanged: read the code, fix the cause, and send again with the same key, since a refused emit records nothing.
  • Push the contact first. An emit never creates a contact, and an unknown ref: is refused. If you’d rather not keep a contact for a one-off recipient, emit to {"email": "…"} instead.

Emitting events lists every refusal and what to do about it.

The 202 means we have the email, not that it arrived. Its outcome comes back to your webhook endpoint as message.delivered, message.bounced, message.complained, message.suppressed, message.failed or message.expired, each carrying the idempotency_key you sent, so you can match it to the booking.

If an email seems to be missing, list your webhooks filtered by type to see what we sent your endpoint and whether it took it.