1. Introduction

The REST API to be used with FreeFinance and FinanzFenster. Please read the examples for an introduction to authentication and some use cases.

Given the large number of available endpoints, they are not listed in this documentation. Instead, the OpenAPI specification file (version 3.1) is available as YAML or JSON.

The format of your choice can be imported into most API clients or the Swagger Editor Beta. You can also use it to generate the request and response models for a programming language of your choice.

1.1. Basics

While a user authorizes itself against the API, it is not the organizational entity that holds all the data. A bookkeeping instance (such as a company, rent and leasing, or a freelancer) is represented as a client. A single user has access to one or several clients, and one client can be accessed by one or several users. Each user has one or more assigned roles for a client, which define the permissions the user has for that client. The initially created user might have all permissions available, while a special employee user might only be able to access specific modules.

Most example responses given in this documentation are shortened for brevity, i.e. they don’t contain all properties that are returned. Refer to the OpenAPI spec files linked above for the complete schema of responses.

1.2. Domains

For testing purposes, we offer two environments to develop against which are separated from production environments:

  • Demo (https://demo.freefinance.at) is a production-like environment that is actively used for demonstrations, trials of the application, and education purposes. It is equal or very close to the production release of the application, is completely separated from production users, and data is persisted on a best-effort basis.

  • QA (https://app.qa.freefinance.at/) is a testing environment that is used to test an upcoming application version still in development and testing. It may be unstable, contain errors, and no guarantees are made regarding data persistence; it can be reset at any time. Using the QA is recommended when you need to prepare for still-unreleased APIs or test your integrations against upcoming releases.

Once your application integration is ready for production, you can set the URL to the production environment, which depends on the specific portal of your user. Different portals exist for different partnerships, but for convenience every region (Austria, Germany, International, and so on, distinguished by a different top-level domain) has a single API URL. For example, the production URL https://app.freefinance.at in the Austrian region has many Austrian sub-portals, but the URL https://api.freefinance.at is available for users in all portals in the Austrian region. This allows application developers to use the API regardless of the specific portal of the user.
The same is true for https://app.freefinance.de and https://api.freefinance.de, and so on.

If you access only your own user and client, you can use the URL you are already familiar with, like http://app.freefinance.at, where the API is also accessible.

2. Authentication

The v2 API expects Bearer authentication as header parameter in every authenticated request.

Example request that includes the Bearer token
GET https://api.freefinance.at/api/2.0/clients
Authorization: Bearer MY-TOKEN

Within the v2 API, "authenticated request" encompasses all resources that require a valid user authenticated by the IDP. A number of unauthenticated resources exist to provide general information about the portal, such as the authentication info resource that returns the IDP of the application.

Example unauthenticated request
GET https://api.freefinance.at/api/2.0/auth/issuer
Example unauthenticated response
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
{
  "url": "https://accounts.freefinance.at/auth/realms/demo",
  "realm": "demo"
}
While unauthenticated resources don’t require the Bearer token to be provided, they still validate the token if one is sent - if the token is invalid or expired, the request will be denied.

Valid tokens can be requested by the IDP URL returned in the /auth/issuer response. The IDP expects authentication using the OpenID Connect (OIDC) standard.

Two different methods exist to authenticate. Which one you choose depends on your use case and the type of your application.

In the following documentation, we distinguish between the terms client (referencing a bookkeeping client your user has access to) and API client (an instance of an API caller, like your webshop, authenticating itself). OpenID Connect has no concept of bookkeeping clients, so any third-party documentation related to OpenID Connect you might look up uses the term client for what is defined as API client here.

2.1. OIDC Client Credentials with Technical User

A user with the permission to connect external accounts to a client can create a technical user for that client. This is a separate, special user with its own fixed role tied to the client, and comes with a set of API client credentials to authenticate directly.

This approach is recommended in the following cases:

  • Your application only needs to connect to a few clients at most. Examples include your own web shop or a script running on a trusted computer accessing your client.

  • You don’t need to tie this API access to your (or any) user.

  • For accessing multiple clients, it is acceptable to store a separate set of API credentials for each.

This approach is not recommended in these cases:

  • You wish to provide a general service offered to users of FreeFinance, with your application being granted access by the user without having to copy credentials.

  • Your application can handle URL redirects and can use the OIDC standard Authorization Code flow.

A technical user can be created in the web UI:
Navigate to My profileUser & PermissionConnected devices and choose the "Create technical user" option in the sidebar.

The technical user has no password and can’t be used to log in to the application over the UI.

You will be given an API client id and API client secret. Store them responsibly like you would a password - they grant access to your client with all the permissions given to the "API user" role. If possible, restrict the permissions of that user to the minimum required for your use case.

Dont' confuse the API client_id, which belongs to the technical user and is part of the id/secret pair for authentication, with the client ID, which is the numerical number of your bookkeeping client. Refer to the Clients section for further clarification.

To change the permissions of the API user, you can use the UI and navigate to My ProfileUser & PermissionPermissions and select the Technical User role. Revoke all permissions not needed for your use case.

You can now obtain a valid token from the IDP, which is a different URL than the API URL. You can request the IDP URL from the API by calling /auth/issuer, and then appending the OIDC path (/protocol/openid-connect/token) to the returned URL for the full path.

The payload is in URl-encoded form with parameters as key-value pairs separated by "&". Supply your API client ID (client_id) and secret (client_secret) this way, and the grant_type set to the fixed value client_credentials.

Example token request with technical user credentials
POST https://accounts.freefinance.at/auth/realms/demo/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials &
client_id=technical_api_user_XXXXX_XXXXXXXXXX &
client_secret=abc...xyz
Example token response with technical user credentials
HTTP/2 200 OK
content-type: application/json
...
{
  "access_token": "eyJhbGciO...",
  "expires_in": 300,
  "refresh_expires_in": 0,
  "token_type": "Bearer",
  "not-before-policy": 123456789,
  "scope": "profile email"
}

When the access token expires (see the expires_in value in seconds, typically 5 minutes), you can simply issue a new token with the same credentials. A refresh token is not necessary and not provided.

Using this token, you can now issue API calls as the technical user and are bound by the permissions of its "API user" role.

The API client credentials don’t expire, but they can be invalidated in the UI by deleting the technical user.

2.2. OIDC Authorization Code Flow

By redirecting to our IDP with an authorization request, a user can safely log in on the trusted IDP and authorize your application to access the API. The returned authorization code can then be exchanged for an access token and refresh token.

This approach is recommended in the following cases:

  • Your application can handle URL redirects, i.e. there is a reachable URL for the app that can be redirected to. This can either be a https URL or a custom scheme registered to an Android or iOS app.

  • The application wants to access the API as the user that is granting access with all permissions granted to that user. (Note that API scopes will be added to further restrict access in the future.)

  • The application is offered as a general service for all users of FreeFinance.

The approach is not recommended if only a single client needs to be accessed. Creating a technical user is more straightforward and doesn’t require URL redirects.

Your application must be able to either store a secret safely server-side (for web applications with a back-end issuing the API calls) or implement the PKCE method to ensure that the authorization code is not intercepted. The API client must be created first, so contact us and provide the following information:

  • The name of your application

  • The use case and APIs you wish to access

  • A list of valid redirect URLs that are allowed to be used

  • Whether your application can store a server-side secret (and is issued a client secret) or implements PKCE instead

Once the API client is created and ready, you can begin the authorization process by redirecting the user of your application to the IDP, as returned by the /auth/issuer resource, and the OIDC path (/protocol/openid-connect/auth). You provide the following information as query parameters to that redirect:

  • The API client ID you were issued

  • One of the redirect URLs you registered (URL encoded)

  • The OIDC token scope. openid provides you with an ID token and a short-running refresh token, offline_access with a long-running token.

  • A freely chosen string of your choice in the state parameter, which will be returned when the authorization is successful. You can use this string to pass identifiers or references to link the authorization back to your internal user.

  • If no client secret was issued and PKCE is used, the code_challenge for PKCE using S256 for code_challenge_method. Since many OIDC compatible libraries handle this for you, it will not be explained in detail - further information can be found in the corresponding RFC.

The scope you request for the token determines the duration of the tokens, but has no bearing on the permissions the user has for specific clients.
Example request initiating the authorization code flow with PKCE
GET https://accounts.freefinance.at/auth/realms/demo/protocol/openid-connect/auth?response_type=code&client_id=MY-APP-CLIENT&redirect_uri=https%3A%2F%2Fmy.app%2Fmy-redirect&scope=offline_access&code_challenge=MY-CHALLENGE-CODE&code_challenge_method=S256

The browser will be redirected to log in with the IDP and give consent to grant your application access. If consent is granted, the browser is redirected back to the redirect URL you provider with two query parameters: code and state.

You can then exchange the code for tokens with a POST request. The grant_type is set to the fixed value authorization_code, and the exact redirect URL used in the original redirect request must be provided.

Example request exchanging the authorization code for tokens with PKCE
POST https://accounts.freefinance.at/auth/realms/demo/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
client_id = MY-APP-CLIENT-ID &
grant_type = authorization_code &
code = THE-RETURNED-AUTHORIZATION-CODE &
redirect_uri = https%3A%2F%2Fmy.app%2Fmy-redirect &
code_verifier = MY-CODE-CHALLENGE-VERIFIER

If you have been issued an API client secret, pass it as client_secret instead of the code_verifier.

The API client secret must be stored securely server-side and never be made accessible to anyone, transmitted to your user, or hard-coded in the frontend of your application. Only your backend should make the token request!

If the code and either the secret or the PKCE challenge is verified, you are returned an access_token and request_token.

Example response returning tokens from an authorization code
HTTP/2 200 OK
content-type: application/json
...
{
"access_token": "eyJhbGciO...",
"expires_in": 300,
"refresh_expires_in": 0,
"refresh_token": "eyJhbGciOiJIUzUxMiIs...",
"token_type": "Bearer",
"not-before-policy": 123456789,
"session_state": "...",
"scope": "profile offline_access email"
}

Observe the expires_in value (in seconds) for the access_token and the refresh_token_expires_in value for the refresh_token. If you requested the offline_access scope, your refresh token will be valid for a long time (30 days).

You can use the access_token for the Bearer authorization in your API calls, and request a new token after it expires with your refresh_token.

Example request requesting a new access token with a refresh token
POST https://accounts.freefinance.at/auth/realms/demo/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
client_id = MY-APP-CLIENT-ID &
grant_type = refresh_token &
refresh_token = MY-REFRESH-TOKEN &
scope = offline_access

As before, if you have been issued an API client secret, pass it as an additional client_secret parameter.

The refresh_token can be used to continuously obtain new access tokens for the user that granted your application access. Keep it as confidential as you would a password and store it securely either in your backend or the available secret storage providers of the web browser or mobile operating systems like Android and iOS!

3. Basic API features

3.1. Organization

The API is divided into sections that correspond to feature groups or modules in Freefinance. Each module has a short, three-letter abbreviation and its corresponding tag in the OpenAPI definition file. The descriptions of the tags describe the module, and some modules are further divided by subtags for readability (such as CBI-INC and CBI-OUT for cash based invoicing, divided into incoming and outgoing invoices).

3.2. Base Path and Clients

The base API endpoint path is /api/2.0/, usually followed by the three-letter module abbreviation and the resource.
GET <host>/api/2.0/fnd/countries for example would return all countries available in the API. FND refers to the foundation module, which represents basic countries, regions, currencies, and units of measure that can be used in the API.

In all following examples for API endpoints, /api/2.0/ is omitted for brevity, but still required in the actual API call. If a path is given, such as /fnd/countries, the full path would be /api/2.0/fnd/countries.

To obtain all available clients, the /clients endpoint is available. It lists the numerical ID for a client, which is part of the resource path for all resources pertaining to that client. For example, the GET call would return a client with ID 10000, and to list all customers of that client, the path would look like this:
GET /clients/10000/mas/customers
Note that the module prefix is still part of the path after the client ID - the vast majority of resources are sub-paths of a client.

3.3. Content types

The API uses JSON as primary exchange format.
Property keys use snake case, such as unit_of_measure, for readability.

Resources that return files, like /pdf, exporters, and image endpoints return the proper file type. For uploads with sidecar json metadata, the API supports multipart requests (recommended) or having the attachment encoded as base64 inside a json property.

Date values are expected and returned in ISO format, such as "invoice_date": "2024-03-18" or "date_time": "2025-07-29T17:18:40.2161948". IDs for resources and references are UUIDs, with a few exceptions, such as client IDs (integers).

3.4. Result formats

Most endpoints that return lists are sliced, which means that only a partial response is returned if the total result count exceeds the limit for a single response. They have the following format:

Example sliced response
{
  "total_count": 4,
  "content": [
    {
      "id": 21333,
      "name": "Example company"
    }
  ]
}

If the total_count exceeds the number of objects in the array, the next page can be returned by including the offset query parameter. For our previous example of fetching the customers of a client, the call would look like this:
GET /clients/10000/mas/customers?offset=100.

Most resources have a default result size of 100. By passing the limit parameter, that size can be adjusted to a maximum of 500. For example, customers no. 100-300 can be fetched with this URL:
GET /clients/10000/mas/customers?limit=200&offset=100.

If a resource has different limits, it will be noted in its documentation.

3.5. Sorting

Depending on the resource, results can be sorted in the backend by passing the sort query parameter. Multiple sort parameters can be defined for nested sorting with a comma delimiter, and the sort order can be defined per property with the :ASC and :DESC suffixes. If no suffix is provided, the property is sorted in ascending order.

There is no standardized way to document sort parameters in the OpenAPI spec, you’ll have to refer to the documentation of a specific endpoint. In general, the major properties such as dates, totals, names etc. are sortable by passing the response property name, such as total. If a sort field is not recognized, it will be ignored.

Here is an example for sorting over multiple properties when querying the suppliers of a client:
GET /clients/10000/mas/suppliers?sort=display_name:ASC,supplier_number:DESC,zip_code

There is no support in the OpenAPI specification for indicating which properties of a particular resource can be sorted by, so no documented lists can be provided in a standardized way. Usually, the most important properties of the object can be used as sort parameter, and resources may have a default sort order if none is provided as parameter.

3.6. Request and response references

Many of the entities exposed in the API have references to other entities. For example, a customer of a client belongs to a country and region, which are given as reference.

The vast majority of entities use a UUID as identifier given in the id property, and are referenced with that UUID.

For responses, that reference is a nested object that contains not only the id itself, but also the most important data related to that reference. This is done to ease the readability of responses and include enough data that the reference can be shown in a meaningful way. There is no need for chains of API GET calls to get the basic information of a referenced entity.

As an example, a customer of a client can reference a contra_account, which is the default receivables contra account used in double-entry accounting invoices. The response format with the reference looks like this:

Example customer response (shortened)
{
      "id": "759...45",
      "first_name": "John",
      "last_name": "Doe",
      "contra_account": {
        "id": "4232...96",
        "name": "Kundenkonto",
        "code": "20100",
        "user_description": "Receivables (John Doe)",
        "category": "DEBIT"
      }
}

When creating or modifying an existing customer, the contra_account is referenced only by the ID of a valid account. The entire nested account object is not required and not part of the request schema. You don’t need to pass null for optional properties, it is equivalent to simply omitting it from the request.

Example customer payload (shortened)
{
      "first_name": "John",
      "last_name": "Doe",
      "contra_account":  "4232...96"
}

3.7. Query parameter arrays

In case a query parameter accepts multiple values at once, the values can be given as comma-separated list, or by adding the query parameter multiple times. For example, the endpoint
GET /clients/10000/pos/cash_registers
accepts an optional query parameter states to find cash registers that have any of the given states. To retrieve all cash registers that are either active or locked, the parameter can be set in the following ways:
GET /clients/10000/pos/cash_registers?states=ACTIVE,LOCKED
or
GET /clients/10000/pos/cash_registers?states=ACTIVE&states=LOCKED

This OR-logic is the default for querying over multiple possible values. If the resource uses AND-logic for that particular property, it will be stated in the description of the parameter.

3.8. Error responses

If a request fails for validation or business logic related reasons, the error response has this format:

Example error response
HTTP/2 400 Bad Request
content-type: application/json;charset=UTF-8
{
  "error": "cbs-account-not-found",
  "message": "Invalid account 01fa...42 or account not usable for desired action!",
  "identifier": "sA5qOX",
  "details": [],
  "payload": [
    "01f1a...42"
  ]
}
  • The error is a unique code per error type, usually prefixed with the module relevant for that error.

  • message gives a pre-translated error message that can be forwarded to the user or for debugging purposes.

  • identifier is a short random code to correlate this error to output in our logs - if you contact support, you can include it to provide additional context.

  • Lastly, payload includes relevant identifiers for parsing the error in combination with the error code for convenience.

If you receive validation errors for missing properties that are required, the error response refers to the field with camel case due to a technical limitation. The property in the schema is still using snake case.

The application also uses these error codes:

Error code Causes

400 Bad Request

Validation errors, failed prerequisites, business logic errors, a reference inside the payload can’t be found

401 Unauthenticated

No authentication → acquire a new token from the IDP

403 Forbidden

Invalid client, no access to the client, no access to the resource (missing feature or permissions)

404 Not Found

API endpoint not found, ID referenced in a path parameter was not found

409 Conflict

A supplied unique identifier is already taken

4. Examples

4.1. Clients

As explained in the introduction, the client is the organizational entity that holds a bookkeeping instance. It is included in most API calls as a path parameter, so the first step of using the API is to query your available clients and select one for subsequent API requests.

Once you have successfully authenticated (see the Authentication section), you can call the GET clients API:

Example clients API request
GET https://demo.freefinance.at/api/2.0/clients
Authorization: Bearer eyJ...
Example clients API response
HTTP/2 200 OK
content-type: application/json;charset=UTF-8
...
{
  "total_count": 1,
  "content": [
    {
      "id": 21334,
      "display_name": "Max Muster",
      "product_type": "CMA"
    }
  ]
}

Most users will have only one client available. If you only ever need to access one specific client, you can store the ID permanently, as it does not change, and don’t need to query your available clients.

If you are using the Technical User authentication with client credentials, the API client id and API client secret pair are used just for authentication. The API client ID (called client_id in the OIDC spec) contains the numerical ID of your bookkeeping client and a random number, and can’t be used as path parameter for the v2 API. For example, your API client id can be 21334_21715633, but your client ID is just 21334.

With the numerical ID of the client you can now make API calls within the context of that client by prepending clients/{id} to the actual resource. Here is an example to query the bookkeeping accounts of your client with the cbs/accounts API:

Example accounts API request
GET https://demo.freefinance.at/api/2.0/clients/21334/cbs/accounts?limit=1
Authorization: Bearer eyJ...
Example accounts API response
HTTP/2 200 OK
content-type: application/json;charset=UTF-8
...
{
  "total_count": 340,
  "content": [
    {
      "id": "8b7f...b3",
      "code": "0100",
      "account_type": {
        "id": "5fee...df",
        "code": "L"
      },
      "category": "DEBIT",
      "taxes": {
        "tax_1": {
          "default_tax_class_entry": {
            "id": "4db0...0e",
            "code": "020",
            "tax_rate_value": 20
          },
          "tax_class": {
            "id": "c2b0...18",
            "code": "ASTD"
          },
          "forced_tax_class_entry": false
        }
      }
    }
  ]
}

This example demonstrates the v2 API features outlined in the previous chapter:

  • The numerical client ID is a fixed path parameter for all API calls. If the client is invalid, or you don’t have access, you’ll receive a 403 response regardless of the resource you call.

  • The response is sliced, i.e. you only receive a part of all available data. The total_count shows you how many total results are available in the query, but the array only includes a slice. You can use the limit and offset parameters to iterate through the slices.

  • Nested objects with an id reference other resources. For example, the tax_class can be accessed with the fis/tax_classes resource.

4.2. Bookkeeping basics

The main use case of the API is to create invoices and bookings of some kind - like creating a new invoice for a customer. As these invoices are then included in the bookkeeping, the lines they contain must include an account and one or several tax class entries.

The number of possible tax class entries (and resulting tax rates) depends on the country of the client. European countries only have one tax to pay for a line (the value added tax, or VAT), while non-european countries might have several for the state, county, etc.

The combination of these two is required for every invoice line and receipt line you create. They can either be supplied directly, or defaulted by storing them in an item, and referencing that item in a line with defaulting enabled.
Either way, you have to give a valid combination of account and tax class entry at least once - either with the API here, or by setting them up in the UI for your items beforehand.

If you wish to supply them with the API, please continue with the examples in this chapter.

4.2.1. Accounts

An account is used in an invoice or booking line to reference the kind of income or expense that is later grouped and calculated for tax reports. Incomes usually have few different kinds (like basic incomes, discounts, cash credit etc.) whereas expenses have more distinguished categories (depreciating assets, office expenses, energy and so on).

Every account has specific uses, which determine where in API resources it can be given as reference. Invoices you send to your customers have lines with accounts that have the INV use for example, while Point of Sale receipt lines have the POS use. Accounts can have multiple uses, they exist to help you fetch possible accounts for your use case.

The previous chapter gave an example to fetch all available accounts for a client. If you wish to fetch all available accounts for creating invoices, you simply append the use query parameter.

Example accounts API request with INV use
GET https://demo.freefinance.at/api/2.0/clients/21334/cbs/accounts?use=INV
Authorization: Bearer eyJ...
Example accounts API response
HTTP/2 200 OK
content-type: application/json;charset=UTF-8
...
{
  "total_count": 44,
  "content": [
    {
      "id": "a678...5fd",
      "name": "Revenue (income)",
      "code": "4000",
      "taxes": {
        "tax_1": {
          "default_tax_class_entry": {
            "id": "9eca...cc",
            "code": "020",
            "tax_rate_value": 20
          },
          "forced_tax_class_entry": false
        }
      }
    }
  ]
}

Note that the account has a taxes object, with tax_1 through tax_5 as possible properties. For European countries, only tax_1 is relevant. The default tax class entry of an account is also listed, but not automatically defaulted in the API - the caller has to supply the tax class entry itself.

If you require to fetch a specific account, you can either do so by the id directly (returning the account itself or a 404 error), or by using the code query parameter and supplying a code, which may still yield multiple results.

4.2.2. Tax class entries

While the account represents what specific category an income or expense line belongs to, the tax class entry determines what specific taxes apply to the line. Every account has one tax_class per tax group (tax 1-5), with multiple accounts in a similar tax group sharing the same class for a group, and every such tax class has one or multiple tax class entries, which determine what specific tax has to be paid, most often distinguished by different tax rates.

Here is an example to illustrate this:
The Austrian account 4000 is the standard income account for non-specific revenues. Since in Austria there is only one VAT, the taxes object only ever has a tax_1 value. The tax_class has the code ESTD, which is the tax group for non-specific incomes - most income accounts have it.
The tax class now has several entries: they represent each possible tax rate that is due for a specific line. You can fetch all available rates with the fis/tax_classes/{id}/entries resource, passing the tax class id as parameter:

Example request for tax class entries of a tax class
GET https://demo.freefinance.at/api/2.0/clients/21334/fis/tax_classes/e7db9b7b-c7e1-7940-eb1b-b0ab89bfdc0a/entries?limit=2sort=tax_rate%3Adesc
Authorization: Bearer eyJ...
Example tax class entries response (abbreviated)
HTTP/2 200 OK
content-type: application/json;charset=UTF-8
...
{
  "total_count": 6,
  "content": [
    {
      "id": "9eca...cc",
      "code": "020",
      "tax_rate": {
        "id": "74a4...0b",
        "tax_rate": 20
      }
    },
{
      "id": "db92...90",
      "code": "013",
      "tax_rate": {
        "id": "22e4...fd",
        "tax_rate": 13
      }
    }
  ]
}

In the response we see the tax class entry 020, whose ID matches the default tax class entry of the account 4000 in the previous example. It represents a tax rate of 20%, the standard rate for VAT in Austria. If your line item requires a different tax rate (for example, 10% for food), you simply supply the tax class entry with the matching rate for the invoice or receipt line.

Tax rates are also a resource by themselves (given away by the fact that they have their own id), but for the purposes of creating invoices, items and receipts only the tax class entry is required.

Some special-purpose accounts have tax classes with only one tax class entry and are used for rebooks, corrections and so on.

Others might have the forced_tax_class_entry flag set - the tax class may have multiple entries, but this specific account only allows the entry marked as default - trying to use any other entry will result in a validation error.

4.3. Incoming and outgoing invoices

This section introduces the creation of CBI invoices, one of the most used features of the API. Before you jump in, make sure you have read the bookkeeping essentials.

Incoming and outgoing invoices are the primary method of creating bookkeeping incomes and expenses. Two types exist: incoming invoices define expenses, and outgoing invoices define incomes.

An invoice may have none, one or several bookings, and each booking in turn has a journal. Journals are realized incomes and outgos and created implicitly by the backend when required.

Payment works differently depending on the type of your bookkeeping registered with the client - see the following sections for details.

Creating open, unpaid outgoing invoices is not enabled by default. This can be changed in the UI (Fiscal SettingsBase SettingsDefault values for incoming/outgoing invoicesOutstanding receivables/payables)
  • For cash method accounting (CMA), an unpaid invoice does not yet create a booking. The booking and its journal are created once the invoice receives the first payment, and partial payments create proportional journals from the invoice (small rounding errors can occur in the lines). Invoices can be deleted as long as no payment was made, otherwise the invoice can only be canceled.

  • In double-entry accounting (DEA), every invoice automatically creates a booking and journal of the entire invoice against an unpaid contra account (payables and receivables). The invoice can then have one or several payments, which create simple rebooks from the unpaid account to the payment account. Invoices can only be canceled.

When querying invoices with the GET /cbi/incoming_invoices and GET /cbi/outgoing_invoices resources, only the header data of the invoice is returned. Full information including the lines is only provided when fetching a singular invoice by its ID, and bookings and journals are fetched with their own sub-resources.

4.3.1. Incoming Invoices

Incoming invoices are the direct way to create expenses. The accounts used for incoming invoice lines are expense accounts, which you can retrieve in the account API with the EXPENSE use.

For clients with both cash method accounting (CMA) and double entry accounting (DEA), this example creates an unpaid incoming invoice with the minimal set of required fields.

In CMA, no booking is created, and the invoice does not have a sequence number yet.

Example request for minimal incoming invoice
POST https://demo.freefinance.at/api/2.0/clients/21334/cbi/incoming_invoices
Content-Type: application/json
Authorization: Bearer eyJhbGci...Vm-yw
{
  "invoice_date": "2025-12-31",
  "lines": [
    {
      "account": "9331...67", (1)
      "taxes": { (2)
        "tax_1": {
          "tax_class_entry": "4db0...0e"
          }
        },
      "amount": 120,
      "amount_type": "NET" (3)
    }
  ]
}
1 The /cbs/accounts API returns available accounts. Set the use query param to EXPENSE get all valid accounts for creating an incoming invoice.
2 Depending on the tax configuration of your country, one or several taxes can be defined. EU countries only require the VAT, so the tax_1 group is set.
3 The amount type defines if the supplied amount is net before tax (NET) or total with tax (GROSS).
If certain accounts are used (e.g. assets), the supplier becomes mandatory for the invoice.
Example response for a CMA client
HTTP/1.1 201
Content-Type: application/json;charset=UTF-8
{
  "invoice": {
    "id": "8af8...25",
    "invoice_date": "2025-10-30",
    "currency": {
      "code": "EUR" (1)
    },
    "net": 120.00,
    "tax": 24.00,
    "total": 144.00, (2)
    "open": 144.00,
    "open_in_main_currency": 144.00,
    "booked": false,
    "lines": [
      {
        "id": "2174...f6",
        "line_number": 1, (3)
        "amount": 120,
        "amount_type": "NET",
        "net": 120.00,
        "tax": 24.00,
        "total": 144.00, (2)
        "account": {
          "id": "9331...67",
          "code": "7600"
        },
        "taxes": {
          "tax_1": {
            "amount": 24.00,
            "tax_class_entry": {
              "id": "4db04f15-e5b0-74f8-cd4b-b2b06e0c0f0e",
              "code": "020",
              "tax_rate_value": 20
            }
          }
        },
        "allowed_changes": [ (4)
          "DELETION",
          "ACCOUNT_INFO",
          "AMOUNT",
          "DESCRIPTION"
        ]
      }
    ],
    "allowed_changes": [ (4)
      "DELETION",
      "PAYMENT",
      "SUPPLIER"
    ]
  }
}
1 The currency, if not supplied, is set to the main currency of the client.
2 The totals of the invoice and lines are calculated in the backend. You can also supply the totals in the payload for validation, triggering an error on a mismatch.
3 The number of each line is assigned by the order of the lines in the payload lines array. Adding and removing lines of an existing invoice will overwrite the number of each existing line to match the order of the payload each time.
4 The allowed_changes array lets you know what kind of changes are possible for this invoice and each line. Depending on the payment and booking state, inclusion of this invoice in tax reports etc. certain changes are prohibited in any later PUT requests or operations.

The next example creates a paid incoming invoice with a supplier from the /mas/suppliers resource.

Example request for a paid incoming invoice with supplier (CMA)
POST https://demo.freefinance.at/api/2.0/clients/21334/cbi/incoming_invoices
Content-Type: application/json
Authorization: Bearer eyJhbGci...Vm-yw
{
    "supplier": "09c8...e", (1)
    "currency": "EUR",
    "note": "My internal note",
    "paid_date": "2025-12-10", (2)
    "paid_contra_account": "8742...fb", (3)
    "description": "A purchase for my company",
    "invoice_reference": "IV2025.0001",
    "lines": [
        {
            "account": "9331...67",
            "taxes": {
                "tax_1": {
                    "tax_class_entry": "4db0...0e"
                }
            },
            "amount": 12,
            "amount_type": "NET"
        }
    ]
}
1 Use the /mas/suppliers API to find available suppliers.
2 When the date of payment is present, the invoice will be marked as paid in full, a booking will be created, and a sequence number assigned. For CMA paid invoices the invoice date is not required.
3 The paid contra account must always be supplied together with the paid date. See the /cbs/accounts API to find available accounts, using MEANS_OF_PAYMENT for the use parameter.
Response
HTTP/1.1 201
Content-Type: application/json;charset=UTF-8
{
  "invoice": {
    "id": "6555...50",
    "invoice_sequence": "ER2025 1",
    "invoice_date": "2026-01-01",
    "paid_date": "2025-12-10",
    "currency": {
      "code": "EUR"
    },
    "net": 12.00,
    "tax": 2.40,
    "total": 14.40,
    "open": 0,
    "open_in_main_currency": 0,
    "description": "A purchase for my company",
    "invoice_reference": "IV2025.0001",
    "booked": true,
    "supplier": {
      "id": "09c8...1e",
      "supplier_number": "L 1152",
      "display_name": "My supplier"
    },
    "supplier_name": "My supplier",
    "note": "My internal note",
    "lines": [
      {
        "id": "ebc266a9-4f80-41da-8a45-be17c9189c5b",
        "line_number": 1,
        "amount": 12,
        "amount_type": "NET",
        "net": 12.00,
        "tax": 2.40,
        "total": 14.40,
        "account": {
          "id": "9331...67",
          "code": "7600"
        },
        "taxes": {
          "tax_1": {
            "amount": 2.40,
            "tax_class_entry": {
              "id": "4db0...0e",
              "code": "020",
              "tax_rate_value": 20
            }
          }
        },
        "allowed_changes": [
          "DELETION",
          "...",
          "DESCRIPTION"
        ]
      }
    ],
    "allowed_changes": [
      "LINES_COUNT",
      "...",
      "SUPPLIER"
    ]
  }
}

The booked flag (and presence of a sequence) indicates that an invoice booking was created. To obtain all bookings for the invoice, simply use the bookings resource with the invoice ID.

Example request to fetch invoice bookings
GET https://demo.freefinance.at/api/2.0/clients/21334/cbi/incoming_invoices/6555...50/bookings
Authorization: Bearer eyJhbGci...Vm-yw
Response
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
  "total_count": 1,
  "content": [
    {
      "id": "57b7...a1",
      "type": "PAYMENT",
      "number": 1,
      "cancelled": false,
      "reconciled": false,
      "journals": [
        {
          "id": "220b...04",
          "number": "A2025 1",
          "journal_type": "OUTGO",
          "net": 2.40,
          "tax": 2.40,
          "total": 14.40
        }
      ],
      "payment_type": "PAYMENT",
      "payment_date": "2025-12-10",
      "payment_amount": 14.40,
      "contra_account": {
        "id": "8742...fb",
        "code": "2700"
      }
    }
  ]
}
Double Entry Accounting

In double-entry accounting, creating an unpaid invoice also immediately creates a payables/receivables booking, and the invoice is always issued a sequence. The invoice date is always required.

Paid incoming invoices where the invoice and paid date are equal do not follow this rule - in this case, only a singular payment booking is generated, similar to CMA.

Incoming invoices with different invoice and paid dates and outgoing invoices always create an unpaid initial booking, and may create additional payment bookings (either immediately or with a later payment).

Using the same minimal example for an incoming invoice in CMA clients, you’ll receive an invoice that already has a booking:

Response for an unpaid incoming invoice (shortened)
{
  "invoice": {
    "id": "c9ba...61",
    "invoice_sequence": "ER2025 1",
    "invoice_date": "2025-12-01",
    "booked": true,
    "allowed_changes": [
      "CANCELLATION"
    ]
  }
}

The booked flag is already true, and deletions are not possible. Fetching the bookings shows you the full amount booked against payables:

Response for the bookings of an unpaid incoming invoice in DEA
{
  "total_count": 1,
  "content": [
    {
      "id": "7df1...567",
      "type": "INITIAL_BOOKING",
      "number": 1,
      "journals": [
        {
          "id": "065b...f3",
          "number": "A2025 1",
          "journal_type": "OUTGO"
        }
      ],
      "contra_account": {
        "id": "316a8310-297b-4e2d-8c0d-d78cc149e4d5",
        "code": "3300"
      }
    }
  ]
}

The next example has a different invoice date and paid date, which results in a booking to the payables contra account and an additional payment booking against a means of payment account:

Request for a DEA incoming invoice with diverging dates
POST https://demo.freefinance.at/api/2.0/clients/21333/cbi/incoming_invoices
Content-Type: application/json
Authorization: Bearer eyJhbGci...Vm-yw
{
    "invoice_date": "2025-12-01",
    "paid_date": "2025-12-02",
    "paid_contra_account": "6b60...0c",
    "lines": [
        {
            "account": "87b6...4a",
            "amount": 20,
            "amount_type": "GROSS",
            "taxes": {
                "tax_1": {
                    "tax_class_entry": "4db0...0e"
                }
            }
        }
    ]
}
Response
HTTP/1.1 201
Content-Type: application/json;charset=UTF-8
{
  "invoice": {
    "id": "71f2e...9d",
    "invoice_sequence": "ER2025 2",
    "invoice_date": "2025-12-01",
    "paid_date": "2025-12-02",
    "open": 0,
    "open_in_main_currency": 0,
    "booked": true,
    "allowed_changes": [
      "LINES_COUNT",
      "INVOICE_DATE",
      "PAID_DATE",
      "DUE_DATE",
      "TOTAL_AMOUNT"
    ]
  }
}

The paid_contra_account present in the request payload is not directly present on the resulting invoice, it is used to create a booking instead. Also, cancellations are no longer possible: any existing payment bookings, even those created implicitly, must be canceled first before the invoice itself can be canceled.

Fetching the bookings now returns two, one for the initial payables booking and one for the actual payment:

Response for the bookings of a paid incoming invoice in DEA
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
  "total_count": 2,
  "content": [
    {
      "id": "8c2f...03",
      "type": "INITIAL_BOOKING",
      "number": 1,
      "journals": [
        {
          "id": "4760...ac",
          "number": "A2025 2",
          "journal_type": "OUTGO"
        }
      ],
      "contra_account": {
        "id": "3168...d5",
        "code": "3300"
      }
    },
    {
      "id": "8a61...02",
      "type": "PAYMENT",
      "number": 2,
      "journals": [
        {
          "id": "e009...8d",
          "number": "U2025 8",
          "journal_type": "REBOOK"
        }
      ],
      "payment_type": "PAYMENT",
      "payment_date": "2025-12-02",
      "payment_amount": 20.00,
      "contra_account": {
        "id": "6b60...0c",
        "code": "2800"
      }
    }
  ]
}
There is no difference in the result between an invoice already being created as paid, or first being created as unpaid and then paid in a follow-up request. The option of creating an already paid invoice is just for convenience.

