email-broadcasts

Send a broadcast

Sends a draft or scheduled broadcast, either immediately or at scheduled_at. Calling this on a scheduled broadcast replaces its schedule or sends it now if scheduled_at is omitted. Calling it again on a broadcast that is already accepted retries dispatch rather than returning an error. A broadcast that has started sending, or that already reached a final state, returns a conflict.

The audience's contacts at send time become the recipients after suppressions. The request returns 422 if any of these are true:

  • The sender domain is not verified.
  • No audience is selected, or the selected audience no longer exists.
  • The audience has no contact with an email address, or every contact is suppressed for the broadcast's category.
  • No template is set, or the set template has no published version.
  • The organization has used its broadcast allowance for the current billing period, or (for an immediate send) is already at its concurrent-broadcast limit; schedule the send instead to wait for a free slot.

If the audience is empty when a scheduled send starts, the broadcast becomes failed with a readable reason instead of silently dropping it.

post/v1/email/broadcasts/{broadcast_id}/send

Request

  • Base URL: https://us1.platform.bird.com
  • URL: https://us1.platform.bird.com/v1/email/broadcasts/{broadcast_id}/send
  • Auth: one of:
    • HTTP bearer
    • API key in cookie bird_session

Path parameters

broadcast_idstring required

Broadcast identifier. Starts with eb_.

Headers

Idempotency-Keystring

Client-supplied key. On operations supporting request deduplication, a retained response is replayed for duplicate requests with the same key within the idempotency window (3 hours by default). This protection requires a workspace, organization, or staff-account scope. User-only and unscoped unauthenticated operations, streams, and operations with a separate replay contract do not use this response replay.

On a supported operation, if idempotency protection is unavailable before execution, the API returns 503 IdempotencyUnavailable (E01033) without executing this attempt. Retry with backoff using the same key and request. An operation that takes effect before its response is retained can still execute again on retry.

Two distinct 409 errors signal misuse:

  • request_in_progress (E01004): The same key is currently being processed by a concurrent request. Wait briefly and retry. The lock expires within 30 seconds.
  • idempotency_key_reuse (E01005): The same key has already completed against a different request body or method. Generate a new key.

Recommended key format is <event-type>/<entity-id> (for example welcome-user/usr_abc123).

X-Workspace-Idstring

Workspace context for the request. Required for dashboard authentication and master keys. Workspace keys and access tokens already identify their workspace; send that workspace or omit the header. A different one is rejected.

X-Organization-Idstring

Organization context for the request. Required for dashboard authentication. An API key or access token carries its own organization, so send either that organization or no header at all; a different one is rejected.

Request body

scheduled_atstring date-time

When to send the broadcast. It has to be at least 30 seconds and at most 365 days from now. Leave it out to send straight away.

Example request

{
  "scheduled_at": "2026-12-01T09:00:00Z"
}

Response

Broadcast accepted for delivery.

idstring required

Broadcast ID.

display_labelstring

Label for selecting this broadcast on list and single-broadcast reads. With email_management read access, uses the retained subject from the template version resolved when execution starts, then its non-generated name. Draft and scheduled broadcasts use the name. Omitted when no authoritative label is available; clients can show a localized Untitled broadcast fallback. Without that access, uses the canonical broadcast ID. Absent from mutation responses.

audience_idstring
html_bytesinteger

Size of the HTML body this broadcast sends, in bytes, or 0 when its content has no HTML part. Measured on the template version the broadcast sends, using the selected language. Recipient merge values can change its size. Returned on a single broadcast read, and absent from the list and from the broadcast that creating, updating, sending or canceling one returns, none of which measure the content. Absent too when the broadcast has no template or its content can no longer be read.

text_bytesinteger

Size of the plain-text body this broadcast sends, in bytes, or 0 when its content has no plain-text part. Measured, and absent, the same way as html_bytes.

category'marketing' | 'transactional' required

What kind of email this is, which decides how suppressions apply to it. A marketing broadcast is held back from every suppressed address. A transactional one still goes to addresses suppressed for a complaint or an unsubscribe, because those suppressions are about marketing mail.

ip_pool_idstring

The IP pool this broadcast sends from, or ipp_shared when it sends through the shared pool. Absent when it sends on your organization's default pool.

headersobject

Any custom email headers set on the broadcast. Returned on a single broadcast read and on the broadcast that creating, updating, sending or canceling one returns, and absent from the list. The unsubscribe headers we add ourselves are not included.

