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

# Create a client

> Adds a client to the customer register, with the same rules as the Envoi app. `type` is `business` (the
default) or `consumer`. A Norwegian business needs its organisation number (`orgNumber`); a business in another
country needs that country's fields.

A client that already exists (a business with the same organisation number, or a consumer with the same email)
is not created twice: the answer is `409 client-exists` with the existing client's id in `details.existingId`.



## OpenAPI

````yaml /api-reference/openapi.json post /v1/clients
openapi: 3.0.4
info:
  title: Envoi API
  description: >-
    The Envoi API lets your own systems work with an Envoi account: create and
    send invoices, record payments,

    keep the customer register in step, and read the books. An online store such
    as Nordvik Handel AS can, for example,

    create an invoice in Envoi for every B2B order from its web shop and mark it
    paid when the money arrives.


    **Base URL.** `https://api.envoi.no`. Every path starts with `/v1`.


    **Authentication.** An Owner or Admin creates an API client in Envoi under
    *Settings → API clients* (in the app: *Innstillinger → API-klienter*) and

    gets a Client ID and a client secret (shown once). Exchange them for an
    access token at `POST /v1/oauth/token`

    (OAuth 2.0 client credentials) and send it as `Authorization: Bearer
    {access_token}`. A token lives 15 minutes;

    ask for a new one when it expires. Every request is checked against Envoi's
    database, not only the token:

    deleting the client, rotating its secret or removing a scope takes effect on
    the next request.


    **Accounts and environments.** A token acts on one account, named by its
    Account ID in the token request's

    `audience`: `P11112001` is the production account, `T11112001` its test
    environment. Nothing done

    in the test environment reaches a real customer: emails are captured, not
    delivered. An id from another account, or

    from the other environment, is `404 not-found`, never `403`.


    **Scopes.** Each endpoint needs one scope, such as `invoices:read`. Levels
    nest: `admin` includes `write`,

    `write` includes `read`. A request without the scope is `403
    insufficient_scope`.


    **Errors.** Every error is JSON: `{ "code": "...", "message": "..." }`,
    sometimes with `details`, and with

    `errors` (one entry per field) on `400 validation-failed`. Branch on `code`;
    the message is for developers

    and may change. The token endpoint uses the OAuth shape `{ "error": "..." }`
    instead.


    **Idempotency.** Every POST needs an `Idempotency-Key` header. A retry with
    the same key and the same request

    gets the first answer again instead of creating a second invoice or payment.


    **Paging.** Lists take `page` (from 1) and `pageSize` (1 to 100) and answer

    `{ "items": [...], "page": 1, "pageSize": 20, "totalCount": 57, "hasMore":
    true }`.


    **Values.** JSON in camelCase. Dates are `yyyy-MM-dd`; timestamps ISO 8601
    in UTC. Money is in kroner as a

    decimal number; `currency` is always `NOK` in v1. VAT codes are the SAF-T
    standard codes (for sales:

    `3` = 25 %, `31` = 15 %, `33` = 12 %, `5` = exempt, `6` = outside the VAT
    Act, `7` = no VAT treatment, `52` = export).


    **Rate limits.** Each API client may send 600 requests a minute; the token
    endpoint allows 60 requests a minute

    per IP address and 30 per Client ID. Above that the answer is `429` with a
    `Retry-After` header.
  version: v1
servers:
  - url: https://api.envoi.no
    description: >-
      Production and test environment (the token's audience picks the
      environment)
security: []
tags:
  - name: Authentication
    description: >-
      Get an access token with the OAuth 2.0 client credentials grant, and see
      which API client and account a token belongs to.
  - name: Invoices
    description: >-
      Create draft invoices, send them, record payments, and read invoices. The
      same rules as in the Envoi app.
  - name: Clients
    description: The account's customer register.
  - name: Accounting
    description: >-
      Read-only views of the built-in books: the chart of accounts and journal
      entries. Only when Envoi's built-in accounting is turned on for the
      account.