4.3.2. Outgoing Invoices

The endpoints (/cbi/outgoing_invoices) are the same as their counterpart for incoming invoices, with some minor differences in available fields (customer vs. supplier) and a different class of accounts for the invoice lines. Operations like payment and cancellation also work the same way.

For DEA, outgoing invoices always have an unpaid initial booking when creating a paid invoice, regardless of the dates being equal or not.

If you have the Invoicing (INV) module available, outgoing invoices might seem redundant when you can create fully-featured invoices with PDFs instead. However, outgoing invoices still have their use when you need to register an income without requiring a full INV invoice with all its features, or simply don’t need to create a PDF. Both CBI outgoing invoices and INV invoices can be created and used in the same client.

This example creates an unpaid outgoing invoice with the minimal set of required fields. You’ll note that it has the same structure as the incoming invoice, only the account and tax class entry differ - you’d query the accounts for the INCOME use instead, and use the available tax class entries for them.

Example request for a minimal outgoing invoice
POST https://demo.freefinance.at/api/2.0/clients/21334/cbi/outgoing_invoices
Content-Type: application/json
Authorization: Bearer eyJhbGci...Vm-yw
{
    "invoice_date": "2025-12-31",
    "lines": [
        {
            "account": "86ed...72",
            "amount": 100,
            "amount_type": "NET",
            "taxes": {
                "tax_1": {
                    "tax_class_entry": "dec0...28"
                }
            }
        }
    ]
}
Response
HTTP/1.1 201
Content-Type: application/json;charset=UTF-8
{
  "invoice": {
    "id": "38c5...b6",
    "invoice_sequence": "AR2025 1",
    "invoice_date": "2025-12-31",
    "currency": {
      "code": "EUR"
    },
    "net": 100.00,
    "tax": 20.00,
    "total": 120.00,
    "open": 120.00,
    "booked": false,
    "lines": [
      {
        "id": "7ed5...64",
        "line_number": 1,
        "amount": 100,
        "amount_type": "NET",
        "net": 100.00,
        "tax": 20.00,
        "total": 120.00,
        "account": {
          "id": "86ed...72",
          "code": "4000"
        },
        "taxes": {
          "tax_1": {
            "tax_class_entry": {
              "id": "dec05...28",
              "code": "020",
              "tax_rate_value": 20
            }
          }
        }
      }
    ]
  }
}

