> ## Documentation Index
> Fetch the complete documentation index at: https://docs.watchdog.no/llms.txt
> Use this file to discover all available pages before exploring further.

# Upload an invoice

> Submit a `primary` invoice document and optional `attachments` as `multipart/form-data`.

Files (primary and attachments) must be PDF, **Peppol BIS Billing 3.0** (UBL 2.1 — root `Invoice` or `CreditNote`), or **Finvoice 3.0** (root `Finvoice`). XML is matched on the root element name only; documents are not schema-validated. If any file fails validation, no invoice is created.

Deduplicated by primary-document MD5 within the organization. A replay returns 409 `duplicate_invoice`, or `primary_md5_locked_by_deleted_invoice` if the original was soft-deleted (an admin must hard-delete to free the MD5).

New invoices also require current processing capacity. A 403 `invoice_processing_capacity_exhausted` means no invoice or documents were retained; retry only after organization capacity may have changed. A temporary admission-coordination failure returns 503 `service_unavailable` with `Retry-After: 1`.

The 201 includes a `Location` header at `/v1/invoices/{invoice_id}`. Poll that resource with an `invoices:read` token and respect its `Retry-After` header while either processing status is `pending` or `processing`.



## OpenAPI

````yaml https://api.watchdog.no/doc post /v1/invoices/upload
openapi: 3.1.0
info:
  title: Watchdog API
  version: 0.0.0
  description: >-
    Watchdog public API. Server-to-server document upload and related
    operations.


    ## Authentication


    All `/v1/*` endpoints require a bearer token in the `Authorization` header
    (e.g. `Authorization: Bearer wdg_<your-token>`).


    **Generate a token:** sign in at [app.watchdog.no](https://app.watchdog.no),
    open **Settings → API keys**, and click **Create new key** (admins only).
    The plaintext token is shown **once** at creation and cannot be recovered —
    copy it into a secrets manager immediately. If lost, revoke and replace.


    Tokens carry one or more scopes: `invoices:write` for uploads and
    `invoices:read` for polling. New integration keys receive both
    automatically. The required scope per endpoint is listed under
    "Authorizations" below.
servers: []
security: []
paths:
  /v1/invoices/upload:
    post:
      tags:
        - invoices
      summary: Upload an invoice
      description: >-
        Submit a `primary` invoice document and optional `attachments` as
        `multipart/form-data`.


        Files (primary and attachments) must be PDF, **Peppol BIS Billing 3.0**
        (UBL 2.1 — root `Invoice` or `CreditNote`), or **Finvoice 3.0** (root
        `Finvoice`). XML is matched on the root element name only; documents are
        not schema-validated. If any file fails validation, no invoice is
        created.


        Deduplicated by primary-document MD5 within the organization. A replay
        returns 409 `duplicate_invoice`, or
        `primary_md5_locked_by_deleted_invoice` if the original was soft-deleted
        (an admin must hard-delete to free the MD5).


        New invoices also require current processing capacity. A 403
        `invoice_processing_capacity_exhausted` means no invoice or documents
        were retained; retry only after organization capacity may have changed.
        A temporary admission-coordination failure returns 503
        `service_unavailable` with `Retry-After: 1`.


        The 201 includes a `Location` header at `/v1/invoices/{invoice_id}`.
        Poll that resource with an `invoices:read` token and respect its
        `Retry-After` header while either processing status is `pending` or
        `processing`.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/UploadInvoiceRequest'
      responses:
        '201':
          description: Invoice created and queued for processing.
          headers:
            Location:
              schema:
                type: string
              description: >-
                URL of the pollable invoice resource
                (`/v1/invoices/{invoice_id}`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadInvoiceResponse'
        '400':
          description: >-
            Invalid multipart body, or a per-file validation failed. Per-file
            errors include `field` and `filename` so the client can pinpoint the
            offending file.
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorBody_invalid_request'
                  - $ref: '#/components/schemas/ErrorBody_too_many_attachments'
                  - $ref: '#/components/schemas/ErrorBody_per_file_validation'
        '401':
          description: >-
            Authentication failed — missing, malformed, unknown, or revoked
            token. See `WWW-Authenticate` header per RFC 6750.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody_invalid_token'
        '403':
          description: >-
            The token is disabled or lacks the required scope, or current
            organization capacity forbids accepting another invoice.
            `invoice_processing_capacity_exhausted` retains no invoice or
            documents; retry only after organization capacity may have changed.
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorBody_token_disabled'
                  - $ref: '#/components/schemas/ErrorBody_insufficient_scope'
                  - $ref: >-
                      #/components/schemas/ErrorBody_invoice_processing_capacity_exhausted
        '409':
          description: >-
            The primary document duplicates an existing or soft-deleted invoice.
            Both codes include `existing_invoice_id`.
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorBody_duplicate_invoice'
                  - $ref: >-
                      #/components/schemas/ErrorBody_primary_md5_locked_by_deleted_invoice
        '411':
          description: '`Content-Length` header is required for this endpoint.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody_length_required'
        '413':
          description: Request body exceeds the per-file or combined-body cap.
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorBody_payload_too_large'
                  - $ref: '#/components/schemas/ErrorBody_per_file_validation'
        '415':
          description: >-
            Unsupported MIME type, or the primary file failed magic-byte
            validation.
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/ErrorBody_unsupported_media_type'
                  - $ref: '#/components/schemas/ErrorBody_per_file_validation'
        '429':
          description: >-
            Rate limit exceeded. Pre-auth IP cap is 300 req/min; per-token cap
            is 120 req/min. Response headers indicate when the caller may retry.
          headers:
            Retry-After:
              description: >-
                Seconds until the caller may retry. Only present on 429
                responses.
              schema:
                type: string
                example: '34'
            X-RateLimit-Limit:
              description: Maximum requests allowed in the current window.
              schema:
                type: string
                example: '300'
            X-RateLimit-Remaining:
              description: Requests remaining in the current window.
              schema:
                type: string
                example: '0'
            X-RateLimit-Reset:
              description: >-
                Unix timestamp in seconds when the current window resets. Divide
                by 1000 if you have a millisecond-precision clock.
              schema:
                type: string
                example: '1713793234'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody_rate_limit_exceeded'
        '500':
          description: Internal error processing the upload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody_internal_error'
        '503':
          description: >-
            Invoice admission coordination is temporarily unavailable. The same
            request may be retried shortly.
          headers:
            Retry-After:
              schema:
                type: string
                example: '1'
              description: Retry delay in seconds for this temporary coordination failure.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody_service_unavailable'
      security:
        - BearerAuth:
            - invoices:write
components:
  schemas:
    UploadInvoiceRequest:
      type: object
      required:
        - primary
      properties:
        primary:
          type: string
          format: binary
          description: Primary invoice document. Max 50 MB.
        attachments:
          type: array
          items:
            type: string
            format: binary
          description: >-
            Optional supporting documents (0–9). Max 25 MB each. Combined body
            (primary + attachments) max 100 MB.
    UploadInvoiceResponse:
      type: object
      properties:
        invoice_id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - queued
        documents:
          type: object
          properties:
            primary:
              $ref: '#/components/schemas/UploadInvoiceDocumentRef'
            attachments:
              type: array
              items:
                $ref: '#/components/schemas/UploadInvoiceDocumentRef'
          required:
            - primary
            - attachments
        created_at:
          type: string
          format: date-time
      required:
        - invoice_id
        - status
        - documents
        - created_at
    ErrorBody_invalid_request:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - invalid_request
            message:
              type: string
            description:
              type: string
            request_id:
              type: string
          required:
            - code
            - message
            - request_id
      required:
        - error
    ErrorBody_too_many_attachments:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - too_many_attachments
            message:
              type: string
            description:
              type: string
            request_id:
              type: string
          required:
            - code
            - message
            - request_id
      required:
        - error
    ErrorBody_per_file_validation:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - invalid_xml_invoice
                - unnamed_file
                - unsupported_media_type
                - file_too_small
                - payload_too_large
            message:
              type: string
            description:
              type: string
            request_id:
              type: string
            field:
              type: string
            filename:
              type: string
          required:
            - code
            - message
            - request_id
            - field
            - filename
      required:
        - error
    ErrorBody_invalid_token:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - invalid_token
            message:
              type: string
            description:
              type: string
            request_id:
              type: string
          required:
            - code
            - message
            - request_id
      required:
        - error
    ErrorBody_token_disabled:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - token_disabled
            message:
              type: string
            description:
              type: string
            request_id:
              type: string
          required:
            - code
            - message
            - request_id
      required:
        - error
    ErrorBody_insufficient_scope:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - insufficient_scope
            message:
              type: string
            description:
              type: string
            request_id:
              type: string
          required:
            - code
            - message
            - request_id
      required:
        - error
    ErrorBody_invoice_processing_capacity_exhausted:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - invoice_processing_capacity_exhausted
            message:
              type: string
            description:
              type: string
            request_id:
              type: string
          required:
            - code
            - message
            - request_id
      required:
        - error
    ErrorBody_duplicate_invoice:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - duplicate_invoice
            message:
              type: string
            description:
              type: string
            request_id:
              type: string
            existing_invoice_id:
              type: string
              format: uuid
          required:
            - code
            - message
            - request_id
            - existing_invoice_id
      required:
        - error
    ErrorBody_primary_md5_locked_by_deleted_invoice:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - primary_md5_locked_by_deleted_invoice
            message:
              type: string
            description:
              type: string
            request_id:
              type: string
            existing_invoice_id:
              type: string
              format: uuid
          required:
            - code
            - message
            - request_id
            - existing_invoice_id
      required:
        - error
    ErrorBody_length_required:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - length_required
            message:
              type: string
            description:
              type: string
            request_id:
              type: string
          required:
            - code
            - message
            - request_id
      required:
        - error
    ErrorBody_payload_too_large:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - payload_too_large
            message:
              type: string
            description:
              type: string
            request_id:
              type: string
          required:
            - code
            - message
            - request_id
      required:
        - error
    ErrorBody_unsupported_media_type:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - unsupported_media_type
            message:
              type: string
            description:
              type: string
            request_id:
              type: string
          required:
            - code
            - message
            - request_id
      required:
        - error
    ErrorBody_rate_limit_exceeded:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - rate_limit_exceeded
            message:
              type: string
            description:
              type: string
            request_id:
              type: string
          required:
            - code
            - message
            - request_id
      required:
        - error
    ErrorBody_internal_error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - internal_error
            message:
              type: string
            description:
              type: string
            request_id:
              type: string
          required:
            - code
            - message
            - request_id
      required:
        - error
    ErrorBody_service_unavailable:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - service_unavailable
            message:
              type: string
            description:
              type: string
            request_id:
              type: string
          required:
            - code
            - message
            - request_id
      required:
        - error
    UploadInvoiceDocumentRef:
      type: object
      properties:
        document_id:
          type: string
          format: uuid
        file_name:
          type: string
        mime_type:
          type: string
          enum:
            - application/pdf
            - application/xml
            - text/xml
      required:
        - document_id
        - file_name
        - mime_type
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: >-
        API key from Settings → API Keys. Plaintext is shown once at creation
        and cannot be recovered. Header: `Authorization: Bearer wdg_<token>`.

````