The Buffer API, explained
Yes, Buffer has a public API again. It is a GraphQL API at api.buffer.com, and Buffer's help center, read 29 October 2026, says it is included on every Buffer plan, including Free. A personal API key acts on your own Buffer account; apps that act for other Buffer users register an OAuth client and use the authorization-code flow with PKCE. The old REST API at api.bufferapp.com is the legacy version, and Buffer publishes a guide for moving off it.
Facts on this page come from Buffer's developer documentation and help center, read on 29 October 2026. Buffer adds to the API often, so check its changelog before you build on a detail.
Is the Buffer API still available?
Yes. Many articles still say Buffer shut its API down. Those describe the legacy REST
API at api.bufferapp.com/1/. The current API is a separate GraphQL API at
https://api.buffer.com, and Buffer's
REST migration guide maps the old endpoints to it. Buffer
describes it as covering the core functionality of the legacy API.
Buffer handles publishing to each social network. Your code hands a post to Buffer, and Buffer sends it to the connected channel. The help center lists Instagram, Threads, LinkedIn, X, Facebook, TikTok, Google Business Profile, Mastodon, YouTube, Pinterest, and Bluesky as channels the API can create posts for.
What the API can and can't do
From Buffer's help center:
- Can: create, edit, schedule, and delete posts, including images and video from a public link, first comments, and threads on X, Bluesky, Threads, and Mastodon; create and list ideas; read organizations, channels, and scheduled and sent posts; manage tags (an early preview).
- Can't yet: full analytics (only an experimental set of post metrics, readable with a personal key, not by OAuth apps), reading or replying to comments, Start Page posts, custom video thumbnails, and direct file uploads.
If a channel requires approval, posts created through the API are saved as drafts awaiting approval, the same as in the Buffer dashboard.
API key or OAuth app
Buffer offers two ways in (authentication):
-
Personal API key. Create one under
Settings → API and send it as
Authorization: Bearer <key>. It acts for your account only and can reach every organization and channel on it; there is no per-organization scoping. The help center says only an organization owner can create keys. Use a key for your own scripts, automations, and agents. -
OAuth app client. For an app that other Buffer users connect,
register an app client on the same settings page. Buffer requires the
authorization-code flow with PKCE for every client. Confidential clients get a
client secret; public clients (mobile, desktop, single-page apps) use PKCE alone.
Ask for
offline_accessto get a refresh token. The building apps guide covers redirect URIs, token storage, and going to production.
Keep keys and tokens on your server. Buffer's own advice is to never commit a key or put it in client-side code.
Scheduling a post from TypeScript
Every call is a POST of a GraphQL document to
https://api.buffer.com. To post, you need an organization ID, then a
channel ID from that organization, then a createPost mutation. A key can
reach several organizations, so the example checks each one for a channel. This example schedules a Bluesky
post an hour from now with plain fetch:
const apiKey = process.env["BUFFER_API_KEY"];
if (!apiKey) throw new Error("Set BUFFER_API_KEY");
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null;
// Every Buffer API call is a POST of a GraphQL document to one endpoint.
// The response is checked before use, so it starts as unknown.
async function bufferGraphql(query: string, variables: object = {}): Promise<Record<string, unknown>> {
const response = await fetch("https://api.buffer.com", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify({ query, variables }),
});
if (response.status === 429) {
// Retry-After is in seconds. A 30-day window can be weeks away, so surface it.
throw new Error(`Rate limited; retry after ${response.headers.get("retry-after")} s`);
}
const body: unknown = await response.json();
if (!isRecord(body)) throw new Error("Unexpected response");
if (Array.isArray(body["errors"]) && body["errors"].length > 0) {
throw new Error(JSON.stringify(body["errors"]));
}
if (!isRecord(body["data"])) throw new Error("No data");
return body["data"];
}
const { account } = await bufferGraphql(`{ account { organizations { id name } } }`);
const organizations = isRecord(account) ? account["organizations"] : undefined;
// A key can reach several organizations; use the first Bluesky channel in any of them.
let channel: Record<string, unknown> | undefined;
for (const organization of Array.isArray(organizations) ? organizations : []) {
if (!isRecord(organization)) continue;
const { channels } = await bufferGraphql(
`query Channels($organizationId: OrganizationId!) {
channels(input: { organizationId: $organizationId }) { id service }
}`,
{ organizationId: organization["id"] },
);
const found = (Array.isArray(channels) ? channels : []).find(
(item) => isRecord(item) && item["service"] === "bluesky",
);
if (isRecord(found)) {
channel = found;
break;
}
}
if (!channel) throw new Error("Connect a Bluesky channel in Buffer first");
const { createPost } = await bufferGraphql(
`mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
... on PostActionSuccess { post { id status dueAt } }
... on MutationError { message }
}
}`,
{
input: {
channelId: channel["id"],
text: "Release notes for v2 are up.",
schedulingType: "automatic",
mode: "customScheduled",
dueAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(), // UTC, an hour from now
},
},
);
if (!isRecord(createPost)) throw new Error("Unexpected createPost result");
// Validation problems arrive as a MutationError on HTTP 200, not in `errors`.
if (!isRecord(createPost["post"])) throw new Error(String(createPost["message"]));
const post = createPost["post"];
// A channel that requires approval returns a draft (needs_approval), not a scheduled post.
console.log(post["status"], post["id"], post["dueAt"]);
We type-checked this against Buffer's
GraphQL reference types and ran it on 29 October 2026 with
a stub fetch standing in for api.buffer.com. With the
Bluesky channel in the second of two organizations, it made four requests and printed
scheduled with the post's ID and time. A stubbed
needs_approval draft printed that status instead, and a stubbed
MutationError and 429 each stopped the script with the
message shown. It runs on Node.js
22.12 or later and Bun.
Two details catch people out:
-
Two kinds of error. Problems with your input, such as text over a
channel's limit, come back as a
MutationErrorinsidedatawith HTTP 200. Authentication failures, missing records, and rate limits come back inerrors. On post mutations such ascreatePost, select bothPostActionSuccessandMutationError; other mutations name their own success and error types in the reference. -
Media is a URL, not an upload. There is no upload endpoint.
Host the file yourself at a public, direct HTTPS URL and pass it in
assets. Buffer fetches it when the post publishes, which can be days later, so signed or expiring URLs fail aftercreatePostsucceeded (hosting media).
Scheduling modes
createPost needs a mode and a
schedulingType (posts and scheduling):
| Value | What it does |
|---|---|
mode: addToQueue | Next free slot in the channel's posting schedule |
mode: shareNext | Front of the queue |
mode: shareNow | Publish immediately |
mode: customScheduled | At dueAt, an ISO 8601 UTC time |
schedulingType: automatic | Buffer's workers publish the post |
schedulingType: notification | Buffer reminds someone to publish it by hand |
Set saveToDraft: true to create a draft instead. A post's
status moves through values such as scheduled,
sending, sent, and error; Buffer's API docs
describe no webhooks for post status, so poll it.
Rate limits
Limits depend on your Buffer plan and apply over 15 minutes, 24 hours, and 30 days. All of your API keys and MCP connections share one set of limits, so another key doesn't add requests (rate limits):
| Plan | Keys and request limits |
|---|---|
| Free | 1 key, 1 app client; 150 per 15 min, 600 per 24 h; 1,000 per channel per 30 days, up to 3,000 |
| Essentials | 3 keys, 3 app clients; 450 per 15 min, 1,800 per 24 h; 3,000 per channel per 30 days, up to 30,000 |
| Team | 5 keys, 5 app clients; 1,500 per 15 min, 6,000 per 24 h; 10,000 per channel per 30 days, up to 100,000 |
An OAuth app client has its own limits, separate from your plan's, which grow with the number of Buffer users who authorize it (up to 1,000 counted):
| Window | Base, plus per authorized user |
|---|---|
| 15 minutes | 100, plus 10 |
| 24 hours | 500, plus 25 |
| 30 days | 15,000, plus 100 |
Every response carries RateLimit and RateLimit-Policy
headers, one per window, with the requests remaining (r) and seconds
to reset (t). Going over returns HTTP 429 with a
Retry-After header in seconds and an error naming the window. Buffer
says a 429 doesn't use quota. Queries also have their own limits: a complexity cost
of 175,000 points, depth 25, 30 aliases, 50 directives, and 15,000 tokens.
These request limits are separate from each channel's daily posting limits, which
you read with the dailyPostingLimits query.
Moving off the legacy REST API
Buffer's migration guide lists the changes: one endpoint
instead of one URL per resource, always POST, cursor pagination
(first and after) instead of page and count, typed errors in
the response body instead of HTTP status codes, and an API key or OAuth with PKCE
instead of the old OAuth client flow. REST profiles are now channels, and updates are
posts.
Buffer API alternatives
Buffer's API fits best when you or your users already run social accounts in Buffer: Buffer holds the channel connections and the posting schedule, and your code feeds it posts. If that doesn't match what you're building, the options fall into three groups.
- Post directly to each network. Your own platform apps and tokens, no intermediary, and each platform's own review and limits. Our guides cover the X API, Bluesky API, and Threads API.
- A unified posting API. A provider holds the platform apps and account connections, and you call one API. Their own sites describe Ayrshare, Post for Me, Upload-Post, Outstand, and bundle.social as unified social media APIs, and Zernio (getlate.dev now redirects there) as marketing infrastructure behind one API key. Each has its own pricing and platform list; compare them on their sites.
- Another scheduler with an API. Postiz (open source, which you can self-host) and PostFast are schedulers, like Buffer, that also expose an API.
Using Buffer with Social SDK
Social SDK is an open-source TypeScript library that puts direct
platform adapters and managed backends behind one posts.publish call
with a typed outcome per destination. Buffer is one of those backends, alongside
Zernio, Post for Me, PostFast, Postiz, and Upload-Post, so you can start on Buffer
and switch later without rewriting your posting code. The
Buffer backend page covers API keys, the OAuth
PKCE helpers, scheduling, media URLs, and how Buffer statuses map to outcomes.
The Buffer adapter is on the main branch and not yet in the 0.5.0 npm release, which includes the Zernio, Post for Me, and PostFast backends and the direct adapters. You can write and test your posting code today against the mock backend, which needs no keys:
npm install @opencoredev/social-sdk
import { createSocial, connectedAccountRef } from "@opencoredev/social-sdk";
import { mockBackend } from "@opencoredev/social-sdk/testing";
// The mock backend needs no keys and makes no network calls. In production,
// pass a real backend here instead; the publish call below stays the same.
const social = createSocial({ backend: mockBackend({ scenario: "mixed-success-failure" }) });
const account = (platform: "x" | "bluesky", accountId: string) =>
connectedAccountRef({ backend: "default", platform, accountId });
const result = await social.posts.publish({
targets: [{ account: account("x", "mock-account-1") }, { account: account("bluesky", "mock-account-2") }],
content: { text: "Release notes for v2 are up." },
});
// One outcome per destination: a failure on one network doesn't hide a success on another.
for (const outcome of result.outcomes) {
console.log(outcome.account.platform, outcome.state);
}
We type-checked this against @opencoredev/social-sdk 0.5.0 from npm and
ran it on 29 October 2026. It printed x published and
bluesky failed: the mixed-success-failure scenario fails
every second target, so you can see how your code handles a partial result. The
mock quickstart has the other
scenarios, and the managed backends overview compares
what each provider's adapter supports.
Common questions
- Does Buffer have an API?
-
Yes, a GraphQL API at
https://api.buffer.com. Create a key under Settings → API, or register an OAuth app client to act for other users. - Is the Buffer API free?
- Buffer's help center says API access is included on every plan, including Free, which allows 1 API key and 1,000 requests per connected channel every 30 days, up to 3,000. Paid plans allow more keys and requests.
- Can I upload images or video through the Buffer API?
-
Not as a file upload. Pass a public, stable HTTPS URL in
assets, and keep it reachable until the post publishes. - Can I get analytics from the Buffer API?
- Only an experimental set of post metrics, readable with a personal API key. Buffer says not to rely on it for reporting, and OAuth apps can't read it.
- Does my old api.bufferapp.com code still work?
- That is the legacy REST API. Buffer's migration guide maps each endpoint to the GraphQL API; plan to move to it.
Sources
Read on 29 October 2026:
- What is Buffer's API? (help center): plans, supported channels, what is and isn't supported, approval drafts
- Quick start: endpoint, API key, first request
- Authentication: key scope, OAuth 2.0 with PKCE, scopes
- Building apps: app clients, redirect URIs, production
- Posts and scheduling: modes,
dueAt, statuses - Hosting media: public URL requirements
- Rate limits: plan and app client limits, headers, 429 handling, query limits
- REST API migration: legacy endpoint mapping
- API reference:
createPost,ShareMode,SchedulingType,PostStatus