The next example creates a paid outgoing invoice with a customer from the /cbi/customers resource and a different currency (USD).

Example request for an outgoing invoice with customer in foreign currency
POST https://demo.freefinance.at/api/2.0/clients/21334/cbi/outgoing_invoices
Content-Type: application/json
Authorization: Bearer eyJhbGci...Vm-yw
{
    "invoice_date": "2025-12-31",
    "currency": "USD",
    "currency_rate": 0.8,
    "customer": "af8f...87",
    "lines": [
        {
            "account": "a678...fd",
            "amount": 100,
            "amount_type": "NET",
            "taxes": {
                "tax_1": {
                    "tax_class_entry": "9eca...cc"
                }
            }
        }
    ]
}
Response
HTTP/1.1 201
Content-Type: application/json;charset=UTF-8
{
  "invoice": {
    "id": "5565fbcb-3d06...dd",
    "invoice_date": "2025-12-31",
    "currency": {
      "code": "USD"
    },
    "currency_rate": 0.8,
    "net": 100.00,
    "tax": 20.00,
    "total": 120.00,
    "open": 120.00,
    "open_in_main_currency": 96.00,
    "booked": false,
    "customer": {
      "id": "af8f...87",
      "customer_number": "C 1142",
      "display_name": "My Customer"
    },
    "lines": [
      {
        "id": "0ce7...0d",
        "line_number": 1,
        "amount": 100,
        "amount_type": "NET",
        "net": 100.00,
        "tax": 20.00,
        "total": 120.00,
        "account": {
          "id": "a678...fd",
          "name": "Revenue (income)",
          "code": "4000"
        },
        "taxes": {
          "tax_1": {
            "tax_class_entry": {
              "id": "9eca...cc",
              "code": "020",
              "tax_rate_value": 20
            }
          }
        },
        "allowed_changes": [
          "DELETION",
          "ACCOUNT_INFO",
          "AMOUNT",
          "DESCRIPTION"
        ]
      }
    ],
    "allowed_changes": [
      "DELETION",
      "PAYMENT",
      "REST_PAYMENT",
      "LINES_COUNT",
      "INVOICE_DATE",
      "INVOICE_YEAR",
      "DUE_DATE",
      "CURRENCY",
      "TOTAL_AMOUNT",
      "DESCRIPTION",
      "NOTE",
      "INVOICE_REFERENCE",
      "PAYMENT_REFERENCE",
      "DELIVERY_SPAN",
      "CUSTOMER"
    ]
  }
}

