Skip to content

Jobs via the API

7 min read

Everything you can do in the dashboard's Jobs you can also do from your own code. The base URL is https://api.danskadresseapi.dk.

Key: Send a live key with the batch scope as Authorization: Bearer <key>. A pub key can't be used, and the plan must be Pro or Enterprise.

Endpoints

CallDoes
POST /v1/jobsCreates a job. Responds 202 with the job and a Location header
GET /v1/jobsThe team's jobs, newest first (?limit= up to 100, ?foer=<timestamp> for the next page)
GET /v1/jobs/{id}Status, progress, rows per second, estimated time (eta_sek), job_fejl and raekke_fejl
GET /v1/jobs/{id}/resultatThe result — ?format=ndjson (default) or ?format=csv
GET /v1/jobs/{id}/fejlThe rows that gave no (reliable) answer — ?format=csv (default) or ndjson, optionally ?aarsag= and ?sprog=en
POST /v1/jobs/{id}/pausePauses a queued or running job — the part in progress is finished and saved
POST /v1/jobs/{id}/genoptagResumes a paused job where it left off. Responds 202 (possibly with an advarsel about the quota)
DELETE /v1/jobs/{id}Cancels a queued, running or paused job

Quota: Only the job's rows count — each row that returned a result as one lookup. Rows that fail, and the POST call itself, cost nothing. Status, result, error rows, pausing and cancelling don't count towards the quota, but follow the plan's rate limit.

Create a job

There are three ways to send the file:

1. Raw file — set Content-Type for the format, and give type (and optionally variant and navn) in the URL:

FormatContent-Type
CSVtext/csv
TSVtext/tab-separated-values
NDJSONapplication/x-ndjson

2. JSON with the elements — {"type": …, "variant": …, "navn": …, "input": [ … ]} with Content-Type: application/json.

3. JSON with raw data — {"type": …, "format": "csv", "data": "<the file's contents>"}.

variant is adresser or adgangsadresser for cleansing (datavask) and bygninger or enheder for BBR. If you leave it out, the first one is used. The file format is described in File format for jobs.

The response is the job with status koe. If the quota won't cover the whole job, there is an advarsel (warning) in the response — the job then stops with kvote_stop when the quota is used up. If creation is rejected, the cause is in error.details.kode — see When a job fails — error codes.

The status response

JSON
{
  "id": "7c0e6f0a-3b1d-4f5e-9a51-2d4c8b1e9f20",
  "type": "datavask",
  "variant": "adresser",
  "kilde": "api",
  "navn": "customers-october",
  "status": "koerer",
  "total": 48522,
  "behandlet": 21000,
  "fejl": 140,
  "procent": 43.3,
  "raekker_pr_sek": 1840,
  "eta_sek": 15,
  "afregnede_opslag": 20860,
  "fejlbesked": null,
  "oprettet": "2026-10-04T14:11:31.000Z",
  "startet": "2026-10-04T14:11:32.000Z",
  "faerdig": null,
  "udloeber": null,
  "resultat": null,
  "job_fejl": null,
  "venter_paa_kapacitet": false,
  "venter_kapacitet_siden": null,
  "pauset": null,
  "raekke_fejl": {
    "total": 140,
    "pr_aarsag": { "ikke_tolket": 140 },
    "advarsler": { "total": 512, "pr_aarsag": { "flere_mulige": 380, "intet_match": 132 } },
    "daekket_raekker": 21000
  }
}
  • status — see the meaning in Follow a job and download the result.
  • fejl — number of rows without an answer. raekke_fejl breaks them (and the warnings) down per reason.
  • job_fejl — is null unless the job was stopped by an error. Then the code is in job_fejl.kode, and any reference for support in job_fejl.reference.
  • venter_paa_kapacitet — true while a running job waits with its next part because the API is busy (regular calls go first; Enterprise jobs only wait at a higher load than Pro jobs). The job waits at most about 2 minutes per part, nothing is lost, and venter_kapacitet_siden tells when the wait began.
  • pauset — when the job was paused (only with status pauset).
  • resultat — links to the result once there is something to download.

Download the result

GET /v1/jobs/{id}/resultat works when the status is faerdig — and gives the partial result when the status is pauset, fejlet, kvote_stop or annulleret. The result is streamed part by part, so even large results can be downloaded.

  • 409 — the job isn't done yet.
  • 410 — the result has expired and been deleted.

The X-Job-Status and X-Job-Raekker headers tell you which status and how many rows you got.

Examples

cURL — create a cleansing job from a CSV file, follow it and download the result:

Bash
# Create the job
curl -X POST "https://api.danskadresseapi.dk/v1/jobs?type=datavask&variant=adresser&navn=customers" \
  -H "Authorization: Bearer $ADDR_KEY" \
  -H "Content-Type: text/csv" \
  --data-binary @addresses.csv

# Follow the status (use the id from the response)
curl "https://api.danskadresseapi.dk/v1/jobs/JOB_ID" \
  -H "Authorization: Bearer $ADDR_KEY"

