Send your first email
This is the fastest path from zero to a delivered email: create an API key, send through Bird's shared onboarding domain, and watch the result come back. No sending domain to verify, no DNS records to publish. That comes later, when you're ready for production.
1. Create an API key
In the dashboard, go to Developers → API keys and create a key. Keys are scoped to a region and look like bk_us1_... or bk_eu1_...; the region in the prefix tells you which API host to call: https://us1.platform.bird.com or https://eu1.platform.bird.com.

The full key is shown once, at creation time. Copy it somewhere safe, then export it so the step 2 snippets can read it:
Code example
export BIRD_API_KEY="bk_us1_..."2. Send an email
Send from onboarding@messagebird.dev, Bird's shared onboarding domain, available to every workspace with no setup. Address it to delivered@messagebird.dev, a sandbox recipient that always delivers, so the result is deterministic without a real mailbox.
The curl call names the US host; if your key starts with bk_eu1_, call https://eu1.platform.bird.com instead. The SDK reads the region from your key, so it sets no host. Install it with npm install @messagebird/sdk.
import { BirdClient } from "@messagebird/sdk";
const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! });
const msg = await bird.email.send({
from: { email: "onboarding@messagebird.dev", name: "Bird" },
to: ["delivered@messagebird.dev"],
subject: "Hello from Bird",
html: "<p>My first Bird email.</p>",
});
console.log(msg.id, msg.status);from bird import APIError, Bird
with Bird() as client:
try:
message = client.email.send(
from_={"email": "onboarding@messagebird.dev", "name": "Bird"},
to=["delivered@messagebird.dev"],
subject="Hello from Bird",
html="<p>My first Bird email.</p>",
)
print(message.id, message.status)
except APIError as err:
print("send failed:", err)package main
import (
"encoding/json"
"errors"
"log"
"net/http"
"os"
bird "github.com/messagebird/bird-sdk-go"
"github.com/messagebird/bird-sdk-go/option"
)
func main() {
client, err := bird.NewClient(option.WithAPIKey(os.Getenv("BIRD_API_KEY")))
if err != nil {
log.Fatal(err)
}
http.HandleFunc("POST /send", func(w http.ResponseWriter, r *http.Request) {
msg, err := client.Email.Send(r.Context(), bird.EmailSendParams{
From: "onboarding@messagebird.dev",
To: []string{"delivered@messagebird.dev"},
Subject: "Hello from Bird",
HTML: "<p>My first Bird email.</p>",
})
if err != nil {
var apiErr *bird.APIError
if errors.As(err, &apiErr) {
http.Error(w, apiErr.Error(), apiErr.StatusCode)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
_ = json.NewEncoder(w).Encode(msg)
})
log.Fatal(http.ListenAndServe(":3000", nil))
}<?php
// Send your first email. Set BIRD_API_KEY in your environment, then run:
// php examples/quickstart-email.php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use MessageBird\Bird;
$bird = new Bird(getenv('BIRD_API_KEY') ?: '');
$message = $bird->email->send(
from: 'Bird <onboarding@messagebird.dev>',
to: ['delivered@messagebird.dev'],
subject: 'Hello from Bird',
html: '<p>My first Bird email.</p>',
);
echo $message->getId(), ' ', $message->getStatus(), "\n";bird email send \
--from 'Bird <onboarding@messagebird.dev>' \
--to delivered@messagebird.dev \
--subject 'Hello from Bird' \
--html '<p>My first Bird email.</p>'curl -X POST "https://us1.platform.bird.com/v1/email/messages" \
-H "Authorization: Bearer $BIRD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": { "email": "onboarding@messagebird.dev", "name": "Bird" },
"to": ["delivered@messagebird.dev"],
"subject": "Hello from Bird",
"html": "<p>My first Bird email.</p>"
}'Using a different language or framework? The per-SDK quickstarts have the same three steps for every SDK.
3. See the result
The API responds with 202 immediately: Bird has accepted the email and delivers it asynchronously. The *_count fields track your recipients through the delivery states; right now one recipient is accepted and none are delivered.
Code example
{
"id": "em_01ky7ma8y2es1s2akzk53tmjn0",
"status": "accepted",
"category": "marketing",
"from": { "email": "onboarding@messagebird.dev" },
"to": [{ "email": "delivered@messagebird.dev" }],
"subject": "Hello from Bird",
"accepted_count": 1,
"processed_count": 0,
"delivered_count": 0,
"deferred_count": 0,
"bounced_count": 0,
"complained_count": 0,
"rejected_count": 0,
"open_count": 0,
"click_count": 0,
"track_opens": true,
"track_clicks": true,
"created_at": "2026-07-23T13:58:20.866Z"
}Fetch the message by its em_ ID to see where it is. A message moves from accepted through processed to delivered; in the sandbox that takes under a second, so the read usually shows the final state already:
const msg = await bird.email.get("em_abc123");
msg.status; // "accepted" | "processed" | "delivered" | "bounced" | …
msg.delivered_count;
msg.bounced_count;message = client.email.get("em_abc123")
print(message.id, message.status, message.delivered_count)package main
import (
"context"
"fmt"
"log"
"os"
bird "github.com/messagebird/bird-sdk-go"
"github.com/messagebird/bird-sdk-go/option"
)
func main() {
client, err := bird.NewClient(option.WithAPIKey(os.Getenv("BIRD_API_KEY")))
if err != nil {
log.Fatal(err)
}
msg, err := client.Email.Get(context.Background(), "em_abc123")
if err != nil {
log.Fatal(err)
}
fmt.Println(*msg.Status, *msg.DeliveredCount)
}$message = $bird->email->get('eml_01krdgeqcxet5s7t44vh8rt9mg');
echo $message->getStatus();bird email get <message-id>curl -X GET "https://{region}.platform.bird.com/v1/email/messages/{message_id}" \
-H "Authorization: Bearer $TOKEN"The read now shows status: "delivered", delivered_count: 1, and a delivered_at timestamp.
Because you sent to delivered@messagebird.dev, the outcome is guaranteed: the message flows through Bird's real delivery pipeline, including the events and webhooks you'd see in production, but never touches a real mailbox. Want to see a bounce instead? Send to bounce@messagebird.dev. The testing sandbox guide lists every sandbox address and the bounce, complaint, suppression, and deferral outcomes each one simulates.
About the onboarding domain
The shared onboarding@messagebird.dev sender is permanently available, but deliberately limited:
- Apart from the @messagebird.dev sandbox addresses, it only delivers to verified members of your workspace; any other recipient is rejected with a 422.
- Sends are capped at 50 recipients per organization per UTC day, counting every to, cc, and bcc address, sandbox recipients included. Past the cap the API returns a 429.
When you're ready to email real customers, verify your own sending domain and put your own address in from; everything else in the request stays the same.
Next steps
- Per-SDK quickstarts: the same flow in your language and framework.
- Sending domains: verify your own domain for production sending.
- Testing sandbox: every sandbox address and the events it triggers.
- Email API reference: the full request and response schema.