> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developers.brevo.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developers.brevo.com/_mcp/server.

# Integration guide

This guide covers the HTTP contract for implementing OAuth in your own application — language-agnostic, with curl examples. To start from working code, the [Quickstart](/docs/oauth-quickstart) scaffolds a complete Node.js reference implementation.

![The consent screen a user sees when authorizing an OAuth app, listing the requested scopes](https://fdr-prod-docs-files-public.s3.us-east-1.amazonaws.com/brevo.docs.buildwithfern.com/46bc439a6a78197525f41c4a45004001b5b19607daaf02df89e7f85ab6dec100/docs/assets/images/apps-oauth-authorize-illustration.jpeg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIA6KXJSKKNFOCF7G4B%2F20260905%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260905T173110Z&X-Amz-Expires=604800&X-Amz-Signature=61cb09886d53a78481f77153e33e696a73ceeef90ea83a4ecf81f1fc7e7e9e13&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject)

## Prerequisites

* A registered Brevo OAuth app — create one with `brevo app create` or `brevo app init`
* Your `client_id`, `client_secret`, and a registered `redirect_uri`

## Get your credentials

```bash
brevo app credentials --app-id <APP_ID> --reveal-secret
```

```
App name:      my-app
App ID:        e22eb778-a2a8-488a-a5e8-466b6dad9385
Client ID:     8768a0ad5801806c7946ca7c29648cc2
Client secret: <CLIENT_SECRET>
Scopes:        contacts:read, contacts:write, crm:read, crm:write
Redirect URL 1: http://localhost:3009/auth/callback
```

Store `client_secret` on the server side only. Never expose it in client-side code, browser environments, or version control.

## Step 1 — Redirect to Brevo

Send the user to the Brevo authorization endpoint. Your server constructs this URL and redirects the user's browser.

```
https://oauth.brevo.com/realms/partner/oauth/authorize
  ?response_type=code
  &client_id=<CLIENT_ID>
  &redirect_uri=<REDIRECT_URI>
  &scope=contacts%3Aread%20contacts%3Awrite
  &state=<RANDOM_STATE>
```

**Parameters:**

| Parameter       | Required    | Description                                                                                                                                     |
| --------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `response_type` | Yes         | Must be `code`                                                                                                                                  |
| `client_id`     | Yes         | Your app's client ID                                                                                                                            |
| `redirect_uri`  | Yes         | Must exactly match a URL registered on your app                                                                                                 |
| `scope`         | Yes         | Space-separated list of scopes (URL-encoded as `%20`). Must be a subset of the scopes registered on your app. See [Scopes](/docs/oauth-scopes). |
| `state`         | Recommended | Random string — verify in the callback to prevent CSRF attacks                                                                                  |

Scopes are space-separated in the request and URL-encoded — `contacts:read contacts:write` becomes `scope=contacts%3Aread%20contacts%3Awrite`. The colon (`:`) must also be encoded as `%3A`. Most HTTP libraries handle this when you pass scopes as a normal string parameter.

Always generate a new random `state` value for each authorization request and verify it in the callback. Requests with a missing or mismatched `state` must be rejected.

The user sees the Brevo login page, authenticates with their Brevo credentials, and is redirected to your `redirect_uri`.

## Step 2 — Handle the callback

After the user authenticates, Brevo redirects to your `redirect_uri` with:

```
https://your-app.com/callback?code=AUTH_CODE&state=YOUR_STATE
```

In your callback handler:

1. Verify `state` matches the value you generated in Step 1 — reject mismatches
2. Extract `code` — it expires in **10 minutes**, exchange it immediately
3. Proceed to Step 3

**Error responses on the callback.** If the user denies access or the request is rejected, the callback receives `?error=<code>&error_description=...` instead of `?code=...`. The most common error codes:

| Error             | Cause                                                                                                                 |
| ----------------- | --------------------------------------------------------------------------------------------------------------------- |
| `access_denied`   | The user clicked **Deny** on the consent screen                                                                       |
| `invalid_scope`   | The `scope` parameter contains a scope that's not in the catalog, or one that exceeds what your app is registered for |
| `invalid_request` | A required parameter is missing or malformed (e.g. `redirect_uri` doesn't match a registered URL)                     |

Handle these cases explicitly in your callback route.

## Step 3 — Exchange code for tokens

POST the authorization code to the token endpoint along with your client credentials.

```bash
curl --request POST \
  --url https://oauth.brevo.com/realms/partner/oauth/token \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode 'client_id=<CLIENT_ID>' \
  --data-urlencode 'client_secret=<CLIENT_SECRET>' \
  --data-urlencode 'code=<AUTHORIZATION_CODE>' \
  --data-urlencode 'redirect_uri=<REDIRECT_URI>'
```

**Response:**

```json
{
  "access_token": "eyJhbGci...",
  "refresh_token": "eyJhbGci...",
  "expires_in": 3600,
  "token_type": "Bearer",
  "scope": "contacts:read contacts:write"
}
```

**Token fields:**

| Field           | Type   | Description                                                                                                                                                                                                                 |
| --------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `access_token`  | string | Bearer token for API requests. Include in `Authorization` header.                                                                                                                                                           |
| `refresh_token` | string | Exchange for a new access token when the current one expires. Valid for 30 days.                                                                                                                                            |
| `expires_in`    | number | Access token lifetime in seconds — `3600` (1 hour).                                                                                                                                                                         |
| `token_type`    | string | Always `Bearer`.                                                                                                                                                                                                            |
| `scope`         | string | Space-separated list of scopes actually granted. May be narrower than what you requested if the server filtered a scope that wasn't registered on your app. Use this to detect missing permissions before making API calls. |

The `access_token` is a signed JWT — decode the payload to read the `scope` claim, `exp`, and other claims without an extra network call. To validate a token from another service, POST it to the [introspection endpoint](/docs/oauth-scopes#inspecting-scopes-in-a-token).

## Step 4 — Call the Brevo API

Include the access token as a Bearer token in the `Authorization` header on every API request.

```bash
curl --request GET \
  --url https://api.brevo.com/v3/account \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <ACCESS_TOKEN>'
```

**Response:**

```json
{
  "email": "user@example.com",
  "firstName": "Jane",
  "lastName": "Smith",
  "companyName": "Acme Corp"
}
```

All Brevo API endpoints accept Bearer token authentication. See the [API reference](/reference) for available endpoints.

## Step 5 — Refresh the access token

When the access token expires, use the refresh token to obtain a new one without prompting the user to re-authorize.

```bash
curl --request POST \
  --url https://oauth.brevo.com/realms/partner/oauth/token \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=refresh_token' \
  --data-urlencode 'client_id=<CLIENT_ID>' \
  --data-urlencode 'client_secret=<CLIENT_SECRET>' \
  --data-urlencode 'refresh_token=<REFRESH_TOKEN>'
```

The response has the same shape as Step 3. Always store the new `access_token` — and if a new `refresh_token` is returned, replace the stored one.

## Security checklist

* Never expose `client_secret` in client-side code or public repositories
* Generate a unique `state` per request and validate it in the callback
* Use HTTPS for all `redirect_uri` values in production
* Store tokens server-side — not in `localStorage` or unprotected cookies
* Always replace the stored refresh token when the server returns a new one
* Never commit `.env.local` or files containing credentials
* **Request the minimum scopes your app needs.** Each scope on the consent screen is a permission the user is granting — see [Scopes](/docs/oauth-scopes)