API Status Get API Key

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

  1. Log in to your Buffer account
  2. Go to Settings → API
  3. Create a new API key
  4. 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 .gitignore or 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_KEY and 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_id and client_secret. Public clients (mobile, desktop, and single-page apps that can't safely store a secret) receive only a client_id and 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

  1. Your app redirects the user to Buffer's authorization page.
  2. The user logs in and approves your app.
  3. Buffer redirects back to your app with an authorization code.
  4. Your app exchanges the code for access and refresh tokens.
  5. 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