All posts

Resend API: send and receive email, with code from the docs

Send email with the Resend API, receive it by webhook, reply in thread, and how Resend Inboxes compare with ACK for agent email.

The Resend API sends an email with one authenticated POST to https://api.resend.com/emails, carrying from, to and subject plus an HTML or text body. It can also receive mail: Resend posts an email.received webhook to your app, and you fetch the message body with a second API call.

This guide walks through both, using code copied from Resend's documentation. Then it covers Resend Inboxes, its hosted mailbox for things like AI agent email, and where ACK differs.

How do you authenticate with the Resend API?

Every request goes to https://api.resend.com with your API key as a bearer token: Authorization: Bearer re_xxxxxxxxx. Resend's API introduction gives the default rate limit as 10 requests per second per team, and a 429 when you go over it.

How do you send an email with the Resend API?

Call POST /emails. The send email reference lists from, to and subject as required, and to takes up to 50 addresses. This is Resend's own cURL example:

Shell
curl -X POST 'https://api.resend.com/emails' \
     -H 'Authorization: Bearer re_xxxxxxxxx' \
     -H 'Content-Type: application/json' \
     -d $'{
  "from": "Acme <onboarding@resend.dev>",
  "to": ["delivered@resend.dev"],
  "subject": "hello world",
  "html": "<p>it works!</p>"
}'

The response is the new email's ID:

JSON
{
  "id": "49a3999c-0ce1-4ea6-ab68-afcd6dc2e794"
}

With the Node SDK, the same send looks like this:

TypeScript
import { Resend } from 'resend';

const resend = new Resend('re_xxxxxxxxx');

const { data, error } = await resend.emails.send({
  from: 'Acme <onboarding@resend.dev>',
  to: ['delivered@resend.dev'],
  subject: 'hello world',
  html: '<p>it works!</p>',
});

If your code might retry a send, the same reference documents an Idempotency-Key header. Keys expire after 24 hours and can be up to 256 characters.

How do you receive email with Resend?

Resend receives mail at a domain you've verified, after you add an MX record, or at a Resend-managed <id>.resend.app subdomain on your account. Any username at that address works, so test@<id>.resend.app arrives without any setup.

To hear about new mail, you create a webhook for the email.received event, in the dashboard or through the Webhooks API. A delivery looks like this:

JSON
{
  "type": "email.received",
  "created_at": "2026-02-22T23:41:12.126Z",
  "data": {
    "email_id": "56761188-7520-42d8-8898-ff6fc54ce618",
    "created_at": "2026-02-22T23:41:11.894Z",
    "from": "onboarding@resend.dev",
    "to": ["delivered@resend.dev"],
    "bcc": [],
    "cc": [],
    "received_for": ["forwarded@example.com"],
    "message_id": "<111-222-333@email.example.com>",
    "subject": "Sending this example",
    "attachments": [
      {
        "id": "2a0c9ce0-3112-4728-976e-47ddcd16a318",
        "filename": "avatar.png",
        "content_type": "image/png",
        "content_disposition": "inline",
        "content_id": "img001"
      }
    ]
  }
}

There's no body in it. Resend's docs say webhooks "do not include the email body, headers, or attachments, only their metadata", which keeps payloads small for serverless functions with request size limits. You fetch the content with the email_id. Resend's Get Email Content guide does it in a Next.js route:

TypeScript
// app/api/events/route.ts
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { Resend } from 'resend';

const resend = new Resend('re_xxxxxxxxx');

export const POST = async (request: NextRequest) => {
  const event = await request.json();

  if (event.type === 'email.received') {
    const { data: email } = await resend.emails.receiving.get(
      event.data.email_id,
    );

    if (!email) {
      return NextResponse.json({ error: 'Email not found' }, { status: 404 });
    }

    console.log(email.html);
    console.log(email.text);
    console.log(email.headers);

    return NextResponse.json(email);
  }

  return NextResponse.json({});
};

Over plain HTTP it's a GET on the received email endpoint:

Shell
curl -X GET 'https://api.resend.com/emails/receiving/4ef9a417-02e9-4d39-ad75-9611e0fcc33c' \
     -H 'Authorization: Bearer re_xxxxxxxxx'

The response carries html, text, headers, the attachment list and an authentication object with the SPF, DKIM and DMARC results.

How do you verify a Resend webhook?

Resend signs deliveries with svix-id, svix-timestamp and svix-signature headers. The verification guide offers resend.webhooks.verify() in the SDK, or the Svix library directly:

JavaScript
import { Webhook } from 'svix';

const secret = process.env.WEBHOOK_SECRET;

// These were all sent from the server
const headers = {
  'svix-id': 'msg_p5jXN8AQM9LWM0D4loKWxJek',
  'svix-timestamp': '1614265330',
  'svix-signature': 'v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=',
};
const payload = '{"test": 2432232314}';

const wh = new Webhook(secret);
// Throws on error, returns the verified content on success
wh.verify(payload, headers);

