REST API Migration
If you've been using our REST API (api.bufferapp.com/1/), this guide will help you move over to the GraphQL API. It's faster to work with, returns only the data you need, and supports the core functionality of the legacy API offering.
What's changing
| REST API | GraphQL API | |
|---|---|---|
| Base URL | https://api.bufferapp.com/1/ |
https://api.buffer.com |
| HTTP method | GET, POST per endpoint | Always POST |
| Endpoints | One per resource (/profiles.json, /updates/:id.json) |
Single endpoint for everything |
| Auth | OAuth 2.0 access token | API key via Authorization: Bearer header |
| Response shape | Fixed - server decides what fields to return | You choose exactly which fields you need |
| Pagination | Offset-based (page=1&count=10) |
Cursor-based (first, after) |
| Errors | HTTP status codes (401, 404, etc.) | Typed error unions in the response body |
Authentication
The REST API used OAuth 2.0 with a client ID/secret flow. For automating your own workflows, our GraphQL API uses a simpler API key approach. OAuth support for third-party apps is coming soon.
REST (before):
GET https://api.bufferapp.com/1/user.json?access_token=YOUR_TOKEN
GraphQL (now):
Authorization: Bearer YOUR_API_KEY
Get your API key from Settings > API. Every request to https://api.buffer.com must include:
Authorization: Bearer YOUR_API_KEY
The body is always a JSON object with a query field. See the Authentication guide for more details.
Endpoint mapping
User / Account
REST: GET /user.json
GraphQL:
query {
account {
id
email
name
organizations {
id
name
}
}
}
curl -X POST 'https://api.buffer.com' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-d '{"query": "query {\n account {\n id\n email\n name\n organizations {\n id\n name\n }\n }\n}"}'
const response = await fetch('https://api.buffer.com', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_KEY',
},
body: JSON.stringify({
query: `
query {
account {
id
email
name
organizations {
id
name
}
}
}
`,
}),
});
const data = await response.json();
console.log(data);
<?php
$query = '
query {
account {
id
email
name
organizations {
id
name
}
}
}
';
$payload = [
'query' => $query,
];
$ch = curl_init('https://api.buffer.com');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer YOUR_API_KEY',
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
print_r($data);
The REST API returned plan and activity_at. With GraphQL, you get richer account data including all your organizations in a single request - no separate calls needed.
Profiles -> Channels
Profiles are now called channels. The concept is the same, a connected social media account.
REST: GET /profiles.json
GraphQL:
query {
channels(input: { organizationId: "your_org_id" }) {
id
name
service
avatar
isQueuePaused
}
}
curl -X POST 'https://api.buffer.com' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-d '{"query": "query {\n channels(input: { organizationId: \"your_org_id\" }) {\n id\n name\n service\n avatar\n isQueuePaused\n }\n}"}'
const response = await fetch('https://api.buffer.com', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_KEY',
},
body: JSON.stringify({
query: `
query {
channels(input: { organizationId: "your_org_id" }) {
id
name
service
avatar
isQueuePaused
}
}
`,
}),
});
const data = await response.json();
console.log(data);
<?php
$query = '
query {
channels(input: { organizationId: "your_org_id" }) {
id
name
service
avatar
isQueuePaused
}
}
';
$payload = [
'query' => $query,
];
$ch = curl_init('https://api.buffer.com');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer YOUR_API_KEY',
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
print_r($data);
REST: GET /profiles/:id.json
GraphQL:
query {
channel(input: { id: "your_channel_id" }) {
id
name
service
displayName
avatar
}
}
curl -X POST 'https://api.buffer.com' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-d '{"query": "query {\n channel(input: { id: \"your_channel_id\" }) {\n id\n name\n service\n displayName\n avatar\n }\n}"}'
const response = await fetch('https://api.buffer.com', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_KEY',
},
body: JSON.stringify({
query: `
query {
channel(input: { id: "your_channel_id" }) {
id
name
service
displayName
avatar
}
}
`,
}),
});
const data = await response.json();
console.log(data);
<?php
$query = '
query {
channel(input: { id: "your_channel_id" }) {
id
name
service
displayName
avatar
}
}
';
$payload = [
'query' => $query,
];
$ch = curl_init('https://api.buffer.com');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer YOUR_API_KEY',
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
print_r($data);
Key difference: You now need an
organizationIdto list channels. Query your account first to get it.
Updates -> Posts
Updates are now called posts.
List queued posts
REST: GET /profiles/:id/updates/pending.json?count=10&page=1
GraphQL:
query {
posts(
first: 10
input: {
organizationId: "your_org_id"
filter: {
status: [scheduled]
channelIds: ["your_channel_id"]
}
sort: [{ field: dueAt, direction: asc }]
}
) {
edges {
node {
id
text
status
dueAt
channelId
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
curl -X POST 'https://api.buffer.com' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-d '{"query": "query {\n posts(\n first: 10\n input: {\n organizationId: \"your_org_id\"\n filter: {\n status: [scheduled]\n channelIds: [\"your_channel_id\"]\n }\n sort: [{ field: dueAt, direction: asc }]\n }\n ) {\n edges {\n node {\n id\n text\n status\n dueAt\n channelId\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}"}'
const response = await fetch('https://api.buffer.com', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_KEY',
},
body: JSON.stringify({
query: `
query {
posts(
first: 10
input: {
organizationId: "your_org_id"
filter: {
status: [scheduled]
channelIds: ["your_channel_id"]
}
sort: [{ field: dueAt, direction: asc }]
}
) {
edges {
node {
id
text
status
dueAt
channelId
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`,
}),
});
const data = await response.json();
console.log(data);
<?php
$query = '
query {
posts(
first: 10
input: {
organizationId: "your_org_id"
filter: {
status: [scheduled]
channelIds: ["your_channel_id"]
}
sort: [{ field: dueAt, direction: asc }]
}
) {
edges {
node {
id
text
status
dueAt
channelId
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
';
$payload = [
'query' => $query,
];
$ch = curl_init('https://api.buffer.com');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer YOUR_API_KEY',
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
print_r($data);
List sent posts
REST: GET /profiles/:id/updates/sent.json?count=10
GraphQL:
query {
posts(
first: 10
input: {
organizationId: "your_org_id"
filter: {
status: [sent]
channelIds: ["your_channel_id"]
}
sort: [{ field: createdAt, direction: desc }]
}
) {
edges {
node {
id
text
sentAt
externalLink
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
curl -X POST 'https://api.buffer.com' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-d '{"query": "query {\n posts(\n first: 10\n input: {\n organizationId: \"your_org_id\"\n filter: {\n status: [sent]\n channelIds: [\"your_channel_id\"]\n }\n sort: [{ field: createdAt, direction: desc }]\n }\n ) {\n edges {\n node {\n id\n text\n sentAt\n externalLink\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}"}'
const response = await fetch('https://api.buffer.com', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_KEY',
},
body: JSON.stringify({
query: `
query {
posts(
first: 10
input: {
organizationId: "your_org_id"
filter: {
status: [sent]
channelIds: ["your_channel_id"]
}
sort: [{ field: createdAt, direction: desc }]
}
) {
edges {
node {
id
text
sentAt
externalLink
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`,
}),
});
const data = await response.json();
console.log(data);
<?php
$query = '
query {
posts(
first: 10
input: {
organizationId: "your_org_id"
filter: {
status: [sent]
channelIds: ["your_channel_id"]
}
sort: [{ field: createdAt, direction: desc }]
}
) {
edges {
node {
id
text
sentAt
externalLink
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
';
$payload = [
'query' => $query,
];
$ch = curl_init('https://api.buffer.com');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer YOUR_API_KEY',
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
print_r($data);
Create a post
REST: POST /updates/create.json with profile_ids[], text, scheduled_at, media[photo], etc.
GraphQL:
mutation {
createPost(input: {
text: "Hello from the new API!"
channelId: "your_channel_id"
schedulingType: automatic
mode: addToQueue
}) {
... on PostActionSuccess {
post {
id
text
status
}
}
... on MutationError {
message
}
}
}
curl -X POST 'https://api.buffer.com' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-d '{"query": "mutation {\n createPost(input: {\n text: \"Hello from the new API!\"\n channelId: \"your_channel_id\"\n schedulingType: automatic\n mode: addToQueue\n }) {\n ... on PostActionSuccess {\n post {\n id\n text\n status\n }\n }\n ... on MutationError {\n message\n }\n }\n}"}'
const response = await fetch('https://api.buffer.com', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_KEY',
},
body: JSON.stringify({
query: `
mutation {
createPost(input: {
text: "Hello from the new API!"
channelId: "your_channel_id"
schedulingType: automatic
mode: addToQueue
}) {
... on PostActionSuccess {
post {
id
text
status
}
}
... on MutationError {
message
}
}
}
`,
}),
});
const data = await response.json();
console.log(data);
<?php
$query = '
mutation {
createPost(input: {
text: "Hello from the new API!"
channelId: "your_channel_id"
schedulingType: automatic
mode: addToQueue
}) {
... on PostActionSuccess {
post {
id
text
status
}
}
... on MutationError {
message
}
}
}
';
$payload = [
'query' => $query,
];
$ch = curl_init('https://api.buffer.com');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer YOUR_API_KEY',
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
print_r($data);
Key differences:
- Posts are created for a single
channelIdinstead of an array ofprofile_ids. To post to multiple channels, send one mutation per channel.- Use
mode: addToQueueto add to the queue, or setdueAtfor a specific time.- Errors are returned as typed unions in the response, not as HTTP status codes.
Scheduling
REST: GET /profiles/:id/schedules.json and POST /profiles/:id/schedules/update.json
The GraphQL API handles scheduling differently. Instead of managing recurring time slots, you control scheduling per post:
- Add to queue: Set
mode: addToQueue - Custom time: Set
dueAtto an ISO 8601 timestamp - Post now: Set
mode: now
Links
REST: GET /links/shares.json?url=https://example.com
This endpoint has no direct equivalent in the GraphQL API. If you were using it for analytics, Buffer's analytics features are available in the dashboard.
Configuration
REST: GET /info/configuration.json
Platform configuration (character limits, supported media types, etc.) is no longer exposed as a standalone endpoint. These constraints are enforced server-side - if you exceed a limit, you'll get an InvalidInputError with a clear message.
Pagination
The REST API used offset-based pagination (page=1&count=10). Our GraphQL API uses cursor-based pagination, which is more reliable when data changes between requests.
REST (before):
GET /profiles/:id/updates/sent.json?page=2&count=20
GraphQL (now):
query {
posts(
first: 20
after: "cursor_from_previous_page"
input: { organizationId: "your_org_id" }
) {
edges {
node { id, text }
}
pageInfo {
hasNextPage
endCursor
}
}
}
curl -X POST 'https://api.buffer.com' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-d '{"query": "query {\n posts(\n first: 20\n after: \"cursor_from_previous_page\"\n input: { organizationId: \"your_org_id\" }\n ) {\n edges {\n node { id, text }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}"}'
const response = await fetch('https://api.buffer.com', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_KEY',
},
body: JSON.stringify({
query: `
query {
posts(
first: 20
after: "cursor_from_previous_page"
input: { organizationId: "your_org_id" }
) {
edges {
node { id, text }
}
pageInfo {
hasNextPage
endCursor
}
}
}
`,
}),
});
const data = await response.json();
console.log(data);
<?php
$query = '
query {
posts(
first: 20
after: "cursor_from_previous_page"
input: { organizationId: "your_org_id" }
) {
edges {
node { id, text }
}
pageInfo {
hasNextPage
endCursor
}
}
}
';
$payload = [
'query' => $query,
];
$ch = curl_init('https://api.buffer.com');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer YOUR_API_KEY',
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
print_r($data);
To paginate, pass the endCursor from the previous response as the after argument in your next query. See the Pagination guide for more details.
Error handling
The REST API used HTTP status codes (401, 404, 429, etc.) and numeric error codes in the body. Our GraphQL API always returns HTTP 200 and uses typed error unions instead.
REST (before):
HTTP 403
{
"code": 1023,
"error": "Profile update quota exceeded."
}
GraphQL (now):
{
"data": {
"createPost": {
"message": "Queue limit reached",
"limit": 100
}
}
}
Always include ... on MutationError { message } as a catch-all in your mutations. See Error Handling for the full guide.
Concepts that don't carry over
A few REST API features work differently or aren't yet available in our GraphQL API:
/updates/:id/share.json(post immediately) - Usemode: nowoncreatePostinstead/updates/:id/move_to_top.jsonand reorder/shuffle - Queue management is handled through scheduling/user/deauthorize.json- API keys can be revoked from your account settings/links/shares.json- Share counts are not available via the API/info/configuration.json- Platform limits are enforced server-side with clear validation errors