Skip to main content
This walkthrough uses 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 the org_… segment from its URL as the organization ID.
The response identifies the organization and reports configured and effective levels under 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:
Both require Read access. Add 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 returns 201 and a Location header. No idempotency key is required.
A new invoice returns 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

The master record now says Acme Consulting AS in Bergen; the invoice still says Acme AS in Oslo. The recipient master record has also changed; the invoice retains its original recipient snapshot. New invoices created with these IDs use the updated master records. PATCH preserves omitted fields and address members; null clears optional values, and 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, and search to match an invoice number or title. For example, list this supplier’s September invoices, newest issue date first:
Both dates are inclusive. Omit either date for an open-ended range. If 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

Detail contains relationship counts; lines and documents are separate paginated collections. Party fields are invoice snapshots. Their IDs identify canonical parties, while their names and addresses describe the invoice as captured. PATCH sends only changed invoice fields. If you supply 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:
The upload authorization creates a Document placeholder, not an import. Send bytes to the returned signed URL using every returned header. Do not send your API bearer token to storage. An upload replay can return 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. The Location header names the import. 200 with 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.
For an accepted response, poll its run every few seconds using the returned 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

Lists return { 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.