paths:
  /v1/clients:
    post:
      tags:
        - Clients
      summary: Create a client
      description: >-
        Adds a client to the customer register, with the same rules as the Envoi
        app. `type` is `business` (the

        default) or `consumer`. A Norwegian business needs its organisation
        number (`orgNumber`); a business in another

        country needs that country's fields.


        A client that already exists (a business with the same organisation
        number, or a consumer with the same email)

        is not created twice: the answer is `409 client-exists` with the
        existing client's id in `details.existingId`.
      operationId: createClient
      parameters:
        - name: Idempotency-Key
          in: header
          description: >-
            Required on every POST, at most 255 characters. Use a new UUID for
            each logical operation. The first request with a key runs, and its
            answer (any status below 500) is stored for 7 days. A retry with the
            same key, method, path and body gets the stored answer again with
            `Idempotency-Replayed: true`, and nothing runs twice. The same key
            with a different body or path is `422 idempotency-key-reused`; the
            same key while the first request is still running is `409
            idempotency-key-in-progress`. A 5xx answer is not stored, so the
            same key may be retried. A key belongs to the API client's grant
            (one environment).
          required: true
          schema:
            maxLength: 255
            type: string
          example: 5d3f1c52-7a4e-4f7e-9f5c-1b2d3e4f5a6b
      requestBody:
        description: The client.
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateClientRequest'
            example:
              name: Fjordkaffe AS
              type: business
              orgNumber: '912345688'
              country: 'NO'
              email: post@fjordkaffe.no
              phone: +47 55 12 34 56
              address: Kaigata 1
              postalCode: '5003'
              city: Bergen
              contactPerson: Ingrid Fjeld
      responses:
        '201':
          description: The new client.
          headers:
            Idempotency-Replayed:
              description: >-
                `true` when this is the stored answer to an earlier request with
                the same `Idempotency-Key`. Absent otherwise.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Client'
              example:
                id: c2a9d4e1-5b7f-4c3a-8e6d-1f2a3b4c5d6e
                name: Fjordkaffe AS
                type: business
                orgNumber: '912345688'
                vatNumber: null
                country: 'NO'
                email: post@fjordkaffe.no
                phone: +47 55 12 34 56
                address: Kaigata 1
                postalCode: '5003'
                city: Bergen
                contactPerson: Ingrid Fjeld
                createdAt: '2026-09-01T08:30:00Z'
                updatedAt: '2026-09-01T08:30:00Z'
        '400':
          description: >-
            - `validation-failed`: a field is missing or wrong, for example
            `orgNumber` (`required`) for a Norwegian business, `type`
            (`unknown-client-type`), `name` or `email`.

            - `client-country-invalid`: `country` is not a known ISO country
            code.

            - `client-org-number-invalid`: the organisation number is not valid
            for the country.

            - `client-vat-number-too-long`: `vatNumber` is too long.

            - `invalid-client`: the client is not valid for another reason.

            - `idempotency-key-required`: the `Idempotency-Key` header is
            missing.

            - `idempotency-key-too-long`: the `Idempotency-Key` is longer than
            255 characters.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                validation-failed:
                  summary: validation-failed
                  value:
                    code: validation-failed
                    message: The request is not valid.
                    errors:
                      - field: orgNumber
                        code: required
                idempotency-key-required:
                  summary: idempotency-key-required
                  value:
                    code: idempotency-key-required
                    message: Every POST needs an Idempotency-Key header.
        '401':
          description: >-
            - `unauthorized`: no token, an invalid or expired one, or a token
            the per-request check refuses (the client or its grant was deleted,
            the secret was rotated, or the account is being closed). Header
            `WWW-Authenticate: Bearer error="invalid_token"`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                unauthorized:
                  summary: unauthorized
                  value:
                    code: unauthorized
                    message: A valid API client access token is required.
        '403':
          description: >-
            - `insufficient_scope`: the grant does not hold `clients:write` (or
            a higher level of it).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                insufficient_scope:
                  summary: insufficient_scope
                  value:
                    code: insufficient_scope
                    message: This API client needs clients:write.
        '409':
          description: >-
            - `client-exists`: the client is already in the register;
            `details.existingId` is its id.

            - `client-duplicate-vat-number`: a client with this VAT number is
            already in the register; `details.existingId` is its id.

            - `idempotency-key-in-progress`: a request with this
            `Idempotency-Key` is still running. Retry after `Retry-After`
            seconds.
          headers:
            Retry-After:
              description: >-
                Sent with `idempotency-key-in-progress`: seconds to wait before
                retrying.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                client-exists:
                  summary: client-exists
                  value:
                    code: client-exists
                    message: A client with this identity already exists.
                    details:
                      existingId: c2a9d4e1-5b7f-4c3a-8e6d-1f2a3b4c5d6e
                idempotency-key-in-progress:
                  summary: idempotency-key-in-progress
                  value:
                    code: idempotency-key-in-progress
                    message: >-
                      A request with this Idempotency-Key is still being
                      processed.
        '413':
          description: >-
            - `payload-too-large`: the body is larger than 1 MB. The key is not
            used up.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                payload-too-large:
                  summary: payload-too-large
                  value:
                    code: payload-too-large
                    message: The request body is larger than 1 MB.
        '422':
          description: >-
            - `idempotency-key-reused`: this `Idempotency-Key` was already used
            for a different request (another body or path).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                idempotency-key-reused:
                  summary: idempotency-key-reused
                  value:
                    code: idempotency-key-reused
                    message: >-
                      This Idempotency-Key was already used for a different
                      request.
        '429':
          description: >-
            - `rate_limited`: this API client sent too many requests. Wait the
            number of seconds in `Retry-After`.
          headers:
            Retry-After:
              description: Seconds to wait before the next request.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                rate_limited:
                  summary: rate_limited
                  value:
                    code: rate_limited
                    message: Too many requests; try again later.
        '500':
          description: >-
            - `server-error`: something unexpected went wrong. The message never
            contains details. On a POST the `Idempotency-Key` is released, so
            retrying with the same key is safe.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                server-error:
                  summary: server-error
                  value:
                    code: server-error
                    message: >-
                      The request could not be completed. It is safe to retry
                      with the same Idempotency-Key.
      security:
        - oauth2:
            - clients:write
        - bearerAuth: []
