API Status Get API Key

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 organizationId to 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 channelId instead of an array of profile_ids. To post to multiple channels, send one mutation per channel.
  • Use mode: addToQueue to add to the queue, or set dueAt for 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 dueAt to an ISO 8601 timestamp
  • Post now: Set mode: now

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) - Use mode: now on createPost instead
  • /updates/:id/move_to_top.json and 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