Appearance
Campaign from a PDF
Send your own print-ready artwork. The platform prints your pages and draws only the postal address blocks on top.
Permissions needed: campaigns:create, campaigns:read. No templates:read — there is no template involved.
Before your first send
Page geometry is not validated
The API checks only that the part is application/pdf, is under 20 MB, and really begins with a PDF signature. Page size, page count and orientation are not checked. A PDF that is not A4 is accepted and printed as-is, and you have paid for it.
Confirm, once per layout:
| Check | Requirement |
|---|---|
| Page size | A4, 210 × 297 mm |
| Pages | 1, or 2 with hasBackPage: true |
| Orientation | Portrait |
| Fonts | Embedded |
| Reserved zones | Kept clear (below) |
bash
# Page geometry, if you have poppler-utils
pdfinfo flyer.pdf | grep -E "Pages|Page size"
# Page size: 595.276 x 841.89 pts (A4)
# Pages: 2Keep the top band clear
The destination address, return address and postage band are drawn onto page 1. Artwork underneath is printed over, and the mail may be undeliverable.
| Zone | From top-left | Size |
|---|---|---|
| Return address | 17 mm, 16 mm | 58 × 17 mm |
| Postage band | 121 mm, 32 mm | 77 × 5 mm |
| Destination address | 121 mm, 44 mm | 77 × 20 mm |
The safe rule: treat everything above 77 mm as reserved on the front page, and keep a 5 mm margin on all other edges. The back page needs only the 5 mm margin.
The call
Multipart only.
bash
curl -X POST "$DRSE_BASE_URL/api/v1/campaigns" \
-H "x-api-key: $DRSE_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-F "name=Autumn flyer" \
-F "pdf=@flyer.pdf;type=application/pdf" \
-F "hasBackPage=true" \
-F "listId=550e8400-e29b-41d4-a716-446655440001"ts
import { readFile } from "node:fs/promises";
const form = new FormData();
form.append("name", "Autumn flyer");
form.append(
"pdf",
new File([await readFile("flyer.pdf")], "flyer.pdf", {
type: "application/pdf",
}),
);
form.append("hasBackPage", "true");
form.append("listId", "550e8400-e29b-41d4-a716-446655440001");
const res = await fetch(`${BASE}/api/v1/campaigns`, {
method: "POST",
headers: {
"x-api-key": KEY,
"Idempotency-Key": idempotencyKey,
// No Content-Type — fetch sets the multipart boundary.
},
body: form,
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);201 Created — qrCode is always null in PDF mode.
json
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Autumn flyer",
"status": "pending",
"progress": 0,
"listId": "550e8400-e29b-41d4-a716-446655440001",
"listName": "Summer list",
"templateId": "018f2c7a-0000-7000-8000-000000000000",
"publishedAt": null,
"createdAt": "2026-07-25T10:00:00.000Z",
"updatedAt": "2026-07-25T10:00:00.000Z",
"qrCode": null
}
}Front and back
hasBackPage | Result |
|---|---|
omitted / false | Page 1 is the front. Extra pages are ignored |
true | Page 1 front, page 2 reverse |
Two pages is the maximum. Sending hasBackPage without a PDF is a 400 hasBackPage only applies to PDF campaigns.
Any recipient source works
Combine the PDF with inline recipients:
bash
curl -X POST "$DRSE_BASE_URL/api/v1/campaigns" \
-H "x-api-key: $DRSE_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-F "name=Autumn flyer" \
-F "pdf=@flyer.pdf;type=application/pdf" \
-F 'recipients=[{"name":"Alice Andersson","street":"1 First St","postCode":"111 11","postTown":"Stockholm","country":"SE"}]'Or with a recipients file — both file parts in one request:
bash
-F "pdf=@flyer.pdf;type=application/pdf" \
-F "recipientsFile=@customers.csv;type=text/csv" \
-F "fileFormat=csv" \
-F 'mapping={ "countrySource": "fixed", "fixedCountry": "SE", "columnMapping": [ ... ] }'Remember the whole multipart body is capped at 40 MB — a 20 MB PDF plus a 10 MB Excel file leaves little headroom.
What PDF mode rejects
| Field | Error |
|---|---|
templateId | 400 — A PDF campaign cannot also specify templateId or content — the PDF is the content |
content | 400 — same |
backContent | 400 — backContent is not supported in PDF mode — use hasBackPage to render the PDF's second page |
qrCode | 400 — QR codes are not supported in PDF mode |
For a QR code, draw it into the PDF yourself. If you need a tracked code, use a template campaign instead — see QR codes.
No personalisation beyond the address
A PDF is the same artwork for every recipient. Only the address blocks vary. For per-recipient content — a name in the greeting, a renewal date — use a template with placeholders: Templates.
Proof before volume
There is no dry-run endpoint. Send one campaign to one recipient — your own address — and inspect the letter:
bash
curl -X POST "$DRSE_BASE_URL/api/v1/campaigns" \
-H "x-api-key: $DRSE_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-F "name=PDF proof" \
-F "pdf=@flyer.pdf;type=application/pdf" \
-F 'recipients=[{"name":"Your Name","street":"Your Street 1","postCode":"111 11","postTown":"Stockholm","country":"SE"}]'Check the delivered mail for: correct scale, nothing clipped at the edges, and no artwork colliding with the address block. Then send the real batch.
Failure modes
| Status | code / message | Fix |
|---|---|---|
| 400 | File must be a PDF | The part's MIME type was not application/pdf |
| 400 | PDF must be under 20 MB | Compress or downsample images |
| 400 | File is not a valid PDF | The bytes are not a PDF — check the upload |
| 400 | A conflicting-field message | Remove templateId / content / qrCode |
| 413 | Request body too large. Maximum 40MB. | The whole body, not just the PDF |
| 422 | list_empty | The list is empty, or every file row was skipped |
Next
- PDF campaigns — zones and responsibilities
- Limits
- Polling & failures