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.
- Open Postcards and go to your Workspace Settings (top-right menu → Team).
- In the side menu, click API Keys.
- Click Create new key, give it a memorable name (e.g. "CRM integration"), and click Create.
- Copy the raw key right away — it's shown only once. It looks like:
sk-pcds-api03-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
-
Store it somewhere safe (1Password, AWS Secrets Manager, your
.envfile — 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
idand a short hexobfuscated_id. Both work everywhere the API takes an{id}, so pick whichever you prefer. - The response is paginated — 50 projects per page. The
metablock 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 byfolder_id.GET /api/v1/folders/{id}— a folder's nestedprojectsarray 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_pagesreturnsdata: []with an accuratemeta.total— not a 404. Usemeta.total_pagesas 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.