API Reference

Imprimo API Reference

Base URL: https://imprimo.pub

Authentication

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.

Rate Limits

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.

Errors

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

Posts

Create a post

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"
}

List posts

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 a post

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"
}

Update a post

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.

Publish a post

POST /v1/posts/:id/publish

Publishes a draft post and sends it to all active subscribers. This:

  1. Renders the Markdown to web HTML and email HTML
  2. Stores the rendered HTML
  3. Queues email sends to all active subscribers via BullMQ

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 a post

DELETE /v1/posts/:id

Deletes a draft post. Published posts cannot be deleted.

Response: 204 No Content


Render

Preview render

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:


Subscribers

Add a subscriber

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.

List subscribers

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"
  }
]

Unsubscribe

DELETE /v1/subscribers/:id

Unsubscribes a subscriber (soft delete — sets status to unsubscribed).

Response: 200 OK — Updated subscriber with status: "unsubscribed".


Deliverability

Pre-publish check

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.


Images

Upload an image

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
}

List images

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 an image

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.


Health

Health check

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"
}

Webhooks

Mailgun events

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.


Typical workflow

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\": \"![Hero]($IMG_URL)\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.