4.4. Items

Items are part of the master data of a client, they are reusable definitions of a revenue line used in INV invoices and cash receipts. For use in the API, creating items beforehand (with accounts, tax class entries, prices and descriptions already included) allows you to create invoices and receipts quickly by referencing an item and enabling item defaulting. Alternatively, if you want to have complete control over the data of the line, you can take the values of the items and apply them to invoice and receipt lines yourself.

4.4.1. Creating an item

Without an item category, an account and tax class entry are required for an item. Below is a minimal example with one tax group - if the country of the client has multiple taxes, an entry is required for all of them.

Example request for a minimal item
POST https://demo.freefinance.at/api/2.0/clients/21334/itm/items
Content-Type: application/json
Authorization: eyJ...
{
    "name": "My Example Item",
    "account": "a678....fd",
    "amount_type": "N",
    "selling_price": 100.00,
    "taxes": {
        "tax_1": {
            "tax_class_entry": "9eca...cc"
        }
    }
}
Example response for a minimal item
HTTP/2 201
Content-Type: application/json
{
  "id": "2de5...c5",
  "name": "My Example Item",
  "account": {
    "id": "a678...fd",
    "code": "4000"
  },
  "amount_type": "N",
  "selling_price": 100.00,
  "currency": "EUR",
  "visible": true,
  "taxes": {
    "tax_1": {
      "tax_class_entry": {
        "id": "9eca...cc",
        "code": "020",
        "tax_rate_value": 20
      }
    }
  }
}

