Send your first transactional email
This is the one-shot path to a first real send: verify your domain, create a sender, render a template, dispatch, and watch the result arrive on a webhook. Everything below is runnable — substitute your own API key, domain, and recipient. For the full day-one-to-at-scale plan (warm-up ramps, bulk patterns, deliverability posture), see the Email channel lifecycle guide. This page gets you to one delivered email; that one takes you to volume. You will:- Verify your sending domain
- Create a sender profile
- Send a templated message
- Subscribe to delivery and bounce events
- Handle the suppression list
- Debug the common first-send errors
- Hand off to the at-scale plan
dv_test_sk_...), they short-circuit
before delivery but still exercise validation, so the integration code you
write in the sandbox runs unchanged on live keys. Do the domain
verification in step 1 before you flip to live traffic.
Every API step ships the same request in all six forms — cURL, Node.js
(TypeScript), Python, Go, Ruby, and PHP — so you can copy the one that
matches your stack. Every response carries the same envelope: a data
payload plus meta.request_id to correlate with webhooks and support.
1. Verify your sending domain
Everything downstream — sender validation, tracking, deliverability — hangs off a verified domain. Add your domain under Channels → Email → Domains, which lists the exact DNS records (SPF, DKIM, DMARC, and the Return-Path CNAME) to publish at your DNS provider. Once the records propagate, run Verify DNS on the domain row and confirm the per-record traffic-light view grades green:- cURL
- Node.js
- Python
- Go
- Ruby
- PHP
- cURL
- Node.js
- Python
- Go
- Ruby
- PHP
valid. If your DNS lives
in a Cloudflare zone, the
Cloudflare auto-configure guide
can create the missing records for you from the same domain row; either
way, re-run Verify DNS after any manual change.
2. Create a sender profile
Sends need a registered sender row so the platform can bind yourfrom
address to a verified domain. Register one under Channels → Email →
Senders, or over the API:
- cURL
- Node.js
- Python
- Go
- Ruby
- PHP
esend_...):
from on a send, the sender marked isDefault
resolves the address; set that flag on the one you want as the fallback.
One sender row is enough for a first send — add aliases later.
3. Send a templated message
The send endpoint takes a finishedhtml (or text) body — it does not
accept a template_id. Either html or text must be non-empty, or the
request is rejected before anything is queued.
The one-shot send — inline body, done in your stack’s form:
- cURL
- Node.js
- Python
- Go
- Ruby
- PHP
data.succeeded carrying one row
per recipient, and the envelope meta.request_id to correlate with the
webhook events below:
{{...}} merge tags in a saved block layout) you
render first, then send the rendered HTML — two passes:
- Render with
POST /api/v1/messages/email-builder/render. The response carries the renderedhtml, an AMP-for-Email alternative, and atemplateIdto correlate with a later save. - Resolve every
{{...}}tag in the rendered HTML, then dispatch it through the same send endpoint. An unresolved tag lands on the wire as the literal string, so resolve them all before sending.
- cURL
- Node.js
- Python
- Go
- Ruby
- PHP
POST /api/v1/messages/email-builder/save — the full mechanics are in
Using Templates.
4. Subscribe to delivery and bounce events
Register one endpoint that subscribes the delivery family and the engagement pair, and filter onchannel: "email":
message.delivered— accepted by the recipient servermessage.failed— bounced (hard or soft) or marked as spamemail.opened/email.clicked— engagement (first-open deduped; clicks fire on every distinct click)
- cURL
- Node.js
- Python
- Go
- Ruby
- PHP
{data, meta} envelope with a generated
signing_secret inside data — store it; you verify every inbound POST
against it:
X-Orbit-Signature header on every delivery and return 2xx
within the delivery window — the walkthrough is in
Receive your first webhook. In sandbox
mode the send is short-circuited, so you can exercise your verification
and retry handling without a live recipient; the
sandbox guide covers the canned outcomes.
5. Handle the suppression list
Hard bounces, exhausted soft bounces, complaints, and unsubscribes are held out of future sends automatically, and the served quota slot is refunded at the pre-send gate. Manage the list yourself under Channels → Email → Suppressions — search it, export, add manual entries, or bulk-import from a previous ESP. Add a manual entry —reason is one of manual, unsubscribe,
complaint, or hard_bounce:
- cURL
- Node.js
- Python
- Go
- Ruby
- PHP
search:
- cURL
- Node.js
- Python
- Go
- Ruby
- PHP
6. Common first-send errors
- Domain unverified (403). A provider-side
403returns as403 VALIDATION_ERRORwithdetails.provider_message. Most often this is a sending domain whose verification regressed or never completed. Re-run Verify DNS, then checkGET /api/v1/email/domains/:domainId/dns-statusuntil every record gradesvalid. CHANNEL_NOT_CONFIGURED(503). The email provider was never set up for the tenant. Finish step 1, then your sender in step 2.- Suppressed recipient (
RECIPIENT_OPTED_OUT, 422). The recipient is on your suppression list and the quota slot is refunded. Route the flow to an alternate channel instead of un-suppressing by reflex. - Invalid
from(NO_SENDER_CONFIGURED, 422). No sender resolved — either no row owns the address and no default applies, or the defaulted address never got registered. Re-check step 2. VALIDATION_ERROR(422) on the body. Malformed addresses, an empty body, or a disallowed attachment. Eitherhtmlortextmust be non-empty; unresolved merge tags would ship the literal{{...}}string to the recipient, so resolve them all before dispatch.RATE_LIMITED(429). You crossed the per-tenant channel pool (roughly 200 sends per minute). Back off and retry.