Machine-to-machine (M2M)

Scoped, short-lived credentials for server-to-server calls — no user, no static key

View as Markdown

Machine-to-machine (M2M) is an OAuth 2.0 client_credentials grant: your own server holds a client_id and client_secret and calls the Brevo API as itself — there’s no end user to redirect and no consent screen. It’s the recommended way to authenticate server-to-server integrations going forward, in place of a static API key.

Why prefer M2M over a static API key

API keyMachine-to-machine
CredentialOne static, all-access secretclient_id + client_secret, scoped to only what you request
What’s sent on each requestThe same long-lived secret, every timeA short-lived access token (~1 hour)
Blast radius of a leakFull account access until you notice and rotateBounded to ~1 hour, and only to the scopes you granted
RotationManual, all-or-nothingbrevo app secret rotate --app-id <id>
SetupCopy from the dashboardbrevo app create --m2m --scopes "..."

Use M2M for any new backend integration that calls Brevo as itself. Reach for a static API key only where you specifically need the simplicity of a single header with no token exchange step.

M2M is a different flow from OAuth apps (the authorization_code grant) — see Which flavor of OAuth do I need? if your app needs to act on behalf of a Brevo user instead of as itself.

Get started

1

Create a Brevo account

Sign up if you don’t have one yet.

2

Install the Brevo CLI

See Installation. If brevo app create’s app-type prompt doesn’t offer Machine to Machine, update to the latest version (see Upgrading).

3

Create an M2M app

brevo app create
? App name: m2m-test
? What distribution type should this app use? Private (Used exclusively by your organisation)
? What type of app are you building? OAuth app (Authorize against Brevo and call the API on a user's behalf)
? Which OAuth flow does this app use? Machine to Machine (Your server calls the API as itself; no user)
Select every scope this app needs — you can add or remove scopes later with `brevo app scopes update --app-id <id> --scopes <a,b,c>`.
? Which scopes does this app need? contacts:read, contacts:write, crm:read, crm:write
App created
App name: m2m-test
App ID: d385abac-dc19-4aec-8bfe-610e70e15691
Client ID: f2c11389ad043b797eade63baf0c9b4a
Client secret: [hidden — run `brevo app credentials --reveal-secret`]

Read the credentials back at any time with brevo app credentials --app-id <app-id> (add --reveal-secret to print the secret).

Pick every scope the app will ever need up front — you can widen or narrow the set later with brevo app scopes update, but each change is a full replace of the granted set. See the Scopes reference for the full catalog (the same scope names used by OAuth apps).

To script app creation non-interactively:

brevo app create --name "My App" --distribution private \
--m2m --scopes "contacts:read,crm:read" --json

--m2m requires --scopes, and --m2m cannot combine with --redirect-uri, --ui-app/--ui-config, or --distribution public — an M2M app has no user to redirect and is private-distribution only.

4

Get an access token

brevo app token --app-id d385abac-dc19-4aec-8bfe-610e70e15691
Authorization: Bearer eyJhbGciOi...

Or request it directly, without the CLI:

curl --request POST \
--url https://oauth.brevo.com/realms/partner/oauth/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=<CLIENT_ID>' \
--data-urlencode 'client_secret=<CLIENT_SECRET>'
{ "access_token": "eyJhbGciOi...", "token_type": "Bearer", "expires_in": 3600, "scope": "all" }

Always request an explicit scope — omitting it currently returns a token scoped to every permission the app has, rather than failing closed. Pass the scopes you actually need, space-separated, e.g. --data-urlencode 'scope=contacts:read crm:read' (or brevo app token --app-id <id> --scope contacts:read,crm:read).

The token is a Bearer token, valid for ~1 hour. There’s no refresh token in the client_credentials grant — when it expires, request a new one the same way.

5

Call the API

Replace the api-key header with Authorization: Bearer <token>:

curl https://api.brevo.com/v3/account \
--header 'Authorization: Bearer <ACCESS_TOKEN>'

Manage the app over its lifetime

Every lifecycle command is keyed by --app-id.

# List every M2M app in the account
brevo app list --type m2m
# Read credentials back (secret hidden unless you ask)
brevo app credentials --app-id <app-id> --reveal-secret
# Zero-downtime secret rotation — the old secret keeps working for a grace window
brevo app secret rotate --app-id <app-id>
# Widen or narrow granted scopes (full replace, not a delta)
brevo app scopes update --app-id <app-id> --scopes contacts:read,crm:read
# Delete the app
brevo app delete --app-id <app-id>

See the CLI reference for every flag.

Limitations (v1)

  • No access-token revocation. Deleting the app or rotating the secret stops new tokens from being issued, but a token already handed out stays valid until it expires (~1 hour). Store secrets in a secret manager and rotate promptly if one leaks.
  • One account per credential. Multi-account M2M credentials aren’t supported yet.
  • No refresh token. Request a new token with the same client_credentials call when the old one expires — this is standard for the grant, not a Brevo-specific gap.