Emails

Send, batch send, schedule, retrieve, reschedule and cancel emails.

Send an email

POST/api/v1/emails

Accepts one email and returns its id. The email is queued and handed to the sending server for your domain's region; follow its progress with retrieve, events or webhooks. Works with full_access and sending_access keys.

Body

fromstringrequired
Sender address, optionally with a display name: Acme <[email protected]>. Must be on a verified domain of the workspace (or [email protected] for tests to workspace members). Optional when a template defines it.
tostring | string[]required
Recipient address(es). At least one; at most 50 recipients across to, cc and bcc.
subjectstringrequired
Subject line (no line breaks). Optional when a template defines it.
htmlstring
HTML body. Provide html, text or both (or a template). When only html is given, a plain-text part is generated from it.
textstring
Plain-text body.
ccstring | string[]
Cc recipients.
bccstring | string[]
Bcc recipients (not shown in the message headers).
reply_tostring | string[]
Reply-To address(es). Multiple addresses are joined into one Reply-To header.
scheduled_atstring (ISO 8601)
Send later, e.g. 2026-10-01T09:00:00Z. Must be in the future and at most 30 days ahead. The email is stored with last_event: "scheduled" until then and can be rescheduled or canceled.
tags{ name, value }[]
Up to 50 tags for your own bookkeeping, echoed in webhook payloads. Names and values: ASCII letters, digits, _ and -, at most 256 characters. An object { name: value } is accepted too.
headersobject
Custom headers. Only List-Unsubscribe, List-Unsubscribe-Post (value List-Unsubscribe=One-Click, and only with an https List-Unsubscribe URL) and X-Entity-Ref-ID are passed through to the message (values up to 900 characters, no line breaks); other headers are not added to the email.
template{ id, variables }
Render a published template instead of html/text. id is the template id or its alias; variables is an object of values. See Sending with a template.
attachments—
Not supported yet: any non-empty value is rejected with 422 attachments_not_supported.

Headers

Idempotency-Keystring
Optional, 1–256 characters. Retries with the same key return the first response. See Idempotency.
# Send an email
curl -X POST 'https://api.bytesms.com/api/v1/emails' \
  -H 'Authorization: Bearer tp_live_xxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -d '{"from":"Acme <[email protected]>","to":["[email protected]"],"subject":"Your order has shipped","html":"<p>Your order <strong>#4711</strong> is on its way.</p>","reply_to":"[email protected]","tags":[{"name":"category","value":"shipping"}]}'
Response
{
  "id": "cmg4k2x7a0001l8v9h3q2d5rt"
}

Sending with a template

With template, the published version of the template is rendered server-side. from, subject and reply_to in the request override the template's; html/textcan't be combined with a template. Variable values are HTML-escaped in the HTML body. A variable the template uses without a fallback value must be provided, otherwise the send fails with 422; an unpublished template is 422 too.

# Send with a template
curl -X POST 'https://api.bytesms.com/api/v1/emails' \
  -H 'Authorization: Bearer tp_live_xxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -d '{"from":"Acme <[email protected]>","to":"[email protected]","template":{"id":"order-shipped","variables":{"first_name":"Jane","order_id":4711}}}'

Who you can send from and to

  • from must be on a domain with status verified in this workspace — otherwise 403 invalid_from_address (or 403 validation_error if the workspace has no verified domain yet).
  • [email protected] may only send to members of the workspace and to @test.bytesms.com test addresses.
  • A domain-scoped API key may only send from its domain (403 restricted_api_key).
  • Recipients on your suppression list (previous hard bounces and complaints) are skipped; if every recipient is suppressed, the email ends failed.

Send a batch

POST/api/v1/emails/batch

Send up to 100 emails in one request. The body is a JSON array of email objects with the same fields as send, except scheduled_at (not supported in batches). Validation, sending rules and the quota check are all-or-nothing: either every email is accepted or none is. Supports Idempotency-Key.

# Send a batch
curl -X POST 'https://api.bytesms.com/api/v1/emails/batch' \
  -H 'Authorization: Bearer tp_live_xxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -d '[{"from":"Acme <[email protected]>","to":["[email protected]"],"subject":"Hi A","text":"Hello A"},{"from":"Acme <[email protected]>","to":["[email protected]"],"subject":"Hi B","text":"Hello B"}]'
Response
{
  "data": [
    {
      "id": "cmg4k2x7a0002l8v9w1b8n4qe"
    },
    {
      "id": "cmg4k2x7a0003l8v9c7m0p2ya"
    }
  ]
}

Retrieve an email

GET/api/v1/emails/{email_id}

Returns the email with its current last_event and the full event history. Requires a full_access key.