status'draft' | 'scheduled' | 'accepted' | 'sending' | 'sent' | 'canceling' | 'canceled' | 'failed' required

Where the broadcast itself has got to. This is separate from what happened to individual recipients, which the broadcast's own sent_count, delivered_count, bounced_count and complained_count tell you. Those four are fields on the broadcast, not on everything that carries this status, and reading the broadcast's recipients or its events gives the same outcomes one recipient at a time.

  • draft: Created, and not sent or scheduled yet.
  • scheduled: Due to send at scheduled_at. You can still edit it, and you can still change the time, right up until sending starts.
  • accepted: Taken for immediate sending. Nothing has gone out yet.
  • sending: On its way. Some recipients have been sent to and some have not.
  • sent: Every recipient has been sent to.
  • canceling: A cancellation is under way and the remaining sends are stopping.
  • canceled: The cancellation finished. Anything already on its way to a recipient when you canceled cannot be pulled back.
  • failed: The broadcast could not be sent. Reading the broadcast gives failure_reason, which says why. If it had already started sending, the recipients it reached keep their delivery status and carry on producing events.

A draft is deleted rather than canceled, because it was never sent.

failure_reason'empty_audience' | 'audience_unavailable' | 'content_invalid' | 'insufficient_funds' | 'quota_exceeded' | 'internal_error' | 'null' nullable

Why the broadcast failed. Set when status is failed, and null the rest of the time.

  • empty_audience: There was nobody to send to. Either the audience has no members, or every address in it is suppressed.
  • audience_unavailable: The audience no longer exists, so there was nothing to resolve.
  • content_invalid: The broadcast could not be set up to send. failure_detail explains what went wrong. It is one of these:
    • The broadcast has no template, or the template it uses no longer exists. Choose an existing template and send the broadcast again.
    • The template has no published version, or its published version has no subject and no body. Publish the template, or add content and publish it.
    • The template uses a loop or reads a value that a broadcast cannot provide. Remove it, or use a contact property instead, then publish the template again.
    • The template requires a language, but the broadcast has not selected one. Set template.language to one of the template's languages and send the broadcast again.
    • The selected language is not available on the published template version. Choose one of that version's languages, or publish a version that includes the selected language.
    • The sending domain is no longer verified. Verify the domain again.
    • The configured IP pool has no usable IP address.
    • We could not hand the prepared message to the delivery system. This is a problem on our side.
  • insufficient_funds: There was not enough in the workspace balance to pay for the send.
  • quota_exceeded: The send would have gone past your organization's daily or monthly email allowance, whichever runs out first. This can happen when the broadcast is being prepared, or partway through sending if the remaining recipients no longer fit. failure_detail gives you the count and the limit.
  • internal_error: Something went wrong on our side. Retry, and open a support ticket if it keeps happening.
failure_detailstring nullable

A sentence explaining the failure in more detail than failure_reason does, and null when the broadcast has not failed. Show it to the person using your app. Do not write code that reads it, because the wording can change. Branch on failure_reason instead.

recipient_countinteger required

Number of recipients after suppressed addresses are removed from the audience. This is 0 until sending starts and the audience becomes a recipient list.

sent_countinteger

How many recipients the broadcast has been sent to, counting every recipient whose status is processed or later. The number rises while the broadcast is sending and stops changing once the broadcast has finished. These counters are exact. The email stats endpoints report on the same sending but are approximate, so use these numbers when you need the precise count. Absent when the broadcast comes back from creating, updating, sending or canceling it, none of which read the counters. List and single-broadcast reads return 0 when no delivery events are recorded. When counters are unavailable, those reads still return 200 and omit the counters. Read the broadcast again for the numbers.

delivered_countinteger

How many recipients' messages were accepted by their mail server. Absent when sent_count is.

bounced_countinteger

How many recipients the message could not be delivered to at all. Absent when sent_count is.

complained_countinteger

How many recipients marked the message as spam. Absent when sent_count is.

open_countinteger

How many times the message was opened, added up across every recipient. One recipient opening it twice counts twice. Absent when sent_count is.

click_countinteger

How many times a link in the message was clicked, added up across every recipient. One recipient clicking twice counts twice. Absent when sent_count is.

sending_ipsstring[]