# Download the result as CSV
curl -o result.csv "https://api.danskadresseapi.dk/v1/jobs/JOB_ID/resultat?format=csv" \
  -H "Authorization: Bearer $ADDR_KEY"

# Download the error rows
curl -o errors.csv "https://api.danskadresseapi.dk/v1/jobs/JOB_ID/fejl?sprog=en" \
  -H "Authorization: Bearer $ADDR_KEY"

Python

Python
import os, time, requests

API = "https://api.danskadresseapi.dk"
H = {"Authorization": "Bearer " + os.environ["ADDR_KEY"]}

with open("coordinates.csv", "rb") as f:
    r = requests.post(API + "/v1/jobs", params={"type": "reverse"},
                      headers={**H, "Content-Type": "text/csv"}, data=f)
r.raise_for_status()
job = r.json()

while job["status"] in ("koe", "koerer"):
    time.sleep(3)
    job = requests.get(API + "/v1/jobs/" + job["id"], headers=H).json()

if job["job_fejl"]:
    print("Error code:", job["job_fejl"]["kode"], job["job_fejl"]["besked"])

if job["resultat"]:
    res = requests.get(API + "/v1/jobs/" + job["id"] + "/resultat",
                       params={"format": "csv"}, headers=H)
    open("result.csv", "wb").write(res.content)

JavaScript (Node 18+)

JavaScript
const API = "https://api.danskadresseapi.dk";
const H = { Authorization: "Bearer " + process.env.ADDR_KEY };

const create = await fetch(API + "/v1/jobs", {
  method: "POST",
  headers: { ...H, "Content-Type": "application/json" },
  body: JSON.stringify({
    type: "bbr",
    variant: "bygninger",
    input: ["57a791c4-25b0-4e57-8143-aa5b08efec51", "6c16b07d-acb1-414e-9d24-443340fe382e"],
  }),
});
let job = await create.json();
if (!create.ok) throw new Error(job.error.message + " (" + job.error.details?.kode + ")");

while (job.status === "koe" || job.status === "koerer") {
  await new Promise((r) => setTimeout(r, 3000));
  job = await (await fetch(API + "/v1/jobs/" + job.id, { headers: H })).json();
}

const ndjson = await (await fetch(API + "/v1/jobs/" + job.id + "/resultat", { headers: H })).text();
const rows = ndjson.trim().split("\n").map((l) => JSON.parse(l));

Instead of asking for the status again and again, you can subscribe to the job.completed (and job.failed) webhook and download the result when it arrives. See Notifications and webhooks for jobs.

Cancel a job

Bash
curl -X DELETE "https://api.danskadresseapi.dk/v1/jobs/JOB_ID" \
  -H "Authorization: Bearer $ADDR_KEY"

The job gets status annulleret, and what was processed by then can be downloaded. If the job has already ended, the API responds 409.

Pause and resume

Bash
curl -X POST "https://api.danskadresseapi.dk/v1/jobs/JOB_ID/pause" \
  -H "Authorization: Bearer $ADDR_KEY"

curl -X POST "https://api.danskadresseapi.dk/v1/jobs/JOB_ID/genoptag" \
  -H "Authorization: Bearer $ADDR_KEY"

pause works for a queued or running job: the part in progress is finished and saved, and the job gets status pauset. genoptag only works for a paused job (otherwise 409) — the quota is checked first, and the job continues exactly after the last saved row. A fejlet, kvote_stop or annulleret job is continued with Resume in the dashboard.

Synchronous batch calls and the quota

The synchronous batch endpoints (e.g. POST /v1/datavask/adresser/batch) check the quota for the whole batch before anything is processed. If there isn't room for all the elements, the API responds 429 — nothing is processed, and nothing is billed:

JSON
{
  "error": {
    "type": "quota_exceeded",
    "message": "…",
    "code": 429,
    "details": { "antal": 500, "tilbage": 120, "grund": "kvote", "upgrade_url": "https://danskadresseapi.dk/dashboard/fakturering" }
  }
}
  • details.antal — the number of elements you sent.
  • details.tilbage — how many lookups there is room for. Send a smaller batch (at most tilbage elements), upgrade, or wait until the quota resets.
  • Concurrent batch calls on the same team each reserve their room, so together they never exceed the quota. The call that doesn't get room gets 429 with details.reserveret_samtidig (lookups reserved by the other calls).
  • details.retry_after_seconds and the Retry-After header are only included when the quota resets within an hour.

rate_limit_exceeded or quota_exceeded? With rate_limit_exceeded you are sending too fast — wait and send the same call again unchanged. With quota_exceeded sending the same call again won't help until the quota resets — send a smaller batch, or upgrade.

A job behaves differently: it runs until the quota is used up, stops with status kvote_stop, and the partial result can be downloaded. That is why a job is often the better choice for large amounts.

More

Was this helpful?