Authorization flow
Reference for the authorization request, the redirect and the token exchange.
This page describes each step of the OAuth 2 authorization flow in detail. For a guided first run, see the Quickstart.
ConventionsValues in capitals, such as
YOUR_CLIENT_ID, are placeholders. URLs on the reserved.exampledomain are fictitious:https://app.example/oauth/callbackstands for your redirect URL, andhttps://api.fiduciary.examplefor theapi_urlreceived during the flow.
Overview
| Step | Who | What happens |
|---|---|---|
| 1 | Your application | Redirects the user's browser to the Horus authorization page. |
| 2 | The user | Logs in, selects a license, and authorizes your integration. |
| 3 | Horus | Redirects the browser to your redirect URL with a code, your state and the license's api_url. |
| 4 | Your server | Exchanges the code for an access token and a refresh token, on the api_url. |
| 5 | Your server | Calls the API on the api_url with the access token. |
Step 1 — Authorization request
Redirect the user's browser to:
GET https://my-horus.com/fr/api/oauth2/authorize
Use https://my-horus.com/nl/api/oauth2/authorize to display the Horus pages in Dutch.
| Parameter | Required | Description |
|---|---|---|
client_id | Yes | Your integration's client ID. |
response_type | Yes | Always code. |
redirect_uri | Yes | The URL of the page in your application that receives the result. It must be registered for your integration. |
state | Recommended | A random value generated by your application for this request, returned unchanged in step 3. See The state parameter. |
Query parameter values must be URL-encoded. Example:
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
The state parameter
state parameterstate protects your users against cross-site request forgery (CSRF). Without it, an attacker could make a user's browser land on your callback with an authorization code of the attacker's choosing, and connect the wrong license to your user's account.
- Before redirecting, generate a random, unpredictable value (at least 16 random bytes).
- Store it in the user's session.
- Send it as
state. - On the callback, compare the returned
statewith the stored value. If it is missing or different, reject the request and do not exchange the code.
Use a new value for every authorization request. Never use a constant, a user ID or any other guessable value.
Step 2 — User consent
On the Horus pages, the user:
- logs in with their Horus account,
- selects the license to connect,
- authorizes your integration to access the data available through their account.
Step 3 — Redirect to your application
Access granted
The user's browser is redirected to your redirect_uri with these parameters:
| Parameter | Description |
|---|---|
code | The authorization code, valid for 2 minutes. |
state | The value sent in step 1. Verify it. |
api_url | The base URL of the API of the license the user selected. |
https://app.example/oauth/callback?code=Hjhfn45k&state=Xk9pQ2vL7tRb4mZw&api_url=https://api.fiduciary.example
Store theapi_url
api_urlis specific to the selected license. Store it with the connection and use it for the token exchange and every later request. Never hard-code it.
Access denied
If the user refuses, the browser is redirected to the same URL with an error parameter:
https://app.example/oauth/callback?error=access_denied
Step 4 — Token exchange
From your server, send a POST request to the token endpoint of the received api_url:
POST {api_url}/oauth2/access_token
Content-Type: application/x-www-form-urlencoded
| Parameter | Description |
|---|---|
client_id | Your integration's client ID. |
client_secret | Your integration's client secret. |
code | The authorization code received in step 3. |
grant_type | Must be auth_code. |
redirect_uri | The same value as in step 1. |
auth_code, notauthorization_codeThe Horus API expects
grant_type=auth_code, which differs from the value used by many OAuth 2 libraries. If you use such a library, check that you can override it.
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"// C#: exchange the authorization code on the license's api_url
var response = await http.PostAsync($"{apiUrl}/oauth2/access_token",
new FormUrlEncodedContent(new Dictionary<string, string>
{
["client_id"] = clientId,
["client_secret"] = clientSecret,
["code"] = code,
["grant_type"] = "auth_code",
["redirect_uri"] = "https://app.example/oauth/callback"
}));Response:
{
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "h4vtwzwlfip68zxLCJhbGhsgciO...",
"refresh_token": "8xLOxBtZp8r2tN3kq..."
}| Field | Description |
|---|---|
token_type | Always Bearer. |
expires_in | Lifetime of the access token, in seconds (1 hour). |
access_token | The token to send with every API request. |
refresh_token | Single-use token to obtain a new token pair. See Token lifecycle. |
Step 5 — Call the API
Send the access token in the Authorization header of every request, to the license's api_url:
curl "$API_URL/users.me" \
-H "Authorization: Bearer $ACCESS_TOKEN"Security checklist
- The
client_secretand the token exchange stay on your server. stateis random, stored per user session, and verified on every callback.- The authorization code is exchanged immediately (2-minute validity).
api_url, access token and refresh token are stored together, per license, and encrypted at rest.- Tokens and secrets never appear in logs or URLs.
Updated about 1 hour ago