Base URL: https://imprimo.pub
All /v1/* endpoints require a Bearer token in the Authorization header:
Authorization: Bearer tk_your_api_key_here
API keys are created in the dashboard under Settings → API Keys. Each key is hashed on creation — store it securely, as it cannot be retrieved later.
All /v1/* endpoints are rate-limited per account based on your plan:
| Plan | Requests/min |
|---|---|
| Free | 100 |
| Pro | 500 |
| Team | 1,000 |
| Business | 1,000 |
Every response includes rate limit headers:
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 487
When exceeded, the API returns 429 Too Many Requests with a Retry-After: 60 header.
All errors return JSON with an error field:
{ "error": "Post not found" }
| Status | Meaning |
|---|---|
| 400 | Bad request — missing required fields or invalid JSON |
| 401 | Unauthorized — invalid or missing API key |
| 403 | Forbidden — subscriber limit reached for your plan |
| 404 | Not found — resource doesn't exist or isn't yours |
| 409 | Conflict — duplicate (subscriber exists, post already published) |
| 429 | Rate limit exceeded |
POST /v1/posts
Creates a new draft post.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | no | Post title (used as email subject line) |
slug |
string | no | URL slug for the web archive |
subtitle |
string | no | Subtitle, shown below the title |
preview_text |
string | no | Email preview text (40-90 chars recommended) |
markdown |
string | no | Post content in Markdown |
Example:
curl -X POST https://imprimo.pub/v1/posts \
-H "Authorization: Bearer $IMPRIMO_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "This Week in Email #42",
"markdown": "## The Gmail Shift\n\nGoogle'\''s February update changed everything...",
"subtitle": "What changed and what to do about it",
"preview_text": "Gmail is enforcing DMARC. Here'\''s your playbook."
}'
Response: 201 Created
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"title": "This Week in Email #42",
"slug": "this-week-in-email-42",
"status": "draft",
"created_at": "2026-03-21T10:30:00Z"
}
GET /v1/posts
Returns all posts for your publication, newest first. Does not include markdown or rendered HTML.
Response: 200 OK
[
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"title": "This Week in Email #42",
"slug": "this-week-in-email-42",
"status": "draft",
"published_at": null,
"created_at": "2026-03-21T10:30:00Z",
"updated_at": "2026-03-21T10:30:00Z"
}
]
GET /v1/posts/:id
Returns a single post with full content, including rendered HTML. If rendered HTML is not yet cached, it renders on the fly and stores the result.
Response: 200 OK
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"publication_id": "a1b2c3d4-...",
"title": "This Week in Email #42",
"slug": "this-week-in-email-42",
"subtitle": "What changed and what to do about it",
"preview_text": "Gmail is enforcing DMARC.",
"markdown_source": "## The Gmail Shift\n\n...",
"rendered_web_html": "<html>...</html>",
"rendered_email_html": "<html>...</html>",
"status": "published",
"published_at": "2026-03-21T12:00:00Z",
"created_at": "2026-03-21T10:30:00Z",
"updated_at": "2026-03-21T12:00:00Z"
}
PUT /v1/posts/:id
Updates a draft post. Only draft posts can be updated — published posts return 404.
Request body: Same fields as create (all optional). Only provided fields are updated. Updating markdown clears cached rendered HTML.
Response: 200 OK — Updated post object.
POST /v1/posts/:id/publish
Publishes a draft post and sends it to all active subscribers. This:
Request body: None.
Example:
curl -X POST https://imprimo.pub/v1/posts/f47ac10b-.../publish \
-H "Authorization: Bearer $IMPRIMO_KEY"
Response: 200 OK
{
"id": "f47ac10b-...",
"title": "This Week in Email #42",
"status": "published",
"published_at": "2026-03-21T12:00:00Z",
"emails_queued": 2847
}
Returns 409 Conflict if the post is already published.
DELETE /v1/posts/:id
Deletes a draft post. Published posts cannot be deleted.
Response: 204 No Content
POST /v1/render/preview
Stateless Markdown rendering. Takes Markdown and returns both web and email HTML without creating or storing anything. Useful for live preview, CI/CD validation, or testing your content pipeline.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
markdown |
string | yes | Markdown content to render |
Example:
curl -X POST https://imprimo.pub/v1/render/preview \
-H "Authorization: Bearer $IMPRIMO_KEY" \
-H "Content-Type: application/json" \
-d '{"markdown": "# Hello World\n\nThis is a **test**."}'
Response: 200 OK
{
"web_html": "<!DOCTYPE html><html>...<h1>Hello World</h1><p>This is a <strong>test</strong>.</p>...</html>",
"email_html": "<!doctype html><html>...<h1>Hello World</h1><p>This is a <strong>test</strong>.</p>...</html>",
"frontmatter": {}
}
The web_html is a complete HTML document with styles, suitable for display in a browser. The email_html is table-based HTML with inlined CSS, safe for all email clients.
Markdown support:
title, subtitle, etc.)POST /v1/subscribers
Adds a subscriber to your publication. New subscribers start in pending status until they confirm via double opt-in.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | yes | Subscriber's email address |
name |
string | no | Subscriber's name |
Example:
curl -X POST https://imprimo.pub/v1/subscribers \
-H "Authorization: Bearer $IMPRIMO_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "reader@example.com", "name": "Jane"}'
Response: 201 Created
{
"id": "b2c3d4e5-...",
"publication_id": "a1b2c3d4-...",
"email": "reader@example.com",
"name": "Jane",
"status": "pending",
"confirmation_token": "abc123...",
"subscribed_at": "2026-03-21T10:30:00Z"
}
Returns 409 Conflict if the subscriber already exists. Returns 403 Forbidden if your plan's subscriber limit is reached.
GET /v1/subscribers
Returns all subscribers for your publication, newest first.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
status |
string | Filter by status: active, pending, unsubscribed, bounced |
Example:
curl "https://imprimo.pub/v1/subscribers?status=active" \
-H "Authorization: Bearer $IMPRIMO_KEY"
Response: 200 OK
[
{
"id": "b2c3d4e5-...",
"email": "reader@example.com",
"name": "Jane",
"status": "active",
"subscribed_at": "2026-03-21T10:30:00Z"
}
]
DELETE /v1/subscribers/:id
Unsubscribes a subscriber (soft delete — sets status to unsubscribed).
Response: 200 OK — Updated subscriber with status: "unsubscribed".
POST /v1/deliverability/check
Renders your Markdown as email HTML and runs deliverability checks against it. Use this before publishing to catch issues that could hurt inbox placement.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
markdown |
string | yes | Markdown content to check |
title |
string | no | Post title (checked for length) |
subtitle |
string | no | Post subtitle |
Example:
curl -X POST https://imprimo.pub/v1/deliverability/check \
-H "Authorization: Bearer $IMPRIMO_KEY" \
-H "Content-Type: application/json" \
-d '{
"markdown": "# Big Sale!\n\nAct now — click here for a free offer!",
"title": "Sale"
}'
Response: 200 OK
{
"level": "yellow",
"checks": [
{
"id": "spam-phrases",
"level": "yellow",
"label": "Spam trigger phrases",
"detail": "Found: \"act now\", \"click here\", \"free offer\". Consider rephrasing.",
"value": 3
},
{
"id": "subject-short",
"level": "yellow",
"label": "Short subject line",
"detail": "Subject is 4 characters. Aim for 5+.",
"value": 4
}
]
}
Overall level:
| Level | Meaning |
|---|---|
green |
All checks pass — safe to publish |
yellow |
Warnings — publishable but review recommended |
red |
Blocking issues — publishing will likely hurt deliverability |
Checks performed:
| Check | Level | Trigger |
|---|---|---|
| Gmail clipping | red | Email HTML exceeds 102KB |
| Image-only email | red | Less than 10% text content relative to images |
| Missing unsubscribe | red | No unsubscribe link found in rendered HTML |
| Low text-to-image ratio | yellow | 10-60% text relative to images |
| Too many links | yellow | More than 10 links |
| Spam trigger phrases | yellow | Common spam phrases detected in content |
| Approaching clip limit | yellow | Email HTML between 75-102KB |
| Missing alt text | yellow | Images without alt attributes |
| Short subject line | yellow | Title shorter than 5 characters |
Only non-passing checks are returned. A green response has an empty checks array.
POST /v1/images
Content-Type: multipart/form-data
Upload an image to your account's image library. Stored on Cloudflare R2 with immutable cache headers.
Request body: Multipart form with a file field.
Constraints:
Example:
curl -X POST https://imprimo.pub/v1/images \
-H "Authorization: Bearer $IMPRIMO_KEY" \
-F "file=@hero-banner.png"
Response: 201 Created
{
"id": "a1b2c3d4-...",
"key": "images/abc123def456.png",
"url": "https://cdn.imprimo.pub/images/abc123def456.png",
"filename": "hero-banner.png",
"content_type": "image/png",
"size": 245000
}
GET /v1/images
Returns all images uploaded by your account, newest first.
Response: 200 OK — Array of image objects (same schema as upload response, plus uploaded_at).
DELETE /v1/images/:id
Deletes an image from R2 storage and removes it from your library.
Response: 204 No Content
Returns 404 Not Found if the image doesn't exist or isn't yours.
GET /health
Public endpoint (no auth required). Returns system health status.
Response: 200 OK (healthy) or 503 Service Unavailable (degraded)
{
"status": "ok",
"timestamp": "2026-03-21T10:30:00Z",
"database": "ok",
"redis": "ok"
}
POST /webhooks/mailgun
Receives delivery events from Mailgun. Authenticated via HMAC signature verification (not Bearer token). Configure this URL in your Mailgun dashboard.
Events tracked: delivered, opened, clicked, failed (→ bounced), complained
Events update the send status for the corresponding subscriber and are stored in webhook_events for audit.
A complete publish flow with an image:
# 0. (Optional) Upload an image and use its URL in your Markdown
IMG_URL=$(curl -s -X POST https://imprimo.pub/v1/images \
-H "Authorization: Bearer $IMPRIMO_KEY" \
-F "file=@hero-banner.png" \
| jq -r '.url')
# 1. Create a draft (embedding the uploaded image)
POST_ID=$(curl -s -X POST https://imprimo.pub/v1/posts \
-H "Authorization: Bearer $IMPRIMO_KEY" \
-H "Content-Type: application/json" \
-d "{\"title\": \"Weekly Update\", \"markdown\": \"\n\n# News\n\nContent...\"}" \
| jq -r '.id')
# 2. Check deliverability (optional but recommended)
curl -s -X POST https://imprimo.pub/v1/deliverability/check \
-H "Authorization: Bearer $IMPRIMO_KEY" \
-H "Content-Type: application/json" \
-d '{"markdown": "# News\n\nContent...", "title": "Weekly Update"}' \
| jq '.level'
# → "green"
# 3. Publish
curl -X POST "https://imprimo.pub/v1/posts/$POST_ID/publish" \
-H "Authorization: Bearer $IMPRIMO_KEY"
# → { "emails_queued": 2847 }
Web archive is live at your-pub.imprimo.pub/archive/weekly-update. Emails are delivered asynchronously via Mailgun. Images are served from Cloudflare R2 via cdn.imprimo.pub with immutable cache headers.