Newsletters
Newsletter endpoints let you automate draft creation and scheduling while keeping the same sending safeguards as the dashboard: verified newsletter domains, audience opt-outs, suppressions, pacing, one-click unsubscribe, test sends, and preflight checks.
Last updated
Create a draft
POST /v1/newslettersScope: newsletters:write. Body:
{
"audience_id": "aud_abc123",
"newsletter_domain_id": "nwsdom_abc123",
"name": "July changelog",
"subject": "What shipped in July",
"preview_text": "A quick product update",
"blocks": [
{ "type": "heading", "level": 1, "text": "July updates" },
{ "type": "callout", "title": "Quick note", "body": "..." },
{
"type": "video",
"video_url": "https://cdn.example.com/demo.mp4",
"thumbnail_url": "https://cdn.example.com/demo.jpg",
"label": "Watch demo"
}
],
"send_window_hours": 6,
"archive_visibility": "private"
}Provide blocks, body_html, or body_text. Blocks are the preferred API and SDK format for agents because Shipmail renders them to email-safe HTML and text. The response includes the stored blocks plus derived body_html and body_text with status draft.
Body resolution on create: blocks win, then body_html (stored as one custom_html block), then body_text (paragraph blocks). When you also send body_text alongside blocks or HTML, it sets the plain-text override instead of deriving text from blocks.
In public API requests, prose fields inside blocks are plain text. This includes paragraph.body, callout.body, quote.body, list items, and column bodies. Use newlines for paragraph breaks. Use body_html or a custom_html block only when you need HTML.
Use GET /v1/newsletter-domains to find available newsletter_domain_id values before creating a draft.
Simple semantic HTML gets email-safe default spacing and typography for headings, paragraphs, lists, quotes, tables, and images. Add inline styles when you want a fully custom template.
Do not use raw <video> or <iframe> embeds in send HTML. For broad email-client support, link a hosted video from an image thumbnail with descriptive alt text.
Block types
Each block is a JSON object with a type field. Up to 200 blocks per draft. See also newsletter authoring for dashboard editing details.
heading { type, text, level? } level: 1 | 2 | 3
paragraph { type, body }
list { type, ordered?, items }
callout { type, title?, body, variant? } variant: accent | info | warning | success
button { type, label, url, align? } align: left | center | right
image { type, url, alt, link_url?, caption?, caption_url? }
video { type, video_url, thumbnail_url, label }
columns { type, ratio?, left, right } ratio: 50-50 | 33-67 | 67-33
each column: title?, body, image_url?, image_alt?, cta_label?, cta_url?
link_list { type, title?, items: [{ label, url, description? }] }
quote { type, body, cite? }
divider { type }
spacer { type, height? } height: 8-80
code { type, code }
custom_html { type, html }GET /v1/newsletter-assets
POST /v1/newsletter-assets?filename=demo.mp4
POST /v1/newsletter-assets
{
"url": "https://cdn.shipmail.to/newsletter-images/org_123/demo.png",
"filename": "demo.png"
}Scope: newsletters:read for list and newsletters:write for upload. Upload raw JPEG, PNG, WebP, GIF, MP4, MOV, or WebM bytes. Video uploads generate a thumbnail and return asset metadata plus current storage usage: url, image_url or video_url, thumbnail URL, duration, dimensions, checksum, content type, and byte size. Repeated uploads dedupe by checksum. To keep existing Shipmail CDN media visible in the media library, post JSON with url and, for videos, thumbnail_url. URLs must belong to the same workspace newsletter media namespace. New media counts toward workspace newsletter storage: 2 GB on Solo, 10 GB on Pro, and 50 GB on Team.
Read and update drafts
GET /v1/newsletters
GET /v1/newsletters/:id
PATCH /v1/newsletters/:idList supports cursor and limit pagination. Patch accepts the same fields as create, all optional, and only updates editable drafts. Responses include stored blocks when the draft was authored with blocks.
Body resolution on patch: explicit blocks win, then body_html, then body_text when the draft has no blocks yet, then the existing blocks. Sending only body_text updates the plain-text override when blocks already exist.
PATCH /v1/newsletters/nws_abc123
{
"blocks": [
{ "type": "heading", "level": 1, "text": "August updates" },
{ "type": "paragraph", "body": "A quick product update." }
]
}Test and preflight
POST /v1/newsletters/:id/test-send
POST /v1/newsletters/:id/preflight
POST /v1/newsletters/:id/previewTest sends accept { recipient_email } and can only go to people who can log in to the account. Preflight returns structured readiness items for domain verification, Reply-To mailbox, audience consent, body, links, images, footer identity, test-send history, and account restrictions. If a guardrail fails, test-send returns a validation error with the failed preflight items in details.
Preflight includes url_breakdown: href count, image src count, video href count, text URL count, unique URL count, maximum unique URLs, and offending URLs. Images and video thumbnails count toward the 20 unique URL limit. Preview returns sanitized send HTML, public archive HTML, text, warnings, and the same URL breakdown.
Schedule and lifecycle
POST /v1/newsletters/:id/schedule
POST /v1/newsletters/:id/cancel
POST /v1/newsletters/:id/resumeSchedule accepts { scheduled_at } as an ISO 8601 timestamp. It must be in the future and the newsletter must pass preflight. Scheduling materializes recipients from the audience, skipping opt-outs and suppressions, then sends at the stored hourly pace. Failed preflight guardrails return 422 validation_error with one detail per failed preflight item.
Changelog example
const newsletter = await shipmail.newsletters.createFromChangelog({
audience_id: "aud_abc123",
newsletter_domain_id: "nwsdom_abc123",
name: "July changelog",
subject: "What shipped in July",
preview_text: "A quick product update",
tone: "friendly",
entries: [
{
title: "Reusable media library",
body: "Search recent newsletter images and videos, then insert them again.",
media: [{ kind: "image", url: "https://cdn.example.com/media-picker.png", alt: "Media picker" }],
},
],
final_cta: { label: "Read the changelog", url: "https://example.com/changelog" },
});
await shipmail.newsletters.preview(newsletter.id);
await shipmail.newsletters.sendTest(newsletter.id, {
recipient_email: "owner@example.com",
});
await shipmail.newsletters.preflight(newsletter.id);
await shipmail.newsletters.schedule(newsletter.id, {
scheduled_at: "2026-08-01T09:00:00.000Z",
});