Note that the account and tax_class_entry IDs were taken from our examples from the previous chapter. You can retrieve all available accounts by using the INV account use for the cbs/accounts resource.

The amount_type specifies if the selling_price is Net before tax, or Total after tax.

You can also supply a number and use it to save the internal ID or code of your application for the item. This makes it easier later to fetch an item directly by this number, in case you don’t want to store the generated item ID in your application. The API will however not ensure uniqueness, so you have to handle the case of multiple items being returned.

The item can also be assigned an item_category. If the item category has valid defaults for the account and tax class entry, they will be copied down to the item and need not be supplied in the item creation payload.

4.4.2. Fetching items

The itm/items resource accepts the category query parameter for the ID of a category the item belongs to, and a generic search parameter to filter for the name, description, number or barcode. This way you can either present a list of items for selection, or fetch the one result to use directly in subsequent API calls to create invoices or receipts.

Example search for an item
GET https://demo.freefinance.at/api/2.0/clients/21334/itm/items?category=ad94...d5&search=apple
Authorization: Bearer {{$auth.token("ffappl")}}
Example response for searching items
HTTP/2 200
Content-Type: application/json
{
  "total_count": 1,
  "content": [
    {
      "id": "088c...0c",
      "name": "Apple Tree",
      "item_category": {
        "id": "ad94...d5",
        "name": "Apple Trees"
      },
      "account": {
        "id": "a678...fd",
        "code": "4000"
      },
      "amount_type": "N",
      "selling_price": 100.00,
      "currency": "EUR",
      "taxes": {
        "tax_1": {
          "tax_class_entry": {
            "id": "9eca...cc",
            "code": "020",
            "tax_rate_value": 20
          }
        }
      }
    }
  ]
}

