> ## 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.

# Get an access token

> OAuth 2.0 client credentials (RFC 6749 section 4.4). Send the Client ID and client secret with HTTP Basic
(`Authorization: Basic base64(client_id:client_secret)`, each part form-url-encoded first) **or** as
`client_id` and `client_secret` in the form, not both. The body is always a form
(`application/x-www-form-urlencoded`).

`audience` names the account: `https://api.envoi.no/v1/accounts/P11112001` for production or
`https://api.envoi.no/v1/accounts/T11112001` for its test environment. The client needs an active grant for that
environment.

The token lives 15 minutes (`expires_in`). There is no refresh token: ask again with the same credentials.
Errors use the OAuth shape `{ "error": "..." }`.



## OpenAPI

````yaml /api-reference/openapi.json post /v1/oauth/token
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/oauth/token:
    post:
      tags:
        - Authentication
      summary: Get an access token
      description: >-
        OAuth 2.0 client credentials (RFC 6749 section 4.4). Send the Client ID
        and client secret with HTTP Basic

        (`Authorization: Basic base64(client_id:client_secret)`, each part
        form-url-encoded first) **or** as

        `client_id` and `client_secret` in the form, not both. The body is
        always a form

        (`application/x-www-form-urlencoded`).


        `audience` names the account:
        `https://api.envoi.no/v1/accounts/P11112001` for production or

        `https://api.envoi.no/v1/accounts/T11112001` for its test environment.
        The client needs an active grant for that

        environment.


        The token lives 15 minutes (`expires_in`). There is no refresh token:
        ask again with the same credentials.

        Errors use the OAuth shape `{ "error": "..." }`.
      operationId: createToken
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/TokenRequest'
            example:
              grant_type: client_credentials
              audience: https://api.envoi.no/v1/accounts/P11112001
        required: true
      responses:
        '200':
          description: The access token.
          headers:
            Cache-Control:
              description: Always `no-store`.
              schema:
                type: string
            Pragma:
              description: Always `no-cache`.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
              example:
                access_token: >-
                  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzZjBjNmMyZSJ9.c2lnbmF0dXJl
                token_type: Bearer
                expires_in: 900
                scope: bilag:read clients:write invoices:write reports:read
        '400':
          description: >-
            - `invalid_request`: the body is not a form, or a `client_secret` is
            in the form next to an HTTP Basic header.

            - `unsupported_grant_type`: `grant_type` is not
            `client_credentials`.

            - `invalid_target`: the `audience` is malformed, names another
            account, or names an environment the client holds no active grant
            for.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthError'
              examples:
                invalid_target:
                  summary: invalid_target
                  value:
                    error: invalid_target
                unsupported_grant_type:
                  summary: unsupported_grant_type
                  value:
                    error: unsupported_grant_type
        '401':
          description: >-
            - `invalid_client`: unknown client, wrong secret, or a deleted
            client. The same answer for all three.
          headers:
            WWW-Authenticate:
              description: '`Basic realm="envoi"`.'
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthError'
              examples:
                invalid_client:
                  summary: invalid_client
                  value:
                    error: invalid_client
        '403':
          description: '- `access_denied`: the account is being closed; no new tokens.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthError'
              examples:
                access_denied:
                  summary: access_denied
                  value:
                    error: access_denied
        '429':
          description: >-
            - `rate_limited`: too many token requests (60 a minute per IP
            address, 30 a minute per Client ID). 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/OAuthError'
              examples:
                rate_limited:
                  summary: rate_limited
                  value:
                    error: rate_limited
      security:
        - clientBasic: []
        - {}
components:
  schemas:
    TokenRequest:
      required:
        - audience
        - grant_type
      type: object
      properties:
        grant_type:
          type: string
          description: Always `client_credentials`.
        audience:
          type: string
          description: >-
            The account the token is for:
            `https://api.envoi.no/v1/accounts/{accountId}`. The Account ID's
            prefix picks the environment: `P` is production, `T` is the test
            environment.
        client_id:
          type: string
          description: The Client ID, when the credentials are not sent with HTTP Basic.
          nullable: true
        client_secret:
          type: string
          description: >-
            The client secret, when the credentials are not sent with HTTP
            Basic. Refused next to an HTTP Basic header.
          nullable: true
      description: The form the token endpoint reads (`application/x-www-form-urlencoded`).
    TokenResponse:
      required:
        - access_token
        - expires_in
        - scope
        - token_type
      type: object
      properties:
        access_token:
          type: string
          description: 'The bearer token. Send it as `Authorization: Bearer {access_token}`.'
        token_type:
          type: string
          description: Always `Bearer`.
        expires_in:
          type: integer
          description: >-
            Seconds until the token expires (900, 15 minutes). There is no
            refresh token: ask for a new one with the same credentials.
          format: int32
        scope:
          type: string
          description: The grant's scopes, space separated.
      description: An access token.
    OAuthError:
      required:
        - error
      type: object
      properties:
        error:
          type: string
          description: The OAuth error code.
      description: The token endpoint's error body (RFC 6749 section 5.2).
  securitySchemes:
    clientBasic:
      type: http
      description: >-
        For the token endpoint only: the Client ID and client secret as HTTP
        Basic (each part form-url-encoded first, RFC 6749 section 2.3.1).
      scheme: basic

````