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

# The OAuth flow

> OAuth 2.0 is a protocol that lets your app request access to a Qonto user account without getting their password. Your app can ask for [specific permissions](/api-reference/business-api/introduction/authentication/scopes) that the user will be able to review and validate.

Once the user has granted you access to his account you will be able to get a token that can be use to access Qonto API on his behalf.

> [!NOTE]
>
> **New to OAuth 2.0?** Check out these generic guides before diving in:
>
> * [OAuth 2 Explained In Simple Terms](https://www.youtube.com/watch?v=ZV5yTm4pT8g) — ByteByteGo video (4:30)
> * [What is OAuth 2.0?](https://auth0.com/intro-to-iam/what-is-oauth-2) — Auth0 written guide

> [!NOTE]
>
> **Already have an API key?** If you only need to automate your own Qonto account, you may not need OAuth at all. Check the [authentication introduction](https://docs.qonto.com/api-reference/business-api/introduction/authentication/methods.md) to pick the right method for your use case.

## Step by step

![Oauth 2 flow](https://qonto-assets.s3.eu-central-1.amazonaws.com/oauth-clients/oauth_flow.png "OAuth 2.0 authentication flow")

### 1. Authorize

The first step is to redirect the user to the Qonto OAuth server. The Qonto user will be invited to authenticate.

> [!WARNING]
>
> **This URL must be opened by the end user's own browser — never called by your backend or any HTTP client (`curl`, `fetch`, `requests`...).** Your server only builds the URL string, then either redirects the user to it using an HTTP `302` status code or opens a pop-up with that URL. Calling this endpoint yourself (e.g. with `curl`) and forwarding the resulting redirect location to the user breaks the CSRF cookie binding tied to the browser session, and causes `request_forbidden: No CSRF value available in the session cookie`.

> [!NOTE]
>
> **Typical integration pattern:** the user visits a route on *your* app (e.g. `GET /integrations/qonto/authorize`). Your backend builds the authorization URL below and responds with an HTTP `302` redirect to it — the user's browser then follows that redirect to Qonto.

![Oauth 2 flow](https://qonto-assets.s3.eu-central-1.amazonaws.com/oauth-clients/flow/OAuth-login-screen.png "User login")

Then they will have to allow your application to access one of the organizations they are part of.

![Oauth 2 flow](https://qonto-assets.s3.eu-central-1.amazonaws.com/oauth-clients/flow/OAuth-consent-screen.png "User consent")

> [!WARNING]
>
> If you need your integration to run unattended (e.g. automations, scheduled tasks), include `offline_access` in your `scope` parameter. Without it, you will not receive a refresh token and your integration will stop working after 1 hour.

> [!NOTE]
>
> **CSRF protection with `state`:** Generate a unique, unpredictable value (e.g. a random 32-byte hex string), include it in the authorization URL as `state`, and store it in the user's session. When the user is redirected back to your `redirect_uri`, verify that the received `state` matches what you stored. Reject the flow if they differ — this protects against cross-site request forgery.

> [!NOTE]
>
> **Optional: restrict org selection.** You can pass `organization_id` to lock the flow to a specific organization, or `registration_id` to pre-select an org from the onboarding flow. In both cases the org selection screen is skipped.

> **What the Qonto user sees**
>
> 1. Your app should include a "Connect with Qonto" button which should trigger this endpoint;
>
> > [!WARNING]
> >
> > If you are triggering this endpoint on the Sandbox environment ([https://oauth-sandbox.staging.qonto.co](https://oauth-sandbox.staging.qonto.co)), please make sure to be logged in on the [Sandbox web-app](https://sandbox.staging.qonto.co/devportal) first through your [Developer Portal](https://developers.qonto.com/) account.
>
> > [!NOTE]
> >
> > If you just want to connect your app to one single organization, you can just send the endpoint URL to the Qonto user from whom you need to get the consent.
>
> 2. After clicking on your button, the Qonto user will be redirected to Qonto and will have to authenticate;
> 3. (optional) The Qonto will have to select an organization;
> 4. The Qonto user will have to give their consent to your app for the organization they have just selected (or for the organization identified by the `organization_id` or the `registration_id` query parameter);
> 5. The Qonto user will be automatically redirected back to your `redirect_uri`.

cf. [this endpoint](https://docs.qonto.com/api-reference/business-api/authentication/oauth2/retrieve_authorization.md) for a full technical description of the query parameters.

> [!NOTE]
>
> These snippets only **build** the authorization URL string using each language's URL-encoding library — they never call it. There is no cURL example here on purpose: cURL is a request tool, and issuing the request yourself (from your server) is exactly the mistake to avoid (see warning above).

**Python**

```python theme={null}
import urllib.parse
import secrets

state = secrets.token_urlsafe(32)  # store in session for CSRF verification

params = {
    "client_id": "your-client-id",
    "redirect_uri": "https://your-app.com/callback",
    "response_type": "code",
    "scope": "offline_access organization.read",
    "state": state,
}
auth_url = "https://oauth.qonto.com/oauth2/auth?" + urllib.parse.urlencode(params)

# Do not requests.get(auth_url) yourself. On your backend, respond to the
# user's request with an HTTP 302 redirect to auth_url, e.g. in Flask:
# return redirect(auth_url)
```

**Node.js**

```javascript theme={null}
const crypto = require('crypto');

const state = crypto.randomBytes(32).toString('hex'); // store in session

const params = new URLSearchParams({
  client_id: 'your-client-id',
  redirect_uri: 'https://your-app.com/callback',
  response_type: 'code',
  scope: 'offline_access organization.read',
  state,
});
const authUrl = `https://oauth.qonto.com/oauth2/auth?${params.toString()}`;

// Do not fetch(authUrl) yourself. On your backend, respond to the user's
// request with an HTTP 302 redirect to authUrl, e.g. in Express:
// res.redirect(authUrl)
```

**After consent**, Qonto redirects the user to your `redirect_uri` with a temporary `code` and the `state` you provided:
`https://your-app.com/callback?code=AUTH_CODE&state=YOUR_STATE`

**400 invalid\_grant**

Invalid `client_id` or `redirect_uri` in the authorization request.

**404**

The `client_id` was not found. Double-check the value from your Developer Portal.

### 2. Exchange the authorization code for an access token

Once user has granted access to his account, he will be rederected to your application via your `redirect_uri` with a temporary authorization code.

On your backend, you will have to exchange this code for an `access_token`.

> [!WARNING]
>
> **Do not use `Authorization: Basic` headers.** Qonto requires `client_id` and `client_secret` as form body parameters. Sending them in the header will result in an `invalid_client` error.

> [!WARNING]
>
> Never expose your `client_secret` on the client side — this call must be made from your backend.

> [!WARNING]
>
> The `redirect_uri` must exactly match the value registered with Qonto, including trailing slashes and query strings. Pass it as a plain string and let your HTTP library handle the form encoding — do not manually pre-encode it (e.g. avoid `https%3A%2F%2F...`). A mismatch causes `invalid_grant`.

**cURL**

```bash theme={null}
curl -X POST https://oauth.qonto.com/oauth2/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=authorization_code' \
  -d 'code=YOUR_CODE' \
  -d 'client_id=YOUR_CLIENT_ID' \
  -d 'client_secret=YOUR_CLIENT_SECRET' \
  -d 'redirect_uri=https://your-app.com/callback'
```

**Python**

```python theme={null}
import requests

# Verify state matches what you stored in the session before proceeding

response = requests.post(
    "https://oauth.qonto.com/oauth2/token",
    data={
        "grant_type": "authorization_code",
        "code": code,  # received in redirect_uri callback
        "client_id": "your-client-id",
        "client_secret": "your-client-secret",
        "redirect_uri": "https://your-app.com/callback",
    },
)
tokens = response.json()
access_token = tokens["access_token"]
refresh_token = tokens["refresh_token"]  # requires offline_access scope
```

**Node.js**

```javascript theme={null}
// Verify state matches what you stored in the session before proceeding

const response = await fetch('https://oauth.qonto.com/oauth2/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code,  // received in redirect_uri callback
    client_id: 'your-client-id',
    client_secret: 'your-client-secret',
    redirect_uri: 'https://your-app.com/callback',
  }),
});
const { access_token, refresh_token } = await response.json();
// refresh_token requires offline_access scope
```

The response contains an `access_token` (valid **1 hour**) and, if `offline_access` was requested, a `refresh_token` (valid **90 days**).

> [!NOTE]
>
> Store the `refresh_token` securely — use it to obtain new access tokens without user interaction. See the [Refresh token endpoint](https://docs.qonto.com/api-reference/business-api/authentication/oauth2/create_tokens.md).

### Error responses

**400 invalid\_request**

**Causes:** Missing required parameter (`code`, `client_id`, `client_secret`, `redirect_uri`, or `grant_type`), or wrong `Content-Type` header (must be `application/x-www-form-urlencoded`).

**400 invalid\_client**

**Causes:** Wrong `client_id` or `client_secret`, or credentials sent via an `Authorization: Basic` header instead of as form body parameters.

**400 invalid\_grant**

**Causes:** Authorization code expired (10-minute TTL), code already used, or `redirect_uri` does not exactly match the registered value.

**400 invalid\_scope**

**Causes:** One or more of the requested scopes are not activated on your OAuth application.

cf. [API reference](https://docs.qonto.com/api-reference/business-api/authentication/oauth2/create_tokens.md) for full parameter details.

### 3. Use your access token (Bearer token)

To perform authenticated requests on the Qonto API, you will have to provide the `access_token` in the `Authorization` header, as describe in this example:

**cURL**

```bash {4} theme={null}
curl GET 'https://thirdparty.qonto.com/organization' \
  --header 'Accept: application/json' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
```

**Python**

```python {5} theme={null}
import requests

response = requests.get(
    "https://thirdparty.qonto.com/organization",
    headers={"Authorization": f"Bearer {access_token}"},
)
organization = response.json()
```

**Node.js**

```javascript {3} theme={null}
const response = await fetch('https://thirdparty.qonto.com/organization', {
  headers: {
    Authorization: `Bearer ${access_token}`,
  },
});
const { organization } = await response.json();
```

### 4. Refresh your access token

The `access_token` expires after **1 hour**. Rather than restarting the entire OAuth flow, use the `refresh_token` to obtain a new one silently on your backend.

> [!WARNING]
>
> **Refresh tokens are one-time use.** Every refresh call invalidates the token you used and returns a new `refresh_token` in the response. You must store this new token immediately — using an old refresh token will result in an `invalid_grant` error. If two processes attempt to refresh the same token concurrently, the second request will always fail.

**cURL**

```bash {3, 5} theme={null}
curl -X POST https://oauth.qonto.com/oauth2/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=refresh_token' \
  -H 'Accept: application/json' \
  -d 'refresh_token=YOUR_REFRESH_TOKEN' \
  -d 'client_id=YOUR_CLIENT_ID' \
  -d 'client_secret=YOUR_CLIENT_SECRET'
```

**Python**

```python {6, 7} theme={null}
import requests

response = requests.post(
    "https://oauth.qonto.com/oauth2/token",
    data={
        "grant_type": "refresh_token",
        "refresh_token": stored_refresh_token,
        "client_id": "YOUR_CLIENT_ID",
        "client_secret": "YOUR_CLIENT_SECRET",
    },
)
tokens = response.json()
# Store both new tokens — the old refresh_token is now invalidated
access_token = tokens["access_token"]
stored_refresh_token = tokens["refresh_token"]
```

**Node.js**

```javascript {5, 6} theme={null}
const response = await fetch('https://oauth.qonto.com/oauth2/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'refresh_token',
    refresh_token: storedRefreshToken,
    client_id: 'YOUR_CLIENT_ID',
    client_secret: 'YOUR_CLIENT_SECRET',
  }),
});
const tokens = await response.json();
// Store both new tokens — the old refresh_token is now invalidated
accessToken = tokens.access_token;
storedRefreshToken = tokens.refresh_token;
```

**Token lifecycle at a glance:**

| Token | Lifetime | What to do when it expires |
| - | - | - |
| `access_token` | 1 hour | Use `refresh_token` to get a new one — no user action needed |
| `refresh_token` | 90 days | Restart the OAuth flow (user must re-authorize) |

As long as you refresh before the 90-day window closes and correctly store the latest `refresh_token` after each call, your integration will run indefinitely without requiring user re-authorization.

For full parameter details and error responses, see the [Refresh token endpoint](https://docs.qonto.com/api-reference/business-api/authentication/oauth2/create_tokens.md).

## Resources

* If you need to understand better the OAuth flow: [**Postman visual flow**](https://www.postman.com/qontoteam/workspace/qonto-public-api/flow/666c37ad3a6c630032f5b496).

* If you need more details about OAuth 2.0: [**Official documentation**](https://oauth.net/2/).

* Testing in the sandbox? See the [**Sandbox guide**](https://docs.qonto.com/guides/authentication/oauth-sandbox.md).

## Related endpoints

* [Create or refresh tokens](https://docs.qonto.com/api-reference/business-api/authentication/oauth2/create_tokens.md)
* [Retrieve an authorization code](https://docs.qonto.com/api-reference/business-api/authentication/oauth2/retrieve_authorization.md)
* [Revoke consents](https://docs.qonto.com/api-reference/business-api/authentication/oauth2/revoke-oauth-consents.md)