4.5. Invoicing

This section introduces the creation of invoices, one of the most used features of the API. Before you jump in, make sure you have read the bookkeeping essentials and optionally the item API.

Make sure not to confuse the invoicing from the INV feature (see the INV tag in the OpenAPI spec) with outgoing invoices from the CBI module (the CBI-OUT tag). CBI outgoing invoices are a simpler version only used for bookkeeping, while the INV module is more extensive with features like the PDF generation, per-line discounts, etc.

In general, you use the CBI module to track invoices created somewhere else, while you can use INV to create invoices in FreeFinance itself. Clients can use both modules concurrently for different use cases, but most users only require one or the other.

The full invoicing API is only available in the higher subscription tiers. If you get 403 errors when trying any invoicing API, check if you have purchased the invoicing feature (Plus tier and above) and your user (or the Technical User) has the appropriate permissions.

Invoices, along with all other document types from the invoicing module (offers, delivery notes, etc.) can be created in two major states: Staging documents are in a draft mode and can be freely edited and changed. Once a document is finalized, all corresponding documents are generated (PDF, XML) and further edits are restricted.

4.5.1. Prerequisites

To create a staging invoice, at least one (default) payment term must exist, since the term defines the due date of the invoice. If no payment term is referenced, the default payment term is applied automatically. (Other invoicing documents like offers don’t require a payment term.)

To create finalized invoices or to finalize existing ones, the following additional prerequisites are required:

  • A sequence group exists and is initialized with the current year. Sequence groups contain one sequence per document type and define the appearance and current sequence number of invoicing documents.

  • At least one layout setup exists. The layout setup defines the appearance of the generated PDF documents, the header and footer, bank information, and so on. Like with the payment terms the default layout setup is applied automatically, if one exists.

