Skip to content

Quickstart ​

From nothing to a campaign in the print queue. Five steps, roughly ten minutes.

1. Create an API key ​

Keys are created in the web app, not over the API.

  1. Open the web app and go to API in the sidebar.
  2. Choose Create key, name it, and grant these permissions:
ResourceActions
templatesread
campaignsread, create
  1. Copy the key. It is shown once. It starts with drse_.
bash
export DRSE_API_KEY="drse_your_api_key"
export DRSE_BASE_URL="https://api.utskick.dr.se"   # ← the production API

Confirm the key works:

bash
curl -s -o /dev/null -w "%{http_code}\n" \
  -H "x-api-key: $DRSE_API_KEY" \
  "$DRSE_BASE_URL/api/v1/campaigns"
# → 200
ts
const BASE = process.env.DRSE_BASE_URL!;
const KEY = process.env.DRSE_API_KEY!;

const res = await fetch(`${BASE}/api/v1/campaigns`, {
  headers: { "x-api-key": KEY },
});
console.log(res.status); // 200

401 means the key is wrong. 403 means it is missing campaigns:read. See Authentication.

2. Design a template in the web app ​

This is the one manual step, and it cannot be automated — physical output has to be checked by eye before it is trusted. See Templates.

  1. Go to Templates → Create.
  2. Lay out your mail. The editor marks the zones that must stay clear for the address block and postage.
  3. Use placeholders (recipient name, street, and so on) so each letter is personalised.
  4. Save.

3. Find the template id ​

bash
curl -s -H "x-api-key: $DRSE_API_KEY" \
  "$DRSE_BASE_URL/api/v1/templates?limit=10"
ts
const res = await fetch(`${BASE}/api/v1/templates?limit=10`, {
  headers: { "x-api-key": KEY },
});
const { data } = await res.json();
const templateId = data[0].id;
console.log(templateId);
json
{
  "data": [
    { "id": "550e8400-e29b-41d4-a716-446655440002", "name": "Summer postcard", "kind": "user", "hasBackPage": false,
      "qrCodeUrl": null, "qrCodeDescription": null, "qrCodeTrackingEnabled": false,
      "createdAt": "2026-07-20T08:00:00.000Z", "updatedAt": "2026-07-20T08:00:00.000Z" }
  ],
  "meta": { "count": 1, "nextCursor": null, "prevCursor": null }
}

Reference: GET /api/v1/templates.

4. Create the campaign ​

This mails real letters

Step 4 spends paper and postage.

The recipients travel with the request, so there is no list to set up. Note the Idempotency-Key: without it, a timeout you retry could print twice.

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": "Quickstart test",
    "templateId": "550e8400-e29b-41d4-a716-446655440002",
    "recipients": [
      {
        "name": "Your Name",
        "street": "Your Street 1",
        "postCode": "111 11",
        "postTown": "Stockholm",
        "country": "SE"
      }
    ]
  }'
ts
const res = await fetch(`${BASE}/api/v1/campaigns`, {
  method: "POST",
  headers: {
    "x-api-key": KEY,
    "Idempotency-Key": crypto.randomUUID(),
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Quickstart test",
    templateId,
    recipients: [
      {
        name: "Your Name",
        street: "Your Street 1",
        postCode: "111 11",
        postTown: "Stockholm",
        country: "SE",
      },
    ],
  }),
});

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": "Quickstart test",
    "status": "pending",
    "progress": 0,
    "listId": "550e8400-e29b-41d4-a716-446655440001",
    "listName": "Quickstart test",
    "templateId": "018f2c7a-0000-7000-8000-000000000000",
    "publishedAt": null,
    "createdAt": "2026-07-25T10:00:00.000Z",
    "updatedAt": "2026-07-25T10:00:00.000Z",
    "qrCode": null,
    "import": { "imported": 1, "skipped": 0 }
  }
}

Two things to notice:

  • listId is a list the platform created for you, named after the campaign.
  • templateId in the response is not the template you sent. It is an internal, frozen snapshot of the content, so later edits to your template cannot change mail already sent.

Reference: POST /api/v1/campaigns.

5. Poll until it finishes ​

bash
curl -s -H "x-api-key: $DRSE_API_KEY" \
  "$DRSE_BASE_URL/api/v1/campaigns/550e8400-e29b-41d4-a716-446655440000"
ts
async function waitForCampaign(id: string) {
  let delay = 2000;
  for (;;) {
    const res = await fetch(`${BASE}/api/v1/campaigns/${id}`, {
      headers: { "x-api-key": KEY },
    });
    const { data } = await res.json();
    console.log(`${data.status} ${data.progress}%`);

    if (data.status === "completed") return data;
    if (data.status === "failed") throw new Error("Campaign failed");

    await new Promise((r) => setTimeout(r, delay));
    delay = Math.min(delay * 2, 30_000); // back off, cap at 30s
  }
}

await waitForCampaign(campaignId);

status moves pending → processing → completed | failed, with progress from 0 to 100.

json
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Quickstart test",
    "status": "completed",
    "progress": 100,
    "listId": "550e8400-e29b-41d4-a716-446655440001",
    "listName": "Quickstart test",
    "templateId": "018f2c7a-0000-7000-8000-000000000000",
    "publishedAt": "2026-07-25T10:04:12.000Z",
    "createdAt": "2026-07-25T10:00:00.000Z",
    "updatedAt": "2026-07-25T10:04:12.000Z"
  }
}

A letter is on its way.

When something goes wrong ​

You seeMeaningFix
401 unauthorizedKey missing, wrong, expired or disabledCheck the header spelling: x-api-key
403 forbiddenKey lacks a permissionGrant it on the web app's API page
404 Template not foundWrong id, or a template in another accountRe-list your templates
422 recipient_validation_failedA row failed. Nothing was createdRead details.issues — each names a row and field
422 list_emptyThe audience resolved to zero recipientsCheck the list is not empty
429 rate_limitedOver a rate limitWait Retry-After seconds, then retry

Full catalog: Errors.

Next ​