Claim your first agent mailbox
This is the happy path from zero to a two-way conversation: claim an inbox on inbox.ai, receive a message into a thread, read it, and reply. No domain to verify and no mail server to run.
1. Create an API key
In the dashboard, go to Developers → API keys and create a key — under the Email group, enable the mailbox and mailbox_management scopes. Keys look like bk_us1_... or bk_eu1_...; the region in the prefix picks the API host.
Ejemplo de código
export BIRD_API_KEY="bk_us1_..."2. Claim a mailbox
Create a mailbox on the shared inbox.ai domain. Omit the local part and Bird mints a free, collision-free address; receive_policy: open accepts any authenticated mail so you can see the loop work.
const mailbox = await bird.email.mailboxes.create({ display_name: "Support" });
console.log(mailbox.address); // "abc123@inbox.ai"mailbox = client.email.mailboxes.create(display_name="Acme Support")
print(mailbox.id)mailbox, err := client.Email.Mailboxes.Create(context.Background(), bird.EmailMailboxesCreateParams{
DisplayName: "Support",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(mailbox.Id, *mailbox.Address)$mailbox = $bird->email->mailboxes->create(
(new MailboxCreate())
->setDisplayName('Acme Support'),
);
echo $mailbox->getId(), ' ', $mailbox->getAddress();bird email mailboxes create \
--display-name 'My Agent' \
--receive-policy opencurl -X POST "https://us1.platform.bird.com/v1/email/mailboxes" \
-H "Authorization: Bearer $BIRD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"display_name": "My Agent",
"receive_policy": "open"
}'The response carries the mailbox id and the address claimed for you. Send an email to that address from any mail client to give the next step something to read.
3. Read the thread
Inbound mail becomes a thread on the mailbox. List threads, then read the messages in the first one.

for await (const thread of bird.email.threads.list({ mailbox_id: "mbx_01abc" })) {
console.log(thread.id, thread.subject);
}for thread in client.email.threads.list(mailbox_id="mbx_01krdgeqcxet5s7t44vh8rt9mg"):
print(thread.id, thread.subject)for thread, err := range client.Email.Threads.List(context.Background(), bird.EmailThreadsListParams{}) {
if err != nil {
log.Fatal(err)
}
fmt.Println(thread.Id)
}foreach ($bird->email->threads->list(['mailbox_id' => 'mbx_01krdgeqcxet5s7t44vh8rt9mg']) as $thread) {
echo $thread->getId(), ' ', $thread->getSubject(), "\n";
}bird email threads listcurl -X GET "https://{region}.platform.bird.com/v1/email/threads" \
-H "Authorization: Bearer $TOKEN" \
--url-query "label=urgent" \
--url-query "participant=billing@acme.com" \
--url-query "subject=quarterly invoice" \
--url-query "limit=25"Then read that thread's messages:
for await (const msg of bird.email.threads.messages.list("thr_01abc")) {
console.log(msg.id, msg.direction);
}for message in client.email.threads.messages.list("thr_01krdgeqcxet5s7t44vh8rt9mg"):
print(message.id, message.subject)for msg, err := range client.Email.Threads.Messages.List(context.Background(), "thr_123", bird.EmailThreadsMessagesListParams{}) {
if err != nil {
log.Fatal(err)
}
fmt.Println(msg.Id, msg.Direction)
}foreach ($bird->email->threads->messages->list('thr_01krdgeqcxet5s7t44vh8rt9mg') as $message) {
echo $message->getId(), ' ', $message->getDirection(), "\n";
}bird email threads messages list <thread-id>curl -X GET "https://{region}.platform.bird.com/v1/email/threads/{thread_id}/messages" \
-H "Authorization: Bearer $TOKEN" \
--url-query "label=unread" \
--url-query "limit=25"Each message carries its direction (inbound) and its id (a received message is prefixed rem_). Add include=extracted_text to inline the quote-stripped body: the new content, without the quoted history an agent would otherwise have to strip itself.
Rather than poll, subscribe to the email_mailbox.message_received webhook to be told the moment mail lands — see the events reference. Or hold open the mailbox's event stream: GET /v1/email/mailboxes/{id}/events pushes a notification the moment anything arrives.
4. Reply
Reply to the received message. The reply folds into the same thread and sends from your mailbox's address:
const reply = await bird.email.threads.messages.reply("thr_01abc", "rem_01xyz", {
text: "Thanks for reaching out!",
});
console.log(reply.id);reply = client.email.threads.messages.reply(
"thr_01krdgeqcxet5s7t44vh8rt9mg", "msg_01krdgeqcxet5s7t44vh8rt9mg",
text="Thanks for reaching out!",
)
print(reply.id)reply, err := client.Email.Threads.Messages.Reply(context.Background(), "thr_123", "rem_456", bird.EmailThreadsMessagesReplyParams{
Text: "Thanks for reaching out!",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(reply.Id)$reply = $bird->email->threads->messages->reply(
'thr_01krdgeqcxet5s7t44vh8rt9mg',
'ems_01krdgeqcxet5s7t44vh8rt9mg',
(new EmailThreadMessageReplyRequest())
->setText('Thanks — looking into it now.')
->setReplyAll(true),
);
echo $reply->getId();bird email threads messages reply <thread-id> <message-id> \
--text 'Thanks, confirming we received your request.'curl -X POST "https://{region}.platform.bird.com/v1/email/threads/{thread_id}/messages/{message_id}/reply" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"text": "Thanks, confirming we received your request."
}'That is the whole loop: claim, receive, read, reply. To start a conversation instead of answering one, compose a new message on the mailbox (POST /v1/email/mailboxes/{id}/messages), which opens a fresh thread.
Next steps
- Agent mailboxes — how threads, receive rules, sending, and retention work.
- MCP server — drive the same loop from an AI agent via the MCP server, no HTTP glue.
- CLI — bird email mailboxes and bird email threads for the terminal.
- SDK quickstarts — Go, Python, and TypeScript SDK walkthroughs.