The prerequisite setup can be completed in the UI. Payment terms and layout setups don’t expire, but sequence groups require new sequences to be created every year.

4.5.2. Staging invoices

Here is the minimal example for creating an invoice, with an unreferenced customer and explicit values for the account and tax class. A default payment term must exist and will be selected automatically.

Example request for a minimal invoice
POST https://demo.freefinance.at/api/2.0/clients/21334/inv/invoices
Content-Type: application/json
Authorization: eyJ...
{
    "date": "2025-11-11",
    "customer_name": "Ernest Example",
    "lines": [
        {
            "name": "An invoice line",
            "item_price": 123.45,
            "amount": 2.00,
            "price_type": "NET",
            "account": "a678...fd",
            "taxes": {
                "tax_1": {
                    "tax_class_entry": "9eca...cc"
                }
            }
        }
    ]
}
Example response for a minimal invoice
HTTP/2 201
Content-Type: application/json
{
  "id": "232a...73",
  "type": "INVOICE", (1)
  "state": "STAGING", (2)
  "date": "2025-11-11",
  "due_date": "2025-12-11", (3)
  "paid": false,
  "net": 246.90,
  "tax": 49.38,
  "total": 296.28, (4)
  "open": 296.28,
  "open_in_main_currency": 296.28,
  "currency": "EUR",
  "customer_name": "Ernest Example",
  "payment_term": {
    "id": "178c...19",
    "name": "Standard"
  },
  "payment_term_description": "30 days",
  "created_at": "2025-11-27T21:10:55.1...",
  "updated_at": "2025-11-27T21:10:55.2...",
  "lines": [
    {
      "id": "4da1...75",
      "number": 1, (5)
      "name": "An invoice line",
      "account": {
        "id": "cec4...f5",
        "code": "4000"
      },
      "taxes": {
        "tax_1": {
          "amount": 49.38,
          "tax_class_entry": {
            "id": "9eca...cc",
            "code": "020",
            "tax_rate_value": 20
          }
        }
      },
      "net": 246.90,
      "open": 296.28,
      "open_in_main_currency": 296.28,
      "total": 296.28,
      "amount": 2.00,
      "item_price": 123.45,
      "price_type": "NET"
    }
  ]
}
1 The type is determined by the API endpoint you used. It might seem redundant, but is included for completeness as there are resources to fetch all invoicing document types in a general search.
2 The state is determined by the endpoint as well and can’t be set or changed by anything other than API operations.
3 The due date was set by the payment term. You can supply it for verification purposes, but it must match the due days setting in the payment term. If you configured the term to not have a set number of days, you must supply the due_date in the payload.
4 The totals of the invoice and each line were calculated by the API. You can also supply the totals for verification, triggering an error on mismatch.
5 The number of each line is assigned by the order of the lines in the payload lines array. Adding and removing lines of an existing invoice will overwrite the number of each existing line to match the order of the payload each time.

Another way of supplying invoice lines is by referencing an item. This can be either done just as a reference (and all other values of the lines are still determined by your payload) or with the optional per-line property item_defaulting, which instructs the API to apply all item values that weren’t supplied in the payload.

You can enable or disable item defaulting for each line separately.

Additionally, the customer can also be given as a reference instead of just a string with a name. Creating customers in your client master data and later referencing them in invoices is a prerequisite for detailed customer revenue reports and (for double-entry accounting) tracking the receivables of each customer in their own bookkeeping accounts.

The next example references the example item created in the previous chapter and a customer by their IDs.

Example request for a minimal invoice using references
POST https://demo.freefinance.at/api/2.0/clients/21334/inv/invoices
Content-Type: application/json
Authorization: eyJ...
{
    "date": "2025-11-11",
    "customer": "af8f...87",
    "lines": [
        {
            "item": "088c...0c",
            "item_defaulting": true,
            "amount": 2.00
        }
    ]
}
Without the item_defaulting flag, the item is only set as a reference, which means that all mandatory line properties must be present in the payload.

4.5.3. Finalized invoices

Any invoicing document can be finalized immediately on creation (with finalize: true in the payload) or later on during an update or with an explicit API call (with /inv/invoices/{id}/finalize).

Finalization requires a layout setup for the generated PDF (and optionally XML) file as part of the Prerequisites, with the default being applied if none is set in the payload.

Example request for a finalized invoice
POST https://demo.freefinance.at/api/2.0/clients/21334/inv/invoices
Content-Type: application/json
Authorization: eyJ...
{
    "date": "2025-11-11",
    "customer": "af8f...87",
    "finalize": true,
    "lines": [
        {
            "item": "088c...0c",
            "item_defaulting": true,
            "amount": 2.00
        }
    ]
}
Example response for a finalized invoice
HTTP/2 201
Content-Type: application/json
{
  "id": "352a...a6",
  "type": "INVOICE",
  "number": "R-2025.003", (1)
  "state": "FINALIZED", (2)
  "date": "2025-11-11",
  "total": 296.28,
  "open": 296.28,
  "customer": {
    "id": "af8f...87",
    "display_name": "Airbus, Emily Smith"
  },
  "payment_term": {
    "id": "3708...d0", (3)
    "name": "Standard"
  },
  "payment_term_description": "30 days",
  "current_file": {
    "key": {
      "provider": "DMS",
      "provider_id": "c016...e0" (4)
    },
    "file_name": "Invoice_I-2025.003.pdf"
  },
  "lines": [
    {
      "id": "581f...13",
      "number": 1,
      "type": "LINE",
      "name": "An invoice line",
      "account": {
        "id": "a678...fd",
        "name": "Revenue (income)",
        "code": "4000"
      },
      "taxes": {
        "tax_1": {
          "amount": 49.38,
          "tax_class_entry": {
            "id": "9eca...cc",
            "code": "020",
            "tax_rate_value": 20
          }
        }
      },
      "total": 296.28,
      "amount": 2.00,
      "item_price": 123.45,
      "price_type": "NET"
    }
  ]
}
1 The number is issued from the sequence group and can’t be changed, even if the invoice is re-opened.
2 The FINALIZED state prevents most changes to the invoice. It can be re-opened if a mistake was made, which causes the PDF file to be deleted.
3 The payment term is also defaulted and referenced in the invoice.
4 The PDF file is referenced by a provider (which, for generated documents, is always the built-in DMS) and the id within that provider.

To fetch the generated PDF file for the finalized invoice, you can use the files API. The provider_id in the current_file together with the provider itself (DMS in this case) is to be used as path parameter.

Example request to fetch the PDF of a finalized invoice
GET https://demo.freefinance.at/api/2.0/clients/21334/doc/providers/DMS/files/c016...e0/content
Authorization: eyJ...
Example file response
HTTP/2 200
Content-Disposition: attachment; filename="Invoice_I-2025.003.pdf"
Content-Type: application/pdf;charset=UTF-8

4.5.4. Verification of total sums

Supplying the expected invoice totals in the payload for invoice creation is optional. But if you use net amounts, we strongly recommend including the totals to prevent sudden discrepancies.

There are multiple valid methods for computing invoice totals, and many tax jurisdictions don’t enforce one over the others. Here are some examples:

  • The tax amount and totals of each line are calculated and rounded to a currency amount. The invoice totals are the sums of all line totals. This is the approach used by FreeFinance.

  • The totals of each line (with more than 2 decimal places) are not rounded, but added up to the invoice totals. Rounding occurs in the last step for all totals.

If the calculation method of your system doesn’t align with ours, rounding differences will occur in some circumstances.

Not providing any totals and ignoring the possibility of rounding discrepancies lets you avoid any verification errors, but may cause large problems later on in production environments.

In the worst case the total invoice amount calculated by the API unexpectedly shifts by a few cents. This might go largely unnoticed if you don’t verify the sums you receive in the response in turn.

