API Getting Started


The Postcards API lets you connect your own apps, CRMs, CMSs, marketing automations, and AI agents to Postcards — pulling the email templates you've built in the editor and rendering them as ready-to-use HTML, all without ever opening the editor in a browser.

In this article we'll walk you through everything you need to send your first authenticated request: creating an API key, picking the right endpoint, and exporting your first email project as HTML.


Important: The API is read-only for projects and folders — you can list, fetch, and export, but creating or editing projects is done in the editor. Exporting consumes from your team's monthly export quota.


What you can do with the API

  • List your team's projects — pull every email project in the workspace, optionally filtered by folder, paginated 50 at a time.
  • Inspect folder structure — list folders, get a folder with its projects.
  • Check usage — see your active plan and how many exports you've used this billing period.
  • Export a project to HTML or ZIP — render any project to production-ready HTML, optionally with images and fonts hosted on the Postcards CDN, optionally with {{variable}}   placeholders replaced.

Typical use cases:

  • Pulling rendered HTML into your transactional email service (Postmark, SendGrid, Mailgun, Customer.io).
  • Personalizing a single template per recipient via {{variables}}  .
  • Building automated workflows ("when a row is added in Airtable, render and send this Postcards project").
  • Backing up your email designs by exporting them as ZIPs.

1. Create your API key

API keys are created from the workspace settings page in Postcards.

  1. Open Postcards and go to your Workspace Settings (top-right menu → Team).
  2. In the side menu, click API Keys.
  3. Click Create new key, give it a memorable name (e.g. "CRM integration"), and click Create.
  4. Copy the raw key right away — it's shown only once. It looks like:
sk-pcds-api03-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  1. Store it somewhere safe (1Password, AWS Secrets Manager, your .env   file — anywhere private). If you lose it, you can't recover it — only revoke and create a new one.


Important: Each key is scoped to the team it was created in. Anyone holding the key has the same read/export access as a member of that team — treat it like a password.


2. Make your first request

The base URL of the API is:

https://api-postcards.designmodo.com/api/v1/

Every request must include an Authorization: Bearer <your-key>   header. Let's list the first page of projects in your team:

curl https://api-postcards.designmodo.com/api/v1/projects \
  -H 'Authorization: Bearer sk-pcds-api03-XXXX'

If everything is set up correctly you'll get back JSON like this:

{
  "data": [
    {
      "id": 305876,
      "obfuscated_id": "32b3f40e",
      "name": "Spring Newsletter",
      "folder_id": null,
      "edited_at": "2026-05-18 11:19:49",
      "created_at": "2026-05-18T11:19:49.000000Z",
      "updated_at": "2026-05-19T02:59:36.000000Z"
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 50,
    "total": 537,
    "total_pages": 11
  }
}

A few things to notice:

  • Each project has two identifiers — a numeric id   and a short hex obfuscated_id  . Both work everywhere the API takes an {id}  , so pick whichever you prefer.
  • The response is paginated — 50 projects per page. The meta   block tells you how to navigate.
  • The default sort order is most-recently edited first.

3. Pagination

All list endpoints return at most 50 items per page and include a meta   block describing the current position. This applies to:

  • GET /api/v1/projects   — full project list, optionally filtered by folder_id  .
  • GET /api/v1/folders/{id}   — a folder's nested projects   array is paginated too (folder metadata is not — there's only one folder per request).

Query parameters

Parameter Type Default Description
page   integer 1   Page number, 1-based.
per_page   integer 50   Items per page. Maximum 100  .

The meta   block

Every paginated response includes a meta   object with four fields:

Field Description
page   The page you requested (echoed back).
per_page   Items per page after clamping (1..100  ).
total   Total number of items that match the filter.
total_pages   ceil(total / per_page)  . 0   when there are no items.

Walking through all pages

A typical loop in pseudocode:

let page = 1;

while (true) {
  const res = await api(`/api/v1/projects?page=${page}&per_page=100`);
  for (const project of res.data) {
    // ...do something with each project
  }
  if (page >= res.meta.total_pages) break;
  page++;
}

Pick per_page=100   if you want fewer round-trips, or stick with the default 50 if your client struggles with larger payloads.

Important: A page request beyond total_pages   returns data: []   with an accurate meta.total   — not a 404. Use meta.total_pages   as the loop bound, not "until the response is empty".


4. Export a project to HTML