components:
  schemas:
    CreateClientRequest:
      required:
        - name
      type: object
      properties:
        name:
          type: string
          description: The client's name. Required.
        type:
          type: string
          description: '`business` (the default) or `consumer`.'
          nullable: true
        orgNumber:
          type: string
          description: Organisation number. Required for a Norwegian business (9 digits).
          nullable: true
        vatNumber:
          type: string
          description: A foreign client's VAT number.
          nullable: true
        country:
          type: string
          description: ISO 3166-1 alpha-2 country code; `NO` when left out.
          nullable: true
        email:
          type: string
          description: Email address.
          nullable: true
        phone:
          type: string
          description: Phone number.
          nullable: true
        address:
          type: string
          description: Street address.
          nullable: true
        postalCode:
          type: string
          description: Postal code.
          nullable: true
        city:
          type: string
          description: City.
          nullable: true
        contactPerson:
          type: string
          description: Contact person.
          nullable: true
      description: A new client.
    Client:
      required:
        - address
        - city
        - contactPerson
        - country
        - createdAt
        - email
        - id
        - name
        - orgNumber
        - phone
        - postalCode
        - type
        - updatedAt
      type: object
      properties:
        id:
          type: string
          description: The client's id.
          format: uuid
        name:
          type: string
          description: The client's name.
        type:
          type: string
          description: '`business` or `consumer`.'
        orgNumber:
          type: string
          description: Organisation number; empty when none.
        vatNumber:
          type: string
          description: A foreign client's VAT number, or null.
          nullable: true
        country:
          type: string
          description: ISO 3166-1 alpha-2 country code, for example `NO`.
        email:
          type: string
          description: Email address; empty when none.
        phone:
          type: string
          description: Phone number; empty when none.
        address:
          type: string
          description: Street address.
        postalCode:
          type: string
          description: Postal code.
        city:
          type: string
          description: City.
        contactPerson:
          type: string
          description: Contact person; empty when none.
        createdAt:
          type: string
          description: When it was created (UTC).
          format: date-time
        updatedAt:
          type: string
          description: When it last changed (UTC).
          format: date-time
      description: A client in the account's customer register.
    Error:
      required:
        - code
        - message
      type: object
      properties:
        code:
          type: string
          description: >-
            A stable, machine-readable code, for example `not-found` or
            `insufficient_scope`. Branch on this, never on the message.
        message:
          type: string
          description: >-
            A short English sentence for a developer. Never contains customer
            data and may change.
        details:
          type: object
          additionalProperties: {}
          description: >-
            Extra facts for some codes, for example `existingId` on
            `client-exists`, `outstanding` on `payment-exceeds-outstanding`,
            `maxMonths` on `period-too-long`. Absent otherwise.
          nullable: true
        errors:
          type: array
          items:
            $ref: '#/components/schemas/FieldError'
          description: >-
            Present only on `validation-failed`: one entry per field that
            failed.
          nullable: true
      description: The error body of every `/v1` endpoint except the token endpoint.
    FieldError:
      required:
        - code
        - field
      type: object
      properties:
        field:
          type: string
          description: >-
            The field's path in the request, for example `dueDate` or
            `lines[0].quantity`.
        code:
          type: string
          description: A stable kebab-case code, for example `required` or `greater-than`.
      description: A field that failed validation.
  securitySchemes:
    oauth2:
      type: oauth2
      description: >-
        OAuth 2.0 client credentials. The token request also needs the
        `audience` form field (the Account ID), which OAuth tooling does not
        always send; see `POST /v1/oauth/token`.
      flows:
        clientCredentials:
          tokenUrl: https://api.envoi.no/v1/oauth/token
          scopes:
            bilag:read: >-
              Read access to the books (the chart of accounts and journal
              entries).
            bilag:write: >-
              Write access to the books (the chart of accounts and journal
              entries). Includes bilag:read.
            clients:admin: Admin access to clients. Includes clients:write and clients:read.
            clients:read: Read access to clients.
            clients:write: Write access to clients. Includes clients:read.
            invoices:admin: >-
              Admin access to invoices. Includes invoices:write and
              invoices:read.
            invoices:read: Read access to invoices.
            invoices:write: Write access to invoices. Includes invoices:read.
    bearerAuth:
      type: http
      description: >-
        The `access_token` from `POST /v1/oauth/token`, as `Authorization:
        Bearer {access_token}`. It lives 15 minutes.
      scheme: bearer
      bearerFormat: JWT

````