Your customers may suddenly receive an invoice with an amount that is higher than the initial purchase your system calculated. If your application also registers payments (with the open amount you expected), an invoice that was paid in your application is still marked as unpaid in ours (as those few cents still need to be paid).

Supplying invoice totals for verification prevents that, as any discrepancies instantly cause the invoice creation to fail.
If you can’t adapt your system’s calculation to ours, the best alternative we can offer is to lock in the total amounts. This allows minor discrepancies in the net and tax amounts, but the sum of those discrepancies usually ends up to be an insignificant amount. We find that to be an acceptable tradeoff to ensure that created invoices, with their amount and state, match your application’s expectations.

To achieve this and ensure consistent total amounts with newly created invoices, follow these steps:

  • Set the price_type of all lines to GROSS. This ensures that the line total is calculated as the item_price (with tax already included) multiplied with the amount. The net and tax totals are calculated backwards from the total amount and the tax rate.

  • Include the line total amounts in the payload rounded to a currency amount. If your application rounds with the invoice totals only, you must first calculate the invoice total as a currency amount and divide that amount between the line totals.

  • Do not include the net and tax sums in your payload to bypass verification of those fields. This means that the unavoidable discrepancies are permitted, but contained to net and tax totals.

Here is an example:

Example request for an invoice with totals verified
POST https://demo.freefinance.at/api/2.0/clients/21334/inv/invoices/
Content-Type: application/json
Authorization: eyJ...
{
    "date": "2025-11-11",
    "customer_name": "Ernest Example",
    "total": 296.28,
    "lines": [
        {
            "name": "An invoice line",
            "item_price": 148.14,
            "amount": 2.00,
            "price_type": "GROSS",
            "total": 296.28,
            "account": "a678...fd",
            "taxes": {
                "tax_1": {
                    "tax_class_entry": "9eca...cc"
                }
            }
        }
    ]
}

Any discrepancy in the invoice total or line totals will raise a validation error, but net and tax totals may shift by a few cents.

4.6. PDF and image upload

With this API you can upload files (PDF, XML and images) to the staging folder of the client. The files are then available in the My InvoicesPosting by PDF/File view of the application, which allows creating incoming/outgoing invoices and rebooks with them.

When uploading a PDF file or image, the built-in OCR detection feature extracts key information like the date, invoice amount, supplier etc. from it. This metadata is then used for suggestions when creating invoices.

For example, if the VAT number of a supplier is found, the suppliers in your master data are queried for a supplier with such a number. The invoice total is defaulted as amount, and so on. The list of known metadata keys in the OpenAPI specification shows you what kind of information can be extracted.

With proper preparation of your customers and suppliers, you can minimize the work required to create invoices: If they have VAT numbers and defaults for account and tax class entry, the UI can match them and fill all required fields when creating an incoming/outgoing invoice. You only need to do a quick review and a final click, rather than entering all data manually each time.

While the API also allows adding a list of metadata to the upload with well-known keys, custom metadata is ignored by the application to prevent inconsistent behavior and errors due to faulty data. If you don’t need the OCR detection feature and want to provide metadata yourself, contact us to mark your application as trusted.

Well-known XML formats like X-Invoice are parsed automatically. The same is true if a PDF has an embedded XML - OCR is skipped and the XML data has priority.

The upload API only allows files with a maximum size of 2 MB. If your files exceed this limit (and you can’t compress them), connect an external storage provider like Dropbox, Google Drive, or Onedrive.

4.6.1. Upload a document to the staging folder

Unlike other resources in the API, the staging upload accepts a multipart/form-data request.
It consists of two parts: an optional JSON payload with metadata like a description, and one or multiple binary attachments. The file name is taken from the Content-Disposition of the first attachment and must not be already present in the folder.

Minimal example for a simple file upload
POST https://demo.freefinance.at/api/2.0/clients/21334/doc/providers/DMS/staging
Content-Type: multipart/form-data; boundary=WebAppBoundary
Authorization: Bearer eyJhbGci...Vm-yw
--WebAppBoundary
Content-Disposition: form-data; name="content"; filename="document.pdf"
Content-Type: application/pdf

<!-- raw content of document.pdf -->

--WebAppBoundary--
Response of a file upload
HTTP/1.1 201
Content-Type: application/json;charset=UTF-8
{
  "key": {
    "provider": "DMS",
    "provider_id": "30ac...c6"
  },
  "file_name": "document.pdf",
  "content_type": "application/pdf",
  "parent_folder": {
    "provider": "DMS",
    "provider_id": "975c...36"
  },
  "size": 11782
}

The uploaded file is now ready in the staging folder and will be processed by OCR. If no OCR processing is required, you can supply the property "skip_ocr": true.

If your application doesn’t support multipart requests, you can upload the file base64-encoded as part of the JSON payload instead. This creates additional overhead, so only use it if multipart support can’t be achieved.

Example for a file upload with base64-encoded content
POST https://demo.freefinance.at/api/2.0/clients/21334/doc/providers/DMS/staging/json
Content-Type: application/json
Authorization: Bearer eyJhbGci...Vm-yw
{
    "file_name": "document-json.pdf",
    "content_type": "application/pdf",
    "content": "JVBERi0xLjIN...0YNCg==",
    "description": "My example file with base64"
}

The response is the same as with the multipart upload.

4.6.2. Upload a document with metadata

If your application is allowed to provide metadata, you can supply it with the multipart or base64-json request.

Example for a file upload with sidecar JSON metadata
POST https://demo.freefinance.at/api/2.0/clients/21334/doc/providers/DMS/staging
Content-Type: multipart/form-data; boundary=WebAppBoundary
Authorization: Bearer eyJhbGci...Vm-yw
--WebAppBoundary
Content-Disposition: form-data; name="metadata"
Content-Type: application/json

{
  "description": "Example PDF", (1)
  "skip_ocr": true, (2)
    "metadata": [ (3)
    {
      "key": "MERCHANT_NAME", (4)
      "text_value": "My Test Company"
    },
    {
      "key": "PAID_DATE",
      "date_value": "2021-05-01"
    },
    {
      "key": "TOTAL_AMOUNT",
      "numeric_value": 100.00,
      "qualifier": 1 (5)
    },
    {
      "key": "TAX_RATE",
      "numeric_value": 20,
      "qualifier": 1
    },
    {
      "key": "TOTAL_AMOUNT",
      "numeric_value": 200.00,
      "qualifier": 2
    },
    {
      "key": "TAX_RATE",
      "numeric_value": 10,
      "qualifier": 2
    }

  ]
}
--WebAppBoundary
Content-Disposition: form-data; name="content"; filename="document-1.pdf"
Content-Type: application/pdf

<!-- raw content of document1.pdf -->

--WebAppBoundary--
1 If one is provided, the description will be used instead of the file name for listings in the UI.
2 With this flag no OCR processing will be done, which is useful if you supply the metadata yourself.
3 If you have to use the base64-json variant, supply the metadata array as a regular property of the payload.
4 To find available keys, you can consult the OpenAPI specification. The data type depends on the key (text, date, numeric, boolean).
5 The qualifier limits the scope of this metadata entry to a sub-element of the invoice, in this case a single invoice line. All entries with the same qualifier apply to one specific line instead of the entire invoice.

5. FAQ

Q: Where do I get a token for the API?
A: Tokens are obtained from a dedicated IDP under a different URL. The API can provide it to you with the /auth/issuer resource. See Authentication.

Q: I got the client id and secret for a Technical User and only get 403 errors when trying any resource under a client. What do I have to use as path parameter for the client?
A: The API client id for the Technical User consists of two number separated by a '_' character and is accompanied by an API client secret. The first half before the separator is the actual client id for the bookkeeping client and the second is a random suffix. If your API client id looks like 1234_987654321, then 1234 is the client ID you should use as path parameter for API requests, like /api/2.0/clients/1234/cbs/accounts. See Clients.