This is where the API earns its keep. Pick a project from the list above (we'll use 305876  ) and call the export endpoint:

curl -X POST https://api-postcards.designmodo.com/api/v1/projects/305876/export \
  -H 'Authorization: Bearer sk-pcds-api03-XXXX' \
  -H 'Content-Type: application/json' \
  -d '{ "imageHosting": true, "minify": true }'

You'll get back JSON with a single html   field containing the complete, ready-to-send HTML:

{
  "html": "<!DOCTYPE html><html>...<img src=\"https://cdn.postcards.app/...\">...</html>"
}

What the options do

Option Type Description
imageHosting   boolean When true  , images and fonts are referenced from the Postcards CDN and the response is JSON. When false   (default), you get a binary ZIP with images bundled locally.
cdn   boolean When true  , host all assets (CSS, fonts, images) on the CDN. Available on Pro plans and higher.
minify   boolean Strip whitespace from the output HTML — smaller payloads, slightly harder to debug.
format   string Response format when imageHosting=true  . "json"   (default) wraps the HTML in { "html": "..." }  . "html"   returns raw HTML as the response body with Content-Type: text/html  . Ignored when imageHosting=false   (you always get a ZIP).
variables   object Map of {{placeholder}}   keys to scalar values. The placeholder text in the template is replaced before rendering.

Three response shapes

The combination of imageHosting   and format   decides what you get back:

imageHosting   format   Response
false   (default) Binary ZIP archive (Content-Type: application/zip  ) containing index.html   plus local images/   and fonts/   folders.
true   "json"   (default) JSON { "html": "..." }   with CDN-hosted assets. Easy to consume from any client.
true   "html"   Raw HTML body (Content-Type: text/html; charset=utf-8  ) with CDN-hosted assets. Pipe straight into an email service or save to a file.

Raw HTML example

curl -X POST https://api-postcards.designmodo.com/api/v1/projects/305876/export \
  -H 'Authorization: Bearer sk-pcds-api03-XXXX' \
  -H 'Content-Type: application/json' \
  -d '{ "imageHosting": true, "format": "html", "minify": true }' \
  -o newsletter.html

You now have newsletter.html   on disk — ready to send.

Personalizing with {{variables}}  

Anywhere in your email design where you've written {{username}}   or {{order_number}}  , the API can substitute real values:

curl -X POST https://api-postcards.designmodo.com/api/v1/projects/305876/export \
  -H 'Authorization: Bearer sk-pcds-api03-XXXX' \
  -H 'Content-Type: application/json' \
  -d '{    "imageHosting": true,    "minify": true,    "variables": {      "username": "Alex",      "company": "Acme Inc",      "order_number": "A-22817"    }  }'

The resulting HTML will read "Hi Alex from Acme Inc!" instead of "Hi {{username}} from {{company}}!". Variables that you don't pass stay as-is in the output — handy for partial substitution.


5. Other useful endpoints

Once you're comfortable with the basics, these are the rest of the endpoints you'll likely use:

Endpoint What it does
GET /api/v1/projects?folder_id={id}   Filter projects by folder. Use the numeric id  , obfuscated_id  , or null   for root-level projects.
GET /api/v1/projects/{id}   Get metadata for a single project (no content payload).
GET /api/v1/folders   List all folders in your team.
GET /api/v1/folders/{id}   Get one folder together with a paginated list of its projects.
GET /api/v1/usage   Plan name, billing period, and exports used this period. Call this before bulk operations.

All list endpoints are paginated with the same page   / per_page   query parameters (default 50, max 100).


6. Error responses

Errors always come back as JSON with an error   field plus an HTTP status code:

Status Body Meaning
401   { "error": "unauthorized" }   Missing or invalid API key.
403   { "error": "plan_required", "required_plan": "pro", "feature": "cdn" }   The feature you requested isn't available on your team's plan.
404   { "error": "not_found" }   Resource doesn't exist or doesn't belong to your team.
422   { "error": "validation", "details": {...} }   Invalid request body.
429   { "error": "rate_limit_exceeded", "limit": 60, "window_seconds": 60 }   You're sending requests too fast. Wait and retry.
429   { "error": "export_limit_exceeded", "limit": 20 }   Monthly export quota exhausted.

7. Rate limits

The API enforces two rate-limit buckets per API key:

  • Read endpoints — 60 requests per minute. Includes GET /projects  , GET /folders  , GET /usage  .
  • Export endpoint — 10 requests per minute.

If you hit a limit, the response sets a Retry-After   header telling you how many seconds to wait. The two buckets are independent — being throttled on exports doesn't slow down your list requests.

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.

Still need help? Contact Us Contact Us