Skip to navigation

Integration guide

Implement the OAuth 2.0 authorization code flow in your application
View as Markdown

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 scaffolds a complete Node.js reference implementation.

The consent screen a user sees when authorizing an OAuth app, listing the requested scopes

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

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:

ParameterRequiredDescription
response_typeYesMust be code
client_idYesYour app’s client ID
redirect_uriYesMust exactly match a URL registered on your app
scopeYesSpace-separated list of scopes (URL-encoded as %20). Must be a subset of the scopes registered on your app. See Scopes.
stateRecommendedRandom 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:

ErrorCause
access_deniedThe user clicked Deny on the consent screen
invalid_scopeThe scope parameter contains a scope that’s not in the catalog, or one that exceeds what your app is registered for
invalid_requestA 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.

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:

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

Token fields:

FieldTypeDescription
access_tokenstringBearer token for API requests. Include in Authorization header.
refresh_tokenstringExchange for a new access token when the current one expires. Valid for 30 days.
expires_innumberAccess token lifetime in seconds — 3600 (1 hour).
token_typestringAlways Bearer.
scopestringSpace-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.

Step 4 — Call the Brevo API

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

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

Response:

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

All Brevo API endpoints accept Bearer token authentication. See the API 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.

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