curl and jq with the API preview.
Set API_URL to your development environment; the examples use a local Worker at port 3500.
These endpoints are under development and are separate from the current API at api.watchdog.no.
Connect and check access
In the development app, open Settings → Personal → API keys → Create API key. Choose Write for this walkthrough and select the organization you will use. Read is enough for reporting, selectors, filters, related documents, downloads, and workflow status. Admin also includes Write and supports organization-admin operations. Copy the token when it is shown. Open your organization in the app and use theorg_… segment from
its URL as the organization ID.
access.
Use GET /v1/me without an organization header to inspect caller identity. Compare it with the saved key details after reloading settings.
An Admin key is limited to Write where you are a member. Every organization-scoped personal-key request requires
X-Organization-Id; a supplied header for a Clerk session must match its active organization.
Migrated organization keys can omit the header only to use their original organization.
All-organization access follows your current and future memberships. Selected access remains limited
to the chosen organizations. Membership and role changes apply after Clerk webhook synchronization. Key revocation is immediate. A missing permission or
unauthorized organization returns 403; a revoked key returns 401. See
authentication and API keys for role limits and account requirements.
This walkthrough covers invoices, documents, supplier/recipient and team selectors, and workflow runs.
Use the agreement walkthrough to manage agreements, prices, and source links
with the same Write key. Alert and compliance-check execution endpoints remain deferred.
List existing suppliers and recipients
Use List suppliers and List recipients to find existing parties:search=Acme to filter names case-insensitively. Results are
ordered by name and ID, with 50 records per page by default and a maximum of 100. Each response
contains compact party summaries in data and a next_cursor; pass that value as cursor with
the same search to continue. A null cursor means the final page. Retrieve a party by ID for its
full master record.
Create a structured invoice
For an organization with no parties, create a supplier and recipient first. Each create returns201 and a Location header. No idempotency key is required.
201 and its detail. An invoice with the same business identity returns
409 without creating another invoice. Validation failures return 400 without committing
anything. To recover the original response after a timeout, optionally include an
Idempotency-Key.
Amounts, quantities, and rates are decimal strings. Rates are fractions: "0.25" means 25%.
Dates are YYYY-MM-DD. Unknown values are null. Do not send computed financially_valid,
financial_errors, origin, or original source values in create/edit inputs. Structured invoices
must pass the documented financial checks, with a tolerance of 1 currency unit.
Maintain a canonical party
address: null clears the whole address. Unchanged normalized
values preserve timestamps. Names must contain a letter or digit and be at most 500 characters,
emails valid, websites absolute HTTP(S) URLs,
and country codes two letters. Unknown optional values are null.
Organization numbers remain optional. Recognized country-specific formats and checksums are validated;
valid identifiers are stored canonically, preserving leading zeros and identifier families.
For example, NO123456785MVA becomes 123456785 / NO, FI19675438 becomes 19675438 / FI,
and PL5851101301 becomes 5851101301 / PL.
Set org_country explicitly when known. An existing or explicitly supplied registration country is
authoritative; a conflicting recognized prefix is rejected. For a missing country, the API validates
a recognized prefix first. A failed prefix stops inference. Only without a recognized prefix does it
try the address country. Country is inferred only when that country’s checksum passes.
Unsupported or format-only identifiers receive basic input checks without country inference.
Validation does not verify registry existence.
Known registration countries do not change on address edits. A missing country may be filled when
the number or address country changes and validation succeeds. Explicit org_country: null clears it
and suppresses inference for that write; later validated ingestion may refill it. Contact-only edits
preserve unchanged legacy identifiers. Clearing the number does not clear registration country.
Duplicate normalized organization numbers, or duplicate normalized names among unnumbered parties, return 409 conflict, with
error.existing_party.type and .id when a conflicting record is available to you. Retrieve that
record before deciding which ID to use; the API never merges parties. Validation returns 400.
Canonical edits do not rewrite invoice evidence, invalidate snapshot-based assessments, or start
matching/compliance work.
Find invoices
Use canonical party IDs to filter invoices, andsearch to match an invoice number or title.
For example, list this supplier’s September invoices, newest issue date first:
next_cursor is not null,
repeat the request with the same filters and sort, adding --data-urlencode "cursor=$NEXT_CURSOR",
where NEXT_CURSOR is the returned value. Stop when it is null.
GET /v1/invoices/metrics accepts the same filters and returns counts and totals by currency;
omit pagination and sort parameters. To filter by team, first list /v1/teams, then pass its ID
as team_ids. Read access includes both party and team filters.
Read and edit
line_items, send the complete resulting
collection, retaining IDs of existing lines. Omitted lines are removed; lines without IDs are
added. Financial changes must leave the complete invoice financially valid. A failed edit rolls
back every supplied change. Metadata-only edits can leave an imported financial inconsistency in
place. Editing does not refresh compliance analysis or imply approval.
DELETE /v1/invoices/{id} soft-deletes an invoice. POST /v1/invoices/{id}/restore restores it.
Both are repeatable and return the invoice detail.
Upload a file and import it
Use a PDF or XML invoice file on your computer. The example below uses XML:412 because the object already exists; admission verifies its bytes.
There is no upload completion call. For a PDF, use its filename and application/pdf; actual
content is inspected by the workflow. Add already-uploaded, distinct Document IDs in
attachment_document_ids, in the order you want retained. Each file must be at least 100 bytes;
primaries are at most 50 MiB and each attachment at most 25 MiB, with no attachment-count or combined-size cap.
PDF-primary extraction still has model input limits. If the combined content exceeds those limits,
the import fails with validation_error and stops automatic retries. All uploaded Documents remain
available; no attachments are silently omitted. Submit a smaller source set as a new import.
To attach an uploaded Document to an existing invoice, use the Document commands.
Handle both admission responses:
202 { "outcome": "accepted", "import_id": "…", "workflow_run_id": "…", "workflow_run_item_id": "…" }: processing is admitted. TheLocationheader names the import.200with this shape is an idempotent replay.200 { "outcome": "duplicate", "invoice_id": "…" }: the primary file already belongs to a published invoice. Read that invoice; no import or workflow was created.
workflow_run_id:
queued and running are in progress. After completed, GET
/v1/workflow-runs/$RUN_ID/items for item outcomes and invoice references. A single import has
one item; admission also returns workflow_run_item_id for its direct item URL. Read
/v1/invoices/imports/$IMPORT_ID for current import details, sources, and evidence. Terminal import
results are:
An import can complete successfully with
financially_valid: false on its invoice. This retains
the source’s inconsistent amounts and concrete financial errors for correction. It is different
from unusable extraction, which fails the import. Detailed extraction confidence remains in import
evidence.
POST /v1/invoices/imports/{id}/cancel cancels queued/running work; repeating cancellation
succeeds. POST /v1/invoices/imports/{id}/retry starts a new run for a
failed/cancelled import. Retrying unchanged malformed files will fail again. A missing or changed
source requires a new Document and import. Replaying an earlier admission refers to its original
execution; always read the import for its current run and status.
GET /v1/workflow-runs?status=queued,running discovers active work. Poll known IDs together using
GET /v1/workflow-runs?ids=<id>,<id>&limit=100 without a status filter to observe completion.
See the workflow-run guide for history, cancellation, results, and polling rules.
GET /v1/invoices/imports lists imports. Read /v1/invoices/{id}/documents for source Document IDs,
then authorize a download with GET /v1/documents/{id}/download. Fetch its short-lived download_url
without your API bearer token. The same Read access covers invoice and agreement source downloads.
Lists, metrics, and errors
{ data, next_cursor }. Send next_cursor as cursor with the same filters and sort;
stop when it is null. Defaults are 50 records, sort=created_at, and direction=desc; the maximum
limit is 100. Null sort values come last. Active invoices are the default; deleted=true selects
only deleted invoices. Comma-separated filters and all five sorts are listed in OpenAPI.
Use GET /v1/invoices/metrics with the same filters, omitting pagination and sort. Monetary totals
are grouped by currency; never add amounts in different currencies together.
Errors include { error: { code, message, request_id } }. Validation failures also contain bounded
details when available. Keep request_id when reporting a problem. Check error.code as well as
HTTP status: 409 source_unavailable requires checking files, while 409 conflict can describe
idempotency, capacity, or incompatible state. For 429, wait for Retry-After. A 503 import
admission can include error.import_id; poll that retained import before deciding to retry.