Appearance
Campaign from a saved list
Send to an audience already in the platform — the pattern for a recurring send to a stable list.
Permissions needed: templates:read, lists:read, campaigns:create, campaigns:read.
1. Find the list
bash
curl -s -H "x-api-key: $DRSE_API_KEY" "$DRSE_BASE_URL/api/v1/lists"ts
const res = await fetch(`${BASE}/api/v1/lists`, {
headers: { "x-api-key": KEY },
});
const { data, meta } = await res.json();
console.log(`${meta.listCount} lists`);json
{
"data": [
{ "id": "550e8400-e29b-41d4-a716-446655440001", "name": "Summer list",
"createdAt": "2026-07-25T09:00:00.000Z", "updatedAt": "2026-07-25T09:30:00.000Z",
"recipientCount": 250 }
],
"meta": { "totalRecipientCount": 250, "listCount": 1 }
}Check recipientCount before sending
A campaign against an empty list fails with 422 list_empty. Checking here makes that a pre-flight rather than a failed create.
2. Confirm the template
bash
curl -s -H "x-api-key: $DRSE_API_KEY" \
"$DRSE_BASE_URL/api/v1/templates?limit=100"ts
const res = await fetch(`${BASE}/api/v1/templates?limit=100`, {
headers: { "x-api-key": KEY },
});
const { data } = await res.json();Store the id in configuration rather than matching on name at send time — a rename in the web app would silently break sending, or match the wrong template. See Templates.
3. Create the campaign
bash
curl -X POST "$DRSE_BASE_URL/api/v1/campaigns" \
-H "x-api-key: $DRSE_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "Summer promo — July",
"templateId": "550e8400-e29b-41d4-a716-446655440002",
"listId": "550e8400-e29b-41d4-a716-446655440001"
}'ts
const res = await fetch(`${BASE}/api/v1/campaigns`, {
method: "POST",
headers: {
"x-api-key": KEY,
"Idempotency-Key": idempotencyKey, // persisted before this call
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Summer promo — July",
templateId: "550e8400-e29b-41d4-a716-446655440002",
listId: "550e8400-e29b-41d4-a716-446655440001",
}),
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
const campaignId = body.data.id;201 Created
json
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Summer promo — July",
"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
}
}No import field: this call did not create recipients, it reused a list.
4. Poll to completion
ts
async function waitFor(campaignId: string) {
let delay = 2000;
for (;;) {
const res = await fetch(`${BASE}/api/v1/campaigns/${campaignId}`, {
headers: { "x-api-key": KEY },
});
const { data } = await res.json();
if (data.status === "completed") return data;
if (data.status === "failed") {
throw new Error(`Campaign ${campaignId} failed at ${data.progress}%`);
}
await new Promise((r) => setTimeout(r, delay));
delay = Math.min(delay * 2, 30_000);
}
}See Polling & failures.
Adding a QR code
Include it in the create call so the code exists before rendering starts:
json
{
"name": "Summer promo — July",
"templateId": "550e8400-e29b-41d4-a716-446655440002",
"listId": "550e8400-e29b-41d4-a716-446655440001",
"qrCode": {
"url": "https://example.com/summer",
"description": "Summer landing page",
"trackingEnabled": true
}
}The response's qrCode then carries the slug and trackingUrl. See Track QR scans.
Overriding template details
Per campaign, without editing the template:
json
{
"name": "Summer promo — July",
"templateId": "550e8400-e29b-41d4-a716-446655440002",
"listId": "550e8400-e29b-41d4-a716-446655440001",
"returnAddress": "Acme AB, Box 42, 111 22 Stockholm",
"skipTemplateLogo": true
}Full list: Campaigns.
Failure modes
| Status | code / message | Fix |
|---|---|---|
| 404 | List not found | The listId is wrong or belongs to another account |
| 404 | Template not found | The templateId is wrong, or you passed a campaign's snapshot id |
| 422 | list_empty — List has no recipients | Add recipients first |
| 409 | List no longer exists | Deleted mid-request, possibly by 90-day retention |
| 403 | forbidden | The key is missing campaigns:create or lists:read |
Retention can empty a stable list
A list untouched for 90 days is deleted. A quarterly send is right at that edge — if the send is the only activity, the list may vanish between runs. Keep the audience in your own system as the source of truth, or re-import before sending.
Next
- Campaign with inline recipients — no list to maintain
- Import into a list — keep this list populated
- Production checklist