Quickstart

Connect to a Horus Office license and make your first API calls.

This guide takes you from zero to your first successful API calls. Allow about 15 minutes.

You will:

  1. send a user to the Horus authorization page,
  2. receive an authorization code and the API URL of the license they selected,
  3. exchange the code for an access token,
  4. call the API.

Before you start

You need:

RequirementWhere to get it
A client_id and a client_secretSent to you when you register your integration.
A redirect URL registered for your integrationProvided during registration. Use exactly the registered value in the steps below.
A Horus account with access to at least one licenseIdeally a test license or a test folder: requests run on real data.
📘

About the values in this guide

Values written in capitals, such as YOUR_CLIENT_ID, are placeholders: replace them with your own values.

Example URLs use the reserved .example domain (e.g. https://app.example/oauth/callback). They do not exist and only show the shape of the value. Never copy them as-is.

Step 1 — Send the user to the authorization page

Your application redirects the user's browser to the Horus authorization page:

https://my-horus.com/fr/api/oauth2/authorize

Use /nl/api/ instead of /fr/api/ to display the pages in Dutch.

Add these query parameters:

ParameterValue
client_idYour integration's client ID.
response_typeAlways code.
redirect_uriYour redirect URL: the page of your application that will receive the result. It must be registered for your integration, and URL-encoded.
stateA value generated by your application for this request. See below.

Example, with a redirect URL https://app.example/oauth/callback:

https://my-horus.com/fr/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=https%3A%2F%2Fapp.example%2Foauth%2Fcallback&state=Xk9pQ2vL7tRb4mZw

What is state for?

state protects your users against cross-site request forgery (CSRF): it lets you check that a redirect received on your callback really answers a request your application started, for this user.

How to use it:

  1. Before redirecting the user, generate a random, unpredictable value (at least 16 random bytes), for example Xk9pQ2vL7tRb4mZw.
  2. Store it in the user's session on your side.
  3. Send it as the state parameter.
  4. When the user comes back, Horus returns the same value. Compare it with the one in the session. If it is missing or different, reject the response.
// C#: generate a state value and keep it in the session
var state = Convert.ToHexString(RandomNumberGenerator.GetBytes(16));
HttpContext.Session.SetString("horus_oauth_state", state);

state is optional for the API, but we strongly recommend it. Never use a constant or guessable value.

Step 2 — The user logs in and selects a license

On the Horus pages, the user:

  1. logs in with their Horus account,
  2. selects the license they want to connect to your application,
  3. authorizes your integration.

Your application doesn't have to do anything during this step.

Step 3 — Handle the redirect

Horus then redirects the user's browser to your redirect_uri, with the result in the query string:

https://app.example/oauth/callback?code=Hjhfn45k&state=Xk9pQ2vL7tRb4mZw&api_url=https://api.fiduciary.example
ParameterDescription
codeThe authorization code. It is valid for 2 minutes: exchange it right away (step 4).
stateThe value you sent in step 1. Check it before going further.
api_urlThe base URL of the API of the license the user selected.
🚧

api_url is different for every license

Each fiduciary or SMB using Horus Office has its own database and its own API endpoint. The api_url in the example above is fictitious: yours will be different, and it will differ from one license to another.

Read api_url from the redirect, store it with the user's connection, and use it as the base URL for every subsequent request, including the token exchange. Never hard-code it.

If the user refuses access, they are redirected to the same URL with an error instead:

https://app.example/oauth/callback?error=access_denied

Show the user a message and let them start again.

📘

Testing without a callback page

For a first test, you don't need a working callback page. Open the authorization URL in your browser and complete the login. Even if the redirect page fails to load, the browser's address bar shows the full redirect URL: copy code and api_url from it. Be quick: the code expires after 2 minutes.

Step 4 — Exchange the code for an access token

Send the code to the token endpoint of the api_url received in step 3, from your server. The request contains your client_secret, which must never be exposed in a browser or a mobile app.

To make the examples below easy to copy, first store the API URL in a shell variable:

export API_URL="https://api.fiduciary.example"   # the api_url received in step 3
curl -X POST "$API_URL/oauth2/access_token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "client_id=YOUR_CLIENT_ID" \
  --data-urlencode "client_secret=YOUR_CLIENT_SECRET" \
  --data-urlencode "code=Hjhfn45k" \
  --data-urlencode "grant_type=auth_code" \
  --data-urlencode "redirect_uri=https://app.example/oauth/callback"
ParameterValue
client_id / client_secretYour integration's credentials.
codeThe code received in step 3.
grant_typeauth_code (not the usual authorization_code).
redirect_uriExactly the same value as in step 1.

Response:

{
  "token_type": "Bearer",
  "expires_in": 3600,
  "access_token": "h4vtwzwlfip68zxLCJhbGhsgciO...",
  "refresh_token": "8xLOxBtZp8r2tN3kq..."
}

Store, on your server and per connected license:

  • the api_url,
  • the access_token, valid for 1 hour (expires_in is in seconds),
  • the refresh_token, used to get a new access token without asking the user again. See Token lifecycle.
export ACCESS_TOKEN="h4vtwzwlfip68zxLCJhbGhsgciO..."

Step 5 — Make your first call

Every request sends the access token in the Authorization header. Start by checking which user you are connected as:

curl "$API_URL/users.me" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
{
  "Data": {
    "Id": "14feb291-8b47-44c6-8cbb-97a3f24c33c8",
    "FirstName": "Robert",
    "LastName": "Doe",
    "Email": "[email protected]",
    "Language": "FR",
    "IsAccountant": true
  }
}

Every response with data puts it under a top-level Data key.

Step 6 — Find a folder and read its data

Almost everything in Horus Office belongs to a folder: the accounting record of one company. List the folders the user can access:

curl -X POST "$API_URL/folders.list" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "Mode": 0 }'

Mode: 0 returns all accessible folders. Mode: 1 returns only the folders the accountant is responsible for.

{
  "Data": [
    {
      "Id": "83f26e75-f474-4b09-a1d3-aa698e18fbf0",
      "SearchKey": "REDCORP",
      "FolderName": "RedCorp SRL",
      "VatNumber": "BE0000000097",
      "IsVatLiable": true
    }
  ]
}

Pick a folder's Id: it is the FolderId required by most other endpoints. For example, list its customers:

curl -X POST "$API_URL/companies.list" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "FolderId": "83f26e75-f474-4b09-a1d3-aa698e18fbf0", "Filter": { "IsCustomer": true } }'

🎉 Your integration is connected.

Troubleshooting

SymptomLikely cause
The authorization request is rejected because of the redirect URLredirect_uri is not registered for your integration, or differs from the registered one (scheme, trailing slash, port…). Contact [email protected] to register it.
The token exchange failsThe code expired (2 minutes), grant_type is not auth_code, redirect_uri differs from step 1, or the request was sent to the wrong host.
401 Unauthorized on API callsThe Authorization: Bearer header is missing, or the access token expired after 1 hour. Refresh it.
Calls work for one customer but not anotherEach license has its own api_url and its own tokens. Check that you use the pair that belongs to that license.
state doesn't matchThe callback doesn't come from a request your application started for this user. Reject it and restart the flow.

Next steps


Did this page help you?