# Retrieve an email
curl -X GET 'https://api.bytesms.com/api/v1/emails/cmg4k2x7a0001l8v9h3q2d5rt' \
  -H 'Authorization: Bearer tp_live_xxxxxxxxx'
Response
{
  "object": "email",
  "id": "cmg4k2x7a0001l8v9h3q2d5rt",
  "from": "Acme <[email protected]>",
  "to": [
    "[email protected]"
  ],
  "cc": [],
  "bcc": [],
  "reply_to": "[email protected]",
  "subject": "Your order has shipped",
  "html": "<p>Your order <strong>#4711</strong> is on its way.</p>",
  "text": "Your order #4711 is on its way.",
  "created_at": "2026-09-28T08:15:02.114Z",
  "scheduled_at": null,
  "last_event": "delivered",
  "events": [
    {
      "type": "email.sent",
      "created_at": "2026-09-28T08:15:03.020Z",
      "data": {
        "email_id": "cmg4k2x7a0001l8v9h3q2d5rt",
        "from": "Acme <[email protected]>",
        "to": [
          "[email protected]"
        ],
        "subject": "Your order has shipped"
      }
    },
    {
      "type": "email.delivered",
      "created_at": "2026-09-28T08:15:05.480Z",
      "data": {
        "email_id": "cmg4k2x7a0001l8v9h3q2d5rt",
        "recipients": [
          "[email protected]"
        ],
        "dsn": "2.0.0"
      }
    }
  ]
}
last_eventMeaning
queuedAccepted, waiting to be handed to the sending server.
scheduledWaiting for its scheduled_at.
sentAccepted by our sending server for delivery.
deliveredThe recipient's mail server accepted it (not necessarily the inbox).
bouncedThe recipient's mail server permanently rejected it.
complainedThe recipient marked it as spam.
failedNot sent (e.g. suppressed recipient, sending error).
canceledA scheduled email that was canceled.

List emails

GET/api/v1/emails

Newest first. Items have the same fields as retrieve, without html, text and events.

Query

limitintegerdefault 20
1–100.
afterstring
An email id: return older emails after it (next page).
beforestring
An email id: return newer emails before it. Don't combine with after.
# List emails
curl -X GET 'https://api.bytesms.com/api/v1/emails?limit=20' \
  -H 'Authorization: Bearer tp_live_xxxxxxxxx'
Response
{
  "object": "list",
  "has_more": true,
  "data": [
    {
      "object": "email",
      "id": "cmg4k2x7a0001l8v9h3q2d5rt",
      "...": "..."
    }
  ]
}

List an email's events

GET/api/v1/emails/{email_id}/events

Every recorded event, oldest first: email.sent, email.delivered, email.delivery_delayed, email.bounced, email.complained, email.failed. data holds event details such as the bounce reason.

Response
{
  "data": [
    {
      "type": "email.sent",
      "created_at": "2026-09-28T08:15:03.020Z",
      "data": {
        "email_id": "cmg4k2x7a0001l8v9h3q2d5rt",
        "from": "Acme <[email protected]>",
        "to": [
          "[email protected]"
        ],
        "subject": "Hello"
      }
    },
    {
      "type": "email.bounced",
      "created_at": "2026-09-28T08:15:06.901Z",
      "data": {
        "email_id": "cmg4k2x7a0001l8v9h3q2d5rt",
        "bounce": {
          "type": "Permanent",
          "subType": "5.1.1",
          "reason": "550 5.1.1 User unknown"
        },
        "recipients": [
          "[email protected]"
        ]
      }
    }
  ]
}

Reschedule an email

PATCH/api/v1/emails/{email_id}

Change the scheduled_at of an email that is still scheduled; any other state is 422 validation_error.

Body

scheduled_atstring (ISO 8601)required
New send time, in the future and within 30 days.
# Reschedule
curl -X PATCH 'https://api.bytesms.com/api/v1/emails/cmg4k2x7a0001l8v9h3q2d5rt' \
  -H 'Authorization: Bearer tp_live_xxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -d '{"scheduled_at":"2026-10-01T09:00:00Z"}'
Response
{
  "object": "email",
  "id": "cmg4k2x7a0001l8v9h3q2d5rt"
}

Cancel a scheduled email

POST/api/v1/emails/{email_id}/cancel

Cancels an email that is still scheduled. Its last_event becomes canceled and its quota is released.

# Cancel
curl -X POST 'https://api.bytesms.com/api/v1/emails/cmg4k2x7a0001l8v9h3q2d5rt/cancel' \
  -H 'Authorization: Bearer tp_live_xxxxxxxxx'
Response
{
  "object": "email",
  "id": "cmg4k2x7a0001l8v9h3q2d5rt"
}

Legacy endpoint

POST /api/v1/emails/send accepts the same body but returns the full internal message record and a different error format. It is kept for existing integrations; use POST /api/v1/emails for new code.