The IP addresses this broadcast's messages went out from, up to 100 of them. A broadcast is spread across every address in its pool, so more than one can appear. The receiving mail systems name the address when they deliver, bounce or defer a message, so this stays absent until the first of those comes back. Returned on a single broadcast read, and absent from the list and from the broadcast that creating, updating, sending or canceling one returns, none of which read them. For delivery and latency broken down per address, read the sending-IP stats.

unique_opens_non_prefetchedinteger

How many distinct recipients opened the message at least once, excluding opens auto-fetched by inbox privacy features (such as Apple Mail Privacy Protection and the Gmail image proxy). A recipient who opened several times, or whose inbox prefetched the message, counts once. Absent when sent_count is.

unique_clicksinteger

How many distinct recipients clicked a link in the message at least once. A recipient who clicked several times counts once. Absent when sent_count is.

out_of_band_bouncesinteger

How many recipients bounced after the message had already been accepted for delivery. A recipient who bounced this way more than once counts once. Absent when sent_count is.

delivered_recipientsinteger

How many distinct recipients a delivery landed for. This is the denominator to measure unique_opens_non_prefetched, unique_clicks and complained_count against. It differs from delivered_count, which reports how many recipients are currently in the delivered state: a recipient who was delivered to and then complained moves to complained_count and leaves delivered_count, but stays here, because the message did reach them. Absent when sent_count is.

metadataobject

Any JSON you want to keep on the broadcast. We store it and hand it back in webhook payloads, and that is all it does. If you want to search or filter by it, use tags instead.

track_opensboolean required

Whether opens are tracked for this broadcast.

track_clicksboolean required

Whether link clicks are tracked for this broadcast.

created_atstring date-time required

When the broadcast was created.

scheduled_atstring date-time

When the broadcast is due to send, and absent when it is not scheduled.

started_atstring date-time

When the broadcast started sending. Absent until then. Compare with sent_at, which is when the broadcast finished sending.

sent_atstring date-time nullable required

When the last recipient was sent to and the broadcast became sent. Null until then. Compare with started_at, which is when the broadcast started sending.

canceled_atstring date-time

When the broadcast was canceled, and absent if it never was. This is when cancellation was requested, so it is set as soon as the status is canceling and does not move while the remaining sends stop and the status becomes canceled.

Example response

{
  "id": "eb_01krdgeqcxet5s7t44vh8rt9mg",
  "display_label": "Weekly news",
  "from": {
    "email": "jane@acme.com",
    "name": "Jane Doe"
  },
  "audience_id": "adn_01krdgeqcxet5s7t44vh8rt9mg",
  "template": {
    "id": "emt_01krdgeqcxet5s7t44vh8rt9mg",
    "language": "pt-BR",
    "version_id": "emv_01krdgeqcxet5s7t44vh8rt9mg"
  },
  "html_bytes": 18432,
  "text_bytes": 2104,
  "reply_to": [
    {
      "email": "jane@acme.com",
      "name": "Jane Doe"
    }
  ],
  "recipient_count": 4820,
  "sent_count": 4820,
  "delivered_count": 4712,
  "bounced_count": 96,
  "complained_count": 12,
  "open_count": 3104,
  "click_count": 812,
  "sending_ips": [
    "198.51.100.42"
  ],
  "unique_opens_non_prefetched": 2140,
  "unique_clicks": 693,
  "out_of_band_bounces": 14,
  "delivered_recipients": 4724,
  "tags": [
    {
      "name": "category",
      "value": "welcome"
    }
  ],
  "track_opens": true,
  "track_clicks": true,
  "created_at": "2026-09-01T09:14:02.418Z",
  "scheduled_at": "2026-09-02T08:00:00Z",
  "started_at": "2026-09-02T08:00:03.771Z",
  "sent_at": "2026-09-02T08:11:47.902Z",
  "failure_parameters": {
    "code": "parameter_required",
    "phase": "validation",
    "pointer": "/template/parameters/first_name",
    "observed_version_id": "emv_01krdgeqcxet5s7t44vh8rt9mg",
    "problems": [
      {
        "code": "parameter_required",
        "phase": "validation",
        "severity": "error",
        "pointer": "/template/parameters/first_name",
        "site": "subject",
        "budget": "output_bytes",
        "contact_id": "con_01krdgeqcxet5s7t44vh8rt9mg"
      }
    ]
  }
}

Changes

No recorded changes to this operation across all 2 revisions of this API.