# Send through a relay

Send your newsletter through Resend, Postmark, SES or any provider's API, even where your host blocks SMTP.

Paste this into your agent:

> Write me a relay that sends my Mailpiece newsletter through my mail provider's API. Read mailpiece.dev/docs/relay.md first.

## Settings

Instead of SMTP, Mailpiece can `POST` each newsletter message as JSON to a URL you run. Your small relay turns it into your provider's API call.

The relay is yours: you write it, deploy it anywhere that serves a URL at a public address, and keep the provider's key there. Mailpiece only knows what the newsletter sets on Settings › Outgoing mail: the Relay address, posted to exactly as you set it, and a Token you choose, sent as `Authorization: Bearer <token>`. The token is never shown again; to change it, delete the relay and set it up again.

Mailpiece posts only to a public address. Loopback, private ranges and the cloud metadata address are refused, so a relay on `localhost` beside the app won't work: the delivery waits and the newsletter is told the relay turned it down.

```sh
# a long random string the relay checks
mailpiece settings sending relay update --url https://relay.example.com --token ...
```

## What Mailpiece sends

One `POST` per message, with only the keys that have a value: `from`, `from_name`, `to`, `cc`, `bcc`, `reply_to`, `reply_to_name`, `subject`, `html`, `text`, `list_unsubscribe`, `tags` and `headers`. `from` and `reply_to` are bare addresses, with their names beside them; `to`, `cc` and `bcc` are lists.

`list_unsubscribe` is the one-click unsubscribe link: send it on as the `List-Unsubscribe` header, with `List-Unsubscribe-Post: List-Unsubscribe=One-Click`. `tags` is `["issue"]` on an issue or a welcome email to a subscriber, and `headers` holds the sender's own `X-` headers.

```json
{
  "from": "hello@example.com",
  "from_name": "My Newsletter",
  "to": [ "reader@example.org" ],
  "reply_to": "me@example.com",
  "reply_to_name": "Me",
  "subject": "...",
  "html": "...",
  "text": "...",
  "list_unsubscribe": "<https://...>",
  "tags": [ "issue" ]
}
```

## The Idempotency-Key header

Each issue and welcome email sent to a subscriber carries an `Idempotency-Key` header, the same every time that message is posted. When Mailpiece gets no answer it can trust, it posts the message again with the same key, for hours if it must.

**A relay must honour the key.** Remember each key once its message is sent, for as long as Mailpiece may post it again, and answer a repeat with a `2xx` without sending again. Otherwise a reader can get the same issue twice. Test emails and subscription confirmations carry no key: send them each time.

## What the relay answers

`2xx`: sent, optionally with `{"message_id": "..."}`. `400`, `413` or `422`: refused, never tried again. `401` or `403`: the token was turned down; reported, and tried again later. `429` or `503`: nothing was sent, tried again later.

Anything else, a `502` or `504` included, or no answer at all: Mailpiece cannot tell whether the message went, so it posts it again later under the same key.

## An example for Resend

A `fetch` handler in the style of a Cloudflare Worker, with `RELAY_TOKEN` and `RESEND_API_KEY` set where it runs. It remembers no key itself, and hands each on to Resend, which keeps it for 24 hours: the [Cloudflare Workers](https://mailpiece.dev/integrations/cloudflare-workers) relay adds that. Any AI agent can write one for another provider from this page and the provider's API docs.

```js
const address = (email, name) => name ? `${JSON.stringify(name)} <${email}>` : email

export default {
  async fetch(request, env) {
    if (request.headers.get("Authorization") !== `Bearer ${env.RELAY_TOKEN}`) {
      return new Response("Unauthorized", { status: 401 })
    }
    const key = request.headers.get("Idempotency-Key")
    const message = await request.json()
    const headers = { ...message.headers }
    if (message.list_unsubscribe) {
      headers["List-Unsubscribe"] = message.list_unsubscribe
      headers["List-Unsubscribe-Post"] = "List-Unsubscribe=One-Click"
    }
    const response = await fetch("https://api.resend.com/emails", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${env.RESEND_API_KEY}`,
        "Content-Type": "application/json",
        ...(key && { "Idempotency-Key": key })
      },
      body: JSON.stringify({
        from: address(message.from, message.from_name),
        to: message.to,
        cc: message.cc,
        bcc: message.bcc,
        reply_to: message.reply_to && address(message.reply_to, message.reply_to_name),
        subject: message.subject,
        html: message.html,
        text: message.text,
        headers,
        tags: message.tags?.map(name => ({ name, value: "true" }))
      })
    })
    if (!response.ok) return new Response(await response.text(), { status: response.status })
    const sent = await response.json()
    return Response.json({ message_id: sent.id })
  }
}
```

Still need help? Ask on [GitHub](https://github.com/mailpiece/mailpiece).
