# 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](https://buffer.com) account
2. Go to [Settings → API](https://publish.buffer.com/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:

<!-- AUTH_CODE_EXAMPLES -->

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](https://publish.buffer.com/settings/api) and update your applications.

```javascript
// 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](https://buffer.com) if you don't have one.
- A **registered OAuth client**. Visit [Settings → API](https://publish.buffer.com/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`).

<!-- CODE_TABS -->

```javascript
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
<?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 insights:read 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](#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.

<!-- CODE_TABS -->

```javascript
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
<?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`.

<!-- CODE_TABS -->

```javascript
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
<?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:

```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:

<!-- CODE_TABS -->

```javascript
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
<?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

> ⚠️ **Refresh tokens are single-use.** Every successful refresh returns a **new** `refresh_token` and invalidates the one you sent. Always save the latest refresh token and discard the old one. **Reusing an old refresh token revokes all tokens for that grant** — your user will need to re-authorize.

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`.

<!-- CODE_TABS -->

```javascript
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
<?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);
```

### 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.                       |
| `insights:read`  | View performance metrics for posts.            |
| `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:

```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](getting-started.html): Make your first API request
- [Your First Post](your-first-post.html): Go from authenticated to a scheduled post
