Server-Sent Events (SSE)
Stream real-time AI generation and campaign send progress via Server-Sent Events.
Overview
MisarMail exposes two SSE endpoints for real-time streaming. Both use the standard text/event-stream format and work with the browser's native EventSource API or a manual fetch + ReadableStream pattern.
AI Email Generation Stream
POST api.misar.io/mail/ai/generate-email/stream
Authorization: Bearer msk_your_api_key
Content-Type: application/json
Streams AI-generated email content (subject line, body, or both) as Server-Sent Events.
Request body
{
"type": "full",
"prompt": "Write a re-engagement email for subscribers who haven't opened in 60 days",
"tone": "friendly",
"brandName": "Acme Corp"
}type"subject" | "body" | "full"requiredWhat to generate: just the subject line, just the body, or both.
promptstringrequiredDescription of the email to generate.
tonestringWriting tone. Examples: "professional", "friendly", "urgent".
brandNamestringYour brand name to include in the generated copy.
Response headers
| Header | Value |
|---|---|
Content-Type | text/event-stream |
Cache-Control | no-cache |
X-Accel-Buffering | no |
Event format
data: {"delta":"Here is your","type":"body"}
data: {"delta":" re-engagement email:","type":"body"}
data: {"done":true,"type":"body"}
deltastringIncremental text chunk from the AI model.
type"subject" | "body" | "full"Which part of the email this chunk belongs to.
donebooleantrue on the final event for each type. Followed by data: [DONE].
Browser example
const res = await fetch("https://api.misar.io/mail/ai/generate-email/stream", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer msk_your_api_key",
},
body: JSON.stringify({
type: "full",
prompt: "Re-engagement email for inactive subscribers",
tone: "friendly",
}),
});
let output = "";
const reader = res.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
for (const line of decoder.decode(value).split("\n\n")) {
if (line === "data: [DONE]") return;
if (line.startsWith("data: ")) {
const event = JSON.parse(line.slice(6));
if (event.delta) {
output += event.delta;
emailEditor.textContent = output;
}
}
}
}Campaign Send Progress Stream
GET api.misar.io/mail/campaigns/:id/send-stream
Authorization: Bearer msk_your_api_key
Streams real-time send progress for a campaign while it's being delivered.
Response events
data: {"status":"sending","sent":500,"total":10000,"bounced":12,"delivered":488,"opened":65}
data: {"status":"sent","sent":10000,"total":10000,"bounced":145,"delivered":9855,"opened":2340}
data: [DONE]
status"sending" | "sent" | "cancelled" | "failed"Current campaign status. "sent", "cancelled", and "failed" are terminal.
sentnumberTotal emails sent so far.
totalnumberTotal recipients in the campaign.
bouncednumberEmails that bounced (hard + soft).
deliverednumberConfirmed deliveries.
openednumberTracked opens (pixel).
Stream behavior
- Polls every 2 seconds
- Terminal statuses close the stream immediately
- Times out after 5 minutes and sends
data: [DONE] - Returns
{"error":"not_found"}if campaign ID doesn't belong to your account
Browser example
const campaignId = "your-campaign-uuid";
const res = await fetch(
`https://api.misar.io/mail/campaigns/${campaignId}/send-stream`,
{ headers: { Authorization: "Bearer msk_your_api_key" } }
);
const reader = res.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
for (const line of decoder.decode(value).split("\n\n")) {
if (line.startsWith("data: [DONE]")) return;
if (line.startsWith("data: ")) {
const ev = JSON.parse(line.slice(6));
progressBar.style.width = `${(ev.sent / ev.total) * 100}%`;
if (["sent", "cancelled", "failed"].includes(ev.status)) return;
}
}
}Authentication
Both SSE endpoints require:
- AI generation stream: API key (
msk_) OR an authenticated session cookie - Campaign send stream: API key only (
msk_)
The campaign must belong to the authenticated user's account. Attempting to stream another user's campaign returns 401.