Pass the raw request body. Resend's docs warn that parsing the JSON and stringifying it again breaks the signature. Failed deliveries retry on a schedule from immediately up to 10 hours apart, and an endpoint that keeps failing is disabled, with an email to your team.

How do you reply in the same thread?

You set the headers yourself. Resend's threaded replies guide puts the received message_id in In-Reply-To and starts the subject with Re::

TypeScript
import { Resend, type EmailReceivedEvent } from 'resend';

const resend = new Resend('re_xxxxxxxxx');

export async function replyToEmail(event: EmailReceivedEvent) {
  const messageId = event.data.message_id;

  const { data, error } = await resend.emails.send({
    from: 'Acme <onboarding@resend.dev>',
    to: ['delivered@resend.dev'],
    subject: `Re: ${event.data.subject}`,
    html: '<p>Thanks for your email!</p>',
    headers: {
      'In-Reply-To': messageId,
    },
  });
}

For a longer conversation, the same guide says to add earlier message_ids to the References header too. If you'd rather not track threads yourself, that's what Resend Inboxes are for.

What are Resend Inboxes?

Resend also hosts inboxes on your verified domains. Its docs list AI agent email first among the uses. An inbox stores and threads conversations for one address, with labels, drafts, assignment to teammates and inbox, sent, archive, trash and spam folders. It's available through the dashboard, the API, the CLI and a remote MCP server at https://mcp.resend.com/mcp. Creating one, from the create inboxes guide:

TypeScript
import { Resend } from 'resend';

const resend = new Resend('re_xxxxxxxxx');

const { data, error } = await resend.inboxes.create({
  emailAddress: 'support@example.com',
  name: 'Customer Support',
  fromName: 'Ada from Support',
});

Each inbox also has a built-in agent that triages new mail from your instructions. Resend's docs say it "never sends email on its own": its replies and forwards are saved as drafts for someone on your team to send.

On pricing, every Resend plan includes 3 inboxes, paid plans can add more at $1 per inbox per month, and the Free plan stops at 3. Mail sent or received through an inbox counts towards the plan's transactional email quota.

If you already send through Resend and want a mailbox on your own domain, that's a strong option.

Where does ACK differ?

ACK is built around inboxes for agents, and the differences are in the details your own agent code touches.

Keys that can't send. A Resend key is either full_access or sending_access, and a sending key can be restricted to one domain. For inboxes, the create API key reference says sending_access can reply, forward, create drafts and send drafts. An ACK key can be scoped to a single inbox and given only draft permissions, so an agent you run yourself can write drafts and gets a 403 if it tries to send:

Shell
curl https://api.ack.now/v0/inboxes/support-agent@trial.ack.now\
/api-keys \
  -H "Authorization: Bearer $ACK_API_KEY" \
  --json '{"name": "support-agent, drafts only",
           "permissions": {"inbox_read": true, "message_read": true,
                           "draft_read": true, "draft_create": true,
                           "draft_update": true}}'

Approve your agent's email before it goes out walks through it.

The body arrives with the webhook. Resend's inbox.email.received event carries the email "without html or text", so you fetch the body next. ACK's message.received carries the whole message, with text and HTML bodies and a list of attachments, plus its thread. Mail that fails spam checks comes as message.received.spam, and mail that fails DMARC, or both SPF and DKIM, as message.received.unauthenticated, so an endpoint subscribed only to message.received never sees either. How to receive email with webhooks has the details.

No domain needed to start. Resend inboxes need a verified domain with receiving turned on, or forwarding from an existing address. An ACK inbox can use a shared ACK address, so one request gives your agent an address that receives mail straight away:

Shell
curl https://api.ack.now/v0/inboxes \
  -H "Authorization: Bearer $ACK_API_KEY" \
  --json '{"username": "support-agent"}'

# {"inbox_id": "support-agent@trial.ack.now", …}

Old mail comes with you. ACK imports an mbox file, a zip of .eml files or an IMAP mailbox, threads it like live mail and fires no webhooks, so the agent doesn't answer last year's email.

Pricing works differently. ACK is $19 per organisation per month with 10 inboxes, then $2 a month per inbox. Each inbox can send 1,000 emails a month at full speed, after which sending slows down and there's never an overage charge. At three inboxes on a Resend plan you already pay for, Resend costs less. ACK's MCP server at https://mcp.ack.now/mcp is on the same plan and gives an agent 39 tools.

FAQ

Does the Resend webhook include the email body?

No. Resend's email.received webhook carries metadata only. Call the received email endpoint with the email_id to get the HTML, text and headers.

Does Resend have inboxes for AI agents?

Yes. Resend Inboxes host threaded mailboxes on your verified domain, with drafts, labels and a built-in triage agent that saves its replies as drafts. Every plan includes 3.

Can I use Resend and ACK together?

Yes. They're separate services with separate keys. You could keep your app's notifications on Resend and give each agent an ACK inbox, on its own address or domain.

Try it

ACK has a 14-day trial. You'll need a card to start, trial inboxes use a shared ACK address, and you can send 100 emails in total while receiving is unlimited. The developer quickstart gets an inbox answering mail, and how to give an AI agent its own email address walks through it step by step.