Appearance
QR codes
A QR code belongs to exactly one campaign. The printed code always encodes a redirect on this API, which looks up the current destination and forwards the reader there — so url can be changed after the mail is posted. trackingEnabled additionally records each scan. See QR codes for the model.
Attach before printing starts
The QR image is rendered into the PDF as the campaign is processed. Attaching a code to a campaign that has already reached completed will not change what was printed. The reliable approach is the qrCode field on POST /api/v1/campaigns, which persists the code inside the same transaction as the campaign.
Attach a QR code to a campaign
POST
/api/v1/qr-codesRequires qr-codes:create. A campaign may have at most one QR code.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
campaignId | uuid | yes | Must be a campaign of yours |
url | string | yes | The destination. Must be a valid http: or https: URL |
description | string | no | ≤500 characters. Printed as a caption beside the code |
trackingEnabled | boolean | no | Defaults to false. Controls only whether each scan is recorded. The printed code encodes the redirect either way, so it does not affect what is printed or whether url can be changed later |
bash
curl -X POST https://api.utskick.dr.se/api/v1/qr-codes \
-H "x-api-key: drse_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"campaignId": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://example.com/promo",
"description": "Landing page",
"trackingEnabled": true
}'ts
const res = await fetch("https://api.utskick.dr.se/api/v1/qr-codes", {
method: "POST",
headers: {
"x-api-key": process.env.DRSE_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
campaignId: "550e8400-e29b-41d4-a716-446655440000",
url: "https://example.com/promo",
description: "Landing page",
trackingEnabled: true,
}),
});
const { data } = await res.json();
console.log(data.trackingUrl);201 Created
json
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440003",
"userId": "usr_9f2k7d31",
"name": "Summer promo",
"slug": "summer-promo-a1b2",
"url": "https://example.com/promo",
"description": "Landing page",
"trackingEnabled": true,
"campaignId": "550e8400-e29b-41d4-a716-446655440000",
"createdAt": "2026-07-25T10:01:00.000Z",
"updatedAt": "2026-07-25T10:01:00.000Z",
"trackingUrl": "https://api.utskick.dr.se/api/r/summer-promo-a1b2"
}
}| Field | Notes |
|---|---|
slug | Derived from the QR code's name plus a 4-character random suffix. Globally unique and permanent — it is printed on paper |
name | The code's display name. Defaults to the campaign's name when created through this endpoint |
campaignId | The campaign the code is currently linked to, or null when it has been unlinked in the dashboard's Konverteringar view |
trackingUrl | The address encoded into the printed code — always, whatever trackingEnabled is set to. Only returned here and on campaign create |
Failures
| Status | code | When |
|---|---|---|
| 400 | validation_failed | campaignId not a UUID; url not a valid URL, or not http:/https:; description over 500 characters |
| 401 | unauthorized | Key problem |
| 403 | forbidden | Key lacks qr-codes:create |
| 404 | not_found | Campaign not found — unknown id, or a campaign in another account |
| 409 | conflict | Campaign already has a QR code |
Retrieve a QR code
GET
/api/v1/qr-codes/:idRequires qr-codes:read.
bash
curl -H "x-api-key: drse_your_api_key" \
https://api.utskick.dr.se/api/v1/qr-codes/550e8400-e29b-41d4-a716-446655440003ts
const id = "550e8400-e29b-41d4-a716-446655440003";
const res = await fetch(`https://api.utskick.dr.se/api/v1/qr-codes/${id}`, {
headers: { "x-api-key": process.env.DRSE_API_KEY! },
});
const { data } = await res.json();
// Rebuild the tracking URL yourself — it is not returned here:
const trackingUrl = `https://api.utskick.dr.se/api/r/${data.slug}`;200 OK
json
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440003",
"userId": "usr_9f2k7d31",
"name": "Summer promo",
"slug": "summer-promo-a1b2",
"url": "https://example.com/promo",
"description": "Landing page",
"trackingEnabled": true,
"campaignId": "550e8400-e29b-41d4-a716-446655440000",
"createdAt": "2026-07-25T10:01:00.000Z",
"updatedAt": "2026-07-25T10:01:00.000Z"
}
}No trackingUrl here
This response deliberately has no trackingUrl field — only the create response and the campaign-create response include it. Compose it from slug: {baseUrl}/api/r/{slug}.
Failures
| Status | code | When |
|---|---|---|
| 400 | validation_failed | id is not a UUID (QR code ID must be a valid UUID) |
| 401 | unauthorized | Key problem |
| 403 | forbidden | Key lacks qr-codes:read |
| 404 | not_found | QR code not found — unknown id, or one belonging to another account |
Update a QR code
PATCH
/api/v1/qr-codes/:idRequires qr-codes:update. The slug can never change, so a code already in print keeps resolving; changing url re-points every printed code at the new destination.
Body
Send only the fields you are changing. Unknown fields are rejected with a 400 rather than ignored.
| Field | Type | Notes |
|---|---|---|
url | string | Valid http:/https: URL |
description | string | ≤500 characters |
trackingEnabled | boolean | — |
bash
curl -X PATCH https://api.utskick.dr.se/api/v1/qr-codes/550e8400-e29b-41d4-a716-446655440003 \
-H "x-api-key: drse_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/autumn" }'ts
const id = "550e8400-e29b-41d4-a716-446655440003";
const res = await fetch(`https://api.utskick.dr.se/api/v1/qr-codes/${id}`, {
method: "PATCH",
headers: {
"x-api-key": process.env.DRSE_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({ url: "https://example.com/autumn" }),
});
const { data } = await res.json();200 OK — the updated row, again without trackingUrl.
json
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440003",
"userId": "usr_9f2k7d31",
"name": "Summer promo",
"slug": "summer-promo-a1b2",
"url": "https://example.com/autumn",
"description": "Landing page",
"trackingEnabled": true,
"campaignId": "550e8400-e29b-41d4-a716-446655440000",
"createdAt": "2026-07-25T10:01:00.000Z",
"updatedAt": "2026-07-25T11:20:00.000Z"
}
}Failures
| Status | code | When |
|---|---|---|
| 400 | validation_failed | id not a UUID; an invalid url; description too long; or any field not listed above |
| 401 | unauthorized | Key problem |
| 403 | forbidden | Key lacks qr-codes:update |
| 404 | not_found | QR code not found |
Delete a QR code
DELETE
/api/v1/qr-codes/:idRequires qr-codes:delete.
Printed codes stop working
Deleting a code makes its printed slug resolve to a 404 forever. Mail already in the post will carry a dead QR code. To change where a code points, update url instead.
bash
curl -X DELETE https://api.utskick.dr.se/api/v1/qr-codes/550e8400-e29b-41d4-a716-446655440003 \
-H "x-api-key: drse_your_api_key"ts
const id = "550e8400-e29b-41d4-a716-446655440003";
const res = await fetch(`https://api.utskick.dr.se/api/v1/qr-codes/${id}`, {
method: "DELETE",
headers: { "x-api-key": process.env.DRSE_API_KEY! },
});
const { data } = await res.json(); // { deleted: true }200 OK
json
{ "data": { "deleted": true } }Deleting also removes the code's recorded scans.
Failures
| Status | code | When |
|---|---|---|
| 400 | validation_failed | id is not a UUID |
| 401 | unauthorized | Key problem |
| 403 | forbidden | Key lacks qr-codes:delete |
| 404 | not_found | QR code not found |