Skip to main content

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:
  1. Verify your sending domain
  2. Create a sender profile
  3. Send a templated message
  4. Subscribe to delivery and bounce events
  5. Handle the suppression list
  6. Debug the common first-send errors
  7. Hand off to the at-scale plan
Prefer to skip DNS work while you learn the API? Finish the round trip first against the sandbox — a test key (dv_test_sk_...) or the X-Test-Mode: true header runs the same request shapes with nothing delivered and no real recipient touched. The sandbox guide is Sandbox and test mode; register the real domain in step 1 before your first live send.
Every example below assumes a verified domain and a registered sender. If you draft them against a sandbox key (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:
Then read the graded status for the domain:
Every record you intend to sign with must grade 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 your from address to a verified domain. Register one under Channels → Email → Senders, or over the API:
The response carries your sender id (esend_...):
When you later omit 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 finished html (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:
A clean dispatch returns 202 with data.succeeded carrying one row per recipient, and the envelope meta.request_id to correlate with the webhook events below:
For template variables ({{...}} merge tags in a saved block layout) you render first, then send the rendered HTML — two passes:
  1. Render with POST /api/v1/messages/email-builder/render. The response carries the rendered html, an AMP-for-Email alternative, and a templateId to correlate with a later save.
  2. 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.
Save the template for reuse with 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 on channel: "email":
  • message.delivered — accepted by the recipient server
  • message.failed — bounced (hard or soft) or marked as spam
  • email.opened / email.clicked — engagement (first-open deduped; clicks fire on every distinct click)
The created endpoint returns the {data, meta} envelope with a generated signing_secret inside data — store it; you verify every inbound POST against it:
Verify the 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:
Check whether an address is held before you route a send to it — page the list with search:
Removal is a deliberate action: a re-bounce or a re-complaint re-suppresses the address. For a per-contact reachability check before you route a send, use the contact deliverability health guide.

6. Common first-send errors

  • Domain unverified (403). A provider-side 403 returns as 403 VALIDATION_ERROR with details.provider_message. Most often this is a sending domain whose verification regressed or never completed. Re-run Verify DNS, then check GET /api/v1/email/domains/:domainId/dns-status until every record grades valid.
  • 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. Either html or text must 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.
The send and suppression snippets above carry these codes as inline comments in every language tab, so the expectation travels with the code you copy. The full catalog with remediations is in Error Catalog.

7. At scale

This guide got you one clean send. The Email channel lifecycle guide is the day-one-to-at-scale plan — warm-up ramps before dedicated-IP volume, bulk sends up to 10,000 recipients per call, per-domain open and click tracking, and per-contact deliverability health. If you will also send campaigns or newsletters, split them into separate sending streams so bulk volume can never drag your receipts’ deliverability down with it. The Channels → Email reference indexes the full channel surface.