Authentication
Every request to the Buffer API needs an API key. Here's how to get one and start using it.
Getting your API key
- Log in to your Buffer account
- Go to Settings → API
- Create a new API key
- Copy the key
Using your API key
Include your key in the Authorization header of every request:
{
"Authorization": "Bearer YOUR_TOKEN"
}
-H 'Authorization: Bearer YOUR_TOKEN'
headers: {
'Authorization': 'Bearer YOUR_TOKEN',
}
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_TOKEN',
]
Every request to https://api.buffer.com must include this header. Requests without a valid key will return a 401 Unauthorized error.
Key permissions and scope
- Your API key acts on behalf of your account only
- It can access all organizations and channels in your account
- There is no per-organization scoping at this time
- The key is account-based, not organization-based
If you belong to multiple organizations, your key gives you access to all of them. Use the organization ID in your queries to target a specific one.
Security best practices
- Never commit your API key to version control. Add it to
.gitignoreor use a secrets manager. - Don't expose it in client-side code. API calls should be made from your server, not from a browser or mobile app.
- Use environment variables. Store the key in an environment variable like
BUFFER_API_KEYand reference it in your code. - Rotate your key if compromised. Generate a new one in Settings → API and update your applications.
// Good: read from environment variable
const apiKey = process.env.BUFFER_API_KEY;
// Not advised: hardcoded in source code
const apiKey = "buf_abc123...";
OAuth
Buffer supports OAuth 2.0 so you can build apps that access Buffer accounts on behalf of your users. This guide walks you through the Authorization Code flow with PKCE, which is required for all Buffer OAuth clients.
Prerequisites
Before you start, you'll need:
- A Buffer account. Sign up if you don't have one.
- A registered OAuth client. Visit Settings → API to register your app. Confidential clients (apps that can keep a secret on a server) receive a
client_idandclient_secret. Public clients (mobile, desktop, and single-page apps that can't safely store a secret) receive only aclient_idand authenticate using PKCE alone. - A redirect URI. The URL in your app where Buffer sends users after they approve access. It must match the URI you registered.
How it works
- Your app redirects the user to Buffer's authorization page.
- The user logs in and approves your app.
- Buffer redirects back to your app with an authorization code.
- Your app exchanges the code for access and refresh tokens.
- You use the access token to call the Buffer API.
Step 1: Generate a PKCE code verifier and challenge
PKCE protects the flow from code interception attacks. Generate a random code_verifier and the SHA-256 hash of it (code_challenge).
function generateCodeVerifier() {
const bytes = new Uint8Array(32)
crypto.getRandomValues(bytes)
return btoa(String.fromCharCode(...bytes))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '')
}
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder()
const digest = await crypto.subtle.digest('SHA-256', encoder.encode(verifier))
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '')
}
const codeVerifier = generateCodeVerifier()
const codeChallenge = await generateCodeChallenge(codeVerifier)
<?php
function generateCodeVerifier() {
$bytes = random_bytes(32);
return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
}
function generateCodeChallenge($verifier) {
$hash = hash('sha256', $verifier, true);
return rtrim(strtr(base64_encode($hash), '+/', '-_'), '=');
}
$codeVerifier = generateCodeVerifier();
$codeChallenge = generateCodeChallenge($codeVerifier);
Store the codeVerifier in your session. You'll need it in Step 4.
Step 2: Redirect the user to Buffer
Send the user to the authorization endpoint:
GET https://auth.buffer.com/auth
?client_id=YOUR_CLIENT_ID
&redirect_uri=YOUR_REDIRECT_URI
&response_type=code
&scope=posts:write posts:read ideas:read ideas:write account:read account:write offline_access
&state=RANDOM_STATE_VALUE
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
&prompt=consent
| Parameter | Description |
|---|---|
client_id |
Your app's client ID. |
redirect_uri |
Where Buffer sends the user after they approve. Must match your registered URI. |
response_type |
Always code. |
scope |
The permissions your app needs. See Scopes. |
state |
A random string to prevent CSRF. Verify it on return. |
code_challenge |
The base64url-encoded SHA-256 hash of your code_verifier. |
code_challenge_method |
Always S256. |
The user will see a Buffer login screen (if needed), then a consent screen showing your app and the requested permissions.
Step 3: Handle the callback
After the user approves, Buffer redirects to your redirect_uri:
https://yourapp.com/callback?code=AUTHORIZATION_CODE&state=STATE_VALUE
If they deny, you'll receive an error parameter instead:
https://yourapp.com/callback?error=access_denied&state=STATE_VALUE
Verify the state matches what you stored in Step 2, then extract the code for the next step.
app.get('/callback', (req, res) => {
const { code, state, error } = req.query
if (state !== req.session.oauthState) {
return res.status(403).send('Invalid state parameter')
}
if (error) {
return res.status(403).send('User denied access')
}
// Exchange the code for tokens (Step 4)
})
<?php
session_start();
$code = $_GET['code'] ?? null;
$state = $_GET['state'] ?? null;
$error = $_GET['error'] ?? null;
if ($state !== ($_SESSION['oauth_state'] ?? null)) {
http_response_code(403);
exit('Invalid state parameter');
}
if ($error) {
http_response_code(403);
exit('User denied access');
}
// Exchange the code for tokens (Step 4)
Step 4: Exchange the code for tokens
POST the code and verifier to the token endpoint:
POST https://auth.buffer.com/token
Content-Type: application/x-www-form-urlencoded
client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET # confidential clients only — omit for public clients
&grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https://yourapp.com/callback
&code_verifier=CODE_VERIFIER
Public clients authenticate with the code_verifier alone and must not send a client_secret. Confidential clients send both the client_secret and the code_verifier.
const response = await fetch('https://auth.buffer.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET',
grant_type: 'authorization_code',
code,
redirect_uri: 'https://yourapp.com/callback',
code_verifier: req.session.codeVerifier
}),
})
const tokens = await response.json()
<?php
$ch = curl_init('https://auth.buffer.com/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
CURLOPT_POSTFIELDS => http_build_query([
'client_id' => 'YOUR_CLIENT_ID',
'client_secret' => 'YOUR_CLIENT_SECRET',
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => 'https://yourapp.com/callback',
'code_verifier' => $_SESSION['code_verifier']
]),
CURLOPT_RETURNTRANSFER => true,
]);
$tokens = json_decode(curl_exec($ch), true);
curl_close($ch);
The response is JSON:
{
"access_token": "eyJhbGciOi...",
"refresh_token": "v1.MjAyNi...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "posts:write posts:read ideas:read ideas:write account:read account:write offline_access"
}
| Field | Description |
|---|---|
access_token |
The token to send with API requests. |
refresh_token |
Long-lived token used to obtain a new access_token. Only returned if the offline_access scope is requested |
token_type |
Always Bearer. |
expires_in |
Lifetime of the access_token in seconds. |
scope |
Space-separated list of scopes granted. |
Store the tokens securely on your server.
Step 5: Make API requests
Send the access token in the Authorization header:
await fetch('https://api.buffer.com', {
method: 'POST',
headers: {
Authorization: `Bearer ${tokens.access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: '{ account { id email } }' }),
})
<?php
$ch = curl_init('https://api.buffer.com');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $tokens['access_token'],
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['query' => '{ account { id email } }']),
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);
Refreshing tokens
When the access token expires, exchange your refresh token for a new pair:
POST https://auth.buffer.com/token
Content-Type: application/x-www-form-urlencoded
client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET # confidential clients only — omit for public clients
&grant_type=refresh_token
&refresh_token=REFRESH_TOKEN
Public clients refresh tokens using only their client_id and refresh_token — no client_secret is required. Confidential clients must include the client_secret.
const response = await fetch('https://auth.buffer.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET',
grant_type: 'refresh_token',
refresh_token: storedRefreshToken,
}),
})
const tokens = await response.json()
<?php
$ch = curl_init('https://auth.buffer.com/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
CURLOPT_POSTFIELDS => http_build_query([
'client_id' => 'YOUR_CLIENT_ID',
'client_secret' => 'YOUR_CLIENT_SECRET',
'grant_type' => 'refresh_token',
'refresh_token' => $storedRefreshToken,
]),
CURLOPT_RETURNTRANSFER => true,
]);
$tokens = json_decode(curl_exec($ch), true);
curl_close($ch);
Buffer rotates refresh tokens. Each refresh returns a new refresh_token and invalidates the old one. Always save the latest. Reusing an old refresh token revokes all tokens for that grant.
Scopes
Include the scopes your app needs in the scope parameter on the authorization request.
| Scope | Description |
|---|---|
posts:read |
View posts and queue. |
posts:write |
Create and manage posts on the user's behalf. |
ideas:read |
View ideas. |
ideas:write |
Create and manage ideas on the user's behalf. |
account:read |
View account information. |
account:write |
Update account settings. |
offline_access |
Receive a refresh token for long-lived access. |
Errors
If the authorization flow fails, Buffer returns an error parameter on the redirect.
| Error | Meaning |
|---|---|
access_denied |
The user denied your app. |
invalid_request |
The request is missing or has invalid parameters. |
invalid_client |
The client_id is not recognized. |
invalid_grant |
The code is expired, already used, or invalid. |
invalid_scope |
The requested scope is not valid. |
Token exchange errors are returned as JSON:
{
"error": "invalid_grant",
"error_description": "Authorization code has expired"
}
Revoking access
Users can revoke your app from their Buffer account settings at any time. When access is revoked, all tokens for your app are invalidated. Handle 401 Unauthorized responses by prompting the user to re-authorize.
Next steps
- Quick Start: Make your first API request
- Your First Post: Go from authenticated to a scheduled post