> ## Documentation Index
> Fetch the complete documentation index at: https://docs.alpacarelay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Generate an email

> Call POST /api/v1/emails and read the generated email off a Server-Sent Events stream.

One request, one email. The call returns a **Server-Sent Events** stream rather than a single JSON body, because generation takes a minute or three and an open stream is what keeps proxies from dropping the connection while it works.

```
POST https://app.alpacarelay.com/api/v1/emails
```

## Request

```json theme={null}
{
  "prompt": "Spring launch announcement for our coffee subscription",
  "branding_url": "https://example.com",
  "format": "both"
}
```

| Field | Type | Required | Notes |
| - | - | - | - |
| `prompt` | string | Yes | What the email should be about. Max **4000** characters |
| `branding_url` | string (URI) | No | A site to pull colors and style from |
| `format` | `html` · `mjml` · `both` | No | Defaults to `html` |

> [!TIP]
>
> `prompt` carries the result. Say who it's going to, what you want them to do, and the tone — the same detail you'd give a colleague. `branding_url` then handles the look without you describing it.

## Response

Authentication, limits, and quota are checked **before** the stream opens, so a rejected request is an ordinary JSON error with a 4xx status. Once accepted you get `Content-Type: text/event-stream` and these events:

| Event | Data | Meaning |
| - | - | - |
| `accepted` | `{ request_id }` | Validated, generation started |
| `ping` | `{}` | Keep-alive, roughly every 15 seconds — ignore it |
| `result` | the generated email | Success. Terminal |
| `error` | `{ code, message }` | Failure. Terminal |

Exactly one `result` **or** one `error` arrives, then the stream closes.

### The result event

```json theme={null}
{
  "email_id": "em_3kf9d0a1b2c3",
  "email_name": "Spring Launch Announcement",
  "subject": "Your spring launch is here 🌱",
  "preheader": "Everything new, in one place.",
  "framework": "AIDA",
  "email_type": "marketing",
  "format": "html",
  "html": "<!doctype html><html>…</html>",
  "score": {
    "composite": 87,
    "structural": 90,
    "content": 84,
    "visual": null,
    "branding": null,
    "dimensions": { "coverage": 92 }
  },
  "subject_variants": [
    { "subject": "Your spring launch is here 🌱", "preheader": "Everything new, in one place." },
    {
      "subject": "Spring is here — see what's new",
      "preheader": "Fresh arrivals, just dropped.",
      "predicted_open_rate_bucket": "high",
      "recommended_split_percentage": 50
    }
  ],
  "dark_mode": { "applied": true },
  "preview_url": "https://cdn.alpacarelay.com/previews/em_3kf9d0a1b2c3.png"
}
```

A few fields worth knowing about:

> [accordion] html and mjml depend on format
>
> `html` is present when `format` is `html` or `both`; `mjml` when it's `mjml` or `both`. Ask for `both` if you want to store the editable source alongside what you send.

> [accordion] Reading score
>
> `composite` is the overall quality score, 0–100, and `structural` and `content` are its two main parts. `dimensions` holds each measured dimension by name, and which ones appear can vary. `visual` and `branding` are always `null` today; they stay in the shape for compatibility. Scoring also runs on a timeout, and if it doesn't finish, `score` itself is `null`. A missing score isn't a bad score, so never treat `null` as `0`.

> [accordion] subject\_variants are for A/B testing
>
> The first entry matches the top-level `subject` and `preheader`. The others are alternatives, some carrying a predicted open-rate bucket and a suggested split.

> [accordion] The email is saved in your workspace
>
> `email_id` refers to a real email in AlpacaRelay, not just a response identifier. You can open and keep editing it in the app.

## Try it with curl

```bash theme={null}
curl -N https://app.alpacarelay.com/api/v1/emails \
  -H "Authorization: Bearer $ALPACARELAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"A welcome email for new fitness-app subscribers"}'
```

> [!WARNING]
>
> Use `-N` (`--no-buffer`). Without it curl buffers the response and you'll sit watching nothing until generation finishes.

Output looks like this:

```text theme={null}
event: accepted
data: {"request_id":"5f1c0e9a-…"}

event: ping
data: {}

event: result
data: {"email_id":"em_3kf9d0a1b2c3","subject":"…","html":"<!doctype html>…"}
```

## Node example

The stream is plain text, so no SSE library is needed — but you do have to buffer, because an event can arrive split across chunks:

```js theme={null}
const res = await fetch("https://app.alpacarelay.com/api/v1/emails", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ALPACARELAY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ prompt: "A welcome email for new subscribers" }),
});

// Rejections happen before the stream opens, so they are ordinary JSON.
if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message}`);
}

const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += value;

  // Frames are separated by a blank line; keep any partial tail in the buffer.
  const frames = buffer.split("\n\n");
  buffer = frames.pop() ?? "";

  for (const frame of frames) {
    const event = frame.match(/^event: (.+)$/m)?.[1];
    const data = JSON.parse(frame.match(/^data: (.+)$/m)?.[1] ?? "{}");

    if (event === "result") return data;          // the generated email
    if (event === "error") throw new Error(data.code);
    // "accepted" and "ping" need no handling
  }
}
```

> [!NOTE]
>
> Set a client timeout of at least **six minutes**. Generation usually takes one to three, but the server's own backstop fires later. Disconnecting doesn't stop the generation: it still finishes, is charged, and is saved in your workspace. You just don't receive it on the stream.

## What to expect

* **Generation takes roughly 1–3 minutes.** Treat it as a background job, not a request you block a user on.
* **A credit is taken when generation starts**, and refunded if the generation itself fails. See [Credits and limits](https://docs.alpacarelay.com/developers/limits.md#credits).
* **Three concurrent generations per workspace, and 60 calls a minute per key.** Going over either one is rejected with `rate_limited` before the stream opens.

## Next

> [card] [Errors](https://docs.alpacarelay.com/developers/errors.md)
>
> Every code the endpoint can return, and what to do about each.

> [card] [Credits and limits](https://docs.alpacarelay.com/developers/limits.md)
>
> What a generation costs and how much you can run at once.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.