# Buffer API > Connect Buffer to your agents, automation tools, or build something entirely new. API Endpoint: https://api.buffer.com Authentication: Bearer token via Authorization header ## Guides ### Introduction #### What is the Buffer API? The Buffer API gives you programmatic access to your Buffer account. Create and schedule posts, manage content ideas, pull data on your connected channels, and build custom integrations, all through our GraphQL API. #### What can the API do? - **Create posts:** Schedule text and image posts to any connected channel - **Delete posts:** Remove scheduled or sent posts - **Create ideas:** Save content ideas for later - **Retrieve channels:** List and filter your connected social media profiles - **Retrieve posts:** Fetch scheduled, sent, and draft posts - **Retrieve organizations:** Access organization and account data #### How is the API built? We use [GraphQL](https://graphql.org/), which lets you request exactly the data you need in a single request. The main difference from REST is that instead of hitting different URLs for different resources, you send queries to a single endpoint: ``` https://api.buffer.com ``` #### Get help New to the Buffer API or not sure where to begin? Our [Help Center FAQ](https://support.buffer.com/articles/using-buffers-api-GtIYIQilz5) answers common questions about API access and what you can do with it. Want to ask a question or see what others are building? Join the [Buffer developer community on Discord](https://discord.gg/9skbzTz6vQ). #### Next steps - [Quick Start](https://developers.buffer.com/guides/getting-started.md): Make your first API request in 5 minutes - [Data Model](https://developers.buffer.com/guides/data-model.md): Understand how organizations, channels, and posts relate to each other - [Examples](https://developers.buffer.com/examples/): See working code for common tasks ### Quick Start Buffer's API is built with GraphQL. If you're new to GraphQL, we'd recommend the [official GraphQL documentation](https://graphql.org/learn/) to get familiar with the basics before diving in. #### Register for API Access To get started with the Buffer API, you'll need a Buffer account and an API key. If you don't have an account yet, you can [sign up here](https://buffer.com/signup). Once you have an account, head to [API settings](https://publish.buffer.com/settings/api) to create your API key. #### What the API supports Your API key lets you perform actions against **your own Buffer account**. We currently support the following operations: - Post Creation - Post Deletion - Post Retrieval - Idea Creation - Account Retrieval - Organization Retrieval - Channel Retrieval #### Endpoint The Buffer GraphQL API is available at: ``` https://api.buffer.com ``` If you'd like to use a tool like Postman or another HTTP client, point your requests to the endpoint above and follow [Postman's GraphQL guide](https://learning.postman.com/docs/sending-requests/graphql/graphql-overview/) for setup. #### Authorization Every request must include an `Authorization` header with your API key using the `Bearer` format. #### Making your first request Here's a quick query to fetch your account and organization: You can run this query directly in your browser using the interactive [Buffer API Explorer](https://developers.buffer.com/explorer.html), no local setup required. Once you have your organization ID, you can use it to fetch channels, posts, and other data for that organization. ### Agent Setup Follow this guide only when the user explicitly asks to set up or verify Buffer public API access. When they do, start the steps below rather than only acknowledging or summarizing them. #### Safety boundaries - Do not ask the user to paste their API key into the chat. - Read the key only from the `BUFFER_API_KEY` environment variable. - Never print, log, or commit the key. Check whether the variable is set without displaying its value. - If you cannot read environment variables, say so and stop. - If `BUFFER_API_KEY` is unset or empty, briefly say that it must be made available securely in the agent's environment before setup can continue, then stop. - Make only read-only requests during this setup. Get the user's explicit approval before every GraphQL mutation or other state-changing request. After listing the channels, stop. - Use only direct requests to Buffer's public API. Do not substitute another tool or integration. #### Request format Buffer's API uses GraphQL at `https://api.buffer.com`. Every request is a `POST` with: - `Content-Type: application/json` - `Authorization: Bearer $BUFFER_API_KEY` - A JSON body containing `{"query":"..."}` and `"variables"` when the query uses variables Keep the `Authorization` header value out of all output. #### Setup steps 1. Confirm that you can read environment variables and that `BUFFER_API_KEY` is set, following the safety boundaries above. 2. Read any available [Buffer `llms.txt`](https://developers.buffer.com/llms.txt), [Quick Start](https://developers.buffer.com/guides/getting-started.md), [Authentication](https://developers.buffer.com/guides/authentication.md), and [Your First Post](https://developers.buffer.com/guides/your-first-post.md) pages for background. Treat this page as authoritative for this setup. 3. Verify access by sending this query, then show the user the API response: ```graphql query GetOrganizations { account { organizations { id name } } } ``` If the request fails, briefly report the error without including the request headers or API key, then stop. 4. Show the returned organizations and ask the user which one to use. Do not choose for them. 5. List the selected organization's channels with this query, passing its ID as the `organizationId` variable: ```graphql query GetChannels($organizationId: OrganizationId!) { channels(input: { organizationId: $organizationId }) { id name service } } ``` ```json { "organizationId": "the_selected_organization_id" } ``` Show the result, then stop. Get the user's explicit approval before every GraphQL mutation or other state-changing request. ### 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: 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`). ```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 { 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 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: ```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 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`. ```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 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. | | `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](https://developers.buffer.com/guides/getting-started.md): Make your first API request - [Your First Post](https://developers.buffer.com/guides/your-first-post.md): Go from authenticated to a scheduled post ### Your First Post Let's walk through creating your first post with the Buffer API. By the end, you'll have a post scheduled in your queue. **Prerequisites:** A Buffer account with at least one connected channel, and an API key. See [Authentication](https://developers.buffer.com/guides/authentication.md) if you don't have a key yet. **Time:** ~5 minutes #### Step 1: Get your organization ID First, query your account to find your organization ID: ```graphql query GetOrganizations { account { organizations { id name } } } ``` You'll get a response like this: ```json { "data": { "account": { "organizations": [ { "id": "your_org_id", "name": "My Company" } ] } } } ``` Copy the `id` from the response. You'll need it in the next step. #### Step 2: Get your channel ID Now query your channels using the organization ID from Step 1: ```graphql query GetChannels { channels(input: { organizationId: "your_org_id" }) { id name service } } ``` This returns all your connected social profiles: ```json { "data": { "channels": [ { "id": "your_channel_id", "name": "My Twitter", "service": "twitter" }, { "id": "your_other_channel_id", "name": "My Instagram", "service": "instagram" } ] } } ``` Pick the channel you want to post to and copy its `id`. #### Step 3: Create your post Now create a post using the channel ID from Step 2: ```graphql mutation CreateFirstPost { createPost(input: { text: "Hello from the Buffer API!", channelId: "your_channel_id", schedulingType: automatic, mode: addToQueue }) { ... on PostActionSuccess { post { id text dueAt } } ... on MutationError { message } } } ``` A successful response looks like this: ```json { "data": { "createPost": { "post": { "id": "your_post_id", "text": "Hello from the Buffer API!", "dueAt": "2026-03-05T14:30:00.000Z" } } } } ``` #### Step 4: Verify in Buffer Open your [Buffer dashboard](https://publish.buffer.com). You should see your new post in the queue for the channel you selected. With `mode: addToQueue`, we've assigned it to the next available time slot. #### Scheduling options The example above used `mode: addToQueue`, which adds the post to the next available time slot. You can also schedule a post for a specific time: ```graphql mutation CreateScheduledPost { createPost(input: { text: "Scheduled for a specific time", channelId: "your_channel_id", schedulingType: automatic, mode: customScheduled, dueAt: "2026-03-10T15:00:00.000Z" }) { ... on PostActionSuccess { post { id text dueAt } } ... on MutationError { message } } } ``` The `dueAt` field accepts an ISO 8601 datetime string in UTC. #### Handling errors If something goes wrong, the response includes a `MutationError` with a `message` field instead of `PostActionSuccess`: ```json { "data": { "createPost": { "message": "Channel not found" } } } ``` Common issues: - **Invalid `channelId`** - double-check you copied the right ID from Step 2 - **Missing required fields** - `text` and `channelId` are always required - **Queue limit reached**: your channel's queue is full Always include `... on MutationError { message }` in your mutations to catch errors. See [Error Handling](https://developers.buffer.com/guides/error-handling.md) for more details. #### Next steps - [Create posts with images](https://developers.buffer.com/examples/create-image-post.md): add images to your posts - [Posts & Scheduling](https://developers.buffer.com/guides/posts-and-scheduling.md): learn about scheduling types and post lifecycle - [Data Model](https://developers.buffer.com/guides/data-model.md): understand the full object hierarchy - [Buffer API Explorer](https://developers.buffer.com/explorer.html): explore the schema and run queries right in your browser. No local setup required ### GraphQL for REST Devs If you've worked with REST APIs before, GraphQL will feel a bit different. Here's a quick rundown of the key differences. #### One endpoint, many queries With REST, you use different URLs for different resources: ``` GET /posts GET /channels GET /organizations ``` With GraphQL, everything goes to one endpoint: ``` POST https://api.buffer.com ``` Instead of choosing a URL, you write a **query** that describes what you want. The endpoint is always the same. #### You choose what comes back With REST, the server decides what's in the response. You might get 30 fields back when you only need 2. With GraphQL, you pick exactly which fields you want: ```graphql query { channels(input: { organizationId: "your_org_id" }) { id name } } ``` This returns only `id` and `name`, nothing extra. #### Queries read, mutations write REST uses HTTP verbs to indicate intent: `GET` to read, `POST` to create, `PUT` to update, `DELETE` to remove. GraphQL uses **operation types**: - **`query`** - read data (like GET) - **`mutation`** - create or change data (like POST/PUT/DELETE) Both are sent as POST requests to the same endpoint. The operation type inside the request body tells the server what you're doing. ```graphql # Reading data query { account { id } } # Writing data mutation { createPost(input: { text: "Hello!", channelId: "your_channel_id", schedulingType: automatic, mode: addToQueue }) { ... on PostActionSuccess { post { id } } } } ``` #### Errors work differently REST uses HTTP status codes: 404 for not found, 401 for unauthorized, 500 for server error. GraphQL always returns HTTP 200. Errors are in the response body, in two places: **1. The `errors` array** - for non-recoverable problems (bad auth, server error): ```json { "errors": [ { "message": "Not authorized", "extensions": { "code": "UNAUTHORIZED" } } ] } ``` **2. Typed errors in the data** for recoverable problems (validation, limits): ```graphql mutation { createPost(input: { ... }) { ... on PostActionSuccess { post { id } } ... on MutationError { message } } } ``` Buffer uses typed union errors so you can handle specific error cases in your code. See [Error Handling](https://developers.buffer.com/guides/error-handling.md) for details. #### Tools for exploring - **[Buffer API Explorer](https://developers.buffer.com/explorer.html)**: try queries right in your browser - **[API Reference](https://developers.buffer.com/reference.md)**: browse all available queries, mutations, and types - **Any GraphQL client**: tools like Postman or Insomnia work great too #### Further reading - [Quick Start](https://developers.buffer.com/guides/getting-started.md): make your first Buffer API request - [Official GraphQL docs](https://graphql.org/learn/): learn GraphQL fundamentals ### 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 (personal workflows), or OAuth 2.0 (App Clients) | | **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 personal workflows, the GraphQL API uses a simpler API key approach. To build App Clients that act on behalf of other Buffer users, use OAuth 2.0 (Authorization Code with PKCE) — see the [Authentication guide](https://developers.buffer.com/guides/authentication.md). **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](https://publish.buffer.com/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](https://developers.buffer.com/guides/authentication.md) for more details. #### Endpoint mapping ##### User / Account **REST:** `GET /user.json` **GraphQL:** ```graphql query { account { id email name organizations { id name } } } ``` 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:** ```graphql query { channels(input: { organizationId: "your_org_id" }) { id name service avatar isQueuePaused } } ``` **REST:** `GET /profiles/:id.json` **GraphQL:** ```graphql query { channel(input: { id: "your_channel_id" }) { id name service displayName avatar } } ``` > **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:** ```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 } } } ``` ###### List sent posts **REST:** `GET /profiles/:id/updates/sent.json?count=10` **GraphQL:** ```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 } } } ``` ###### Create a post **REST:** `POST /updates/create.json` with `profile_ids[]`, `text`, `scheduled_at`, `media[photo]`, etc. **GraphQL:** ```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 } } } ``` > **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 `mode: customScheduled` with a `dueAt` timestamp 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 `mode: customScheduled` with `dueAt` as an ISO 8601 timestamp (UTC) - **Post now:** Set `mode: shareNow` Each of these is set via the `mode` field on `createPost`, alongside the required `schedulingType: automatic`. ##### 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):** ```graphql query { posts( first: 20 after: "cursor_from_previous_page" input: { organizationId: "your_org_id" } ) { edges { node { id, text } } pageInfo { hasNextPage endCursor } } } ``` To paginate, pass the `endCursor` from the previous response as the `after` argument in your next query. See the [Pagination guide](https://developers.buffer.com/guides/pagination.md) 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):** ```json HTTP 403 { "code": 1023, "error": "Profile update quota exceeded." } ``` **GraphQL (now):** ```json { "data": { "createPost": { "message": "Queue limit reached" } } } ``` Always include `... on MutationError { message }` as a catch-all in your mutations. See [Error Handling](https://developers.buffer.com/guides/error-handling.md) for the full guide. #### Rate limits The GraphQL API applies rate limits per client - each API key or App Client gets its own quota, measured across rolling 15-minute, 24-hour, and 30-day windows. The exact quotas depend on your Buffer plan. Every response includes `RateLimit` headers so you can track your remaining quota, and exceeding a limit returns HTTP 429 with a `RATE_LIMIT_EXCEEDED` error. See the [Rate Limits guide](https://developers.buffer.com/guides/api-limits.md) for the full details. #### 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: shareNow` 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 #### Tools for exploring the GraphQL API GraphQL API comes with a set of tools to explore the schema and learn more about the supported methods: - **[GraphQL for REST Developers](https://developers.buffer.com/guides/graphql-intro.md)** - a short primer on queries, mutations, and how GraphQL differs from the REST model you're used to. - **[Buffer API Explorer](https://developers.buffer.com/explorer.html)** - run queries and mutations right in your browser, with schema-aware autocomplete and inline docs. No local setup required. - **[Buffer CLI](https://developers.buffer.com/guides/cli.md)** - run queries and mutations from your terminal, and use `buffer schema` to discover fields, input types, and enum values before writing any code. ### Data Model Buffer's API is organized around a few core objects. Here's how they fit together. #### Overview ``` Account └── Organizations (one or more) ├── Channels (one or more per org) │ └── Posts (belong to a channel) └── Ideas (belong to an organization) ``` Your **account** contains one or more **organizations**. Each organization has **channels** (connected social media profiles) and **ideas** (draft content). **Posts** are created on specific channels. #### Account Your account represents your Buffer login. When you authenticate with the API, you're acting as your account. Each account can belong to one or more organizations. ```graphql query { account { id organizations { id name } } } ``` #### Organization An organization is a workspace in Buffer. Most people have one, but if you're managing multiple brands you might have several. Each organization contains channels and ideas. You'll need an organization ID for most operations. Retrieve it first: ```graphql query { account { organizations { id name } } } ``` #### Channel A channel is a connected social media profile, like your company's X account or your personal Instagram. Channels belong to an organization. ```graphql query { channels(input: { organizationId: "your_org_id" }) { id name service } } ``` The `service` field tells you which platform the channel is on (e.g., twitter, instagram, facebook, linkedin). #### Post A post is a piece of content scheduled or published through Buffer. Every post belongs to a channel. When creating a post, you specify the channel ID and the scheduling behavior. ```graphql mutation { createPost(input: { text: "Hello world" channelId: "your_channel_id" schedulingType: automatic mode: addToQueue }) { ... on PostActionSuccess { post { id text } } } } ``` #### Idea An idea is a piece of draft content saved for later. Ideas belong to an organization (not a channel) because they haven't been assigned to a specific platform yet. ```graphql mutation { createIdea(input: { organizationId: "your_org_id" content: { text: "Blog post concept: ..." } }) { ... on Idea { id content { text } } } } ``` #### Common patterns ##### The typical API flow 1. **Authenticate** with your API key 2. **Query your account** to get organization IDs 3. **Query channels** for your target organization 4. **Create posts** on specific channels, or **create ideas** on the organization #### Next steps - [Your First Post](https://developers.buffer.com/guides/your-first-post.md): Walk through the full flow from authentication to a published post - [Posts & Scheduling](https://developers.buffer.com/guides/posts-and-scheduling.md): Deep dive into scheduling options and post types ### Posts & Scheduling Posts are the core content type in Buffer. Here's how they work, from creation through delivery. #### What is a post? A post is a piece of content scheduled or published through Buffer. Every post belongs to a specific [channel](https://developers.buffer.com/guides/data-model.md#channel) (a connected social media profile) and goes through a lifecycle from creation to delivery. Key fields on a post: - **`id`** - unique identifier - **`text`** - the post content - **`channelId`** - which channel this post belongs to - **`dueAt`** - when the post is scheduled to publish - **`status`** - current lifecycle state (scheduled, sent, etc.) - **`assets`** - attached images or media #### Scheduling types When creating a post, you choose how it should be scheduled: ##### `addToQueue` We'll add the post to the next available time slot from your posting schedule. This is the simplest option. ```graphql mutation { createPost( input: { text: "Automatically scheduled post" channelId: "your_channel_id" schedulingType: automatic mode: addToQueue } ) { ... on PostActionSuccess { post { id dueAt } } ... on MutationError { message } } } ``` ##### `customScheduled` You specify an exact date and time using the `dueAt` field in ISO 8601 format (UTC): ```graphql mutation { createPost( input: { text: "Scheduled for a specific time" channelId: "your_channel_id" schedulingType: automatic mode: customScheduled dueAt: "2026-03-10T15:00:00.000Z" } ) { ... on PostActionSuccess { post { id dueAt } } ... on MutationError { message } } } ``` #### Post status lifecycle A post moves through the following states: - **Scheduled** the post is in the queue, waiting for its `dueAt` time - **Sent** the post was successfully published to the social platform - **Error** the post could not be published (e.g., the channel was disconnected) #### Creating posts Use the `createPost` mutation. Required fields: - **`text`** - the post content - **`channelId`** - the target channel - **`schedulingType`** - how to schedule (`automatic`) Always include both `PostActionSuccess` and `MutationError` in your response: ```graphql mutation { createPost( input: { text: "Hello from the API" channelId: "your_channel_id" schedulingType: automatic mode: addToQueue } ) { ... on PostActionSuccess { post { id text dueAt assets { id mimeType } } } ... on MutationError { message } } } ``` See [Create a Text Post](https://developers.buffer.com/examples/create-text-post.md) and [Create an Image Post](https://developers.buffer.com/examples/create-image-post.md) for complete examples. ##### Channel-specific configuration Beyond the shared fields above, the API supports channel-specific configuration through the `metadata` field on `createPost`. Each network has its own metadata input, so you can do things like create a [thread](https://developers.buffer.com/examples/create-threaded-post.md) on X (Twitter), Bluesky, Threads, or Mastodon, add a first comment on LinkedIn, Facebook, or Instagram, set the post type (post, story, reel) on Instagram, or attach a board to a Pinterest pin. You only need to provide the metadata for the network the channel belongs to. To learn more about the available configurations for each network, see the [PostInputMetaData](https://developers.buffer.com/types/PostInputMetaData.md) reference. A few options are configured on the assets themselves rather than in the network `metadata`. For example, Instagram user tags are positioned per image via `image.metadata.userTags` - see [Create an Instagram Post with User Tags](https://developers.buffer.com/examples/create-instagram-post-with-user-tags.md) for a complete example. #### Retrieving posts Query posts for a specific organization, filtered by channel and status: ```graphql query { posts( first: 20 input: { organizationId: "your_org_id" filter: { status: [sent], channelIds: ["your_channel_id"] } } ) { edges { node { id text dueAt channelId } } pageInfo { hasNextPage endCursor } } } ``` Posts are returned using [cursor-based pagination](https://developers.buffer.com/guides/pagination.md). Use `first` and `after` to page through results. See [Get Posts for Channels](https://developers.buffer.com/examples/get-posts-for-channels.md) and [Get Paginated Posts](https://developers.buffer.com/examples/get-paginated-posts.md) for more examples. #### Metrics Once a post is sent, the network reports performance data — reactions, impressions, reach, and so on. Buffer normalizes those values across networks and exposes them on `Post.metrics`. Reading metrics is available for personal workflows and automations only, using a personal API key. See [Post Metrics](https://developers.buffer.com/guides/post-metrics.md) for the full guide and [Get Post Metrics](https://developers.buffer.com/examples/get-post-metrics.md) for a single-post query. #### Supported platforms The API can create posts for the following platforms: - Instagram - Threads - LinkedIn - X (Twitter) - Facebook - Google Business Profiles - Mastodon - YouTube - Pinterest - Bluesky The `service` field on a [Channel](https://developers.buffer.com/guides/data-model.md#channel) tells you which platform it's connected to. #### Next steps - [Create a Text Post](https://developers.buffer.com/examples/create-text-post.md): basic post creation example - [Create an Image Post](https://developers.buffer.com/examples/create-image-post.md): attach images to posts - [Ideas](https://developers.buffer.com/guides/ideas.md): save content for later - [Pagination](https://developers.buffer.com/guides/pagination.md): iterate through all your posts ### Ideas Ideas let you capture content thoughts and plan ahead before scheduling to a specific channel. #### Ideas vs. posts The key difference: - **Posts** belong to a **channel**. They're assigned to a specific social media profile and have a scheduled time. - **Ideas** belong to an **organization**. They're not tied to any channel or schedule yet. Think of ideas as your content backlog. When you're ready to publish, you create a post from an idea by assigning it to a channel. #### Creating an idea Use the `createIdea` mutation with your organization ID: ```graphql mutation { createIdea(input: { organizationId: "your_org_id", content: { title: "Blog post announcement", text: "We just published our guide to social media automation. Share it across all channels this week." } }) { ... on Idea { id content { title text } } ... on MutationError { message } } } ``` The `content` object supports: - **`title`** - a short heading for the idea (optional) - **`text`** - the body content of the idea See [Create an Idea](https://developers.buffer.com/examples/create-idea.md) for a complete example. #### Why ideas belong to organizations Ideas are organization-level because they haven't been assigned to a platform yet. A single idea might eventually become: - A tweet on X - A carousel on Instagram - A post on LinkedIn Since you haven't decided which channel (or channels) to use, the idea lives at the organization level where all your channels are. When you're ready to publish, create a [Post](https://developers.buffer.com/guides/posts-and-scheduling.md) on the appropriate channel using the idea's content. #### Next steps - [Create an Idea](https://developers.buffer.com/examples/create-idea.md): working example - [Posts & Scheduling](https://developers.buffer.com/guides/posts-and-scheduling.md): publish content to channels - [Data Model](https://developers.buffer.com/guides/data-model.md): understand how ideas, posts, channels, and organizations relate ### Content Items A content item is one piece of content. It holds either a channel-less draft, or the channel-specific posts for that content. #### Why this exists Today you create one post per channel, one `createPost` call at a time. Five channels means five calls. Nothing ties those posts together, so nothing tells you that the LinkedIn post and the Instagram post cover the same topic. A content item is that tie. You create the per-channel posts from one item, and the item keeps the title, the tags and the target date that apply to the content as a whole. It is also one call. `createContentItem` takes a list of posts and creates all of them together, so you no longer send one request per channel. Validation covers the whole list: if one post is rejected, none are created. So you never have to clean up a partial result. The posts do not have to match. Each one keeps its own text and its own media, so you can write a long post for LinkedIn and a short one for Mastodon and still group them. What the item records is that the two belong together, not that they say the same thing. A content item also lets you start before you pick a channel. You can save a draft with no channel attached, decide later where it goes, and keep the writing in the meantime. #### The two states A content item is always in one of two states, and `body` tells you which: - **`DraftContent`** - a channel-less draft. No channel is selected, so no network rules apply, including media type restrictions. - **`PostContent`** - the channel-specific posts. Each post targets one channel and follows that channel's rules. The move from a draft to posts goes one way. Once you promote a draft, you cannot edit it as a draft again. You edit the individual posts instead, through the post mutations. Read an item with `contentItem`, and pick the state apart with an inline fragment on each member of the union: ```graphql query GetContentItem { contentItem(input: { id: "some_content_item_id" }) { id title targetDate body { ... on DraftContent { text assets { ... on ImageAsset { source thumbnail } } } ... on PostContent { posts { id text channel { id name } } } } } } ``` #### Creating an item and its posts Use `createContentItem` when you already know the channels. It creates the item and every channel-specific post in one call. The `title`, `tagIds` and `targetDate` fields belong to the content item, not to any single post. Each post carries its own text, its own media and its own scheduled time. ```graphql mutation CreateContentItem { createContentItem(input: { organizationId: "your_org_id", title: "Spring feature announcement", targetDate: "2026-09-15T09:00:00Z", posts: [ { channelId: "linkedin_channel_id", text: "We shipped the thing. Here is what it does and why we built it.", schedulingType: automatic, mode: addToQueue }, { channelId: "mastodon_channel_id", text: "We shipped the thing! Short version: it saves you a step.", schedulingType: automatic, mode: addToQueue } ] }) { ... on CreateContentItemSuccess { content { id title targetDate body { ... on PostContent { posts { id status dueAt channel { id name } } } } } } ... on CreateContentItemFailure { message errors { ... on CreateContentItemVariantInvalidInputError { channelId message } ... on CreateContentItemVariantLimitReachedError { channelId message } ... on CreateContentItemVariantNotFoundError { channelId message } ... on MutationError { message } } } } } ``` Validation is all-or-nothing. If any post in the `posts` list fails validation, no content item and no posts are created. The `CreateContentItemVariant*` errors each name the channel they apply to. `InvalidInputError`, `NotFoundError` and `LimitReachedError` carry a message only, because they apply to the request as a whole. #### Creating a channel-less draft A content item does not need a channel. `createContentItemDraft` saves a channel-less draft, so you can capture and work on content before you decide where it goes. A draft is a state you can stay in, not a step you have to pass through. Nothing forces you to attach channels, and a content item can live as a draft for as long as you need. ```graphql mutation CreateContentItemDraft { createContentItemDraft(input: { organizationId: "your_org_id", title: "Conference recap", targetDate: "2026-09-20T09:00:00Z", draft: { text: "Three things I learned at the conference this week.", aiAssisted: false, assets: [ { image: { url: "https://example.com/keynote.jpg", metadata: { altText: "The keynote stage" } } } ] } }) { ... on CreateContentItemDraftSuccess { contentItem { id title targetDate body { ... on DraftContent { id text aiAssisted assets { ... on ImageAsset { source thumbnail } } } } } } ... on CreateContentItemDraftFailure { message errors { message } } } } ``` #### Editing a draft `updateContentItemDraft` replaces the draft content in full. Send the whole draft every time, not just the fields you changed: anything you leave out is dropped. `tagIds` and `targetDate` behave differently. Omit either one to keep its current value. ```graphql mutation UpdateContentItemDraft { updateContentItemDraft(input: { id: "the_content_item_id", draft: { text: "Three things I learned at the conference, and one I got wrong.", aiAssisted: false }, targetDate: "2026-09-22T09:00:00Z" }) { ... on UpdateContentItemDraftSuccess { contentItem { id targetDate body { ... on DraftContent { text } } } } ... on UpdateContentItemDraftFailure { message errors { ... on ContentItemStateError { message } ... on InvalidInputError { message } } } } } ``` `ContentItemStateError` means the item is no longer a draft. Someone promoted it to posts, possibly at the same time as your request. Once that happens you edit the individual posts through the post mutations instead. #### Attaching channels to a draft `promoteContentItemDraftToPosts` attaches channels to an item that is still a channel-less draft, and creates one post per channel. Promotion goes one way: after this call the content item holds posts, and you can no longer edit it as a draft. ```graphql mutation PromoteContentItemDraftToPosts { promoteContentItemDraftToPosts(input: { id: "the_content_item_id", posts: [ { channelId: "linkedin_channel_id", text: "Three things I learned at the conference this week.", schedulingType: automatic, mode: addToQueue } ] }) { ... on PromoteContentItemDraftToPostsSuccess { contentItem { id body { ... on PostContent { posts { id status dueAt } } } } } ... on PromoteContentItemDraftToPostsFailure { message errors { ... on ContentItemStateError { message } ... on PostChannelNotFoundError { channelId message } ... on PostInvalidInputError { channelId message } ... on PostLimitReachedError { channelId message } ... on MutationError { message } } } } } ``` Validation is all-or-nothing. If any post fails validation, nothing is created and the content item is still a draft. `PostChannelNotFoundError`, `PostInvalidInputError` and `PostLimitReachedError` each name the channel they apply to, so you can point the reader at the post that failed. `ContentItemStateError` and `InvalidInputError` carry a message only, because they apply to the request as a whole. #### Titles, tags and target dates Three fields belong to the content item, not to any single post: - **`title`** - a short description of what the content is about. - **`tags`** - tags for the item as a whole. They survive the move from a draft to posts. - **`targetDate`** - the date you plan this content for. This is a planning aid. It does not schedule anything. The posts carry their own scheduled times. `updateContentItem` changes the title or the target date, and works in either state. Omitted fields keep their current value, and an explicit null clears them. ```graphql mutation UpdateContentItem { updateContentItem(input: { id: "the_content_item_id", title: "Conference recap, part one" }) { ... on UpdateContentItemSuccess { contentItem { id title targetDate } } ... on UpdateContentItemFailure { message errors { message } } } } ``` #### Listing content items `contentItems` lists an organization's items with cursor pagination. You can filter by content status, by tag and by target date, and sort by target date or creation date. This example asks for the channel-less drafts that are planned for a date, soonest first. ```graphql query ListContentItems { contentItems( input: { organizationId: "your_org_id", filter: { contentStatus: draftContent, targetDate: { presence: present } }, sort: [{ field: targetDate, direction: asc }] }, first: 20 ) { edges { cursor node { id title targetDate createdAt tags { id name } body { ... on DraftContent { text } ... on PostContent { posts { id channel { name } } } } } } pageInfo { hasNextPage endCursor } totalCount } } ``` Cursors are opaque. Pass the previous page's `endCursor` as `after` to get the next page, and ask for at most 100 items per page. A cursor is only valid for the request that produced it. Reset `after` to null whenever the filter or the sort changes, because a cursor from a different result set gives undefined results. See [Pagination](https://developers.buffer.com/guides/pagination.md). #### Content items and posts A content item does not replace a post. Posts stay exactly as they are: they belong to a channel, they carry the scheduled time, and you edit and delete them through the post mutations. What a content item adds is the layer above: the grouping, and the state before a channel exists. `deleteContentItem` deletes the item and its posts together, and it is all-or-nothing. Every post must be deletable on its own, so an item cannot be deleted while any of its posts is publishing or already published. An item still holding a channel-less draft has no posts, so nothing blocks it. #### Scopes The content item operations use the post scopes, because a content item creates and reads posts: - `posts:read` for `contentItem` and `contentItems` - `posts:write` for `createContentItem`, `createContentItemDraft`, `updateContentItem`, `updateContentItemDraft`, `promoteContentItemDraftToPosts` and `deleteContentItem` See [Authentication](https://developers.buffer.com/guides/authentication.md). #### Next steps - [Posts & Scheduling](https://developers.buffer.com/guides/posts-and-scheduling.md): the posts a content item creates - [Pagination](https://developers.buffer.com/guides/pagination.md): how cursor pagination works across the API - [Data Model](https://developers.buffer.com/guides/data-model.md): how organizations, channels and posts relate ### Hosting Media When you attach an image, video, or document to a post, the Buffer API doesn't accept a file upload - there's no upload endpoint. Instead, you host the file yourself and pass a publicly accessible URL in the `assets` array. The thumbnail on a link attachment works the same way. #### Why media must be hosted The `url` field on each asset (`image`, `video`, or `document`) must point to a file that is reachable over the public internet without authentication. This means the URL must work for anyone, not just you. Links that require a viewer to be signed in - for example a Google Drive or Dropbox "share" link - will not work. Some assets take more than one URL. A document needs both the file and a thumbnail, and a link attachment can carry its own thumbnail. Every URL you pass has to meet the same bar, not just the main one. > **The URL must stay reachable until the post publishes, not just when you create it.** > Buffer fetches the media when the post goes out, which for scheduled or queued posts can be hours or days later. Avoid expiring or signed URLs (such as S3 pre-signed links or Cloudinary signed-delivery URLs) - they often work the moment you call `createPost` but expire before the post publishes, causing it to fail silently. Use a stable, permanent URL. #### Where to host your media You can use any host that serves files at a direct, public URL. If you don't already have one, these options have a free tier and work well: - [Cloudinary](https://cloudinary.com/) - A media management platform with a generous free tier. After you upload a file you get a direct, publicly accessible URL you can use right away. It also offers optional on-the-fly image and video transformations. Use the delivery URL (e.g. `https://res.cloudinary.com/...`), not the link to the dashboard or media-library page. - [Cloudflare R2](https://www.cloudflare.com/developer-platform/products/r2/) - Object storage with a free tier. A good fit if you already use Cloudflare or are comfortable with a slightly more technical setup. Upload your file and enable public access on the bucket (or attach a public custom domain) so the file is reachable without credentials. #### Verifying that a URL works Before using a URL with the API, open it in a private/incognito browser window: - If the file loads directly without asking you to log in, it will work with the API. - If you see a login prompt, a preview page, or an error, the URL won't work - host the file somewhere that serves it directly. A good media URL is: - Public - loads without authentication - Direct - points straight at the file (not a redirect or preview page) - HTTPS - served over `https://` - Stable - won't expire before the post publishes #### Using the media URL Once your file is hosted, pass its URL in the `assets` array on `createPost` or `editPost`. `assets` is an ordered list where each entry specifies exactly one asset. Image - pass the publicly hosted image URL. You can optionally pass `thumbnailUrl` as well, which is a second hosted URL and needs to be just as public and stable as the image itself: ```graphql assets: [ { image: { url: "https://your-host.example.com/photo.jpg" } } ] ``` Video - pass the publicly hosted video URL. To set the video thumbnail, choose which frame to use with `metadata.thumbnailOffset` (a millisecond offset into the video) on Instagram, TikTok, or Pinterest: ```graphql assets: [ { video: { url: "https://your-host.example.com/clip.mp4" metadata: { thumbnailOffset: 2000 } } } ] ``` Document - pass both the file URL and a thumbnail URL, plus a title. All three are required, so a document post means hosting two files, not one: ```graphql assets: [ { document: { url: "https://your-host.example.com/report.pdf" thumbnailUrl: "https://your-host.example.com/report-cover.jpg" title: "Q3 report" } } ] ``` A full mutation looks like this: ```graphql mutation CreatePost { createPost( input: { text: "Check out our latest update!" channelId: "some_channel_id" schedulingType: automatic mode: addToQueue assets: [ { image: { url: "https://your-host.example.com/photo.jpg" } } ] } ) { ... on PostActionSuccess { post { id } } ... on MutationError { message } } } ``` For complete walkthroughs, see the [Create an image post](https://developers.buffer.com/examples/create-image-post.md) and [Create a video post](https://developers.buffer.com/examples/create-video-post.md) examples. #### Link attachments A link card is not an asset. To attach one, use `metadata.{service}.linkAttachment`, which is supported on Bluesky, Facebook, LinkedIn, Substack, and Threads: ```graphql metadata: { linkedin: { linkAttachment: { url: "https://example.com/announcement" title: "Our latest announcement" description: "What we shipped this quarter" thumbnail: { url: "https://your-host.example.com/card.jpg" } } } } ``` Two things to know: - The `thumbnail.url` is fetched by Buffer the same way asset URLs are, so it has to meet every point in the list above. The link's own `url` is the page you're linking to and is not fetched as media. - `linkAttachment` is mutually exclusive with a non-empty `assets` array. Passing both is rejected, so a post is either a link card or a media post, never both. #### Troubleshooting If a media URL can't be fetched, the mutation returns a `MutationError` with a message: ```json { "data": { "createPost": { "message": "Failed to create post: Failed to fetch image dimensions: Not Found" } } } ``` If you hit this, check the URL against this list: 1. Public - open it in an incognito window; it should load with no login. 2. Direct - it points at the file itself, not a share, preview, or redirect page. 3. HTTPS - it's served over `https://`. 4. Still live - it isn't a signed/expiring URL that may lapse before a scheduled post publishes. If the post has a document or a link attachment, check the thumbnail URL against the same list. A thumbnail that can't be fetched fails the whole mutation. ### Post Metrics Buffer collects performance data from the social networks it publishes to and exposes a normalized view of it through the API. This guide walks through how to read metrics for a single post, how to aggregate them across many posts, what the values mean across networks, and how to use them in common reporting workflows. > **Post metrics are only available with a personal API key. App Clients cannot read them.** > > Reading metrics requires the `insightsRead` scope, which an App Client cannot request through OAuth. A personal API key acts on your own account's behalf and carries that access, so metrics are for personal workflows and automations rather than apps acting for other users. > > See [Authentication](https://developers.buffer.com/guides/authentication.md) for how to create a personal API key. #### Reading metrics for a single post The `Post` type exposes two metrics-related fields: - **`metrics: [PostMetric!]`** — the list of metric values collected for the post. - **`metricsUpdatedAt: DateTime`** — the timestamp of the most recent metric refresh from the network. ```graphql query { post(input: { id: "your_post_id" }) { id text metrics { type name value unit } metricsUpdatedAt } } ``` Each `PostMetric` carries: - **`type: PostMetricType!`** — the normalized identifier (e.g. `reactions`, `impressions`). Stable across networks. Use this when you're keying off metric values programmatically. - **`name: String!`** — a human-readable label (e.g. "Reactions", "Impressions"). Suitable for displaying in a UI without your own lookup table. - **`value: Float!`** — the numeric value. - **`unit: PostMetricUnit!`** — either `count` (integer-style values like impressions or reach) or `percentage` (e.g. `engagementRate` — values between 0 and 100). See [Get Post Metrics](https://developers.buffer.com/examples/get-post-metrics.md) for the single-post pattern and [Get Posts With Metrics](https://developers.buffer.com/examples/get-posts-with-metrics.md) for the paginated pattern. #### Aggregated metrics For reporting workflows that summarize a window of activity — quarterly recaps, channel-level rollups, BI exports — pulling every post and rolling values up client-side is expensive. The `aggregatedPostMetrics` query does the aggregation server-side and returns a single normalized result. ```graphql query { aggregatedPostMetrics( input: { organizationId: "your_organization_id" startDateTime: "2026-01-01T00:00:00Z" endDateTime: "2026-03-31T23:59:59Z" channelIds: ["your_channel_id"] } ) { metrics { type value unit } metricsUpdatedAt } } ``` The result is an `AggregatedPostMetrics` value with two fields: - **`metrics: [PostMetric!]!`** — the aggregated metric values, in the same `PostMetric` shape returned per-post. - **`metricsUpdatedAt: DateTime`** — the latest `metricsUpdatedAt` across the matched posts. `null` when no posts matched the filter. ##### Filter input `AggregatedPostMetricsInput` carries the aggregation window and any narrowing filters: - **`organizationId: OrganizationId!`** — the organization owning the channels. - **`startDateTime: DateTime!`** / **`endDateTime: DateTime!`** — the inclusive aggregation window. Typically UTC midnight on the first and last calendar days. The range is **capped at 365 days**; longer windows are rejected. - **`channelIds: [ChannelId!]`** — optional channel filter. Omit (or pass `null`) to aggregate across every channel in the organization the actor has insights access to. Passing an empty array matches no channels. - **`tags: TagComparator`** — optional tag filter. Omit to include all posts regardless of tags. ##### Baseline metrics and the `postCount` entry Every successful response includes a baseline trio of entries: `postCount`, `reactions`, and `comments`. Posts on networks that don't track reactions or comments contribute `0` to those totals, so the values are always present and additive across the matched set. `postCount` is a synthetic entry — it's the number of posts that matched the filter window, surfaced as a regular `PostMetric` with `type: postCount` and `unit: count`. It only appears on aggregate responses; the per-post `Post.metrics` field never emits it. ##### Cross-channel intersection Beyond the baseline trio, the response includes additional metric types **only when every channel in the filter set supports them**. A single-network filter surfaces that network's richer metrics (e.g. `impressions`, `reach`, `engagementRate` on LinkedIn). A mixed-network filter trims the extras to the intersection — anything not reported by every network in the set is dropped from the result rather than zero-padded. This is intentional: zero-padding a metric that one network doesn't track at all would misrepresent the aggregate. If you need a metric that's network-specific, narrow the filter to channels on a network that reports it. See [Aggregate Post Metrics](https://developers.buffer.com/examples/aggregate-post-metrics.md) for the canonical query shape and [Get Quarterly Performance Report](https://developers.buffer.com/examples/get-quarterly-performance-report.md) for a quarter-long window recipe. #### Normalized metric names Each social network reports performance data slightly differently. Buffer maps the underlying network metrics onto a single normalized enum (`PostMetricType`) so you can write one query that works across every channel. A few mappings to be aware of: - **`reactions`** — the count of positive reactions on the post. Instagram and Twitter `likes`, Mastodon `favorites`, and similar fields all normalize into `reactions`. - **`reposts`** — the count of shares-by-reposting. Twitter `retweets`, Mastodon `reblogs`, and Threads `reposts` all normalize into `reposts`. - **`comments`** — the count of replies and comments. Threads `replies` normalizes here. - **`shares`** — explicit share/forward actions (distinct from `reposts`, which is a repost-as-new-content action). - **`impressions`**, **`reach`**, **`views`** — view-count families. `impressions` may double-count repeat viewers; `reach` counts unique people; `views` is reported separately by networks that distinguish video views. A handful of metrics are network-specific and only appear on posts from those networks: - **`saves`** — Instagram, Pinterest. - **`follows`** — Instagram (new followers attributed to the post). - **`quotes`** — Threads. - **`viewers`** — LinkedIn (unique video viewers). - **`totalTimeWatched`** — LinkedIn (total watch time in minutes). - **`likes`** — Facebook only. This is the **Like-reaction subcount**, distinct from `reactions` (which sums all Facebook reaction types: Like, Love, Care, Haha, Wow, Sad, Angry). Facebook's Graph API surfaces them separately and we preserve that. The schema reference for `PostMetricType` lists every value with its per-network notes and is the source of truth — see the API reference for the full enum. #### Data freshness Metric values are pulled from each network on a daily cadence. Newly sent posts can therefore take up to ~24 hours before metrics first appear, and subsequent refreshes happen on the same daily rhythm. Two implications worth designing around: - **A missing metric does not mean zero.** The `metrics` array on a single post only includes metric types that the network has actually reported for that post. If a metric is absent from the array, the network either hasn't surfaced it yet or doesn't support that metric for that post type. The non-null `value: Float!` means that when a metric _is_ in the array, you can read it without null-checking. (On `aggregatedPostMetrics`, baseline entries are always emitted — missing-from-network posts contribute `0` to the sum.) - **`metricsUpdatedAt` reflects the most recent ingestion**, not the most recent network change. If a post's engagement spikes between two ingestion runs, you won't see the updated value until the next refresh. If your workflow is timing-sensitive, gate downstream actions on `metricsUpdatedAt` being recent enough rather than on the values themselves. On `aggregatedPostMetrics`, `metricsUpdatedAt` is the latest timestamp across the matched posts — i.e. as fresh as the most recently ingested post in the window. #### Deprecated metric types to avoid Seven `PostMetricType` values are deprecated and will be removed on **2026-07-31**. They are not emitted by any current per-network definition and exist only for backwards compatibility with older clients. Migrate to the listed replacement: | Deprecated value | Replacement | | ---------------- | --------------------------------- | | `favorites` | `reactions` | | `retweets` | `reposts` | | `reblogs` | `reposts` | | `repins` | (no replacement — never emitted) | | `replies` | `comments` | | `link_clicks` | (no replacement — StartPage-only) | | `other` | (no replacement — never emitted) | GraphQL tooling will flag these values with deprecation warnings — treat those warnings as a signal to update your client. #### Recipes ##### Quarterly performance report Roll up a quarter of publishing activity into a single aggregate suitable for BI exports, board decks, or year-on-year comparisons. Pass a 90-ish-day window to `aggregatedPostMetrics` with the channels you care about and (optionally) a tag filter to scope the report to a campaign or content type. See [Get Quarterly Performance Report](https://developers.buffer.com/examples/get-quarterly-performance-report.md) for the query shape. To compare quarters, run the query twice with different windows and diff the values client-side. #### Next steps - [Authentication](https://developers.buffer.com/guides/authentication.md) — create a personal API key to read metrics. - [Pagination](https://developers.buffer.com/guides/pagination.md) — page through large result sets when reading metrics across many posts. - [Rate Limits](https://developers.buffer.com/guides/api-limits.md) — what to expect from the API when running large metrics queries. - [Get Post Metrics](https://developers.buffer.com/examples/get-post-metrics.md) — the per-post query. - [Get Posts With Metrics](https://developers.buffer.com/examples/get-posts-with-metrics.md) — the paginated query. - [Aggregate Post Metrics](https://developers.buffer.com/examples/aggregate-post-metrics.md) — the cross-post aggregate query. - [Get Quarterly Performance Report](https://developers.buffer.com/examples/get-quarterly-performance-report.md) — quarter-long window recipe. ### API Standards We've designed the Buffer API around a set of principles that keep things stable and predictable as we evolve the schema. #### Always Add, Never Modify or Remove We only add to the schema. We won't modify or remove existing fields and types. Your queries and mutations will keep working as we ship updates. New fields and types are added alongside existing ones, so you don't need to worry about breaking changes. When we plan to retire a field, we mark it with the `@deprecated` annotation. Deprecated fields include a `reason` that describes the replacement and when the field will be removed. ```jsx type ExampleType { oldField: String @deprecated(reason: "Use `newField`. This will be removed on the 12/10/2021") newField: String } ``` You'll always have advance notice before a field is removed. Keep an eye on the `@deprecated` annotations in the schema, our [Changelog](https://developers.buffer.com/changelog.md), and migrate to the recommended replacements before the removal date. #### Using Input Objects for Operation Arguments We use input objects rather than inline arguments. This keeps things flexible as the API evolves. For example, instead of passing individual scalars: ```jsx type Mutation { createPost(text: String): ... } ``` The API uses a dedicated input type: ```jsx input PostInput { orgId: String text: String } type Mutation { createPost(input: PostInput!): ... } ``` This means new fields can be added to the input type without affecting existing operations. #### Returning Typed Responses Our operations return typed response objects rather than scalar values. This lets responses evolve over time. For example, adding new fields, without breaking the contract. It also enables union types for error handling. ```jsx type Mutation { createPost(...): Post } ``` Instead, we use typed responses that can include additional data and error states: ```jsx type PostActionSuccess { post: Post! } type LimitReachedError { message: String! } union PostActionPayload = PostActionSuccess | LimitReachedError type Mutation { createPost(...): PostActionPayload } ``` #### Being Specific with Nullability We use nullability to communicate exactly what you can expect from each field. A non-null field (marked with `!`) guarantees a value will always be present. A nullable field may return `null`, and your client should handle that case. Here's an example: ```jsx type Post { type: PostStatus! sentAt: DateTime } ``` For this type, there are two states: - **Non-null** - a value will always be provided. You don't need to handle a null state. For example, a post always has a `PostStatus`: ```jsx type: PostStatus! ``` - **Nullable** - a null value may be returned, and your client should handle it. For example, a post only has a `sentAt` for when a post has been published: ```jsx sentAt: DateTime ``` ##### Nullability in Arrays Nullability applies to both the array itself and the type contained within it. **In short**: if an array will never contain null entries, the entry type is marked as non-null. If the array itself can never be null, it's also marked as non-null. ```jsx posts: [Post] ``` Both the array and its entries can be null. You could receive `null`, or an array containing null values such as `[ATTACHMENT, null, ATTACHMENT]`. ```jsx posts: [Post]! ``` The array will never be null (it will always be returned, even if empty), but individual entries may be null. For example, `[null, TAG, null]` or `[]`, but never `null`. ```jsx posts: [Post!] ``` The array may be null, but when present, its entries will never be null. For example, `[]`, `null`, or `[MEDIA, MEDIA]`. When both the array and entries are non-null: ```jsx posts: [Post!]! ``` You are guaranteed a non-null array with non-null entries. ##### Boolean Values Boolean fields are always non-null. You will always receive either `true` or `false`, so there is no need to handle a null state for boolean values. #### Returning Contextual Responses to Clients Our mutation responses return meaningful data related to the action performed, rather than generic status flags. For example, instead of: ```kotlin type PostActionSuccess { success: Boolean! } ``` The response returns the resource that was affected: ```kotlin type PostActionSuccess { post: Post! } ``` This gives you immediately useful data and avoids redundant checks. If you're using Apollo Client, the local cache will automatically update when it receives a response matching the `id` and `__typename` of an already-cached object. #### Maintaining Input Type Ordering We always append new fields to the end of input types. This matters because some code-generated clients send arguments positionally. If a new field were inserted in the middle, existing positional arguments would shift and map to the wrong fields. Here's an example. Given this input type: ```kotlin input IdeaCreationInput { organizationId: String! content: IdeaContentInput! source: String } ``` A code-generated client (e.g. Apollo on Android) produces: ```kotlin public data class IdeaCreationInput( public val organizationId: String, public val content: IdeaContentInput, public val cta: Optional = Optional.Absent, ) ``` And the client sends arguments positionally: ```kotlin IdeaCreateMutation( IdeaCreationInput( idea.organizationId!!, idea.toInput(), Optional.presentIfNotNull(source) ) ) ``` If a new field `groupId` were added in the middle: ```kotlin input IdeaCreationInput { organizationId: String! content: IdeaContentInput! groupId: ID source: String } ``` The generated class would shift: ```kotlin public data class IdeaCreationInput( public val organizationId: String, public val content: IdeaContentInput, public val groupId: Optional = Optional.Absent, public val cta: Optional = Optional.Absent, ) ``` Clients that have not updated would now send the `source` value as `groupId`, causing incorrect behavior. ```kotlin IdeaCreateMutation( IdeaCreationInput( idea.organizationId!!, idea.toInput(), Optional.presentIfNotNull(source) ) ) ``` To avoid this, always use **named arguments** rather than positional arguments when constructing input types in your client code. #### Pagination Paginated responses use cursor-based pagination with the following structure: - `edges`: a list of connections to the response items - `pageInfo`: pagination metadata (see `PaginationPageInfo` below) - `totalCount`: optional, but when present, always non-null. The total count of all results matching the query filters. ##### Request Fields **input** The input filter. The top level includes static, required fields (typically the organization ID), plus an optional `filter` object for narrowing results. - `organizationId`: The organization ID for the request. - `filter`: Filtering criteria applied to results and counts. - Filter values are typically lists of string IDs. - Fields are nullable - omitting a filter field means no filtering is applied for that criterion. - Filtering logic uses an **AND** operation between all defined items. **first** The maximum number of items to return (synonymous with "limit"). **after** The cursor to start fetching from. Cursors are opaque strings. Do not parse or construct them yourself. ##### Response Fields **totalCount** The total number of results matching the query filters, consistent with [GraphQL pagination best practices](https://graphql.org/learn/pagination/) and [GitHub's implementation](https://docs.github.com/en/graphql/reference/objects#branchprotectionruleconflictconnection). **pageInfo** - `startCursor`: The first cursor in the list. Use it to fetch the previous page. - `endCursor`: The last cursor in the list. Use it to fetch the next page. - `hasPreviousPage`: `true` if a previous page is available. Currently always `false` as only forward pagination is supported. - `hasNextPage`: `true` if a next page is available. #### Error Handling We use two categories of errors: - **Non-recoverable errors** appear in the standard GraphQL `errors` array. These represent issues outside your control - authentication failures, missing resources, or server errors. They include an error `code` in the `extensions` object (e.g. `NOT_FOUND`, `FORBIDDEN`, `UNAUTHORIZED`, `UNEXPECTED`). - **Recoverable errors** (user errors) are returned as typed data in the response payload. These are situations you can act on, like input validation failures or account limits being reached. ##### Mutations ###### Modelling Errors Every mutation returns a payload union that includes both the success state and any user-facing errors. The payload follows the naming convention `{MutationName}Payload`. ```graphql union PostActionPayload = PostActionSuccess | ... ``` You can query for the specific error types you need to handle. New error types may be added to a payload over time. ```graphql union PostActionPayload = PostActionSuccess | LimitReachedError | InvalidInputError ``` Every typed error implements the `MutationError` interface: ```graphql interface MutationError { message: String! } ``` Each error type includes the `message` field from the interface: ```graphql type LimitReachedError implements MutationError { message: String! } type InvalidInputError implements MutationError { message: String! } ``` The `message` field contains a human-readable string suitable for display. In most cases you'll use the error type itself to determine what to show, but the `message` provides a sensible default (see **Future Proofing Error Responses** below). ###### Consuming Errors To consume typed errors, use the `... on` pattern to match specific error types in the response. This lets you handle each error differently - for example, showing a specific recovery path to the user. You only need to match the error types you care about. For everything else, use `... on MutationError` as a catch-all: ```graphql mutation CreatePost { createPost { ... on PostActionSuccess { // handle fields } ... on LimitReachedError { message } ... on MutationError { message } } } ``` If you don't need to handle specific error types, you can rely entirely on the `MutationError` interface: ```graphql mutation CreatePost { createPost { ... on PostActionSuccess { // handle fields } ... on MutationError { message } } } ``` ###### Future Proofing Error Responses Some mutations may not have specific typed errors defined yet. To make sure your client handles any errors we add in the future, every mutation payload includes a `VoidMutationError` type: ```graphql type VoidMutationError implements MutationError { message: String! } union PostActionPayload = PostActionSuccess | VoidMutationError ``` We'll never explicitly return a `VoidMutationError`, but its presence in the union means that if you include `... on MutationError` in your query, your client will automatically receive the `message` for any new error types we add later - no code changes needed. ```graphql ... on MutationError { message } ``` For this reason, **always include `... on MutationError`** in your mutation queries. ###### Non-Recoverable Errors Non-recoverable errors are returned in the standard GraphQL `errors` array. These include an error `code` in the `extensions` object for additional context. Common error codes include: - `NOT_FOUND` - the requested resource doesn't exist - `FORBIDDEN` - you don't have permission for this action - `UNAUTHORIZED` - authentication is required or invalid - `UNEXPECTED` - an unexpected server error occurred If you need to show error details to users, use typed errors (as described above) instead of the `errors` array. ##### Queries In most cases, queries return either the requested data or a non-recoverable error in the `errors` array: ```graphql type Query { channels(input: ChannelsInput!): [Channel!]! } ``` A successful result returns the list of `Channel` types. If an error occurs, it appears in the `errors` array. In rare cases, a query may need to return a recoverable error. When this applies, we use a union payload, the same pattern as mutations. For example, if fetching a post requires reconnecting a channel, the response includes a typed error: ```graphql type PostSuccess { post: Post! } type ChannelReconnectRequired implements MutationError { message: String! channelId: String! } union PostPayload = PostSuccess | ChannelReconnectRequired type Query { post(input: PostInput!): PostPayload } ``` This pattern is uncommon for queries but provides a way to surface recoverable errors when needed. ### Character Limits Every social network enforces a maximum length for post text, and Buffer validates against that limit before a post is scheduled or published. This guide covers the limit for each network, how Buffer counts characters, and channel-specific nuances. On most networks Buffer counts length in UTF-16 code units, not in the number of characters you see on screen. Most text counts one to one, but emoji and the bold and italic Unicode letters, commonly used for LinkedIn formatting, count as two units each. A post that looks well under the limit can still be rejected because of this. #### Limit per network The table below shows the character limit Buffer applies to the post `text` for each network. Where a network also supports a first comment, its separate limit is listed too. | Channel | Post limit | First comment | |---|---|---| | Facebook (pages and groups) | 5,000 | 5,000 | | Instagram | 2,196 | 2,200 | | X (Twitter), Free | 280 | - | | X (Twitter), Basic/Premium/Premium+ | 25,000 | - | | LinkedIn (pages and profiles) | 3,000 | 1,250 | | Pinterest | 500 | - | | TikTok (photo and text posts) | 4,000 | - | | TikTok (with a video attached) | 2,200 | - | | Threads | 500 | - | | Bluesky | 300 | - | | YouTube (video description) | 5,000 | - | | Google Business Profiles | 4,000 | - | | Mastodon | Set by the server, 500 by default | - | Some networks limit other fields as well: - Pinterest and YouTube titles: 100 characters - Threads topics: 50 characters, and a topic cannot contain `&` or `.` - TikTok comments: 150 characters On Mastodon the limit comes from the server your channel is on, so read `maxCharacters` from the channel's `MastodonMetadata` rather than assuming 500. Buffer caps it at 20,000 however high the server sets it. Attached images and videos do not use up any of your characters. The one exception is TikTok, where attaching a video lowers the limit as shown above. #### How Buffer counts characters On most networks Buffer measures `text` length in UTF-16 code units, the same value you get from `string.length` in JavaScript. This matters because one character as a reader sees it is not always one code unit: Characters in the Basic Multilingual Plane (ordinary Latin letters, digits, punctuation, most accented Latin, and so on) are one code unit each. Characters outside that plane are encoded as a surrogate pair and count as two code units each. This includes most emoji and the mathematical alphanumeric letters (U+1D400 to U+1D7FF) that power bold and italic text on LinkedIn. So the length Buffer validates is often higher than the number of glyphs you can count by eye. Some networks add their own rules on top: | Network | How the text is counted | |---|---| | Facebook, Google Business Profiles, Pinterest, Threads, TikTok, YouTube | UTF-16 code units | | LinkedIn | UTF-16 code units, and every URL counts as 24 whatever its real length | | Instagram | UTF-16 code units, and every line break counts as 2 | | Mastodon | UTF-16 code units, URLs count as 23, and the server part of an `@user@server.com` mention is not counted | | X (Twitter) | X's own weighted count, where URLs are 23 and emoji are 2 | | Bluesky | Graphemes, so an emoji counts as 1 however it is encoded. URLs count as the host plus up to 16 more characters | ##### Unicode bold and italic formatting Networks don't natively support rich text in the post body, so any bold or italic formatting in a post is really a substitution of mathematical alphanumeric symbols. For example, the bold H is not the letter `H` (U+0048) but 𝗛 (U+1D5DB). Each of these substitute letters lives outside the Basic Multilingual Plane, so almost everywhere it counts as two rather than one. | What you type | Glyphs you see | UTF-16 networks | Bluesky | X (Twitter) | |---|---|---|---|---| | `Hello` (plain) | 5 | 5 | 5 | 5 | | 𝗛𝗲𝗹𝗹𝗼 (bold) | 5 | 10 | 5 | 10 | | 𝘏𝘦𝘭𝘭𝘰 (italic) | 5 | 10 | 5 | 10 | | 🚀 (emoji) | 1 | 2 | 1 | 2 | | 👨‍👩‍👧 (family emoji, ZWJ sequence) | 1 | 8 | 1 | 2 | | `café` (precomposed é) | 4 | 4 | 4 | 4 | | `café` (e plus combining accent) | 4 | 5 | 4 | 4 | Styling therefore costs about half your allowance on every network except Bluesky, which counts graphemes and charges the same for a styled letter as a plain one. A post written entirely in bold letters reaches LinkedIn's 3,000 at roughly 1,500 visible characters, and a free X post's 280 at roughly 140. That is why a post that looks far short of the limit can still fail validation. #### What happens when you exceed the character limit Exceeding the character limit is a typed mutation error. The request returns HTTP `200` and the failure is reported in the mutation payload's `message` field (see [Error Handling](https://developers.buffer.com/guides/error-handling.md)). ```json { "data": { "createPost": { "message": "LinkedIn posts cannot exceed 3000 characters." } } } ``` The other fields report the same way, with their own wording: `LinkedIn first comment cannot exceed 1250 characters.`, `YouTube title cannot exceed 100 characters.`, etc. Because the count is in code units, a `message` like the above can appear even when your text looks shorter than the stated number. #### Next steps - [Posts & Scheduling](https://developers.buffer.com/guides/posts-and-scheduling.md): creating and scheduling posts - [Error Handling](https://developers.buffer.com/guides/error-handling.md): typed mutation errors and error codes - [Rate Limits](https://developers.buffer.com/guides/api-limits.md): request and query limits ### Error Handling The Buffer API uses two categories of errors. Here's how they work and how to handle them in your code. #### Two types of errors | Type | Where | When | HTTP Status | |:-----|:------|:-----|:------------| | **Typed mutation errors** | In the response `data` | User-fixable problems (validation, limits) | 200 | | **Non-recoverable errors** | In the `errors` array | System problems (auth, not found, server) | 200 | GraphQL always returns HTTP 200. Check the response body to determine success or failure. #### Typed mutation errors Mutations return a **union type** that includes both the success case and possible error cases. This lets you handle each error type differently in your code. ##### Basic pattern Always include `... on MutationError` in every mutation: ```graphql mutation { createPost(input: { text: "Hello world", channelId: "your_channel_id", schedulingType: automatic, mode: addToQueue }) { ... on PostActionSuccess { post { id text } } ... on MutationError { message } } } ``` If the mutation succeeds, you get `PostActionSuccess`. If it fails, you get a `MutationError` with a human-readable `message`. ##### Handling specific error types Some mutations return specific error types with additional data. You can match on these for more precise handling: ```graphql mutation { createPost(input: { ... }) { ... on PostActionSuccess { post { id } } ... on LimitReachedError { message } ... on InvalidInputError { message } ... on MutationError { message } } } ``` The `... on MutationError` at the end acts as a catch-all. Because all error types implement the `MutationError` interface, any error type you don't explicitly handle will still return a `message`. ##### InvalidInputError When input validation fails, you get an error message: ```json { "data": { "createPost": { "message": "Text is required" } } } ``` ##### Future-proofing with VoidMutationError Some mutations include a `VoidMutationError` in their union. The API never explicitly returns this type, but it ensures that if new error types are added later, your `... on MutationError` catch-all will still receive the `message` - no code changes needed. **This is why you should always include `... on MutationError` in every mutation.** #### Non-recoverable errors System-level errors appear in the GraphQL `errors` array. These indicate problems you typically can't fix by changing your input. ##### Error codes | Code | Meaning | What to do | |------|---------|------------| | `UNAUTHORIZED` | Missing or invalid API key | Check your `Authorization` header and API key | | `FORBIDDEN` | Valid key, but no permission | Verify you're accessing resources in your own account | | `NOT_FOUND` | Resource doesn't exist | Check the ID you're using is correct | | `UNEXPECTED` | Server error | Retry after a short delay; contact support if persistent | | `RATE_LIMIT_EXCEEDED` | Too many requests | Wait and retry; see [Rate Limits](https://developers.buffer.com/guides/api-limits.md) | ##### Example error response ```json { "data": null, "errors": [ { "message": "Not authorized", "extensions": { "code": "UNAUTHORIZED" } } ] } ``` #### Error handling snippet Here's a reusable pattern for handling both error types: ```javascript async function bufferRequest(query, variables = {}) { const response = await fetch('https://api.buffer.com', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.BUFFER_API_KEY}`, }, body: JSON.stringify({ query, variables }), }); const result = await response.json(); // Check for non-recoverable errors if (result.errors) { const error = result.errors[0]; const code = error.extensions?.code; if (code === 'RATE_LIMIT_EXCEEDED') { // Wait and retry throw new Error('Rate limited, try again later'); } throw new Error(`API error (${code}): ${error.message}`); } return result.data; } ``` For mutations, check the response type: ```javascript const data = await bufferRequest(` mutation CreatePost($input: CreatePostInput!) { createPost(input: $input) { ... on PostActionSuccess { post { id } } ... on MutationError { message } } } `, { input: postInput }); if (data.createPost.post) { // Success console.log('Created post:', data.createPost.post.id); } else if (data.createPost.message) { // Typed error console.error('Mutation error:', data.createPost.message); } ``` #### Best practices - **Always include `... on MutationError { message }` in every mutation.** This catches current and future error types. - **Check the `errors` array on every response.** Even successful mutations can include warnings. - **Don't display raw error messages to end users.** Use the error type to decide what to show. - **Log the full error response for debugging.** Include the query, variables, and complete response. - **Handle `RATE_LIMIT_EXCEEDED` with exponential backoff.** See [Rate Limits](https://developers.buffer.com/guides/api-limits.md) for details on limits and retry headers. #### Next steps - [API Standards](https://developers.buffer.com/guides/api-standards.md): full details on the API's error design philosophy - [Rate Limits](https://developers.buffer.com/guides/api-limits.md): understand request limits and throttling - [Your First Post](https://developers.buffer.com/guides/your-first-post.md): see error handling in action ### Rate Limits To ensure that Buffer API stays healthy, performant, and fair for everyone, Buffer applies API rate limits. Those limits are applied per client and depend on your Buffer plan. We ask developers to use suggested industry standard techniques for honoring enforced rate limits and re-trying requests responsibly. #### Buffer API Rate Limits Buffer applies rate limits per client. The number of API keys and app clients you can create, along with how many requests each client can make over a rolling 15-minute, 24-hour, and 30-day window, depend on your Buffer plan. | Feature | Free | Essentials | Team | | ------------ | ----- | ---------- | ------ | | API Keys | 1 | 3 | 5 | | App Clients | 1 | 3 | 5 | | 15-min limit | 100 | 100 | 100 | | 24-hr limit | 250 | 250 | 500 | | 30-day limit | 3,000 | 7,500 | 15,000 | > Does your integration require higher limits? Reach out to [developersupport@buffer.com](mailto:developersupport@buffer.com). #### Using Response Headers to check Rate Limits Response headers provide you the details you can use to build the back-off logic. Watching `r` (requests remaining) decrease lets you apply client-side throttle, spread the work out, or pause a job before a `429` happens, rather than finding out about a limit only when a request fails. Every response includes rate-limit information as structured `RateLimit` headers. Each of the three windows - 15-minute, 24-hour, and 30-day - contributes one policy, so you'll see three of each header: ```http RateLimit: "100-in-15min";r=98;t=897 RateLimit: "250-in-1day";r=248;t=86397 RateLimit: "3000-in-30days";r=2969;t=696980 RateLimit-Policy: "100-in-15min";q=100;w=900;pk=:ZjJjZjVmNzM5M2Zm: RateLimit-Policy: "250-in-1day";q=250;w=86400;pk=:ZjJjZjVmNzM5M2Zm: RateLimit-Policy: "3000-in-30days";q=3000;w=2592000;pk=:ZjJjZjVmNzM5M2Zm: ``` > The numbers above are from one example response. The quotas you see depend on your plan. `RateLimit` reports the live status of each policy, and `RateLimit-Policy` describes the limit behind it: | Header | Field | What it means | | ------------------ | ----- | ----------------------------------------------------- | | `RateLimit` | `r` | The requests remaining. | | `RateLimit` | `t` | The seconds until that window resets. | | `RateLimit-Policy` | `q` | The quota. | | `RateLimit-Policy` | `w` | The window length in seconds. | | `RateLimit-Policy` | `pk` | The partition key identifying your rate-limit bucket. | Policy names like `100-in-15min` are generated from your quota and window, so they change with your plan. Match a policy by its window length (`w`) rather than by name: | Window | `w` | | ---------- | --------- | | 15 minutes | `900` | | 24 hours | `86400` | | 30 days | `2592000` | ##### Reading the headers The headers come back on every authenticated GraphQL response, so you can read them off requests you already make: ```javascript // POST your GraphQL query to the API as usual const response = await fetch('https://api.buffer.com', { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ query: '{ account { id } }' }), }) // fetch joins the repeated RateLimit headers into one string, one entry per window const policies = response.headers.get('ratelimit').split(/,\s*(?=")/) // ['"100-in-15min"; r=98; t=897', '"250-in-1day"; r=248; t=86397', ...] const remaining = Object.fromEntries( policies.map((p) => [p.match(/"([^"]+)"/)[1], Number(p.match(/r=(\d+)/)[1])]) ) // { '100-in-15min': 98, '250-in-1day': 248, '3000-in-30days': 2969 } ``` ```python import re import requests response = requests.post( 'https://api.buffer.com', headers={ 'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json', }, json={'query': '{ account { id } }'}, ) # urllib3 keeps the repeated headers, so ask for them as a list policies = response.raw.headers.getlist('RateLimit') # ['"100-in-15min"; r=98; t=897', '"250-in-1day"; r=248; t=86397', ...] remaining = { re.search(r'"([^"]+)"', policy).group(1): int(re.search(r'r=(\d+)', policy).group(1)) for policy in policies } # {'100-in-15min': 98, '250-in-1day': 248, '3000-in-30days': 2969} ``` ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $apiKey, 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode(['query' => '{ account { id } }']), // each repeated RateLimit header arrives here as its own line CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$policies) { if (stripos($line, 'ratelimit:') === 0) { $policies[] = trim(substr($line, strlen('ratelimit:'))); } return strlen($line); }, ]); $body = curl_exec($ch); curl_close($ch); $remaining = []; foreach ($policies as $policy) { preg_match('/"([^"]+)"/', $policy, $name); preg_match('/r=(\d+)/', $policy, $r); $remaining[$name[1]] = (int) $r[1]; } // ['100-in-15min' => 98, '250-in-1day' => 248, '3000-in-30days' => 2969] ``` > How repeated headers are exposed varies by HTTP client, as the tabs above show - some hand you a list or separate lines, others join them into one string - so match whichever your language gives you. Note the space after each `;`, which the API sends and a strict parser will trip over. If you use a GraphQL client library instead of a raw HTTP call, read the response headers through whatever mechanism it provides. #### Handling errors caused by rate limiting When you exceed a rate limit, the API returns HTTP `429 Too Many Requests`. A `429` carries a response body which names the exhausted window, and response headers that tell you how long you should wait until making another request. **The response body** ```json { "errors": [ { "message": "Too many requests from this client. Please try again later.", "extensions": { "code": "RATE_LIMIT_EXCEEDED", "window": "15m" } } ] } ``` **The response headers** ```http HTTP/2 429 retry-after: 753 ratelimit: "100-in-15min"; r=0; t=753 ratelimit-policy: "100-in-15min"; q=100; w=900; pk=:ZjJjZjVmNzM5M2Zm: content-type: application/json; charset=utf-8 ``` | Header | Example | What it tells you | | ------------------ | ----------------------------------- | -------------------------------------------------------------------------------------------- | | `Retry-After` | `753` | How many seconds to wait before retrying. This is the number to sleep on. | | `RateLimit` | `"100-in-15min"; r=0; t=753` | Which policy tripped. `r=0` confirms it is exhausted and `t` counts down to its reset. | | `RateLimit-Policy` | `"100-in-15min"; q=100; w=900` | The quota (`q`) and window length in seconds (`w`) of that same policy. | `Retry-After` always matches `t` on the returned policy, and both count down in real time, so you can retry the moment it reaches zero. ##### Implementing the retry logic The retry logic is the same in any language: ``` send the request if the status is 429: if you are out of attempts, or Retry-After is longer than you will wait: surface the failure otherwise wait Retry-After seconds, plus a little jitter, and retry ``` The examples below show a sample implementation of this loop. Each retries a `429` until the request goes through, sleeping `Retry-After` seconds in between. `MAX_ATTEMPTS` caps how many tries it makes and `MAX_WAIT_SECONDS` caps how long any one sleep runs, so it always stops instead of hanging. Treat them as starting points rather than drop-in code - the attempt limit, the wait cap, and how you surface a final failure are all yours to decide. ```javascript const MAX_ATTEMPTS = 3 const MAX_WAIT_SECONDS = 900 // a 30d Retry-After can be weeks away async function bufferRequest(query) { for (let attempt = 1; ; attempt++) { const response = await fetch('https://api.buffer.com', { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ query }), }) if (response.status !== 429) return response.json() // Read Retry-After fresh each time: it counts down, so an older value is stale. const retryAfter = Number(response.headers.get('retry-after')) const { errors } = await response.json() const window = errors[0].extensions.window if (attempt >= MAX_ATTEMPTS || !(retryAfter > 0) || retryAfter > MAX_WAIT_SECONDS) { throw new Error(`Rate limited on the ${window} window, retry in ${retryAfter}s`) } // Retry-After is exact, so every client waiting it out returns at the same // instant. A little jitter spreads them back out. const wait = retryAfter + Math.random() * 5 console.warn(`Rate limited on the ${window} window, waiting ${Math.round(wait)}s`) await new Promise((resolve) => setTimeout(resolve, wait * 1000)) } } ``` ```python import random import time import requests MAX_ATTEMPTS = 3 MAX_WAIT_SECONDS = 900 # a 30d Retry-After can be weeks away def buffer_request(query): for attempt in range(1, MAX_ATTEMPTS + 1): response = requests.post( 'https://api.buffer.com', headers={ 'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json', }, json={'query': query}, ) if response.status_code != 429: return response.json() # Read Retry-After fresh each time: it counts down, so an older value is stale. retry_after = int(response.headers.get('Retry-After', 0)) window = response.json()['errors'][0]['extensions']['window'] if attempt == MAX_ATTEMPTS or not 0 < retry_after <= MAX_WAIT_SECONDS: raise RuntimeError(f'Rate limited on the {window} window, retry in {retry_after}s') # Retry-After is exact, so every client waiting it out returns at the same # instant. A little jitter spreads them back out. wait = retry_after + random.uniform(0, 5) print(f'Rate limited on the {window} window, waiting {round(wait)}s') time.sleep(wait) ``` ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $apiKey, 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode(['query' => $query]), // Read Retry-After fresh each time: it counts down, so an older value is stale. CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$retryAfter) { if (stripos($line, 'retry-after:') === 0) { $retryAfter = (int) trim(substr($line, strlen('retry-after:'))); } return strlen($line); }, ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if ($status !== 429) { return json_decode($body, true); } $window = json_decode($body, true)['errors'][0]['extensions']['window']; if ($attempt === MAX_ATTEMPTS || $retryAfter <= 0 || $retryAfter > MAX_WAIT_SECONDS) { throw new RuntimeException("Rate limited on the {$window} window, retry in {$retryAfter}s"); } // Retry-After is exact, so every client waiting it out returns at the same // instant. A little jitter spreads them back out. $wait = $retryAfter + random_int(0, 5); error_log("Rate limited on the {$window} window, waiting {$wait}s"); sleep($wait); } } ``` If the requests come from an AI agent or an MCP client rather than from your own code, the same rule holds but nothing enforces it for you. An agent will usually rephrase a failed call and try again rather than wait. Tell it to stop on a `429` and report the `Retry-After` value instead of retrying, since the header already says exactly how long the wait is and there is nothing left to guess at. The [Efficient API Usage](https://developers.buffer.com/guides/efficient-api-usage.md) guide has prompts you can paste into an agent's instructions. #### Monitoring Your Usage You don't have to track headers by hand to see where you stand: - **Developer Dashboard** - your [API settings page](https://publish.buffer.com/settings/api) shows live usage per window for each of your clients. - **Buffer CLI** - the [CLI](https://developers.buffer.com/guides/cli.md) reads these headers on every command. Run any command with `--verbose` to print a rate-limit summary for each window, and it warns you when you're running low even without it (silence it with `--quiet`). On a `429` it reports the exhausted window and how long to wait, then exits with code `3`: ``` Error: Rate limit exceeded (`15m` window). Retry after `9m 51s`. (api · code 3) ``` #### Regulating the request rate Make sure you optimize your API usage, make only the requests you need and use techniques to reduce your usage before you hit the rate limit errors: 1. **Slow down before you hit zero.** `r` on the `RateLimit` header tells you what is left in each window. Throttling when it gets low is cheaper than recovering from a `429`. The [CLI](https://developers.buffer.com/guides/cli.md) uses 10% remaining as its warning threshold, which is a reasonable default to copy. 2. **Back off, then retry.** When you do get a `429`, wait `Retry-After` seconds before your next attempt. See [Implementing the retry logic](#implementing-the-retry-logic). 3. **Stagger scheduled jobs.** If several jobs share one API key, starting them all on the hour concentrates your whole budget into a few minutes. Offsetting their start times spreads the same work across the window. 4. **Optimize your API usage.** You can employ quite a few different techniques to use the API more efficiently and reduce the number of requests your app makes. Check out the [Efficient API Usage](https://developers.buffer.com/guides/efficient-api-usage.md) guide for some of the best practices. #### Query Limits In addition to rate limits, we enforce query-level limits to protect against overly complex or expensive GraphQL queries. ##### Query Complexity Each query is assigned a cost based on the fields it requests: - **Scalar fields** (e.g., `id`, `name`): 1 point each - **Object fields** (e.g., `organization`, `channel`): 2 points each - **Nesting multiplier**: Nested fields are multiplied by a factor of 1.5x per level of depth The maximum allowed query cost is **175,000 points**. If your query exceeds this, you will receive an error asking you to simplify it. ##### Query Depth Queries are limited to a maximum depth of **25 levels**. Deeply nested queries can cause exponential resource consumption, so keep your queries as flat as possible. ##### Aliases A maximum of **30 aliases** are allowed per query. Aliases let you rename fields in a response, but excessive use can be used to amplify query cost. ##### Directives Queries are limited to a maximum of **50 directives**. ##### Tokens Queries are limited to a maximum of **15,000 tokens**. This is a parser-level limit on the overall size of the query document. ##### Query Limit Error Responses When a query limit is exceeded, you will receive a GraphQL error response: ```json { "errors": [ { "message": "Query exceeds maximum allowed complexity. Please simplify your query." } ] } ``` The error message will indicate which limit was exceeded (complexity, depth, aliases, directives, or tokens). These limits may change as we evolve the API, so keep an eye on your usage. #### Frequently asked questions **What happens when I hit a rate limit?** The request is rejected with a `429`. The response carries `Retry-After` header telling you how many seconds to wait, and the body naming the limit you hit. **Do rejected requests count against my quota?** A `429` does not consume quota and does not extend how long you are locked out. However, it is highly recommended to implement the `Retry-After` logic to handle the errors gracefully and ensure fair use of the API. **Is there an endpoint that reports my current limits?** No. Read the `RateLimit` headers on any response, or use the [API settings page](https://publish.buffer.com/settings/api) or the [CLI](https://developers.buffer.com/guides/cli.md). See [Monitoring Your Usage](#monitoring-your-usage). **Do limits apply per channel or per organization?** They apply per client, meaning per API key or app client. The number of clients you can create depends on your plan. **Does usage through MCP or an AI assistant count against my limits?** Yes. A request through the MCP server counts the same as a request made with an API key. MCP connections share one rate-limit bucket with your personal API keys, so connecting Buffer to another assistant does not give you more quota. Your [API settings page](https://publish.buffer.com/settings/api) lists your MCP connections alongside your keys. **Are these the same as posting limits?** Rate limits cap how many API requests you can make. Daily posting limits cap how many posts a channel can publish in a day, and you read them with the `dailyPostingLimits` query. Hitting one has no bearing on the other. **I need higher limits. What are my options?** Reduce your request count using the suggestions above first, since most integrations have room to. If your use case genuinely needs more, email [developersupport@buffer.com](mailto:developersupport@buffer.com). ### Pagination The Buffer API uses **cursor-based pagination** for queries that return lists of items. Here's how to page through results. #### How it works Rather than using page numbers, the Buffer API uses cursor strings to track your position in a result set. You request a batch of items, receive a cursor pointing to the last item, then pass that cursor to fetch the next batch. This means you won't skip or duplicate results if new items are added between requests. #### Basic query structure Paginated queries use three key arguments: - **`first`** - how many items to return (the page size) - **`after`** - the cursor to start from (omit for the first page) - **`input`** - filters for the query And the response includes: - **`edges`** - the list of items, each wrapped in a `node` - **`pageInfo`** - pagination metadata ```graphql query { posts( first: 20, input: { organizationId: "your_org_id", filter: { status: [sent] } } ) { edges { node { id text dueAt } } pageInfo { hasNextPage endCursor } } } ``` #### Reading the response The `pageInfo` object tells you about pagination state: - **`hasNextPage`** - `true` if there are more items after this batch - **`endCursor`** - the cursor of the last item, used to fetch the next page - **`startCursor`** - the cursor of the first item - **`hasPreviousPage`** - currently always `false` (only forward pagination is supported) #### Fetching the next page To get the next page, pass the `endCursor` from the previous response as the `after` argument: ```graphql query { posts( first: 20, after: "cursor_from_previous_response", input: { organizationId: "your_org_id", filter: { status: [sent] } } ) { edges { node { id text } } pageInfo { hasNextPage endCursor } } } ``` #### Tips - **Choose an appropriate page size.** A `first` value of 20–50 works well for most use cases. Larger pages mean fewer requests but larger responses. - **Don't parse cursors.** Cursors are opaque strings. Store them and pass them back as-is. - **Respect rate limits.** When fetching many pages, be mindful of the [rate limits](https://developers.buffer.com/guides/api-limits.md). Consider adding a small delay between requests if needed. #### Filtering Most paginated queries support filtering via the `input.filter` object. Filters are combined with **AND** logic - all conditions must match. ```graphql query { posts( first: 20, input: { organizationId: "your_org_id", filter: { status: [scheduled], channelIds: ["your_channel_id", "your_other_channel_id"] } } ) { edges { node { id text channelId } } pageInfo { hasNextPage endCursor } } } ``` This returns only scheduled posts from the specified channels. #### Next steps - [Get Paginated Posts](https://developers.buffer.com/examples/get-paginated-posts.md): working pagination example - [API Limits](https://developers.buffer.com/guides/api-limits.md): rate limiting and query constraints - [Posts & Scheduling](https://developers.buffer.com/guides/posts-and-scheduling.md): more about working with posts ### Efficient API Usage The Buffer GraphQL API lets developers access data efficiently by requesting exactly the fields they need. While the Buffer team aims to build a fast and efficient API, there are a few optimization techniques you can use to significantly reduce the number of requests you send and increase the speed of your integration. #### Request only the data you need Every field you select adds complexity to your requests. Keeping it small and simple will result in faster and more efficient queries. Select only what you use. For example, if you are building a queue view, you may not need `metrics`, `notes`, `assets` and `author` on every post. Fewer fields mean a smaller response and a faster query. #### Combine queries into a single request One of GraphQL's most useful features is that a single query can include multiple top-level fields. Instead of making separate requests for your account details, your channels, and your daily posting limits, you can fetch all three in one round trip: ```graphql query Bootstrap { account { id email timezone } channels(input: { organizationId: "your_org_id" }) { id name service } dailyPostingLimits(input: { channelIds: ["your_channel_id"] }) { channelId isAtLimit scheduled } } ``` Each top-level field resolves independently, and the response contains all three results under a single `data` object. This is a good pattern for app startup or dashboard views, where you'd otherwise fire several requests at once. Many Buffer API fields also accept lists - for example, `dailyPostingLimits` takes an array of `channelIds` - so you can fetch data for many resources in one call rather than looping over them individually. #### Use aliases to run the same query more than once Combining queries works well when each top-level field is different. But if you request the same field twice with different arguments, the server rejects the query - the response is keyed by field name, and there's no way to place two different result sets under one key. Aliases solve this by letting you rename the response key for each request. For example, say you want your scheduled posts and your failed posts in a single request. Alias the two `posts` fields as `scheduled` and `failed`: ```graphql query QueueOverview { scheduled: posts( first: 100 input: { organizationId: "your_org_id", filter: { status: [scheduled] } } ) { edges { node { ...PostFields } } pageInfo { hasNextPage endCursor } } failed: posts( first: 100 input: { organizationId: "your_org_id", filter: { status: [error] } } ) { edges { node { ...PostFields } } pageInfo { hasNextPage endCursor } } } fragment PostFields on Post { id text status dueAt channelId } ``` The response contains `data.scheduled` and `data.failed`, each with its own result set. Aliases are also the way to look up a specific set of records in one request, since `posts` filters on status, channel and date but not on ID. Checking 25 posts one request at a time is 25 requests; the same 25 `post` lookups aliased into one document is one. A query is capped at **30 aliases**, so where a filter can describe what you want, prefer the filter. #### Use fragments to simplify complex queries Another technique that can significantly improve your interactions with the Buffer API is using fragments. The alias query above already uses one. Both `posts` fields need the same five fields on `Post`, so rather than spelling that selection out twice, it is defined once and referenced with the spread syntax (`...PostFields`): ```graphql fragment PostFields on Post { id text status dueAt channelId } ``` Anywhere those fields are needed, `...PostFields` stands in for them. Fragments can help simplify queries, make them more readable, and reduce the maintenance overhead when adding or renaming a field. They also keep query documents small, which matters against the **15,000-token** document limit. #### Filter and sort on the server Fetching a wide set and narrowing it in your own code costs you every request that returned data you threw away. Queries like `posts` support filters on status, channel, post type, tags, and date ranges through `dueAt` and `createdAt` comparators. Using them can significantly reduce the amount of data you don't need: ```graphql query RecentlySent { posts( first: 100 input: { organizationId: "your_org_id" filter: { status: [sent] channelIds: ["your_channel_id"] dueAt: { start: "2026-01-01T00:00:00Z" } } sort: { field: dueAt, direction: desc } } ) { edges { node { id text dueAt channelId } } pageInfo { hasNextPage endCursor } } } ``` #### Use flat fields instead of nested objects Some values can appear in multiple places in the schema. For example, a `Post` carries `channelId` and `channelService` as plain fields, and the same values are also reachable through the `channel` object. Accessing them requires different resources. The flat fields are values the post already has - reading them costs nothing extra. Requesting the `channel` object makes the API load the full channel first, even if all you select inside it is `id` and `service`. ```graphql # Slower: loads the full channel for every post posts(first: 100, input: { organizationId: "your_org_id" }) { edges { node { id channel { id service } } } } # Faster: reads values the post already has posts(first: 100, input: { organizationId: "your_org_id" }) { edges { node { id channelId channelService } } } ``` At 100 posts per page, that's 100 channel loads you didn't need. A good rule of thumb - check whether the value you want already exists on the object you have before reaching through to a nested one. #### Use pagination with a large enough page Pagination allows you to loop through pages to access more results. When you need to access a large number of results, aim to use a large enough page window. At the moment, the Buffer API supports returning up to 100 items per request. One caveat to keep in mind - page size and selection width multiply. Every field you select is resolved once per item in the page, which counts towards the [query complexity budget](https://developers.buffer.com/guides/api-limits.md#query-complexity) and towards the size of the response. See [Pagination](https://developers.buffer.com/guides/pagination.md) for how cursors work. #### Aggregate when possible Some queries in the Buffer API are designed to aggregate large amounts of data. When you want totals rather than individual rows, use them instead of paging through the underlying records and adding them up yourself. `aggregatedPostMetrics` is the clearest example. It rolls up post performance across a date range of up to 365 days and returns the totals in a single request: ```graphql query QuarterRollup { aggregatedPostMetrics( input: { organizationId: "your_org_id" startDateTime: "2026-04-01T00:00:00Z" endDateTime: "2026-06-30T00:00:00Z" } ) { metrics { type name value unit } metricsUpdatedAt } } ``` Every result includes a baseline of `postCount`, `reactions` and `comments`: ```json { "metrics": [ { "type": "postCount", "name": "Posts", "value": 250, "unit": "count" }, { "type": "reactions", "name": "Reactions", "value": 6800, "unit": "count" }, { "type": "comments", "name": "Comments", "value": 750, "unit": "count" } ], "metricsUpdatedAt": "2026-06-30T03:23:11.120Z" } ``` Building those same numbers by hand means paging every sent post in the window, selecting `metrics` on each one, and summing them yourself - several requests and a far larger response for a result the API can return in one. #### Cache what rarely changes Channels, organization IDs and tags mostly change when someone connects or disconnects an account. In most cases, these details don't change as dynamically as other data points. For that reason they are good candidates for client-side caching. This can drop the request volume by an order of magnitude. A few techniques to consider: - **Cache the organization ID and channel list, and refresh them on a schedule.** A daily refresh, or a manual "refresh channels" button in your UI, beats a fetch on every operation. - **Avoid fetching channels just to validate a channel ID.** If you already stored the ID, use it. `createPost` returns a typed `NotFoundError` with `"Channel not found"` when an ID is wrong, so you can recover from the failure instead of paying for a lookup before every write. GraphQL clients like [Apollo](https://www.apollographql.com/docs/react/caching/overview) can do most of this for you. Apollo's `InMemoryCache` normalizes anything with an `id`, so a channel fetched once is reused everywhere it appears. Its default `fetchPolicy` is `cache-first`, which means repeat queries are answered from memory without touching the API: ```javascript import { ApolloClient, InMemoryCache, gql } from '@apollo/client' const client = new ApolloClient({ uri: 'https://api.buffer.com', headers: { Authorization: `Bearer ${process.env.BUFFER_API_KEY}` }, cache: new InMemoryCache(), }) const GET_CHANNELS = gql` query GetChannels { channels(input: { organizationId: "your_org_id" }) { id name service } } ` // First call goes to the API. Later calls are served from the cache. const { data } = await client.query({ query: GET_CHANNELS }) // Ask for fresh data only when it matters, such as after someone // connects or disconnects a channel. await client.query({ query: GET_CHANNELS, fetchPolicy: 'network-only' }) ``` The pattern holds whichever client you use: read from the cache by default, and go to the network deliberately rather than on every operation. #### Poll less, and poll narrowly There are no webhooks, so keeping data in sync means polling. Make each poll cheap and infrequent. Ask only for what changed. Keep a checkpoint of the last time you synced, and filter on `createdAt: { start: $checkpoint }` rather than walking the full history each run. Cursors are tied to the result set that produced them, so store the timestamp, not the cursor, between runs. #### Prompts for AI agents Buffer has an [MCP server](https://developers.buffer.com/guides/integrations/mcp.md), so the same API is available to Claude, Cursor, ChatGPT and other assistants. The quota does not change when you reach it that way: MCP connections and your personal API keys share one rate-limit bucket, so connecting another assistant does not give you more room. The server already steers the agent quite a bit on its own. Its recommended workflow starts at `get_account` for the organization, moves to `list_channels` for channels, and routes summaries and averages to `get_aggregated_post_metrics`. Its tool descriptions tell the agent to prefer the domain tools over raw GraphQL, and to take field and enum names from the schema rather than from memory. So the prompts below deliberately do not repeat any of that. They cover the habits the server does not describe, and they belong in your system prompt, your project instructions, or an `AGENTS.md` file, so they apply to every session rather than being repeated by hand. ##### Load the context once The server tells the agent to start with `get_account` and `list_channels`. It does not say to stop calling them, and an agent that re-lists organizations and channels every turn spends the session relearning what it already knew. ```text Call get_account and list_channels once at the start of this session. Keep the organization ID and channel list in context, and reuse them throughout the session. Do not fetch them again unless I tell you the channels have changed. ``` ##### Say what an ID looks like The server says where to get IDs. It does not say what a valid one looks like, and invented IDs are a large share of failed calls. ```text Buffer IDs are opaque 24-character hex strings. A post's ID on the social network is not its Buffer ID. Placeholders such as "default" or "me" do not exist. If you do not have an ID from get_account or list_channels, ask me for it rather than constructing one. ``` ##### Ask for one page at a time ```text When listing posts, filter by status and date range. Request at most 100 items per page. Fetch one page and show me the result. Only fetch another page when I ask for it. ``` ##### Combine unrelated reads Nothing in the server suggests this, and it is the technique that saves the most in a single session. ```text When you are already using execute_query and need several unrelated things at once, ask for them in a single document. Use multiple root fields for different queries. Use aliases when the same field repeats with different arguments. ``` ##### Fail once, not repeatedly Every request counts against your quota whether it succeeds or fails. Only a `429` is refunded, so a retry loop on a broken assumption is pure waste. ```text If a call fails, read the error message and fix the specific thing it names. Retry at most once. If it fails again, stop and tell me what went wrong instead of trying variations. If a call returns a 429, stop and report the Retry-After wait time. Do not retry in a loop. ``` Unattended code should behave differently here: back off and retry as described in [Rate Limits](https://developers.buffer.com/guides/api-limits.md). The instruction above is for an interactive agent, where surfacing the wait to a human beats sleeping through it. ##### One block to paste To set this up once rather than prompt by prompt: ```text When using the Buffer MCP server: 1. Call get_account and list_channels once per session. Reuse the organization ID and channel list; do not fetch them again. 2. Buffer IDs are opaque 24-character hex strings. A post's ID on a social network is not its Buffer ID. Placeholders such as "default" do not exist. Ask me for an ID rather than inventing one. 3. When listing posts, filter by status and date range. Request at most 100 items per page, and fetch one page at a time. 4. When you need several unrelated things from execute_query, ask for them in one document, using root fields or aliases. 5. If a call fails, fix what the error names and retry at most once. Then stop and tell me. On a 429, report the Retry-After wait. ``` #### Next steps - [Rate Limits](https://developers.buffer.com/guides/api-limits.md): the quotas, the headers, and how to back off - [MCP](https://developers.buffer.com/guides/integrations/mcp.md): connect any MCP-compatible AI tool to Buffer - [Pagination](https://developers.buffer.com/guides/pagination.md): how cursors work - [Content Items](https://developers.buffer.com/guides/content-items.md): create posts for many channels in one call - [Post Metrics](https://developers.buffer.com/guides/post-metrics.md): per-post metrics and aggregation - [Aggregate Post Metrics](https://developers.buffer.com/examples/aggregate-post-metrics.md): a working rollup query ### Buffer CLI Schedule posts, manage channels, and access account data directly from your terminal. Built for developers and AI agents. Every command is generated from Buffer’s public GraphQL schema, with structured JSON output and predictable error handling, making it easy to integrate into scripts, CI pipelines, and agent tooling. The CLI ships with markdown skill files for Claude Code and Codex, allowing AI agents to interact with Buffer without requiring custom prompts or manual setup. #### Install the CLI Install the CLI and verify the version. Requires Node.js 18 or later. ```bash npm install -g @bufferapp/cli buffer --version ``` #### Quick start ##### 1. Run interactive setup `buffer init` writes your API token, default organization, and timezone to the global config. It also offers to install a Buffer skill into Claude Code or Codex. ```bash buffer init ``` > Don't have an API key? See [Authentication](https://developers.buffer.com/guides/authentication.md) for how to generate one. ##### 2. Verify your environment `buffer doctor` checks your Node version, config, token, and network access. ```bash buffer doctor ``` ##### 3. Run your first command Fetch your account, then list your connected channels. ```bash buffer account buffer channels list ``` If you prefer not to persist a token to disk (CI, containers, ephemeral environments), skip `buffer init` and export `BUFFER_API_KEY` instead: ```bash export BUFFER_API_KEY=your-token-here ``` #### Use with AI agents The CLI ships markdown skill files (workflows, pitfalls, rate limits, idempotency) designed to be loaded into an AI agent's context window. ```bash # Print all topics as concatenated markdown buffer context # Or list available topics buffer context --list ``` Install the skill into your agent so it can call the CLI without a custom system prompt: ```bash buffer install claude # writes ~/.claude/skills/buffer/SKILL.md buffer install codex # appends a managed block to ~/.codex/AGENTS.md ``` Both targets are idempotent — re-running keeps the file current. Reverse with `buffer uninstall `. No API call, no auth required. #### Core commands Every command follows the same shape: `buffer [flags]`. You can run the help command to get the information about supported commands. ```bash buffer --help ``` ##### Account & channels ```bash buffer account buffer channels list buffer channels get --id ``` ##### Posts ```bash # List posts on a channel buffer posts list --channel-id # Get a single post buffer posts get --id # Create a post with flags buffer posts create \ --channel-id \ --scheduling-type automatic \ --mode addToQueue \ --text "Hello from the CLI" # Or pass the input as JSON buffer posts create --json '{ "channelId": "...", "schedulingType": "automatic", "mode": "addToQueue", "text": "Hello" }' ``` Input can also come from a file or stdin: ```bash buffer posts create --input post.json cat post.json | buffer posts create --input - ``` ##### Ideas ```bash buffer ideas create --organization-id --text "Idea body" buffer ideas create --json '{ "organizationId": "...", "content": { "text": "Idea body" } }' ``` ##### Dry run All mutations support `--dry-run`. The CLI validates input locally and prints the payload that _would_ be sent, without calling the API. ```bash buffer posts create --json '{"channelId": "abc"}' --dry-run ``` #### Field selection Each command ships with a curated default field set so responses stay small. Use `--fields` to override it with a comma-separated list of dot-notation paths — the CLI builds a minimal GraphQL request from those paths so the API only returns what you ask for. ```bash # Default subset (small, still useful) buffer posts get --id post_123 # Cherry-pick fields, including nested paths buffer posts get --id post_123 --fields id,text,channel.name # Connections expose items.* and pageInfo.* buffer posts list --fields items.id,items.text,pageInfo.endCursor # Brace expansion for sibling fields. Quote it so the shell # doesn't expand the braces before the CLI sees them. buffer posts list --fields 'items.{id,text,status},pageInfo.endCursor' # Opt back into the full response buffer posts get --id post_123 --fields all ``` #### Schema introspection The CLI is generated from the GraphQL schema, so you can discover commands and validate payloads at runtime. ```bash buffer schema list buffer schema describe posts create ``` `buffer schema describe` returns a full method signature as JSON — input types, output shape, required fields, enum values. #### Global flags | Flag | Purpose | | --- | --- | | `--output ` | Rendering. `auto` pretty-prints to a TTY, JSON otherwise. Use `json` for scripts and agents. | | `--quiet` | Suppress spinners, completion lines, update notices, and rate-limit warnings on stderr. | | `--verbose` | Print a one-line rate-limit summary to stderr after every request. | | `--no-color` | Disable ANSI colors in pretty output. | | `--timeout ` | Per-command timeout (default 30000ms). `0` disables. | | `--dry-run` | Validate locally and print the payload that would be sent. Mutations only. | #### Exit codes | Code | Meaning | | --- | --- | | `0` | Success | | `1` | General error | | `2` | Usage error (bad flags, missing fields, validation failure) | | `3` | API error | | `4` | Auth error (missing or invalid token) | | `130` | Interrupted by SIGINT (Ctrl-C) | | `143` | Terminated by SIGTERM | When stdout is closed by the consumer (`buffer ... | head`), the CLI exits `0` silently instead of printing an EPIPE stack trace. #### Configuration The CLI reads config from two files. Repo config overrides global config per key. | Scope | Path | Notes | | --- | --- | --- | | Global | `$XDG_CONFIG_HOME/buffer/config.json` (or `~/.config/buffer/config.json`) | User-level defaults. Only place `apiKey` can be stored. | | Repo | `.buffer/config.json` (walking up from the cwd) | Project-level overrides. `apiKey` is stripped on load. | ```bash # Inspect buffer config get --all buffer config path # Write (defaults to global; use --repo for repo-scoped) buffer config set outputFormat pretty buffer config set timeout 60000 # Remove (idempotent) buffer config unset outputFormat ``` Resolution order for any value: command flag → environment (`BUFFER_API_KEY` for `apiKey`) → repo config → global config → built-in default. #### Shell completion ```bash buffer completion bash --install buffer completion zsh --install buffer completion fish --install ``` `--install` appends a sourcing block to `~/.bashrc` or `~/.zshrc` (idempotent); for fish it writes to `~/.config/fish/completions/buffer.fish`. Restart your shell to pick it up. #### Troubleshooting Run `buffer doctor` to diagnose setup issues. It checks Node version, config validity, API token, default organization, network reachability, rate-limit headroom, and CLI version freshness. ```bash buffer doctor # only failing checks buffer doctor --verbose # include passing checks ``` ### Integrations Connect Buffer with your favorite tools and AI assistants. Browse our integrations below to automate your social media workflow. [Zapier](https://developers.buffer.com/guides/integrations/zapier.md) Automate workflows by connecting Buffer with thousands of apps. [Claude](https://developers.buffer.com/guides/integrations/claude.md) Manage your content from Claude and Claude Code [ChatGPT](https://developers.buffer.com/guides/integrations/chatgpt.md) Manage your content from ChatGPT and Codex [Notion](https://developers.buffer.com/guides/integrations/notion.md) Manage your content from Notion custom agents [Manus](https://developers.buffer.com/guides/integrations/manus.md) Manage your content from Manus [MCP](https://developers.buffer.com/guides/integrations/mcp.md) Give AI assistants direct access to Buffer via Model Context Protocol. [Grok](https://developers.buffer.com/guides/integrations/grok.md) Manage your content from Grok [Cursor](https://developers.buffer.com/guides/integrations/cursor.md) Manage Buffer content directly from your AI-powered code editor. [Antigravity](https://developers.buffer.com/guides/integrations/antigravity.md) Manage your content from Google's agent-first IDE [n8n](https://developers.buffer.com/guides/integrations/n8n.md) Build visual automation workflows with Buffer and hundreds of services. [Raycast](https://developers.buffer.com/guides/integrations/raycast.md) Schedule posts and manage your queue with a quick keyboard shortcut. [Perplexity](https://developers.buffer.com/guides/integrations/perplexity.md) Manage your content from Perplexity Web and Desktop ### Zapier #### Zapier Automate workflows with Zapier Zapier connects Buffer with thousands of apps so you can automate your workflow without writing code. For example, you could auto-post to Discord when you publish, send engagement stats to a spreadsheet, or turn Trello card updates into scheduled Buffer drafts. Connect Buffer to Zapier using OAuth. No API key needed. #### Setup ##### 1. Navigate to Zapier Connections Click "Add connection" to connect Buffer. [Zapier Connections](https://actions.zapier.com/credentials/) ##### 2. Search for MCP Client Search for "MCP Client by Zapier" and select it, then click "Add connection". ##### 3. Configure the connection Fill in the form with the following settings: - Server URL: `https://mcp.buffer.com/mcp` - Transport: Streamable HTTP - OAuth: Yes ##### 4. Connect and approve access Click "Yes, Continue to MCP Client by Zapier" to finish. **Note:** A Buffer sign-in window may open if you're not already logged in. Approve access to complete the connection. #### Try It Out Copy any of these example prompts to get started with Zapier: When a new blog post is published in WordPress, create a draft post in Buffer with the post title and link When a new row is added to my Google Sheet, add a post to my Buffer queue with that content Every Monday at 8am, check my Buffer queue and notify me in Slack if any channel has no posts scheduled ### Claude #### Claude Manage your content from Claude and Claude Code Claude lets you manage your Buffer content using natural language. Connect Buffer to Claude on the web, desktop, or Claude Code using OAuth. No API key needed. ##### Claude Web/Desktop ###### 1. Go to Claude Open [claude.ai](https://claude.ai/) in your browser or desktop app. ###### 2. Open Connectors Navigate to "Customize", then click "Connectors". ###### 3. Search for Buffer Search for "Buffer" in the connector list, then select it. ###### 4. Connect and approve access Click "Connect". Sign in to Buffer if prompted and approve access to complete the connection. ##### Claude Code ###### 1. Install Claude Code Follow Anthropic's quickstart guide for Claude Code. [Install Claude Code](https://code.claude.com/docs/en/quickstart) ###### 2. Add the Buffer MCP server Run the following command to add the Buffer MCP server to Claude Code: ``` claude mcp add --transport http buffer https://mcp.buffer.com/mcp ``` ###### 3. Authenticate with Buffer Start a Claude Code session, run the following command, select the Buffer MCP server, then sign in to Buffer if prompted and approve access. ``` /mcp ``` #### Try It Out Copy any of these example prompts to get started with Claude: Show me all my scheduled Buffer posts for this week Create a draft post in Buffer that says 'We just launched our redesigned dashboard!' for my X channel List my Buffer channels and show me which ones have posts scheduled for tomorrow ### MCP #### MCP Connect any tool to the Buffer MCP server The Model Context Protocol (MCP) is an open standard that lets an AI assistant work with an outside service through a set of tools it can call. Buffer runs a remote MCP server at `https://mcp.buffer.com/mcp`, which enables AI assistants to read your channels, browse your queue and drafts, schedule and edit posts, capture ideas and pull post analytics, all without leaving the conversation. Any MCP-compatible AI tool can connect. If yours doesn't have a Buffer integration guide of its own, follow the setup instructions below. #### Setup ##### 1. Get Your API Key You need an API key to integrate Buffer with MCP. API Key [Get API Key →](https://publish.buffer.com/settings/api) Your key is shared with the API Explorer and will prefill the configuration steps below. ##### 2. Configure Your MCP Client LLM clients that support MCP and headers can connect to Buffer by adding an HTTP MCP server with the following settings: - Server URL: `https://mcp.buffer.com/mcp` - Authorization Header: `Authorization: Bearer YOUR_API_KEY` #### Supported tools The Buffer MCP server exposes a large list of tools. Most of them cover a specific job, like listing channels or scheduling a post. Two generic tools let an assistant reach the rest of the GraphQL API when no specific tool fits. A few conventions apply across the tools: - Every ID is a 24-character hex string. - Every date and time is an ISO 8601 string with a UTC offset, such as `2026-08-11T17:00:00-05:00`. `get_account` returns the account timezone and its current local time, which is what an assistant should use to turn "tomorrow at 5pm" into a real timestamp. - Tools that return lists page with `first` and `after`, returning 20 items by default and up to 100 per page. - `create_post`, `edit_post`, `delete_post`, `create_idea`, the template write tools and `execute_mutation` change your data. Most MCP clients ask you to approve those before they run. ##### Account tools ###### `get_account` Returns the signed-in account and the organizations it belongs to: email, name, timezone, the current time in that timezone, and for each organization its ID, name, plan limits and member count. If you belong to more than one organization, an assistant will name them and ask which one you mean before going further. Takes no parameters. **Example prompt:** "Which Buffer organizations do I have access to?" ##### Channel tools ###### `list_channels` Lists the social accounts connected to an organization, with each channel's ID, name, display name, service, type, avatar and connection status. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `organizationId` | string | Yes | The organization to list channels for | **Example prompt:** "List all my connected Buffer channels." ###### `get_channel` Returns the detail that `list_channels` leaves out for a single channel: its posting schedule, posting goals, queue status, timezone, link shortening settings, and service-specific data such as Pinterest boards, a Mastodon server URL or Instagram reminder settings. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `channelId` | string | Yes | The channel to read | **Example prompt:** "What times does my LinkedIn channel post at?" ##### Post tools ###### `list_posts` Lists posts in an organization. Returns each post's ID, status, text, scheduled and sent times, channel, tags, assets and any publishing error. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `organizationId` | string | Yes | The organization to list posts for | | `channelIds` | string[] | No | Only return posts on these channels | | `status` | string[] | No | Any of `draft`, `needs_approval`, `scheduled`, `sending`, `sent`, `error` | | `tagIds` | string[] | No | Only return posts carrying any of these tags | | `dueAt` | object | No | `start` and `end` bounds on the scheduled time | | `createdAt` | object | No | `start` and `end` bounds on the creation time | | `sort` | object[] | No | Sort by `dueAt` or `createdAt`, `asc` or `desc` | | `first` | integer | No | Page size, 20 by default and 100 at most | | `after` | string | No | Pagination cursor | | `includeMetrics` | boolean | No | Also return per-post analytics. Off by default, since it makes the response much larger | **Example prompt:** "Show me all my draft posts in Buffer so I can review what's pending." ###### `get_post` Reads one post in full: status, content, author, channel, tags, notes, assets and the actions you are allowed to take on it. The `metadata` field carries service-specific data such as Instagram geolocation, a Twitter thread or YouTube privacy settings. A post with the `error` status carries the failure message. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `postId` | string | Yes | The post to read | | `includeMetrics` | boolean | No | Also return this post's analytics and the time they were last refreshed | **Example prompt:** "Why did my Instagram post from yesterday fail?" ###### `create_post` Schedules or publishes a post on one channel. Works for Instagram, Facebook, Twitter, LinkedIn, Pinterest, YouTube, Google Business, Mastodon, TikTok, Threads, Bluesky and Start Page. What each service needs as a minimum: - Twitter, Mastodon, Threads and Bluesky: text only. - Instagram and TikTok: an image or a video. - Pinterest: an image, plus the board to pin to in `metadata.pinterest`. - YouTube: a video, plus a title and category in `metadata.youtube`. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `channelId` | string | Yes | The channel to post to, taken from `list_channels` | | `schedulingType` | string | Yes | `automatic` to publish for you, or `notification` to send you a reminder to post manually | | `mode` | string | No | `addToQueue` (the default), `shareNow`, `shareNext`, or `customScheduled`, which needs `dueAt` | | `text` | string | No | The body of the post | | `dueAt` | string | No | When to publish, required for `customScheduled` and always in the future | | `tagIds` | string[] | No | Tags to file the post under | | `assets` | object[] | No | Media to attach, each one an image, a video, or a document on LinkedIn | | `metadata` | object | No | Service-specific settings, keyed by service | | `saveToDraft` | boolean | No | Save as a draft instead of scheduling it | | `ideaId` | string | No | The idea this post is being created from | | `draftId` | string | No | The draft this post is being created from | **Example prompt:** "Add a post to my Buffer queue that says 'Excited to share our latest update!' for next Monday." ###### `edit_post` Changes an existing post. Every edit is validated as a whole post, the same way a new one is, rather than being merged into the stored version. So an assistant should read the post with `get_post` first and carry the current assets and metadata forward, changing only what you asked for. Dropping a field the post needs will make the edit fail. Leaving `schedulingType` out keeps the post publishing the way it already does, and leaving `mode` and `dueAt` out keeps it in its current slot. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `postId` | string | Yes | The post to change | | `schedulingType` | string | No | `automatic` or `notification` | | `mode` | string | No | `addToQueue`, `shareNow`, `shareNext`, or `customScheduled` | | `text` | string | No | The body of the post | | `dueAt` | string | No | A new publishing time, only valid with `customScheduled` | | `tagIds` | string[] | No | Tags to file the post under | | `assets` | object[] | No | Media to attach. Leave it out to keep the stored media, send an empty array to clear it | | `metadata` | object | No | Service-specific settings, keyed by service | | `saveToDraft` | boolean | No | Move the post back to a draft | | `ideaId` | string | No | The idea this post is being created from | | `draftId` | string | No | The draft this post is being created from | **Example prompt:** "Move my Thursday LinkedIn post to Friday at 9am and shorten the opening line." ###### `delete_post` Deletes a post for good. Not every post can be deleted, so an assistant should check that `deletePost` appears in the post's `allowedActions` first. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `postId` | string | Yes | The post to delete | **Example prompt:** "Delete the draft I made this morning about the pricing update." ###### `get_aggregated_post_metrics` Adds up analytics across many posts over a date range and returns the totals and averages, rather than making an assistant pull every post and do the arithmetic itself. The response always carries post count, reactions and comments. Other metrics, such as reach, impressions and engagement rate, appear only when every channel in the filter supports them. Metrics refresh once a day, so values can lag the social network by up to a day. The date range can cover at most 365 days. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `organizationId` | string | Yes | The organization to aggregate metrics for | | `startDateTime` | string | Yes | Start of the window, inclusive | | `endDateTime` | string | Yes | End of the window, inclusive | | `channelIds` | string[] | No | Channels to include. Leave it out to cover every channel you can see insights for | | `tags` | object | No | `in` limits the aggregate to posts carrying any of these tags, `isEmpty` also counts untagged posts | **Example prompt:** "How did my LinkedIn posts perform last month?" ##### Idea tools ###### `list_ideas` Lists the ideas in an organization. Ideas capture a concept for a future post before it belongs to a channel. Each one returns its title, text, media, tags, target services, target date, board column and timestamps. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `organizationId` | string | Yes | The organization to list ideas for | | `tagIds` | string[] | No | Only return ideas carrying any of these tags | | `includeUntagged` | boolean | No | Also return ideas with no tags. Off by default | | `first` | integer | No | Page size, 20 by default and 100 at most | | `after` | string | No | Pagination cursor | **Example prompt:** "What ideas do I have saved that are tagged for the product launch?" ###### `list_idea_groups` Lists the columns on the ideas board, with each column's ID, name and lock status. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `organizationId` | string | Yes | The organization to list idea groups for | **Example prompt:** "What columns does my Buffer ideas board have?" ###### `create_idea` Saves a new idea. This captures content for later. It does not schedule or publish anything. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `organizationId` | string | Yes | The organization to create the idea in | | `content` | object | Yes | The idea itself: `title`, `text`, `media`, `tags`, target `services` and a target `date`, all optional | **Example prompt:** "Save an idea for a post about our new integration, tagged for Instagram and LinkedIn." ##### Post template tools Post templates are reusable starting points for content. A template body can hold `{{placeholders}}` to fill in later. Each template is public, internal or private: public templates are curated by Buffer, internal ones are shared with everyone in your organization, and private ones are yours alone. ###### `list_post_templates` Lists the templates you can use in an organization. With no filter this returns all three kinds together: the public templates, your organization's internal ones, and your own private ones. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `organizationId` | string | Yes | The organization whose templates to list | | `visibility` | string | No | Narrow to `private`, `internal` or `public` | | `first` | integer | No | Page size, 20 by default and 100 at most | | `after` | string | No | Pagination cursor | **Example prompt:** "What post templates can I use?" ###### `get_post_template` Reads one template: its body, title, description, emoji, visibility and timestamps. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `postTemplateId` | string | Yes | The template to read | **Example prompt:** "Show me the full text of my product announcement template." ###### `create_post_template` Creates a template in an organization. You can set it to private or internal. Public is curated by Buffer and cannot be set here. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `organizationId` | string | Yes | The organization the template belongs to | | `title` | string | Yes | The name of the template | | `body` | string | Yes | The content, which may contain `{{placeholders}}` | | `description` | string | Yes | A short description of what the template is for | | `emoji` | string | No | An emoji to show alongside the template | | `visibility` | string | No | `private`, the default, or `internal` | **Example prompt:** "Turn this post into a template my whole team can reuse." ###### `update_post_template` Changes a template. Only the fields you send are changed. You can edit templates you own, and internal templates in your organization if you are an admin or owner. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `postTemplateId` | string | Yes | The template to change | | `title` | string | No | The name of the template | | `body` | string | No | The content, which may contain `{{placeholders}}` | | `description` | string | No | A short description of what the template is for | | `emoji` | string | No | An emoji to show alongside the template | | `visibility` | string | No | `private` or `internal` | **Example prompt:** "Add a call to action line to my weekly recap template." ###### `delete_post_template` Deletes a template for good. You can delete templates you own, and internal templates in your organization if you are an admin or owner. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `postTemplateId` | string | Yes | The template to delete | **Example prompt:** "Delete the old holiday sale template." ##### Advanced tools These three cover anything the tools above do not. An assistant should read the schema first and then run an operation from it, rather than guessing at field names. ###### `introspect_schema` Returns the complete GraphQL schema for the Buffer API: every query, mutation, type and argument. This is only worth calling before `execute_query` or `execute_mutation`, since the tools above already cover the common jobs. Takes no parameters. ###### `execute_query` Runs a read-only GraphQL query against the Buffer API. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `summary` | string | Yes | A short, plain description of what the operation does, shown to you in the approval prompt | | `query` | string | Yes | The GraphQL query, using names from the schema | | `variables` | object | No | Variables for the query | | `operationName` | string | No | Which query to run, if the document defines several | ###### `execute_mutation` Runs a GraphQL mutation against the Buffer API. This changes your data, so most clients will ask you to approve it. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `summary` | string | Yes | A short, plain description of what the operation does, shown to you in the approval prompt | | `mutation` | string | Yes | The GraphQL mutation, using names from the schema | | `variables` | object | No | Variables for the mutation | | `operationName` | string | No | Which mutation to run, if the document defines several | #### Beyond the tools The GraphQL schema is available as a resource at `buffer://schema`, which is the same content `introspect_schema` returns. Clients that support resources can attach it to a conversation instead of spending a tool call on it. There is also one prompt, `review_weekly_posts`, which reviews the posts you have scheduled for the current week and groups them by channel. Clients that support prompts usually list it as a command you can pick. #### Rate limits Requests made through the MCP server count against the same [rate limits](https://developers.buffer.com/guides/api-limits.md) as any other client: a rolling 15-minute, 24-hour and 30-day window, with quotas that depend on your plan. The limits are per client and shared across every request rather than counted per tool, so one conversation that lists your posts, reads a few of them and then edits one has spent several requests from the same budget. When a window drops below a tenth of its quota, a successful tool response carries a note naming the window that is running low and when it resets. Treat it as a signal to slow down: ask for less in one go, and wait for the reset it names before carrying on. Past the limit the call fails with `429 Too Many Requests`, and the error the assistant sees carries the `Retry-After` value from the API, in seconds. That is how long to wait before asking for anything else. Most of what keeps an assistant inside these limits is how you prompt it. Check out the [prompts for AI agents](https://developers.buffer.com/guides/efficient-api-usage.md#prompts-for-ai-agents) section of the Efficient API Usage guide for best practices and example prompts. #### Security considerations Connecting a client to the MCP server gives it the same reach over your content as you have, so it is worth knowing what that covers before you connect one. - **The connection covers your whole account.** An API key acts for your account and reaches every organization and channel in it. There is no per-organization scoping, so you cannot open one workspace to an assistant while holding another back. See [key permissions and scope](https://developers.buffer.com/guides/authentication.md#key-permissions-and-scope) for what a key can do. - **Treat the key like a password.** MCP clients keep it in a config file, often in plain text, and some sync that file between machines. Keep it out of anything you share or commit, and generate a fresh key in [Settings → API](https://publish.buffer.com/settings/api) if you think it has leaked. - **Check the server URL.** The only Buffer MCP endpoint is `https://mcp.buffer.com/mcp`. A one-click install from a third-party list is worth opening and reading before you trust it. - **Leave the approval prompts on.** `create_post` can publish immediately, and `delete_post` cannot be undone. Most clients ask before they run a tool that writes, and that prompt is your last chance to catch a misread instruction. #### Try It Out Copy any of these example prompts to get started with MCP: List all my connected Buffer channels Add a post to my Buffer queue that says 'Excited to share our latest update!' for next Monday Show me all my draft posts in Buffer so I can review what's pending ### Cursor #### Cursor Manage Buffer from your code editor Use the Buffer MCP server within Cursor to manage social media content directly from your AI-powered code editor. Schedule posts, check your queue, and draft content - all without leaving your IDE. Connect Buffer to Cursor using OAuth. No API key needed. #### Setup ##### 1. Open Cursor settings Go to Preferences → Cursor Settings → Tools & MCPs. ##### 2. Add the Buffer MCP server Click "New MCP Server" and add the following configuration: ``` { "mcpServers": { "buffer": { "url": "https://mcp.buffer.com/mcp" } } } ``` ##### 3. Connect and approve access Click "Connect". **Note:** A Buffer sign-in window may open if you're not already logged in. Approve access to complete the connection. #### Try It Out Copy any of these example prompts to get started with Cursor: List all my connected Buffer channels Draft a Buffer post announcing the new feature I just committed and schedule it for tomorrow morning Show me all my draft posts in Buffer so I can review what's pending ### n8n #### n8n Workflow automation with n8n n8n is a workflow automation tool that connects Buffer with hundreds of apps. Create complex automation workflows, trigger posts based on events, and integrate Buffer into your existing automation pipelines. #### Setup ##### 1. Get Your API Key You need an API key to integrate Buffer with n8n. API Key [Get API Key →](https://publish.buffer.com/settings/api) Your key is shared with the API Explorer and will prefill the configuration steps below. ##### 2. Add MCP Client to Workflow Inside of a workflow, add an "MCP Client" node. ##### 3. Configure the MCP Client Fill in the form with the following details: - Server Transport: HTTP Streamable - MCP Endpoint URL: `https://mcp.buffer.com/mcp` - Authentication: Bearer Auth ##### 4. Add Your Credentials Click on "Credential for Bearer Auth" and configure: - Select "Create new credential" - Add your API key: `YOUR_API_KEY` - Click "Save" - Close the modal ##### 5. Select Buffer MCP Tools Select the MCP tool you want the workflow to use from the available Buffer MCP tools, then configure it as needed. #### Try It Out Copy any of these example prompts to get started with n8n: When a new RSS feed item appears, automatically create a draft post in Buffer with the article title and link Every Friday, pull my scheduled posts in Buffer for the next week and send a summary to Slack When a form submission comes in, create a draft post in Buffer using the submitted content ### Raycast #### Raycast Quick actions from your menu bar Raycast lets you quickly access Buffer from your menu bar on macOS. Create posts, view your queue, and manage your social media with keyboard shortcuts and quick actions. Connect Buffer to Raycast using OAuth. No API key needed. #### Setup ##### 1. Open Install MCP Server In Raycast, search for "Install MCP Server" and press Enter. **Note:** Requires Raycast Pro, since MCP servers run through Raycast AI. ##### 2. Configure the Server Fill in the form with the following details: - Name: Buffer - Transport: HTTP - URL: `https://mcp.buffer.com/mcp` ##### 3. Install and approve access Click on "Install" (or press Cmd+Enter). **Note:** A Buffer sign-in window may open if you're not already logged in. Approve access to complete the connection. #### Try It Out Copy any of these example prompts to get started with Raycast: Add a post saying 'Just shipped a new feature! Stay tuned for details.' to my Buffer queue Show me my upcoming scheduled Buffer posts for this week Create a draft post in Buffer for each of my connected channels with the text 'Happy Monday! What are you working on this week?' ### ChatGPT #### ChatGPT Manage your content from ChatGPT and Codex ChatGPT lets you manage your Buffer content using natural language. Connect Buffer to ChatGPT on the web or the Codex CLI using OAuth. No API key needed. ##### ChatGPT Web ###### 1. Go to ChatGPT Open [chatgpt.com](https://chatgpt.com/) in your browser. ###### 2. Enable Developer mode Open Settings, find the Apps/Connectors section, then toggle on Developer mode under Advanced settings. ###### 3. Add a new app Add a new app and fill in the form with: - Name: Buffer - MCP Server URL: `https://mcp.buffer.com/mcp` - Authentication: OAuth ###### 4. Create and approve access Click "Create" to finish. **Note:** A Buffer sign-in window may open if you're not already logged in. Approve access to complete the connection. ##### Codex CLI ###### 1. Install Codex CLI Follow OpenAI's install guide for Codex CLI. [Install Codex CLI](https://developers.openai.com/codex/cli) ###### 2. Add the Buffer MCP server In your terminal, run the following command to add the Buffer MCP server to Codex: ``` codex mcp add buffer --url https://mcp.buffer.com/mcp ``` **Note:** A browser window opens. Sign in to Buffer if prompted and approve access. #### Try It Out Copy any of these example prompts to get started with ChatGPT: Show me all my scheduled Buffer posts for this week Create a draft post in Buffer that says 'We just launched our redesigned dashboard!' for my X channel List my Buffer channels and show me which ones have posts scheduled for tomorrow ### Notion #### Notion Manage your content from Notion custom agents Notion lets you manage your Buffer content using natural language. Connect Buffer to a Notion custom agent using OAuth. No API key needed. #### Setup ##### 1. Enable custom MCP servers In Notion, go to Settings, then Connections, and open the Manage tab. Enable "Custom MCP servers". ##### 2. Open a custom agent Open or create the custom agent you want to connect, then click "Settings". ##### 3. Add the Buffer MCP server Click "Add connection", then "Add custom MCP", and fill in the form with the following details: - Server URL: `https://mcp.buffer.com/mcp` - Name: Buffer MCP - Auth method: OAuth ##### 4. Connect and approve access Click "Connect", then sign in to Buffer if prompted and approve access to complete the connection. #### Try It Out Copy any of these example prompts to get started with Notion: Show me all my scheduled Buffer posts for this week Create a draft post in Buffer that says 'We just launched our redesigned dashboard!' for my X channel List my Buffer channels and show me which ones have posts scheduled for tomorrow ### Perplexity #### Perplexity Manage your content from Perplexity Web and Desktop Perplexity lets you manage your Buffer content using natural language. Connect Buffer to Perplexity on the web or desktop using OAuth. No API key needed. ##### Perplexity Web ###### 1. Open Perplexity Open [perplexity.ai](https://www.perplexity.ai/) in your browser. ###### 2. Open Connectors Go to "All settings", then "Connectors", and add a "Custom connector". ###### 3. Add the custom connector Fill in the form, check the "I understand custom connectors can introduce..." consent checkbox, and click "Add": - Name: Buffer - MCP Server URL: `https://mcp.buffer.com/mcp` ###### 4. Add the connector Under the "Custom" connectors list, find Buffer and click "Add connector". ###### 5. Sign in and approve access Sign in to Buffer if prompted and approve access to complete the connection. ##### Perplexity Desktop ###### 1. Open Connectors in Perplexity Desktop Open the desktop app, go to "Settings", then "Connectors", and click "Add connector". ###### 2. Add the server config Open the "Advanced" tab, then set the Server Name and paste the configuration below, then click "Save": - Server Name: Buffer ``` { "command": "npx", "args": [ "-y", "mcp-remote", "https://mcp.buffer.com/mcp" ] } ``` **Note:** Requires Node.js. Perplexity needs to find npx on your system. If the connector won't start, point the config at the full path of your Node install. ###### 3. Sign in and approve access Perplexity Desktop will open the OAuth consent screen. Sign in to Buffer if prompted and approve access. #### Try It Out Copy any of these example prompts to get started with Perplexity: List all my connected Buffer channels Add a post to my Buffer queue that says 'Excited to share our latest update!' for next Monday Show me all my draft posts in Buffer so I can review what's pending ### Antigravity #### Antigravity Manage Buffer from Google's agent-first IDE Antigravity lets you manage Buffer directly from Google's agent-first IDE. Draft posts, check your queue, and manage your social media workflow without leaving your IDE. Connect Buffer to Antigravity using OAuth. No API key needed. #### Setup ##### 1. Add the Buffer MCP server to your config Open `~/.gemini/config/mcp_config.json` and add the following configuration: ``` { "mcpServers": { "buffer": { "serverUrl": "https://mcp.buffer.com/mcp" } } } ``` ##### 2. Restart Antigravity Quit Antigravity and open it again so it picks up the new server. ##### 3. Authenticate Buffer Go to Settings → Customizations, find Buffer in the MCP servers list, and click it to authenticate. ##### 4. Connect and approve access Copy the code Antigravity shows, paste it back into Antigravity, then refresh the Installed MCP Servers list to finish. **Note:** A Buffer sign-in window may open if you're not already logged in. Approve access to complete the connection. #### Try It Out Copy any of these example prompts to get started with Antigravity: List all my connected Buffer channels Draft a Buffer post announcing the feature I just shipped and schedule it for tomorrow morning Show me my Buffer posts scheduled for this week, grouped by channel ### Manus #### Manus Manage Buffer from Manus Manus lets you manage your Buffer content using natural language. Ask Manus to draft a post, schedule it, and manage your social media without leaving the chat. Connect Buffer to Manus using OAuth. No API key needed. #### Setup ##### 1. Open Connectors In Manus, go to Settings, then Connectors, click "Create", and select "Custom MCP". ##### 2. Add the Buffer MCP server Fill in the form with the following details, then click "Save": - Server Name: Buffer MCP Server - Transport: HTTP - Server URL: `https://mcp.buffer.com/mcp` ##### 3. Sign in and approve access The first time you use the Buffer MCP server, Manus asks you to log in. **Note:** A Buffer sign-in window may open if you're not already logged in. Approve access to complete the connection. #### Try It Out Copy any of these example prompts to get started with Manus: List all my connected Buffer channels Add a post to my Buffer queue that says 'Excited to share our latest update!' for next Monday Show me my Buffer posts scheduled for this week, grouped by channel ### Grok #### Grok Manage Buffer from Grok Grok lets you manage your Buffer content using natural language. Ask Grok to draft a post, schedule it, and manage your social media without leaving the chat. Connect Buffer to Grok using OAuth. No API key needed. #### Setup ##### 1. Go to Grok Open [grok.com](https://grok.com/) in your browser. ##### 2. Open Plugins In Grok, open "Plugins", click "New Connector", then select "Custom". ##### 3. Add the Buffer MCP server Fill in the form with the following details, then click "Add Connector": - Name: Buffer - Server URL: `https://mcp.buffer.com/mcp` ##### 4. Connect and approve access Click "Connect", then sign in to Buffer if prompted and approve access to complete the connection. #### Try It Out Copy any of these example prompts to get started with Grok: List all my connected Buffer channels Add a post to my Buffer queue that says 'Excited to share our latest update!' for next Monday Show me my Buffer posts scheduled for this week, grouped by channel ## Examples ### Aggregate Post Metrics Aggregate normalized post metrics across a window of sent posts, without paginating through individual posts. Available for personal workflows and automations only, using a personal API key. The returned `metrics` array always includes a baseline trio — `postCount`, `reactions`, and `comments`. Beyond those, additional metric types are included only when every channel in the filter set supports them. ```graphql query AggregatePostMetrics { aggregatedPostMetrics( input: { organizationId: "some_organization_id" startDateTime: "2026-01-01T00:00:00Z" endDateTime: "2026-03-31T23:59:59Z" channelIds: ["some_channel_id"] } ) { metrics { type value unit } metricsUpdatedAt } } ``` ### Create Draft Post Draft posts can be created using the createPost mutation with the `saveToDraft` argument set to `true`. When saving a post as a draft, there are several required arguments: - The channel ID that the post is being created for - The scheduling type to be used for the post (automatic or notification) - The sharing mode to be used for the post (add the post to the queue, share it now or share it next) - The content to be used when creating the Post - The `saveToDraft` flag set to `true` to save the post as a draft instead of scheduling it When a post is saved as a draft, the post status will be set to 'draft' instead of 'scheduled' and the post will not be published until explicitly scheduled. When performing the mutation, the PostActionSuccess type can be used to retrieve the information for the Post that was created. Similarly, the MutationError will provide you with information on the error that was triggered when trying to create the post. ```graphql mutation CreateDraftPost { createPost(input: { text: "Hello there, this is a draft post!", channelId: "some_channel_id", schedulingType: automatic, mode: addToQueue, saveToDraft: true }) { ... on PostActionSuccess { post { id text } } ... on MutationError { message } } } ``` ### Create Idea Create an idea post for a specified Organization, using the provided content. ```graphql mutation CreateIdea { createIdea(input: { organizationId: "some_organization_id", content: { title: "New Idea from GraphQL API" text: "This is the text of the new idea created via the GraphQL API." } }) { ... on Idea { id content { title text } } } } ``` ### Create Image Post Creating a post with an image works in the same way as creating a text post, with the addition of the `assets` argument. `assets` is an ordered list where each entry specifies exactly one of `image`, `video`, or `document` - for an image post, pass an `image` entry with the URL you want to attach. > The `url` must point to a publicly accessible file. See [Hosting Media](https://developers.buffer.com/guides/hosting-media.md) for suggested hosts and how to verify a URL works. ```graphql mutation CreatePost { createPost( input: { text: "Hello there, this is another one!" channelId: "some_channel_id" schedulingType: automatic mode: addToQueue assets: [ { image: { url: "https://images.unsplash.com/photo-1742850541164-8eb59ecb3282?q=80&w=3388&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" } } ] } ) { ... on PostActionSuccess { post { id text assets { id mimeType } } } ... on MutationError { message } } } ``` ### Create Instagram Post With User Tags User tags let you tag Instagram accounts at a specific point on an image. They are attached per image, on the asset's `metadata.userTags` field - not on the Instagram post metadata - so the same approach works for Instagram posts and stories. Each tag is a `UserTagInput` with a `handle` and an `x`/`y` position. The coordinates are **normalized decimal floats between `0.0` and `1.0`**, representing the percentage distance from the **left edge** (`x`) and the **top edge** (`y`) of the image. So `{ x: 0.5, y: 0.5 }` is the center of the image, and `{ x: 0.0, y: 0.0 }` is the top-left corner. > **Common pitfall:** pass `x` and `y` as numbers, not strings, and as normalized `0.0`-`1.0` values - not raw pixel coordinates. Sending pixel values (e.g. `x: 540` on a 1080px-wide image) or strings causes Instagram's Graph API to reject the post with an error like `Failed to create media container for instagram: (#100) Param user_tags[0]['x'] must be a number less than or equal to 1`. To convert a pixel position, divide by the image's dimension: `x = pixelX / imageWidth`, `y = pixelY / imageHeight`. Note that `metadata.altText` is required whenever you provide image `metadata`, so it's included alongside `userTags` below. ```graphql mutation CreateInstagramPostWithUserTags { createPost( input: { text: "Your post caption goes here" channelId: "your_instagram_channel_id" schedulingType: automatic mode: addToQueue assets: [ { image: { url: "https://images.unsplash.com/photo-1742850541164-8eb59ecb3282?q=80&w=3388&auto=format&fit=crop" metadata: { altText: "Describe the image for accessibility" userTags: [ { handle: "first_account_handle", x: 0.5, y: 0.7 } { handle: "second_account_handle", x: 0.5, y: 0.95 } ] } } } ] metadata: { instagram: { type: post, shouldShareToFeed: true } } } ) { ... on PostActionSuccess { post { id text assets { id mimeType } } } ... on MutationError { message } } } ``` The `x`/`y` validation rules are enforced by Instagram, so the same constraints apply whether you tag one account or many. See the [UserTag](https://developers.buffer.com/types/UserTag.md) reference for the field definitions, and [Create an Image Post](https://developers.buffer.com/examples/create-image-post.md) for the basics of attaching images. ### Create Scheduled Post Scheduled posts can be created using the createPost mutation with the `customScheduled` mode and a `dueAt` timestamp. When creating a scheduled post, there are several required arguments: - The channel ID that the post is being created for - The scheduling type to be used for the post (automatic or notification) - The sharing mode set to `customScheduled` to schedule the post for a specific time - The `dueAt` timestamp for when the post should be published - The text to be used when creating the Post When performing the mutation, the PostActionSuccess type can be used to retrieve the information for the Post that was created. Similarly, the MutationError will provide you with information on the error that was triggered when trying to create the post. ```graphql mutation CreatePost { createPost(input: { text: "Hello there, this is another one!", channelId: "some_channel_id", schedulingType: automatic, mode: customScheduled, dueAt: "2026-03-26T10:28:47.545Z" }) { ... on PostActionSuccess { post { id text assets { id mimeType } } } ... on MutationError { message } } } ``` ### Create Text Post Text posts can be created using the createPost mutation. When creating a post, there are several required arguments: - The channel ID that the post is being created for - The scheduling type to be used for the post (automatic or notification) - The sharing mode to be used for the post (add the post to the queue, share it now or share it next) - The text to be used when creating the Post When performing the mutation, the PostActionSuccess type can be used to retrieve the information for the Post that was created. Similarly, the MutationError will provide you with information on the error that was triggered when trying to create the post. ```graphql mutation CreatePost { createPost(input: { text: "Hello there, this is another one!", channelId: "some_channel_id", schedulingType: automatic, mode: addToQueue }) { ... on PostActionSuccess { post { id text assets { id mimeType } } } ... on MutationError { message } } } ``` ### Create Threaded Post Threaded posts (for example a Twitter/X thread or a Bluesky, Threads, or Mastodon thread) are created with the `createPost` mutation by passing a `thread` array inside the service-specific `metadata`. Each entry in the array is one post in the thread, and they are published in order, each replying to the previous one. > **Important:** every post in the thread - including the first one - must be provided as an item in the `thread` array. The thread array is the source of truth for what gets published. The top-level `text` on `CreatePostInput` should be set to the same value as the **first** item in the `thread` array so the two stay in sync. For example, for a three-post thread you have to provide all three posts as `thread` entries, and the top-level `text` repeats the first entry's text. ```graphql mutation CreateThreadedPost { createPost( input: { text: "This is the first post in my thread." channelId: "some_channel_id" schedulingType: automatic mode: addToQueue metadata: { twitter: { thread: [ { text: "This is the first post in my thread." } { text: "Here's the second post, replying to the first." } { text: "And the third post wraps everything up." } ] } } } ) { ... on PostActionSuccess { post { id status } } ... on MutationError { message } } } ``` The same pattern applies to the other services that support threads - swap `twitter` for `bluesky`, `threads`, or `mastodon` in the `metadata` object. Each `ThreadedPostInput` also accepts an ordered `assets` list if you want to attach media to an individual post in the thread. ### Create Video Post Creating a post with a video works in the same way as creating a text post, with the addition of a `video` entry in the `assets` array. `assets` is an ordered list where each entry specifies exactly one of `image`, `video`, or `document` - pass a `video` entry with the URL you want to attach. To set the video thumbnail, choose which frame to use with `metadata.thumbnailOffset` - a millisecond offset into the video - on Instagram, TikTok, or Pinterest. > The asset `url` must point to a publicly accessible file. See [Hosting Media](https://developers.buffer.com/guides/hosting-media.md) for suggested hosts and how to verify a URL works. ```graphql mutation CreatePost { createPost( input: { text: "Hello there, this is another one!" channelId: "some_channel_id" schedulingType: automatic mode: addToQueue assets: [ { video: { url: "https://example.com/video.mp4" metadata: { thumbnailOffset: 2000 } } } ] } ) { ... on PostActionSuccess { post { id text assets { source } } } ... on MutationError { message } } } ``` ### Get Channel Fetch a single channel by its ID. ```graphql query GetChannel { channel(input: { id: "some_channel_id" }) { id name displayName service avatar isQueuePaused } } ``` ### Get Channels Fetch all channels for the provided Organization ID. ```graphql query GetChannels { channels(input: { organizationId: "some_organization_id" }) { id name displayName service avatar isQueuePaused } } ``` ### Get Filtered Channels Fetch all channels for the provided Organization ID. ```graphql query GetChannels { channels(input: { organizationId: "some_organization_id", filter:{ isLocked: false } }) { id name displayName service avatar isQueuePaused } } ``` ### Get Organizations Fetch all of the organizations that belong to the authenticated account. ```graphql query GetOrganizations { account { organizations { id name ownerEmail } } } ``` ### Get Paginated Posts Fetch a list of posts with support for pagination. ```graphql query GetPosts { posts( after: "id_to_start_after", first: 20, input: {organizationId: "some_organization_id", filter: {status: [sent], channelIds: ["some_channel_id"]}} ) { pageInfo { startCursor endCursor hasNextPage } edges { node { id text createdAt channelId } } } } ``` ### Get Post Metrics Fetch performance metrics for a single post. Available for personal workflows and automations only, using a personal API key. ```graphql query GetPostMetrics { post(input: { id: "some_post_id" }) { id text channelId metrics { type name value unit } metricsUpdatedAt } } ``` ### Get Posts For Channels Fetch a list of posts for a specific set of Channel IDs. ```graphql query GetPostsForChannels { posts( input: {organizationId: "some_organization_id", sort: [{ field: dueAt, direction: desc }, { field: createdAt, direction: desc }] , filter: {status: sent, channelIds: ["some_channel_id"]}} ) { edges { node { id text createdAt channelId } } } } ``` ### Get Posts With Assets Fetch a list of posts along with their associated assets (images, videos, etc.) for a specific set of Channel IDs. ```graphql query GetPostsWithAssets { posts( input: {organizationId: "some_organization_id", filter: {status: [sent], channelIds: ["some_channel_id"]}} ) { edges { node { id text createdAt channelId assets { thumbnail mimeType source ... on ImageAsset { image { altText width height } } } } } } } ``` ### Get Posts With Metrics Fetch a paginated list of sent posts together with their performance metrics. Available for personal workflows and automations only, using a personal API key. ```graphql query GetPostsWithMetrics { posts( first: 20 input: { organizationId: "some_organization_id" filter: { status: [sent], channelIds: ["some_channel_id"] } } ) { edges { node { id text dueAt channelId metrics { type name value unit } metricsUpdatedAt } } pageInfo { endCursor hasNextPage } } } ``` ### Get Quarterly Performance Report Roll up a quarter of publishing activity into a single aggregate. Useful for BI exports, board-deck stats, or year-on-year comparisons without paginating through every post. Available for personal workflows and automations only, using a personal API key. The aggregation window is capped to 365 days. ```graphql query QuarterlyPerformanceReport { aggregatedPostMetrics( input: { organizationId: "some_organization_id" startDateTime: "2026-01-01T00:00:00Z" endDateTime: "2026-03-31T23:59:59Z" channelIds: ["some_channel_id", "another_channel_id"] tags: { in: ["some_tag_id"] } } ) { metrics { type name value unit } metricsUpdatedAt } } ``` The result includes a synthetic `postCount` entry (number of matched posts in the window) alongside the metric aggregates. To compare quarters, run the query twice with different `startDateTime`/`endDateTime` windows and diff the values client-side. ### Get Scheduled Posts Fetch a list of posts that are scheduled for future publishing. ```graphql query GetScheduledPosts { posts( input: {organizationId: "some_organization_id", sort: [{ field: dueAt, direction: asc }, { field: createdAt, direction: desc }] filter: {status: [scheduled]}} ) { edges { node { id text createdAt } } } } ``` ## API Reference ### Queries #### account Retrieves the authenticated user's account information **Returns:** `Account!` #### aggregatedPostMetrics Aggregate normalized post metrics across a filtered post set. Useful for yearly summaries, channel-level rollups, and BI exports without paginating through thousands of posts. For per-post metrics, use `posts(input)` or `post(input)` with a `metrics { … }` selection — this query is purely for aggregation. The result always contains a baseline trio of entries: `postCount` (number of matched posts in the window), `reactions`, and `comments`. Posts on networks that don't track reactions or comments contribute 0 to those totals. Beyond the baseline, additional metric types are returned only when every channel in the filter set supports them. A single-network filter surfaces that network's richer metrics (e.g. impressions, reach, engagementRate on LinkedIn); a mixed-network filter trims the extras to those common to every network in the set. **Returns:** `AggregatedPostMetrics!` **Arguments:** - `input`: `AggregatedPostMetricsInput!` - Query's input: organization, date range, optional channel and tag filters. Date range is capped to 365 days. #### channel Fetches a single channel using the provided ID **Returns:** `Channel!` **Arguments:** - `input`: `ChannelInput!` - Query's input. #### channels Fetch all channels for the organization taking into account the current's user permissions **Returns:** `[Channel!]!` **Arguments:** - `input`: `ChannelsInput!` - Query's input. #### configuration Global, per-organization configuration: the connected `channels` the actor can view (with per-feature authorization) plus the service-level capability catalog (`services`). One round trip for every capability domain; clients select only what they need. **Status:** ⚠️ Experimental **Returns:** `Configuration!` **Arguments:** - `input`: `ConfigurationInput!` - Input for the configuration query. #### contentItem Fetch a single content item by id. Errors if no content item with that id exists. This API is an early preview and can change without a deprecation period. **Status:** ⚠️ Experimental **Returns:** `ContentItem!` **Arguments:** - `input`: `ContentItemInput!` - Input for fetching a single content item. #### contentItems Fetch an organization's content items in the requested order, newest first by default. Uses standard cursor pagination: pass the previous page's `pageInfo.endCursor` as `after` to fetch the next page. Requesting more than 100 items in a single page is rejected. This API is an early preview and can change without a deprecation period. **Status:** ⚠️ Experimental **Returns:** `ContentItemsConnection!` **Arguments:** - `after`: `String` - The cursor after which to return results. Cursors are opaque: treat them as a black box and reset to null when the input changes, since a cursor from a different result set produces undefined behavior. - `first`: `Int` - The number of content items to return, up to 100. - `input`: `ContentItemsInput!` - Input for listing content items. #### dailyPostingLimits Returns daily posting limit status for the given channels on the specified date. **Returns:** `[DailyPostingLimitStatus!]!` **Arguments:** - `input`: `DailyPostingLimitsInput!` - Query's input. #### ideaGroups Retrieves idea groups based on the provided input parameters. **Returns:** `[IdeaGroup!]!` **Arguments:** - `input`: `IdeaGroupsInput!` - Input for retrieving idea groups. #### ideas Fetch a paginated list of ideas with optional filtering **Returns:** `IdeasConnection!` **Arguments:** - `after`: `String` - Cursor for pagination, marks where to start fetching from - `first`: `Int` - Maximum number of items to return - `input`: `IdeasInput!` - Filtering criteria for the ideas list #### instagramAudio Refresh metadata and preview availability for one Instagram audio asset. Requires Facebook Login. Instagram Login channels return ChannelRefreshRequired. **Status:** ⚠️ Experimental **Returns:** `InstagramAudioPayload!` **Arguments:** - `input`: `InstagramAudioInput!` - Channel and audio asset used to refresh metadata. #### post Fetches a post by PostID for the given organization: first and last can be set for forward pagination using Relay convention **Returns:** `Post!` **Arguments:** - `input`: `PostInput!` - Query's input. #### posts Fetches posts for the given organization: first and last can be set for forward pagination using Relay convention **Returns:** `PostsResults!` **Arguments:** - `after`: `String` - The cursor of the post to start fetching from - `first`: `Int` - The number of posts to return - `input`: `PostsInput!` - Query's input. #### postTemplate Fetch a single post template by ID. Returns null if not found. **Status:** 🧪 Preview **Returns:** `PostTemplate` **Arguments:** - `input`: `PostTemplateInput!` - Input for fetching a single post template. #### postTemplates Fetch the templates visible to the current actor for the template library: public templates, plus internal templates from the supplied `organizationId`, plus private templates owned by the actor's account. The visibility scope is always pinned to the actor and the supplied organization — the input filter can only narrow within that scope, never widen it. **Status:** 🧪 Preview **Returns:** `PostTemplatesConnection!` **Arguments:** - `after`: `String` - The cursor after which to return results. - `first`: `Int` - The number of templates to return. - `input`: `PostTemplatesInput!` - Input containing the organization scope and optional filters. #### searchInstagramAudio Search Instagram audio for one channel. Requires Facebook Login. Instagram Login channels return ChannelRefreshRequired. **Status:** ⚠️ Experimental **Returns:** `SearchInstagramAudioPayload!` **Arguments:** - `input`: `SearchInstagramAudioInput!` - Channel, catalog type, and search text. #### trendingInstagramAudio Return Meta trending Instagram audio for one channel. Requires Facebook Login. Instagram Login channels return ChannelRefreshRequired. **Status:** ⚠️ Experimental **Returns:** `SearchInstagramAudioPayload!` **Arguments:** - `input`: `TrendingInstagramAudioInput!` - Channel and catalog type for Meta trending audio. ### Mutations #### createContentItem Create a content item together with all of its channel-specific post variants in a single operation. Validation is all-or-nothing: if any variant fails validation, no content item and no variants are created. Variants that fail while being processed after creation are reported per channel in the failure payload; the content item and its variants are still created in that case. This API is an early preview and can change without a deprecation period. **Status:** ⚠️ Experimental **Returns:** `CreateContentItemPayload!` **Arguments:** - `input`: `CreateContentItemInput!` - Input for createContentItem. #### createContentItemDraft Create a content item holding a channel-less draft, before any channels are selected. No network-specific validation applies to the draft content. The draft needs text or at least one asset. This API is an early preview and can change without a deprecation period. **Status:** ⚠️ Experimental **Returns:** `CreateContentItemDraftPayload!` **Arguments:** - `input`: `CreateContentItemDraftInput!` - Input for createContentItemDraft. #### createIdea Create a new idea with the given content and metadata **Returns:** `CreateIdeaPayload!` **Arguments:** - `input`: `CreateIdeaInput!` - Input to create an idea #### createPost Create post for channel **Returns:** `PostActionPayload!` **Arguments:** - `input`: `CreatePostInput!` - The mutation's input #### createPostTemplate Create a post template visible only to the caller (`private`) or to the caller's organization (`internal`). **Status:** 🧪 Preview **Returns:** `CreatePostTemplatePayload!` **Arguments:** - `input`: `CreatePostTemplateInput!` - Input for creating a post template. #### deleteContentItem Delete a content item: the item itself and any posts created from it. Deletion is all-or-nothing. Every post must be deletable on its own, or nothing is deleted and every blocked post is reported at once. A post can only be deleted while it is a draft, awaiting approval, scheduled, or failed, so an item cannot be deleted while any of its posts is publishing or already published. An item still holding a channel-less draft has no posts, so nothing blocks it. An error can also be reported when a post could not be fully processed after the deletion already took effect; re-fetch before retrying. This API is an early preview and can change without a deprecation period. **Status:** ⚠️ Experimental **Returns:** `DeleteContentItemPayload!` **Arguments:** - `input`: `DeleteContentItemInput!` - Input for deleteContentItem. #### deletePost Delete a post by id. **Returns:** `DeletePostPayload!` **Arguments:** - `input`: `DeletePostInput!` - Input for the deletePost mutation. #### deletePostTemplate Delete a post template owned by the caller (or an internal template in the caller's organization, if the caller is an org admin/owner). **Status:** 🧪 Preview **Returns:** `DeletePostTemplatePayload!` **Arguments:** - `input`: `DeletePostTemplateInput!` - Input for deleting a post template. #### editPost Edit post for channel **Returns:** `PostActionPayload!` **Arguments:** - `input`: `EditPostInput!` - The mutation's input #### movePostInQueue Move a queued post to the top or bottom of its channel's queue. Unlike editPost, this is a scheduling-only operation that never re-validates the post's content. **Status:** ⚠️ Experimental **Returns:** `MovePostInQueuePayload!` **Arguments:** - `input`: `MovePostInQueueInput!` - The mutation's input #### promoteContentItemDraftToPosts Promote a channel-less draft into channel-specific posts. One-way: once promoted, the content item can no longer be edited as a draft. If any post fails validation, none are created. A failure can also be reported when a post could not be fully processed after the promotion already took effect; re-fetch the content item to check its state before retrying. This API is an early preview and can change without a deprecation period. **Status:** ⚠️ Experimental **Returns:** `PromoteContentItemDraftToPostsPayload!` **Arguments:** - `input`: `PromoteContentItemDraftToPostsInput!` - Input for promoteContentItemDraftToPosts. #### updateContentItem Update a content item's title or target date. Fields that are omitted keep their current value. This API is an early preview and can change without a deprecation period. **Status:** ⚠️ Experimental **Returns:** `UpdateContentItemPayload!` **Arguments:** - `input`: `UpdateContentItemInput!` - Input for updateContentItem. #### updateContentItemDraft Replace a channel-less draft's content in full, and optionally set the content item's target date in the same write. The draft needs text or at least one asset. Only valid while the content item is still a draft. This API is an early preview and can change without a deprecation period. **Status:** ⚠️ Experimental **Returns:** `UpdateContentItemDraftPayload!` **Arguments:** - `input`: `UpdateContentItemDraftInput!` - Input for updateContentItemDraft. #### updatePostTemplate Update a post template owned by the caller (or an internal template in the caller's organization, if the caller is an org admin/owner). **Status:** 🧪 Preview **Returns:** `UpdatePostTemplatePayload!` **Arguments:** - `input`: `UpdatePostTemplateInput!` - Input for updating a post template. ### Object Types #### Account Account is a representation of a Buffer user. **Fields:** - `id`: `ID!` - Unique identifier for the account - `email`: `String!` - Primary email address for the account - `backupEmail`: `String` - Backup email address for account recovery - `avatar`: `String!` - URL to the account's avatar image - `createdAt`: `DateTime` - Date the account was created in the Core DB. For older customers, it's possible a Publish account existed in the Publish DB for this customer before this date - `organizations`: `[Organization!]!` - Arg `filter`: `OrganizationFilterInput` - `timezone`: `String` - The account-level timezone - this is used as a default input for streaks, posting plans, and new channel channel connections. - `name`: `String` - The account name, different from the organization name - `preferences`: `Preferences` - The accounts preferences - `connectedApps`: `[ConnectedApp!]` - The connected apps for the account #### AggregatedPostMetrics Aggregated post metrics across a filtered post set. Each entry in `metrics` mirrors the shape of a `Post.metrics` entry; the total number of matched posts is carried as a regular `PostMetric` entry with `type: postCount`. **Fields:** - `metrics`: `[PostMetric!]!` - Normalized metric aggregates across the matched posts. Always includes a baseline trio (`postCount`, `reactions`, `comments`) — posts on networks that don't track reactions or comments contribute 0 to those totals. Beyond the baseline, additional metric types are included only when every channel in the filter set supports them. - `metricsUpdatedAt`: `DateTime` - The latest `metricsUpdatedAt` across the matched posts, indicating the freshness of the aggregate. Metrics are refreshed daily, so values can be up to ~24h behind the source network. Null when no posts matched the filter. #### Annotation Annotation representing all the entities in the text **Fields:** - `content`: `String!` - The content of the annotation. Annotations can sometimes be different from the actual text content. E.g., Mastodon mentions have 'text: @buffer', but includes the server name in the content, 'content: @buffer@threads.net' - `indices`: `[Int!]!` - The indices of the annotation in the text - `text`: `String!` - The text representation of the annotation, eg '@buffer' - `type`: `AnnotationType!` - The type of the annotation - `url`: `String!` - The URL the annotation points to #### Author Represent the author of a post or note. **Fields:** - `id`: `AccountId!` - The unique identifier of the author. - `avatar`: `String!` - The avatar URL of the author. - `email`: `String!` - The email address of the author. - `isDeleted`: `Boolean!` - Indicates whether the author is a deleted. - `name`: `String` - The name of the author. Null if the user has not yet set a name. #### BlueskyMetadata Bluesky metadata **Fields:** - `serverUrl`: `String!` - The instance of bluesky of the channel #### BlueskyPostMetadata Bluesky post metadata **Implements:** CommonPostMetadata, ThreadedPostMetadata **Fields:** - `annotations`: `[Annotation!]!` - Annotations representing entities in the text - `linkAttachment`: `LinkAttachment` - Link attachment - `thread`: `[ThreadedPost!]!` - The list of threaded posts (not paginated) - `threadCount`: `Int!` - The number of threaded posts - `type`: `PostType!` - The channel-specific type of the post, eg, post, story, reel for Instagram #### Channel Channel entity **Fields:** - `id`: `ChannelId!` - The ID of the channel - `allowedActions`: `[ChannelAction!]!` - The allowed actions for the current user - `avatar`: `String!` - The avatar URL of the channel - `descriptor`: `String!` - Formatted name of the channel service and type: e.g. 'Twitter Profile' or 'Facebook Page' - `displayName`: `String` - The display name of the channel - nullable (reason?) - `externalLink`: `String` - The channel's URL on the social network (e.g. instagram.com/username or facebook.com/page) Returns null if the channel is not supported - `hasActiveMemberDevice`: `Boolean!` - Whether at least one member of the orginization who have access to this channel also has a user device registered for push notifications - `isDisconnected`: `Boolean!` - Indicates if the channel is properly connected to Buffer - `isLocked`: `Boolean!` - Indicates if the channel is locked - Locked channels can't be used for posting. A channel can be locked when the organization downgrades and reduces the channel quantity of their plan. - `isNew`: `Boolean!` - Indicates if the channel was recently created (in less than 10 seconds). This is used to determine the redirect modal after channel authorization - `isQueuePaused`: `Boolean!` - Indicates is the queue is paused for the channel. A paused queue means schedules posts won't be published. - `linkShortening`: `ChannelLinkShortening!` - Link Shortening settings for the channel - `metadata`: `ChannelMetadata` - Metadata or settings depending on the service type - such as the server URL for Mastodon or Location data for Facebook/GPB - `name`: `String!` - The name of the channel - the handle name, username, etc. - `organizationId`: `OrganizationId!` - The organization ID of the channel - `postingGoal`: `PostingGoal` - The posting goal for the channel - `postingSchedule`: `[ScheduleV2!]!` - Provides the posting slots for each day of the week - `products`: `[Product!]` - Products that support a given channel - `scopes`: `[String]!` - Scopes requested for a given channel - empty array if we don't have them tracked - `service`: `Service!` - Represents the social network - `serviceId`: `String!` - Represents the external ID of the channel on social network API - `showTrendingTopicSuggestions`: `Boolean!` - Indicates if trending topic suggestions should be shown in the composer. When false, users can still access trends via the trending icon button. Defaults to true for backward compatibility. - `timezone`: `String!` - The timezone of the channel - Default if not set is Europe/London - `type`: `ChannelType!` - The type of the channel - Page, Profile, Business, Group, Account, etc. - `weeklyPostingLimit`: `WeeklyPostingLimit` - Weekly posting limit for the channel *(Deprecated: This field is not used anymore)* - `createdAt`: `DateTime!` - The creation date of the channel - `updatedAt`: `DateTime!` - The last time the channel was updated #### ChannelConfiguration Everything for one connected channel: its engagement capabilities, its full `content` config, and per-feature authorization. Each channel carries its own complete config — there is no service-level default to merge against. **Status:** ⚠️ Experimental **Fields:** - `authorizationStatus`: `[FeatureAuthorizationStatus!]!` - Authorization status per feature for this channel. A feature is a capability a channel can expose — e.g. posting, commenting, mentions, insights (see the `Feature` enum). - `channelId`: `ChannelId!` - The connected channel this config describes. - `channelType`: `ChannelType!` - The channel's type within the service (e.g. profile, page, business). - `content`: `[ContentConfiguration!]!` - Content config — one entry per content type, or a single entry grouping several types that share identical rules (post / story / reel / comment reply). - `engagement`: `[EngagementTypeConfiguration!]!` - Engagement capabilities — one entry per engagement type. Empty when engagement isn't supported on the channel. - `service`: `Service!` - The channel's social service (e.g. twitter, instagram). #### ChannelLinkShortening Settings for link shortening **Fields:** - `config`: `LinkShorteningConfig` - Configuration of link shortening integration. Null if disabled. - `isEnabled`: `Boolean!` - If link shortening is enabled for the channel #### ChannelRefreshRequired Error returned when the channel needs a refresh **Status:** ⚠️ Experimental **Implements:** MutationError **Fields:** - `channelIds`: `[ChannelId!]!` - ChannelIds that need refreshing - `message`: `String!` - Error message #### Configuration Organization configuration. `channels` carries the per-connected-channel config the actor can view (including per-`Feature` `authorizationStatus`); `services` carries the service-level capability catalog — the engagement and content config for every supported `service` + `channelType`, independent of which channels are connected (e.g. for composing before a channel is connected). **Status:** ⚠️ Experimental **Fields:** - `channels`: `[ChannelConfiguration!]!` - Per-connected-channel config for every channel the actor can view. - `services`: `[ServiceConfiguration!]!` - Service-level capability catalog for every supported service + channel type. #### ConnectedApp Connected App **Fields:** - `category`: `ConnectedAppCategory` - The category of the connected app, when known. - `clientId`: `ID!` - The id of the connectedApp. - `description`: `String!` - A brief description of the connected app. - `name`: `String!` - The name of the connected app. - `scopes`: `[String!]!` - The access scopes granted to this connection for Buffer's public API resources. Empty when the connection holds none. - `userId`: `ID!` - The id of the user that has granted access to the app. - `website`: `String!` - The website URL of the connected app. - `createdAt`: `DateTime!` - The date and time when the connected app was created. #### ContentConfiguration Config for one or more content types that share the same properties and rules. Grouping several `configurationContentTypes` in one entry avoids duplicating an identical config (e.g. story + reel). **Status:** ⚠️ Experimental **Fields:** - `configurationContentTypes`: `[ConfigurationContentType!]!` - The content type(s) this config applies to (grouped when rules are identical). - `rules`: `[ValidationRule!]!` - Validation rules that decide whether a draft is valid. - `supportedProperties`: `[ContentProperty!]!` - The properties these content types support; absence means unsupported. #### ContentItem A piece of content in Buffer. It starts as a channel-less draft and can later become a set of channel-specific posts, or it can be created with its posts directly. This API is an early preview and can change without a deprecation period. **Status:** ⚠️ Experimental **Fields:** - `id`: `ContentItemId!` - Unique identifier for this content item. - `accountId`: `AccountId!` - Account that owns this content item. - `allowedActions`: `[ContentItemAction!]!` - Actions the calling actor can take on this content item. A client reads this set instead of deriving the authorization rules itself. - `author`: `Author` - The person who created this content item. Null when that person's account no longer exists. - `body`: `ContentItemBody!` - The current content of this item: either a channel-less draft or the channel-specific posts created from it. - `organizationId`: `OrganizationId!` - Organization that owns this content item. - `tags`: `[Tag!]!` - Tags applied to this content item, in the order they were set. A tag applies to the item as a whole, so it survives the move from a channel-less draft to channel-specific posts. - `targetDate`: `DateTime` - Optional date indicating when this piece of content should go out. This is a planning aid only and does not schedule any posts. - `title`: `String` - Optional title describing what this piece of content is about. - `createdAt`: `DateTime!` - When this content item was created. #### ContentItemEdge An edge in a content item connection. **Status:** ⚠️ Experimental **Fields:** - `cursor`: `String!` - A cursor for pagination. - `node`: `ContentItem!` - The content item node at the end of the edge. #### ContentItemsConnection A paginated connection of content items. **Status:** ⚠️ Experimental **Fields:** - `edges`: `[ContentItemEdge!]!` - The list of content item edges. - `pageInfo`: `PaginationPageInfo!` - Pagination information for the connection. - `totalCount`: `Int!` - The total number of content items matching the request. #### ContentItemStateError The content item is not currently a draft: it may already have been published as channel-specific posts, including concurrently with this request. **Status:** ⚠️ Experimental **Implements:** MutationError **Fields:** - `message`: `String!` - Error message. #### CountRule Bounds how many of a property are allowed. `min` null/absent = no lower bound (optional); `min: 1` = required. `max` null = unlimited. At least one of `min` or `max` is always present — a rule that bounds neither is never emitted. **Status:** ⚠️ Experimental **Implements:** ValidationRule **Fields:** - `max`: `Int` - Maximum allowed count. Null = unlimited. - `min`: `Int` - Minimum allowed count. Null = no lower bound (optional). - `property`: `ContentProperty!` - The content property this rule constrains. #### CreateContentItemDraftFailure createContentItemDraft failure. When any part of the input is invalid, nothing is created. **Status:** ⚠️ Experimental **Implements:** MutationError **Fields:** - `errors`: `[InvalidInputError!]!` - The recoverable errors behind this failure. - `message`: `String!` - Summary error message. #### CreateContentItemDraftSuccess Successful createContentItemDraft response. **Status:** ⚠️ Experimental **Fields:** - `contentItem`: `ContentItem!` - The content item first created for this request, including its current state. #### CreateContentItemFailure createContentItem failure. Every recoverable error is reported together so the caller can surface them all at once. When the input fails validation, no content item and no variants are created. When one or more variants fail while being processed after creation, the content item and all of its variants already exist; the per-channel errors identify the variants that need attention. **Status:** ⚠️ Experimental **Implements:** MutationError **Fields:** - `errors`: `[CreateContentItemError!]!` - The recoverable errors behind this failure. - `message`: `String!` - Summary error message. #### CreateContentItemSuccess Successful createContentItem response. **Status:** ⚠️ Experimental **Fields:** - `content`: `ContentItem!` - The content item created by this mutation. #### CreateContentItemVariantInvalidInputError Invalid input for a single post variant in the createContentItem input list. **Status:** ⚠️ Experimental **Implements:** MutationError **Fields:** - `channelId`: `ChannelId!` - Channel targeted by the failing variant. - `message`: `String!` - Error message. #### CreateContentItemVariantLimitReachedError Limit reached for a single post variant in the createContentItem input list. **Status:** ⚠️ Experimental **Implements:** MutationError **Fields:** - `channelId`: `ChannelId!` - Channel targeted by the failing variant. - `message`: `String!` - Error message. #### CreateContentItemVariantNotFoundError The channel targeted by a single post variant in the createContentItem input list was not found or is not accessible. **Status:** ⚠️ Experimental **Implements:** MutationError **Fields:** - `channelId`: `ChannelId!` - Channel targeted by the failing variant. - `message`: `String!` - Error message. #### CreatePostTemplateSuccess Successful result of an end user creating a post template. **Status:** 🧪 Preview **Fields:** - `postTemplate`: `PostTemplate!` - The newly created post template. #### DailyPostingLimitStatus Status of daily posting limits for a channel on a given day. **Fields:** - `channelId`: `ChannelId!` - The channel ID this status refers to. - `isAtLimit`: `Boolean!` - Whether the channel has reached its daily posting limit. - `limit`: `Int` - The network daily posting limit. Null means unlimited. - `scheduled`: `Int!` - Number of posts scheduled for this day. - `sent`: `Int!` - Number of posts already sent on this day. #### DeleteContentItemFailure deleteContentItem failure. When any post cannot be deleted, nothing is deleted: the content item and all of its posts remain unchanged. Every blocked post is reported together so the caller can surface them all at once. **Status:** ⚠️ Experimental **Implements:** MutationError **Fields:** - `errors`: `[PostNotDeletableError!]!` - The recoverable errors behind this failure. - `message`: `String!` - Summary error message. #### DeletePostSuccess deletePost success response returns the post id that was deleted. **Fields:** - `id`: `PostId!` - Post id that was delete. #### DocumentAsset Document asset **Implements:** Asset **Fields:** - `id`: `ID` - The ID of the asset in the database - `document`: `DocumentMetadata!` - Document specific metadata - `mimeType`: `String!` - The MIME type of the asset - `source`: `String!` - URL to the file source - `thumbnail`: `String!` - URL to the static thumbnail of the asset - `type`: `AssetType!` - The type of the asset #### DocumentMetadata Document metadata **Fields:** - `filesize`: `Int` - Document fileSize in bytes - `numPages`: `Int!` - Number of pages in the document - `thumbnails`: `[String!]!` - URLs to the static thumbnails of the document pages - `title`: `String` - Document title #### DraftContent A content draft that is not attached to any channel yet. Because no channel is selected, no network-specific rules apply, including media type restrictions. **Status:** ⚠️ Experimental **Fields:** - `id`: `DraftContentId!` - Unique identifier for this draft. - `aiAssisted`: `Boolean!` - True when the draft content was written with the help of AI. - `assets`: `[Asset!]!` - Images, videos, or documents attached to the draft, in display order. - `text`: `String!` - The written content of the draft. Empty when the draft holds only assets. #### DurationRule Caps the duration of a time-based media property. **Status:** ⚠️ Experimental **Implements:** ValidationRule **Fields:** - `maxDurationSeconds`: `Int!` - Maximum allowed duration in seconds. - `property`: `ContentProperty!` - The content property this rule constrains. #### EmptySuccess Empty mutation success response used when client doesn't need any data back or simply needs to respond to a success or error. **Implements:** MutationSuccess **Fields:** - `_empty`: `String!` - The value is always an empty string '' Note: GraphQL doesn't allow types with no fields, so we have to add this field #### EngagementSupportedReaction A supported reaction. `reactionType` is the opaque identity echoed back on the react mutation; `visual` carries everything needed to render it. **Status:** ⚠️ Experimental **Fields:** - `reactionType`: `ReactionType!` - The kind of reaction a user can apply (e.g. like, love, celebrate). This is the identifier the client sends back to the react mutation when the user picks this reaction; treat it as an opaque value rather than parsing it. - `visual`: `ReactionVisual!` - How to render this reaction (built-in icon or network image). #### EngagementTypeConfiguration Per-engagement-type configuration. One entry per kind; `engagementType` discriminates. Adding a kind is additive — a new `EngagementType` value, not a new type. **Status:** ⚠️ Experimental **Fields:** - `engagementType`: `EngagementType!` - The kind of engagement this entry configures (comment / mention). - `media`: `MediaConfiguration!` - Media configuration. Always present; `supportsMedia: false` = the platform doesn't deliver attached media to Buffer, so none can be shown. - `metadata`: `EngagementMetadata` - Optional network-specific engagement metadata (e.g. Google Business). null when none applies. - `reactions`: `ReactionsConfiguration!` - Reactions. Always present; empty `supportedReactions` = reacting via Buffer isn't supported. - `replying`: `ReplyingConfiguration!` - Reply configuration. Always present; `supportsReplying: false` = replying via Buffer not supported. - `supportedAiFeatures`: `[AiFeature!]!` - AI features supported for this engagement type. Empty when none. - `syncData`: `SyncDataConfiguration!` - Sync configuration — realTime/polling mechanism plus optional forced-sync detail. #### ExclusiveRule `property` cannot be combined with any of `conflictsWith`. **Status:** ⚠️ Experimental **Implements:** ValidationRule **Fields:** - `conflictsWith`: `[ContentProperty!]!` - Properties that `property` cannot be combined with. - `property`: `ContentProperty!` - The content property this rule constrains. #### FacebookMetadata Facebook metadata **Fields:** - `locationData`: `LocationData` - Metadata about the location of the business associated with the channel. Only available for Facebook and GPB #### FacebookPostMetadata Facebook post metadata **Implements:** CommonPostMetadata **Fields:** - `annotations`: `[Annotation!]!` - Annotations representing entities in the text - `firstComment`: `String` - Facebook post's first comment - `linkAttachment`: `LinkAttachment` - Link attachment - `title`: `String` - Title of Facebook reel - `type`: `PostType!` - The channel-specific type of the post, eg, post, story, reel for Instagram #### FeatureAuthorizationStatus Pairs a feature with the channel's authorization status for it (token/scope level). One entry per authorization-gated feature the channel exposes. **Status:** ⚠️ Experimental **Fields:** - `feature`: `Feature!` - The feature this status describes — a capability a channel can expose, e.g. posting, commenting, mentions, insights, reacting to engagements (see the `Feature` enum). - `reason`: `String` - Why the feature is in its current `status`, when a specific cause is known (e.g. the connected platform user lacks rights on the account that owns webhook subscriptions). Informative text for display/support — not a stable machine key, so clients must not parse or switch on it. Null when the status needs no explanation (`ok`) or no specific cause was identified. - `status`: `AuthorizationStatus!` - Whether the channel is currently authorized to use `feature`: `ok` (usable), `needsRefresh` (token refresh required), `notEnoughData` (missing scopes / webhook metadata), `needsUpgrade` (client below the minimum version — applies only to the mobile apps), or `notEnoughRights` (the connected platform user's role is insufficient — reconnecting cannot fix it). See the `AuthorizationStatus` enum. #### FileSizeRule Caps the byte size of a media property. **Status:** ⚠️ Experimental **Implements:** ValidationRule **Fields:** - `maxMegabytes`: `Int!` - Maximum allowed size in megabytes (MB). - `property`: `ContentProperty!` - The content property this rule constrains. #### ForcedSyncConfiguration Forced-sync detail. Present only when the kind's sync mechanism supports a forced (manual) sync. **Status:** ⚠️ Experimental **Fields:** - `forcedSyncRateLimit`: `ForcedSyncRateLimit` - Rate limit for forced syncs. `null` when forced sync is unthrottled. Reuses the `ForcedSyncRateLimit` type defined by the comment schema. - `pollInterval`: `Int!` - How often, in seconds, the channel is automatically polled for new items. #### ForcedSyncRateLimit Rate limit configuration for forced sync operations. **Status:** ⚠️ Experimental **Fields:** - `allowedAttempts`: `Int!` - Number of allowed forced backfill attempts within the period. - `period`: `Int!` - Rate limit period in seconds for forced backfill operations. #### FormatRule Restricts a media property to specific formats (allow-list). **Status:** ⚠️ Experimental **Implements:** ValidationRule **Fields:** - `allowedFormats`: `[MediaFormat!]!` - The formats `property` is allowed to use. - `property`: `ContentProperty!` - The content property this rule constrains. #### GoogleBusinessEngagementMetadata Google Business-specific engagement metadata. Distinct from the channel-level `GoogleBusinessMetadata` (which carries location data) — this is the engagement surface's rating/note metadata. **Status:** ⚠️ Experimental **Fields:** - `hasUserRating`: `Boolean!` - Whether engagements on this channel carry a user star rating. - `permanentNote`: `String!` - A permanent note surfaced to the user about this channel's engagements. Empty when there is nothing to tell the user. #### GoogleBusinessEventMetaData Metadata for a GBP post that is an event **Fields:** - `button`: `GoogleBusinessPostActionType!` - Action button - `endDate`: `DateTime!` - End date of the event - `endTime`: `String` - End time of the event *(Deprecated: get time from the endDate)* - `isFullDayEvent`: `Boolean!` - Indicate whether the event has a start or end time. - `link`: `String` - Link to the action - `startDate`: `DateTime!` - Start date of the event - `startTime`: `String` - Start time of the event *(Deprecated: get time from the startDate)* - `title`: `String!` - Title of the event #### GoogleBusinessMetadata Google Business metadata **Fields:** - `locationData`: `LocationData` - Metadata about the location of the business associated with the channel. Only available for Facebook and GPB #### GoogleBusinessOfferMetaData Metadata for a GBP post that is an offer **Fields:** - `code`: `String` - Coupon code for the offer - `endDate`: `DateTime!` - End date of the offer - `link`: `String` - Link to the offer - `startDate`: `DateTime!` - Start date of the offer - `terms`: `String` - Terms and Conditions - `title`: `String!` - Title of the offer #### GoogleBusinessPostMetadata Google Business Profile post metadata @deprecated: pending proposal for specific GBP post types: update, offer and event metadata types **Implements:** CommonPostMetadata **Fields:** - `annotations`: `[Annotation!]!` - Annotations representing entities in the text - `details`: `GoogleBusinessPostDetails` - Details of the metadata - `title`: `String` - Title if available in the given GBP post type: event and offer - `type`: `PostType!` - The channel-specific type of the post, eg, post, story, reel for Instagram #### GoogleBusinessWhatsNewMetaData Metadata for a GBP post of type Whats new **Fields:** - `button`: `GoogleBusinessPostActionType!` - Action button - `link`: `String` - Link to the action #### IconVisual Render from the built-in client icon set. **Status:** ⚠️ Experimental **Fields:** - `icon`: `ReactionIcon!` - The built-in icon to render. #### Idea Ideas are the main entity in the create space **Fields:** - `id`: `ID!` - Unique identifier for the idea - `content`: `IdeaContent!` - The actual content and metadata of the idea - `groupId`: `ID` - ID of the group this idea belongs to (if any) - `organizationId`: `ID!` - ID of the organization that owns this idea - `position`: `Float` - Numerical position for ordering within a group - `createdAt`: `Int!` - Unix timestamp of when the idea was created - `updatedAt`: `Int!` - Unix timestamp of when the idea was last modified #### IdeaContent Content of an idea **Fields:** - `aiAssisted`: `Boolean!` - Indicates whether AI tools were used in creating this idea - `date`: `DateTime` - DateTime set by user associated with the idea - this often reflects a target publish date. - `media`: `[IdeaMedia!]` - List of media items attached to the idea - `services`: `[Service!]!` - Services tagged by the user - this is typically used to annotate ideas with their target services - `tags`: `[PublishingTag!]!` - Tags used to categorize and organize the idea - `text`: `String` - Main body text or description of the idea - `title`: `String` - Title or headline of the idea #### IdeaEdge Pagination type for Ideas **Fields:** - `cursor`: `String!` - Opaque cursor for pagination, used to fetch subsequent pages - `node`: `Idea!` - The idea object #### IdeaGroup Idea groups are used to organize ideas in the board **Fields:** - `id`: `ID!` - Unique identifier for the idea group. - `isLocked`: `Boolean!` - Whether the idea group is locked. - `name`: `String!` - The name of the idea group. #### IdeaMedia Media attached to an idea **Fields:** - `id`: `ID!` - Unique identifier for the media in Buffer's upload system - `alt`: `String` - Alternative text description for accessibility - `size`: `Int` - File size in bytes - `source`: `IdeaMediaSource` - Source platform information for the media - `thumbnailUrl`: `String` - URL to a smaller version of the media for preview purposes - `type`: `MediaType!` - Type of media (e.g., image, video, gif) - `url`: `String!` - Direct URL to access the media file #### IdeaMediaSource Media source for the idea, e.g. Unsplash, Gifphy, etc. **Fields:** - `id`: `String` - Unique identifier from the source platform - `author`: `String` - Name of the content creator/author - `authorUrl`: `String` - URL to the author's profile on the source platform - `name`: `String!` - Name of the media source platform (e.g., 'Unsplash', 'Giphy') #### IdeaResponse createIdea response type **Fields:** - `idea`: `Idea` - The affected idea - `refreshIdeas`: `Boolean!` - If true, the client should refresh the ideas list because other ideas might have been moved #### IdeasConnection Relay connection for paginated ideas. **Fields:** - `edges`: `[IdeaEdge!]!` - List of idea edges containing the ideas and their cursors - `pageInfo`: `PaginationPageInfo!` - Pagination metadata including hasNextPage and endCursor #### ImageAsset Image asset **Implements:** Asset **Fields:** - `id`: `ID` - The ID of the asset in the database - `image`: `ImageMetadata!` - Image specific metadata - `mimeType`: `String!` - The MIME type of the asset - `source`: `String!` - URL to the file source - `thumbnail`: `String!` - URL to the static thumbnail of the asset - `type`: `AssetType!` - The type of the asset #### ImageMetadata Image metadata **Fields:** - `altText`: `String!` - Alternative text for accessibility - `animatedThumbnail`: `String` - Animated thumbnail URL - `height`: `Int!` - Image height in pixels - `isAnimated`: `Boolean!` - Is the image animated? - `userTags`: `[UserTag!]` - User tags in the image - `width`: `Int!` - Image width in pixels #### ImageVisual Render a network-supplied image (e.g. LinkedIn-branded reactions). **Status:** ⚠️ Experimental **Fields:** - `url`: `String!` - URL of the network-supplied reaction image. #### InstagramAudio An Instagram music track or original sound that can be attached to a Reel. **Status:** ⚠️ Experimental **Fields:** - `id`: `String!` - Meta audio asset ID - `coverArtworkUrl`: `URL` - Temporary artwork URL from Meta. May be null. - `creatorUsername`: `String` - Creator username for original sounds - `displayArtist`: `String` - Artist name for licensed music - `duration`: `Int!` - Duration of the audio in milliseconds - `previewUrl`: `URL` - Temporary preview URL from Meta. May be null. - `title`: `String!` - Display title for the audio - `type`: `InstagramAudioType!` - Whether this asset is licensed music or an original sound #### InstagramAudioSuccess Refreshed metadata for one Instagram audio asset. **Status:** ⚠️ Experimental **Fields:** - `audio`: `InstagramAudio!` - Refreshed audio metadata. #### InstagramGeolocation Instagram Geolocation **Fields:** - `id`: `String` - The id of this location - `text`: `String` - The name of this location #### InstagramMetadata Instagram metadata **Fields:** - `defaultToReminders`: `Boolean!` - Indicates if we should default to reminder for Instagram #### InstagramPostMetadata Instagram post metadata **Implements:** CommonPostMetadata **Fields:** - `annotations`: `[Annotation!]!` - Annotations representing entities in the text - `firstComment`: `String` - Instagram post's first comment - `geolocation`: `InstagramGeolocation` - Geolocation of the post - `isAiGenerated`: `Boolean!` - Whether the post discloses AI-generated content - `link`: `String` - Shop Grid link for the post - `shouldShareToFeed`: `Boolean!` - Indicates whether post should be shared to feed - `stickerFields`: `InstagramStickerFields` - Sticker fields for reminder-based publishing - `type`: `PostType!` - The channel-specific type of the post, eg, post, story, reel for Instagram #### InstagramStickerFields Instagram fields for reminder-based publishing. Upon the reminder for publishing, the user is prompted to copy and paste these fields into the Instagram app to complete the post. **Fields:** - `music`: `String` - Placeholder text for the post's music - `other`: `String` - Additional field for any other post content - `products`: `String` - Placeholder text for the post's linked products - `text`: `String` - Text for the Story or Reel - `topics`: `String` - Placeholder text for the post's topics (Reels only) #### InvalidInputError Error returned when the input is invalid **Implements:** MutationError **Fields:** - `message`: `String!` - Error message #### LengthRule Caps the character length of a text-like property (e.g. 280 for X post text). **Status:** ⚠️ Experimental **Implements:** ValidationRule **Fields:** - `maxLength`: `Int!` - Maximum allowed character length. - `property`: `ContentProperty!` - The content property this rule constrains. #### LimitReachedError Error returned when the limit is reached **Implements:** MutationError **Fields:** - `message`: `String!` - Error message #### LinkAttachment Link attachment **Implements:** ScrapedLink **Fields:** - `expandedUrl`: `String` - Full URL that the link asset has been built from - `text`: `String!` - Description for the scraped link - `thumbnail`: `String` - Selected thumbnail for this link preview - `thumbnails`: `[String!]!` - Thumbnails of media available in the link - `title`: `String!` - Title for the link attachment - `url`: `String!` - URL that the link asset has been built from #### LinkedInMetadata LinkedIn metadata **Fields:** - `shouldShowLinkedinAnalyticsRefreshBanner`: `Boolean!` - Property parsed from scopes indicating whether the client should show the LinkedIn analytics refresh banner #### LinkedInPostMetadata LinkedIn post metadata **Implements:** CommonPostMetadata **Fields:** - `annotations`: `[Annotation!]!` - Annotations representing entities in the text - `firstComment`: `String` - LinkedIn post's first comment - `linkAttachment`: `LinkAttachment` - Link attachment - `type`: `PostType!` - The channel-specific type of the post, eg, post, story, reel for Instagram #### LinkShorteningConfig Link Shortening Configuration **Fields:** - `domain`: `String!` - Domain of the link shortener - eg buff.ly, dub.co, or the user's custom domain. - `name`: `String!` - Human readable string to describe the link shortening service. #### LocationData Location data about the channel **Fields:** - `googleAccountId`: `String` - Google Account Id of the business - `location`: `String` - Location of the business associated with the channel - `mapsLink`: `String` - Link to the map #### MastodonMetadata Mastodon metadata **Fields:** - `maxCharacters`: `Int!` - Maximum character limit allowed for this channel's server instance - `serverUrl`: `String!` - The instance of mastodon of the channel #### MastodonPostMetadata Mastodon post metadata **Implements:** CommonPostMetadata, ThreadedPostMetadata **Fields:** - `annotations`: `[Annotation!]!` - Annotations representing entities in the text - `spoilerText`: `String` - Spoiler text hiding the root text of this post - `thread`: `[ThreadedPost!]!` - The list of threaded posts (not paginated) - `threadCount`: `Int!` - The number of threaded posts - `type`: `PostType!` - The channel-specific type of the post, eg, post, story, reel for Instagram #### MediaConfiguration Media configuration for an engagement type — whether media attached to an engagement reaches Buffer and can therefore be shown to the user. **Status:** ⚠️ Experimental **Fields:** - `additionalInformation`: `String` - User-facing explanation of what the client can do about the missing media, worded for the platform (e.g. "Instagram doesn't send media with mentions to Buffer. Open the mention natively to see the full content."). Display text only — not a stable machine key, so clients must not parse or switch on it. Null when `supportsMedia` is true. - `disclaimer`: `String` - User-facing explanation of why media isn't shown. Display text only — not a stable machine key, so clients must not parse or switch on it. Null when `supportsMedia` is true. - `supportsMedia`: `Boolean!` - Whether media attached to this engagement type is delivered by the platform and can be shown. #### MemberConnection Represents the members connection edge. Later, we can add the list of members with the page info to follow our connection edge pattern. **Fields:** - `totalCount`: `Int!` - The total count of team members counting the org owner and team members from the Publish DB. #### Note Note entity **Fields:** - `id`: `NoteId!` - The unique identifier of the note. - `allowedActions`: `[NoteAction!]!` - The allowed actions a user can perform on the note. - `author`: `Author!` - The author of the note - null if the user is deleted or left the organization. - `text`: `String!` - The content of the note. - `type`: `NoteType!` - The type of the note. - `createdAt`: `DateTime!` - The date and time when the note was created. - `updatedAt`: `DateTime` - The date and time when the note was last edited. #### NotFoundError Error returned when the resource is not found **Implements:** MutationError **Fields:** - `message`: `String!` - Error message #### Organization Organization is a representation of a Buffer Organization. **Fields:** - `id`: `OrganizationId!` - The ID of the organization. - `channelCount`: `Int!` - The total number of channels connected to the organization. - `limits`: `OrganizationLimits!` - The limits of the organization. Can be used to check if the organization has reached the limit of channels, members, etc. - `members`: `MemberConnection!` - The members of the organization. Can be used to check the total number of members in the organization. In the future, it might contain more information about the members. - `name`: `String!` - The name of the organization. - `ownerEmail`: `String!` - The owner email of the organization. - `shouldEnforce2FASetup`: `Boolean!` - Whether the requesting actor should be sent through 2FA setup before they can use this organization. Derived: true only when the org has `settings.enforce2FA` ON, the `organization-enforced-2fa` rollout Split is ON for the org, and the actor has no 2FA configured on their own account. This is the single source of truth for the forced-2FA gate; app-shell and publish-frontend redirect to the setup flow when it's true. Exposed to the API gateway so any stitched consumer reads the same value. #### OrganizationLimits Resource limits for an organization including channels, members, and content limits **Fields:** - `channels`: `Int!` - The maximum number of channels allowed for the organization. - `generateContent`: `Int!` - The maximum number of content generations allowed for the organization. - `ideaGroups`: `Int!` - The maximum number of idea groups allowed for the organization. - `ideas`: `Int!` - The maximum number of ideas allowed for the organization. - `members`: `Int!` - The maximum number of members allowed for the organization. - `postTemplates`: `Int!` - The maximum number of post templates allowed for the organization. - `savedReplies`: `Int!` - The maximum number of saved replies allowed for the organization. - `scheduledPosts`: `Int!` - The maximum number of scheduled posts allowed for the organization. - `scheduledStoriesPerChannel`: `Int!` - The maximum number of scheduled stories allowed per channel. - `scheduledThreadsPerChannel`: `Int!` - The maximum number of scheduled threads allowed per channel. - `tags`: `Int!` - The maximum number of tags allowed for the organization. #### PaginationPageInfo Information to aid in pagination. **Fields:** - `endCursor`: `String` - The last cursor in the list. It can be used to fetch the next page. - `hasNextPage`: `Boolean!` - When set to true, it means there is a next page available. - `hasPreviousPage`: `Boolean!` - When set to true, it means there is a previous page available. Will always return false for now as we only support forward pagination. - `startCursor`: `String` - The first cursor in the list. It can be used to fetch the previous page. #### PinterestBoard A Pinterest board **Fields:** - `id`: `String!` - The ID of the board - `avatar`: `String` - The board avatar - `description`: `String` - The board description - `name`: `String!` - The board name - `serviceId`: `String!` - The ID of the service - `url`: `String!` - The board URL #### PinterestMetadata Pinterest metadata **Fields:** - `boards`: `[PinterestBoard!]!` - The list of boards the user has on Pinterest #### PinterestPostMetadata Pinterest post metadata **Implements:** CommonPostMetadata **Fields:** - `annotations`: `[Annotation!]!` - Annotations representing entities in the text - `board`: `PinterestBoard` - The board the Pin is saved to - `title`: `String` - The title of the Pin - `type`: `PostType!` - The channel-specific type of the post, eg, post, story, reel for Instagram - `url`: `String` - The Pin destination link #### Post Post entity **Fields:** - `id`: `PostId!` - ObjectId of the post - `allowedActions`: `[PostAction!]!` - Indicates what actions the current account can perform on the post - `assets`: `[Asset!]!` - assets - `author`: `Author` - Represents the user who created the post - `channel`: `Channel!` - channel - `channelId`: `ChannelId!` - channel ID (faster than resolving the channnel.id) - `channelService`: `Service!` - channel service (faster than resolving the channnel.service) - `dueAt`: `DateTime` - Date when the post is scheduled to be published - `error`: `PostPublishingError` - error - `externalLink`: `String` - The external URL of the post at the destination service - `ideaId`: `IdeaId` - Is set when the Post is generated from an Idea - `isCustomScheduled`: `Boolean!` - Indicates whether time to publish was manually selected by the user - `metadata`: `PostMetadata` - Metadata of the post which differs based on the social network/service @see post.metadata.graphql - `metrics`: `[PostMetric!]` - Metrics for the sent post. If post is not yet sent, this field will be null - `metricsUpdatedAt`: `DateTime` - Timestamp of when `metrics` were last refreshed from the network. Null until the daily ingestion job has processed the post. Buffer pulls fresh metrics once per day, so this can lag the network value by ~24h. - `notes`: `[Note!]!` - notes - `notificationStatus`: `NotificationStatus` - notificationStatus: notified or markedAsPublished - `schedulingType`: `SchedulingType` - How the post publishes: `notification` for a reminder that asks someone to publish it by hand, `automatic` for one the publishing workers send. - `sentAt`: `DateTime` - Date when the post is published - `sharedNow`: `Boolean!` - Indicates whether the post was shared via publish now action - `shareMode`: `ShareMode!` - Indicates the share mode of the post (e.g., addToQueue, shareNext, shareNow, customScheduled) - `status`: `PostStatus!` - status - `tags`: `[Tag!]!` - tags - sorted by name in ascending order - `text`: `String!` - Text content of the Post - `via`: `PostVia!` - Indicates if the post is created from Buffer or the API - `createdAt`: `DateTime!` - Date when the post was created - `updatedAt`: `DateTime!` - Date when the post was updated #### PostActionSuccess Success response returns the full up-to-date post from after the action was performed. **Fields:** - `post`: `Post!` - Post on which the action was successfully performed. #### PostChannelNotFoundError The channel targeted by a single post in the promoteContentItemDraftToPosts input list was not found or is not accessible. **Status:** ⚠️ Experimental **Implements:** MutationError **Fields:** - `channelId`: `ChannelId!` - Channel targeted by the failing post. - `message`: `String!` - Error message. #### PostContent The fixed set of channel-specific posts created from a content item. Each post targets a single channel and follows that channel's network rules. **Status:** ⚠️ Experimental **Fields:** - `posts`: `[Post!]!` - The posts created for this content item, one per channel. Individual posts are edited through their own mutations. #### PostingGoal Represents a posting goal for a channel, including target, progress, and status information. **Fields:** - `goal`: `Int!` - The target number of posts for this goal. - `periodEnd`: `DateTime!` - The end date of the period for this posting goal. - `periodStart`: `DateTime!` - The start date of the period for this posting goal. - `scheduledCount`: `Int!` - The number of posts that are scheduled to be sent for this goal. - `sentCount`: `Int!` - The number of posts that have been sent (published or ingested) for this goal. - `status`: `PostingGoalStatus!` - The current status of the posting goal. #### PostInvalidInputError Invalid input for a single post in the promoteContentItemDraftToPosts input list. **Status:** ⚠️ Experimental **Implements:** MutationError **Fields:** - `channelId`: `ChannelId!` - Channel targeted by the failing post. - `message`: `String!` - Error message. #### PostLimitReachedError Limit reached for a single post in the promoteContentItemDraftToPosts input list. **Status:** ⚠️ Experimental **Implements:** MutationError **Fields:** - `channelId`: `ChannelId!` - Channel targeted by the failing post. - `message`: `String!` - Error message. #### PostMetric A single metric for a post (or an entry in an aggregated metrics response). **Fields:** - `description`: `String!` - A human-readable description of what the metric represents. - `name`: `String!` - The human-readable name of the metric (e.g. "Reactions", "Eng. Rate"). - `type`: `PostMetricType!` - The type of metric. Cross-network metrics use unified naming (`reactions`, `comments`, etc.); network-specific metrics retain their network's vocabulary (`saves`, `quotes`, etc.). See `PostMetricType` for the full catalog including deprecated values. - `unit`: `PostMetricUnit!` - The unit (count vs percentage) of `value`. - `value`: `Float!` - The metric value. Defaults to 0 when the network did not report the metric. #### PostNotDeletableError A post in this content item cannot be deleted, so the item cannot be deleted either. Either the post is publishing or has already published, and posts in those states can no longer be deleted, or this account is not allowed to delete posts on that channel. **Status:** ⚠️ Experimental **Implements:** MutationError **Fields:** - `channelId`: `ChannelId!` - Channel targeted by the post that cannot be deleted. - `message`: `String!` - Error message. #### PostPublishingError Post publishing error **Fields:** - `message`: `String!` - Error message to display - `rawError`: `String` - The original error from the publishing service (internal use only) - `supportUrl`: `String` - Link to a help center article to help resolve the error #### PostsEdge Represent a node in the pagination result using the Connect Relay convention. **Fields:** - `cursor`: `String!` - Opaque cursor to be used in pagination to fetch from current node. - `node`: `Post!` - Represents the current post in the list. #### PostsResults Results for the posts query. **Fields:** - `edges`: `[PostsEdge!]` - The list of posts that match the query. - `pageInfo`: `PaginationPageInfo!` - Information to aid in pagination. #### PostTemplate A post template used for content inspiration. **Fields:** - `id`: `PostTemplateId!` - The unique identifier for the template. *(🧪 Preview)* - `body`: `String!` - The main content body of the template, may contain {{placeholders}}. *(🧪 Preview)* - `description`: `String!` - A short user-facing description of the template. *(🧪 Preview)* - `emoji`: `String!` - The emoji associated with the template. *(🧪 Preview)* - `organizationId`: `OrganizationId!` - The organization that owns this template. *(🧪 Preview)* - `title`: `String!` - The title of the template. *(🧪 Preview)* - `visibility`: `PostTemplateVisibility!` - The visibility level of the template. `public` is returned for Buffer-managed templates. *(🧪 Preview)* - `createdAt`: `DateTime!` - The date and time the template was created. *(🧪 Preview)* - `updatedAt`: `DateTime!` - The date and time the template was last updated. *(🧪 Preview)* #### PostTemplateEdge An edge in a post template connection. **Status:** 🧪 Preview **Fields:** - `cursor`: `String!` - A cursor for pagination. - `node`: `PostTemplate!` - The post template node at the end of the edge. #### PostTemplatesConnection A paginated connection of post templates. **Status:** 🧪 Preview **Fields:** - `edges`: `[PostTemplateEdge!]!` - The list of post template edges. - `pageInfo`: `PaginationPageInfo!` - Pagination information for the connection. - `totalCount`: `Int!` - The total number of templates matching the filters. #### Preferences Account preferences **Fields:** - `timeFormat`: `String` - `startOfWeek`: `String` - `defaultScheduleOption`: `ScheduleOption!` #### PromoteContentItemDraftToPostsFailure promoteContentItemDraftToPosts failure. Every recoverable error is reported together so the caller can surface them all at once — for example, every post that failed validation across every targeted channel. A failure does not always mean nothing changed: post-processing errors are reported after the promotion has taken effect. **Status:** ⚠️ Experimental **Implements:** MutationError **Fields:** - `errors`: `[PromoteContentItemDraftToPostsError!]!` - The recoverable errors behind this failure. - `message`: `String!` - Summary error message. #### PromoteContentItemDraftToPostsSuccess Successful promoteContentItemDraftToPosts response. **Status:** ⚠️ Experimental **Fields:** - `contentItem`: `ContentItem!` - The promoted content item. #### PublishingTag Tag snapshot associated with content **Fields:** - `id`: `ID!` - `color`: `String!` - Hex color for tag e.g #F523F1 - `colorName`: `TagColorName` - Stable Buffer palette identifier derived from `color`. Clients should map this value to platform- and theme-specific presentation, falling back to `color` when this field is null or unsupported. Returns null for custom or unrecognized colors. - `name`: `String!` #### ReactionsConfiguration Reactions configuration. `restingIcon` is always populated; `supportedReactions` empty = reacting via Buffer isn't supported, one = single-button liking, multiple = a reaction set. **Status:** ⚠️ Experimental **Fields:** - `requiresAuthorization`: `Boolean!` - Whether reacting needs an OAuth scope beyond the ones a base connection grants. When true, check the channel's `engagementReaction` entry in `authorizationStatus` before offering the reaction — the channel may need a reconnect to grant it. On the service-level catalog this is channel-agnostic and may overstate the requirement, since it can vary with a channel's login flow; the per-channel configuration resolves it exactly. - `restingIcon`: `ReactionIcon!` - The resting-state icon — the pre-reaction icon. - `supportedReactions`: `[EngagementSupportedReaction!]!` - The reactions the user can apply. Empty when reacting via Buffer isn't supported. #### ReplyingConfiguration Reply configuration for an engagement type. A rich object (not a bare boolean) so reply metadata can be added later. **Status:** ⚠️ Experimental **Fields:** - `prefixReplyWithMention`: `Boolean!` - Whether replies should be prefixed with the author's @mention. - `supportsReplying`: `Boolean!` - Whether replying to this engagement type via Buffer is supported. #### RequiresRule `property` requires all of `requires` to also be present. **Status:** ⚠️ Experimental **Implements:** ValidationRule **Fields:** - `property`: `ContentProperty!` - The content property this rule constrains. - `requires`: `[ContentProperty!]!` - Properties that must also be present when `property` is used. #### RestProxyError Error proxied from the REST API response **Implements:** MutationError **Fields:** - `code`: `Int` - Error code from the REST API response - https://buffer.com/developers/api/errors - `link`: `String` - Link to our Help center from the REST API response - `message`: `String!` - An error message from the REST API response that we proxied here #### RetweetMetadata Information about the initial Tweet that was retweeted **Implements:** ScrapedLink **Fields:** - `id`: `String!` - Retweet ID - `text`: `String!` - Text of the original tweet - `thumbnails`: `[String!]!` - Thumbnails to media available in the link - `url`: `String!` - Link to original tweet - `user`: `RetweetUserMetadata!` - User who created the original tweet - `createdAt`: `DateTime!` - Date when the original tweet was created #### RetweetUserMetadata Information about the initial author of the Tweet that was retweeted **Fields:** - `avatar`: `String!` - Avatar of the user who created the original Tweet - `name`: `String!` - Name of the user who created the original Tweet - `username`: `String!` - Username of the user who created the original Tweet #### ScheduleV2 Posting schedule for a specific day of the week **Fields:** - `day`: `DayOfWeek!` - The day of the week: mon, tue, wed, thu, fri, sat, sun - `paused`: `Boolean!` - Indicates if this day is paused in the posting schedule. - `times`: `[String!]!` - The times the channel is scheduled to post on the day: HH:MM #### SearchInstagramAudioSuccess Success response for searching Instagram audio **Status:** ⚠️ Experimental **Fields:** - `audio`: `[InstagramAudio!]!` - Matching or trending audio assets #### ServiceConfiguration Service-level configuration for one supported `service` + `channelType`: its engagement capabilities and `content`. The channel-agnostic baseline — today the same fields as `ChannelConfiguration` minus `channelId` and the per-channel `authorizationStatus`. This overlap is incidental, not a contract: it is a distinct type so the service-level catalog can evolve independently of the per-channel config (either side may add fields the other doesn't share). **Status:** ⚠️ Experimental **Fields:** - `channelType`: `ChannelType!` - The channel type within the service (e.g. profile, page, business). - `content`: `[ContentConfiguration!]!` - Content config — one entry per content type, or a single entry grouping several types that share identical rules. Empty when none is exposed. - `engagement`: `[EngagementTypeConfiguration!]!` - Engagement capabilities — one entry per engagement type. Empty when engagement isn't supported for this service + channel type. - `service`: `Service!` - The social service this config describes (e.g. twitter, instagram). #### SubstackPostMetadata Substack post metadata **Implements:** CommonPostMetadata **Fields:** - `annotations`: `[Annotation!]!` - Annotations representing entities in the text - `linkAttachment`: `LinkAttachment` - Link attachment - `type`: `PostType!` - The channel-specific type of the post, eg, post, story, reel for Instagram #### SyncDataConfiguration Sync configuration for an engagement type — the realTime/polling `syncDataMechanism` plus optional forced-sync detail. **Status:** ⚠️ Experimental **Fields:** - `forcedSyncConfiguration`: `ForcedSyncConfiguration` - Forced-sync detail. A forced sync is a user-triggered (manual) refresh that fetches new items on demand instead of waiting for the next scheduled poll or push. `null` when this kind's sync mechanism has no forced-sync support. - `syncDataMechanism`: `SyncDataMechanism!` - How this engagement type syncs on this channel: `realTime` (push) or `polling` (pull). #### Tag Tag entity **Fields:** - `id`: `TagId!` - ObjectId of the tag - `color`: `String!` - Hex color for tag e.g '#F523F1' - `colorName`: `TagColorName` - Stable Buffer palette identifier derived from `color`. Clients should map this value to platform- and theme-specific presentation, falling back to `color` when this field is null or unsupported. Returns null for custom or unrecognized colors. - `isLocked`: `Boolean!` - Locked tag cannot be assigned to new items in the UI. A Tag is locked after a customer downgrades and has more tags than the free plan limit allows - `name`: `String!` - Name of the tag e.g 'Summer sales' #### ThreadedPost A post authored by the user which is posted to a thread. This is commonly used for long-format twitter and meta threads posts to allow authored content to span multiple threads. Threads are represented as a list of replies, each replying to the previous one. **Fields:** - `assets`: `[Asset!]!` - Media assets of the threaded post - `linkAttachment`: `LinkAttachment` - Use metadata.{service}.linkAttachment on the thread item instead. This field will be removed on December 15, 2026. *(Deprecated: Use metadata.{service}.linkAttachment on the thread item instead. This field will be removed on December 15, 2026.)* - `metadata`: `ThreadItemMetadata` - Service-specific fields for this threaded post - `text`: `String!` - The text body content of the threaded post #### ThreadItemBlueskyMetadata Bluesky fields on a single item of a thread **Fields:** - `linkAttachment`: `LinkAttachment` - Link attachment #### ThreadItemMetadata Service-specific fields on a single item of a thread. The populated key matches the post's service. **Fields:** - `bluesky`: `ThreadItemBlueskyMetadata` - Bluesky fields for this thread item - `threads`: `ThreadItemThreadsMetadata` - Threads fields for this thread item #### ThreadItemThreadsMetadata Threads fields on a single item of a thread **Fields:** - `linkAttachment`: `LinkAttachment` - Link attachment #### ThreadsPostMetadata Threads post metadata **Implements:** CommonPostMetadata, ThreadedPostMetadata **Fields:** - `annotations`: `[Annotation!]!` - Annotations representing entities in the text - `linkAttachment`: `LinkAttachment` - Link attachment - `locationId`: `String` - LocationId associated with the post - `locationName`: `String` - Location name associated with the post - `thread`: `[ThreadedPost!]!` - The list of threaded posts (not paginated) - `threadCount`: `Int!` - The number of threaded posts - `topic`: `String` - Topic associated with the post - `type`: `PostType!` - The channel-specific type of the post, eg, post, story, reel for Instagram #### TiktokMetadata Tiktok metadata **Fields:** - `defaultToReminders`: `Boolean!` - Indicates if we should default to reminder for Tiktok #### TiktokPostMetadata Tiktok post metadata **Implements:** CommonPostMetadata **Fields:** - `annotations`: `[Annotation!]!` - Annotations representing entities in the text - `isAiGenerated`: `Boolean!` - Whether the post discloses AI-generated content (TikTok video only) - `title`: `String` - The title of the TikTok post (for photo posts) - `type`: `PostType!` - The channel-specific type of the post, eg, post, story, reel for Instagram #### TwitterMetadata Twitter metadata **Fields:** - `subscriptionType`: `String` - Indicates the type of subscription the user has on Twitter #### TwitterPostMetadata Twitter post metadata **Implements:** CommonPostMetadata, ThreadedPostMetadata **Fields:** - `annotations`: `[Annotation!]!` - Annotations representing entities in the text - `isAiGenerated`: `Boolean!` - Whether the post discloses AI-generated content - `retweet`: `RetweetMetadata` - The details of the tweet being retweeted - `thread`: `[ThreadedPost!]!` - The list of threaded posts (not paginated) - `threadCount`: `Int!` - The number of threaded posts - `type`: `PostType!` - The channel-specific type of the post, eg, post, story, reel for Instagram #### UnauthorizedError Error returned when the user is not authorized to perform the action **Implements:** MutationError **Fields:** - `message`: `String!` - Error message #### UnexpectedError Error returned when unexpected error occurs **Implements:** MutationError **Fields:** - `message`: `String!` - Error message #### UpdateContentItemDraftFailure updateContentItemDraft failure. **Status:** ⚠️ Experimental **Implements:** MutationError **Fields:** - `errors`: `[UpdateContentItemDraftError!]!` - The recoverable errors behind this failure. - `message`: `String!` - Summary error message. #### UpdateContentItemDraftSuccess Successful updateContentItemDraft response. **Status:** ⚠️ Experimental **Fields:** - `contentItem`: `ContentItem!` - The updated content item. #### UpdateContentItemFailure updateContentItem failure. When any part of the input is invalid, nothing is changed. **Status:** ⚠️ Experimental **Implements:** MutationError **Fields:** - `errors`: `[InvalidInputError!]!` - The recoverable errors behind this failure. - `message`: `String!` - Summary error message. #### UpdateContentItemSuccess Successful updateContentItem response. **Status:** ⚠️ Experimental **Fields:** - `contentItem`: `ContentItem!` - The content item after the update. #### UpdatePostTemplateSuccess Successful result of an end user updating a post template. **Status:** 🧪 Preview **Fields:** - `postTemplate`: `PostTemplate!` - The updated post template. #### UserTag User tag in the image **Fields:** - `handle`: `String!` - The handle (username) of the tagged account, without the leading @. - `x`: `Float!` - Horizontal position of the tag as a normalized decimal float between 0.0 and 1.0 - the fraction of the image width from the left edge (0.5 is the horizontal center). - `y`: `Float!` - Vertical position of the tag as a normalized decimal float between 0.0 and 1.0 - the fraction of the image height from the top edge (0.5 is the vertical center). #### VideoAsset Video asset **Implements:** Asset **Fields:** - `id`: `ID` - The ID of the asset in the database - `mimeType`: `String!` - The MIME type of the asset - `source`: `String!` - URL to the file source - `thumbnail`: `String!` - URL to the static thumbnail of the asset - `type`: `AssetType!` - The type of the asset - `video`: `VideoMetadata!` - Video specific metadata #### VideoMetadata Video metadata **Fields:** - `audioCodec`: `String` - Audio codec - `containerFormat`: `String` - Video container format - `durationMs`: `Int!` - Video duration in seconds - `fileSize`: `Int` - Video fileSize in bytes - `frameRate`: `Int` - Video framerate - `height`: `Int!` - Video height in pixels - `isTranscodingRequired`: `Boolean!` - Whether the video needs to be transcoded before it can be broadcasted - `isVideoProcessing`: `Boolean!` - Whether the video is currently being processed (transcoding in progress) - `rotationDegree`: `Int` - Rotation degree - `thumbnailOffset`: `Int` - Offset of the thumbnail chosen for the video, in ms - `title`: `String` - Video title - `videoBitRate`: `Int` - Video bitrate in kbps - `videoCodec`: `String` - Video codec - `width`: `Int!` - Video width in pixels #### VoidMutationError Error implementation that allows clients to resolve the MutationError on mutations that do not currently have typed errors. This allows clients to automatically handle errors that may be added to a mutation in future. Do not directly throw this error, use a custom typed error instead **Implements:** MutationError **Fields:** - `message`: `String!` - Error message #### WeeklyPostingLimit Weekly posting limit for a channel **Fields:** - `limit`: `Int!` - The weekly posting limit for the channel - `scheduled`: `Int!` - The number of posts the channel has scheduled for this week - `sent`: `Int!` - The number of posts the channel has sent this week #### YoutubeCategory **Fields:** - `categoryId`: `String!` - `title`: `String!` #### YoutubeMetadata Youtube metadata **Fields:** - `defaultToReminders`: `Boolean!` - Indicates if we should default to reminder for Youtube #### YoutubePostMetadata Youtube post metadata **Implements:** CommonPostMetadata **Fields:** - `annotations`: `[Annotation!]!` - Annotations representing entities in the text - `category`: `YoutubeCategory` - Post category - `embeddable`: `Boolean!` - Indicates whether the video allows embedding - `isAiGenerated`: `Boolean!` - Whether the post discloses AI-generated content - `license`: `YoutubeLicense` - Video license - `madeForKids`: `Boolean!` - Indicates whether the video is suitable for kids - `notifySubscribers`: `Boolean!` - Indicates whether to notify subscribers on publish video - `privacy`: `YoutubePrivacy` - Privacy setting for post - `title`: `String` - Title of the Youtube post - `type`: `PostType!` - The channel-specific type of the post, eg, post, story, reel for Youtube ### Input Types #### AggregatedPostMetricsInput Input for the `aggregatedPostMetrics` query. **Fields:** - `channelIds`: `[ChannelId!]` - Optional list of channel IDs to filter by. When omitted (null), the aggregate spans every channel in the organization the actor has insights access to. When set to an empty array, no channels match and the result is empty. - `endDateTime`: `DateTime!` - End of the aggregation window. Consumers typically pass UTC midnight of the last calendar day in the window (the backend treats the range as inclusive of that day), for example `2026-01-31T00:00:00Z`. Date range is capped to 365 days. - `organizationId`: `OrganizationId!` - The organization ID - `startDateTime`: `DateTime!` - Start of the aggregation window. Consumers typically pass UTC midnight of the first calendar day in the window, for example `2026-01-01T00:00:00Z`. - `tags`: `TagComparator` - Filter to posts with specific tags. When omitted, all posts are included regardless of tags. #### AnnotationInputFacebook Annotation representing all the entities in the text **Fields:** - `content`: `String!` - The content of the annotation, e.g. '107509875938399' - `indices`: `[Int!]!` - The indices of the annotation in the text, e.g. [6, 9] (from 6 to 9 characters in the text) - `text`: `String!` - The text representation of the annotation, eg 'Buffer' - `url`: `String!` - The URL the annotation points to, e.g. https://www.facebook.com/107509875938399 #### AnnotationInputLinkedIn Annotation representing all the entities in the text **Fields:** - `id`: `String!` - The id of the annotation, e.g. 1521226 - `entity`: `String!` - The entity of the annotation, e.g. urn:li:organization:1521226 - `length`: `Int!` - The length of the annotation, e.g. 6 - `link`: `String!` - The link of the annotation, e.g. https://www.linkedin.com/company/bufferapp - `localizedName`: `String!` - The localized name of the annotation, e.g. Buffer - `start`: `Int!` - The start of the annotation, e.g. 5 - `vanityName`: `String!` - The vanity name of the annotation, e.g. bufferapp #### AssetInput A single entity's asset. Exactly one variant must be provided. **Fields:** - `document`: `DocumentAssetInput` - Document asset - `image`: `ImageAssetInput` - Image asset - `link`: `LinkAssetInput` - Use metadata.{service}.linkAttachment instead. This field will be removed on December 15, 2026. - `video`: `VideoAssetInput` - Video asset #### BlueskyPostMetadataInput Bluesky post metadata **Fields:** - `linkAttachment`: `LinkAttachmentInput` - Link attachment. Mutually exclusive with a non-empty `assets` array — input providing both is rejected. - `thread`: `[ThreadedPostInput!]` - The ordered list of posts that make up the thread (not paginated). This array is the source of truth for what gets published: every post in the thread, including the root post, must be provided here. Posts are published in order, each replying to the previous one. The first item is the root post and should match the top-level `text` on the post input. #### ChannelInput Input for the channel query **Fields:** - `id`: `ChannelId!` - The ID of the channel to be retrieved #### ChannelsFiltersInput Filter to pass when fetching channels. **Fields:** - `isLocked`: `Boolean` - If not defined, it returns all channels Else, if true, it only returns locked channels if false, it only returns not locked channels - `product`: `Product` - If not passed, it return all channels Else, it filters the channels based on what the product supports. #### ChannelsInput Input to pass when fetching channels. **Fields:** - `filter`: `ChannelsFiltersInput` - A list of option filters - passing null means we don't want to filter - `organizationId`: `OrganizationId!` - The Organization id to fetch channels for #### ConfigurationInput Input for the configuration query. **Fields:** - `organizationId`: `OrganizationId!` - The organization to return configuration for. #### ContentItemInput Input for fetching a single content item by id. **Fields:** - `id`: `ContentItemId!` - The unique identifier of the content item to fetch. #### ContentItemsFilter Filters for listing content items. Omitted fields do not filter. **Fields:** - `contentStatus`: `ContentItemStatus` - Only return content items with this content status. When omitted, content items in every status are returned. - `tags`: `TagComparator` - Only return content items carrying one of these tags, or, with `isEmpty`, content items with no tags at all. Matches the tags applied to the content item itself, not tags applied later to an individual post. - `targetDate`: `ContentItemsTargetDateFilter` - Only return content items matching this target date filter. #### ContentItemsInput Input for listing an organization's content items. **Fields:** - `filter`: `ContentItemsFilter` - Filters to apply. When omitted, all of the organization's content items are returned. - `organizationId`: `OrganizationId!` - Organization to list content items for. The caller must be a member of this organization. - `sort`: `[ContentItemsSortInput!]` - Sorting to apply, each entry breaking ties in the one before it. Defaults to newest first. A pagination cursor is only valid for the sort that produced it, so reset `after` to null whenever the sort changes. #### ContentItemsSortInput One sorting criterion for the content items query. List several to break ties. **Fields:** - `direction`: `SortDirection!` - The direction to sort by. - `field`: `ContentItemSortableKey!` - The field to sort by. #### ContentItemsTargetDateFilter Filter for a content item's target date. Provide exactly one field (enforced by @oneOf). **Fields:** - `presence`: `DateTimePresence` - Only return content items by whether a target date is set: `present` returns only dated items, `absent` only undated ones. - `range`: `DateTimeComparator` - Only return content items whose target date falls within this range. A range with no bounds returns every content item that has a target date. #### CreateContentItemDraftInput Input for createContentItemDraft. **Fields:** - `correlationId`: `Uuid` - Client-generated UUID that makes draft creation idempotent. A retry with the same UUID in the same organization returns the first content item in its current state. - `draft`: `DraftContentInput!` - The channel-less draft content to start with. - `organizationId`: `OrganizationId!` - Organization that will own the content item. - `tagIds`: `[TagId!]` - Tags to apply to this content item. Omit to create it with no tags. - `targetDate`: `DateTime` - Optional date indicating when this piece of content should go out. This is a planning aid only and does not schedule any posts. - `title`: `String` - Optional title describing what this piece of content is about. #### CreateContentItemInput Input for createContentItem. **Fields:** - `organizationId`: `OrganizationId!` - Organization that owns the content item and all variants created in it. - `posts`: `[CreatePostInput!]!` - The channel-specific post variants to create, one per channel. Provide at least one variant, and at most one variant per channel. - `tagIds`: `[TagId!]` - Tags to apply to this content item. Omit to create it with no tags. - `targetDate`: `DateTime` - Optional date indicating when this piece of content should go out. This is a planning aid only and does not schedule any posts. - `title`: `String` - Optional title describing what this piece of content is about. #### CreateIdeaInput createIdea input type **Fields:** - `content`: `IdeaContentInput!` - Content and metadata for the new idea - `cta`: `String` - Call-to-action identifier for analytics tracking - `group`: `IdeaGroupInput` - Group placement (null for unassigned group) - `organizationId`: `ID!` - Organization ID that will own the idea - `templateId`: `String` - Template ID used to create the idea #### CreatePostInput Create post's request input. Note: `metadata.{service}.linkAttachment` is mutually exclusive with a non-empty `assets` array. Input providing both is rejected. **Fields:** - `aiAssisted`: `Boolean` - If this post was created with the help of AI - `assets`: `[AssetInput!]!` (default: []) - Ordered list of assets on this post. - `channelId`: `ChannelId!` - Channel's Id for which we want to create the post - `draftId`: `DraftId` - Is set when the Post is generated from a Draft - `dueAt`: `DateTime` - Date when the post is scheduled to be published - `ideaId`: `IdeaId` - Is set when the Post is generated from an Idea - `metadata`: `PostInputMetaData` - Metadata of the post which differs based on the social network/service - `mode`: `ShareMode!` - How the post is being scheduled. - `needsApproval`: `Boolean!` (default: false) - Submit the post for approval instead of scheduling it. A post submitted for approval is always a draft, so this conflicts with turning `saveToDraft` off. Only valid when your posting policy on the target channel requires approval. - `saveToDraft`: `Boolean` - If true, saves the post as a draft instead of scheduling it. When saving as draft: - Post status will be 'draft' instead of 'buffer' - Posting limits are not checked - The post will not be published until explicitly scheduled - `schedulingType`: `SchedulingType!` - Scheduling type to indicate notification publishing or automatic publishing - `source`: `String` - source where the composer was initiated from, used for tracking. - `tagIds`: `[TagId!]` - List of tag IDs - `text`: `String` - Text content of the Post. Note: for threaded posts, this needs to match the first item in the `thread` array. #### CreatePostTemplateInput Input for an end user creating a post template. Buffer-curated taxonomy fields are server-defaulted; setting them is only available to official Buffer clients. **Fields:** - `body`: `String!` - The main content body of the template, may contain {{placeholders}}. - `description`: `String` - A short user-facing description of the template. Nullable for backwards-compat at the GraphQL boundary — the resolver rejects null/empty values with a clear input error so the underlying storage contract (non-empty string) is still honored. - `emoji`: `String` - The emoji associated with the template. - `organizationId`: `OrganizationId!` - Organization the template belongs to. The caller must be a member of this organization. For `internal` visibility this is the team scope; for `private` it's recorded on the template but does not affect visibility. - `title`: `String!` - The title of the template. - `visibility`: `PostTemplateVisibility` - Defaults to `private` if omitted. `public` is rejected — it is only available to official Buffer clients. #### DailyPostingLimitsInput Input for the dailyPostingLimits query. **Fields:** - `channelIds`: `[ChannelId!]!` - List of channel IDs to check limits for. All channels must belong to the same organization. - `date`: `DateTime` - The date to check limits for. Defaults to today if not provided. #### DateTimeComparator Comparator for filtering by date **Fields:** - `end`: `DateTime` - Include results with dates equal to or before the specified date - `start`: `DateTime` - Include results with dates equal to or after the specified date #### DeleteContentItemInput Input for deleteContentItem. **Fields:** - `id`: `ContentItemId!` - The content item to delete. #### DeletePostInput deletePost mutation deletes a post by id. **Fields:** - `id`: `PostId!` - Post id to delete. #### DeletePostTemplateInput Input for an end user deleting a post template. **Fields:** - `id`: `PostTemplateId!` - The ID of the template to delete. #### DocumentAssetInput Document asset **Fields:** - `thumbnailUrl`: `String!` - Document thumbnail URL - `title`: `String!` - Document title - `url`: `String!` - Document URL #### DraftContentInput A channel-less draft to save on a content item. **Fields:** - `aiAssisted`: `Boolean!` (default: false) - Set to true when the draft content was written with the help of AI. - `assets`: `[AssetInput!]!` (default: []) - Images, videos, or documents to attach to the draft, in display order. - `text`: `String!` - The written content of the draft. Can be empty when the draft holds at least one asset. #### EditPostInput Edit post's request input. Note: `metadata.{service}.linkAttachment` is mutually exclusive with a non-empty `assets` array. Input providing both is rejected. **Fields:** - `id`: `PostId!` - ID of the post to edit - `aiAssisted`: `Boolean` - If this post was edited with the help of AI - `approvalChange`: `PostApprovalChange` - Change the post's approval state alongside this edit. Leave unset to keep the post's current approval state. Only valid when your posting policy on the post's channel requires approval, and only on your own drafts. Asking for the state the post is already in does nothing. - `assets`: `[AssetInput!]` - Ordered list of assets on this post. Omit to preserve the existing list, pass an empty array to clear it - `draftId`: `DraftId` - Is set when the Post is generated from a Draft - `dueAt`: `DateTime` - Date when the post is scheduled to be published - `ideaId`: `IdeaId` - Is set when the Post is generated from an Idea - `metadata`: `PostInputMetaData` - Metadata of the post which differs based on the social network/service - `mode`: `ShareMode` - How the post is being scheduled. Omit the field or pass null to make no scheduling change — null does not clear or reset the schedule: a scheduled post keeps its current share mode, queue slot, and any custom time, and the edit applies only the other provided fields. Pass a non-null ShareMode to apply that mode. - `saveToDraft`: `Boolean` - If true, saves the post as a draft instead of keeping it scheduled. When saving as draft: - Post status will be 'draft' instead of 'buffer' - The post will not be published until explicitly scheduled - `schedulingType`: `SchedulingType` - Scheduling type to indicate notification publishing or automatic publishing. Omit it, or send null, to leave the post publishing the way it already does. - `source`: `String` - source where the composer was initiated from, used for tracking. - `tagIds`: `[TagId!]` - tags - `text`: `String` - Text content of the Post. Omit the field to keep the current text; pass an empty string or null to clear it. Note: for threaded posts, this needs to match the first item in the `thread` array. #### FacebookPostMetadataInput Facebook post metadata **Fields:** - `annotations`: `[AnnotationInputFacebook!]` - Annotations representing entities in the text - `firstComment`: `String` - Facebook post's first comment - `linkAttachment`: `LinkAttachmentInput` - Link attachment. Mutually exclusive with a non-empty `assets` array — input providing both is rejected. - `type`: `PostTypeFacebook!` - The channel-specific type of the post, eg, post, story, reel for Facebook #### GoogleBusinessEventMetaDataInput Metadata for a GBP post that is an event **Fields:** - `button`: `GoogleBusinessPostActionType` - Action button. Optional: a post with no button, or `none`, publishes without a call-to-action. On edit, omitting it preserves the existing value. - `endDate`: `DateTime` - End date of the event. Required on create; optional on edit (omitted preserves existing value). - `isFullDayEvent`: `Boolean!` - Indicate whether the event has a start or end time. - `link`: `String` - Link to the action - `startDate`: `DateTime` - Start date of the event. Required on create; optional on edit (omitted preserves existing value). - `title`: `String` - Title of the event. Required on create; optional on edit (omitted preserves existing value). #### GoogleBusinessOfferMetaDataInput Metadata for a GBP post that is an offer **Fields:** - `code`: `String` - Coupon code for the offer - `endDate`: `DateTime` - End date of the offer. Required on create; optional on edit (omitted preserves existing value). - `link`: `String` - Link to the offer - `startDate`: `DateTime` - Start date of the offer. Required on create; optional on edit (omitted preserves existing value). - `terms`: `String` - Terms and Conditions - `title`: `String` - Title of the offer. Required on create; optional on edit (omitted preserves existing value). #### GoogleBusinessPostMetadataInput Google Business Profile post metadata @deprecated: pending proposal for specific GBP post types: update, offer and event metadata types **Fields:** - `detailsEvent`: `GoogleBusinessEventMetaDataInput` - Details of the Event metadata - `detailsOffer`: `GoogleBusinessOfferMetaDataInput` - Details of the Offer metadata - `detailsWhatsNew`: `GoogleBusinessWhatsNewMetaDataInput` - Details of the Whats new metadata - `title`: `String` - Title if available in the given GBP post type: event and offer - `type`: `PostTypeGoogleBusiness!` - The channel-specific type of the post, eg, post, offer, event for Google Business Profile #### GoogleBusinessWhatsNewMetaDataInput Metadata for a GBP post of type Whats new **Fields:** - `button`: `GoogleBusinessPostActionType` - Action button. Optional: a post with no button, or `none`, publishes without a call-to-action. On edit, omitting it preserves the existing value. - `link`: `String` - Link to the action #### IdeaContentInput content input for creating/updating an idea **Fields:** - `aiAssisted`: `Boolean` - Whether AI tools were used in creation - `date`: `DateTime` - Target date for the idea, often used for planning publish schedules - `media`: `[IdeaMediaInput!]` - List of media items to attach - `services`: `[Service!]` - Services associated with the idea for targeting specific platforms - `tags`: `[TagInput!]` - Tags to categorize the idea - `text`: `String` - Main body text or description - `title`: `String` - Title or headline of the idea #### IdeaGroupInput idea group input for create/update **Fields:** - `groupId`: `ID` - Target group ID (null for unassigned group) - `placeAfterId`: `ID` - ID of idea to place after (null for top position) #### IdeaGroupsInput Input type for retrieving idea groups by organization ID. **Fields:** - `organizationId`: `ID!` - Unique identifier for the organization. #### IdeaMediaInput **Fields:** - `url`: `String!` - The URL of the media - `alt`: `String` - Alternative text for the media - `thumbnailUrl`: `String` - Thumbnail URL for the media - `type`: `MediaType!` - The type of media (image, gif, video, link, document, unsupported). Note: 'video' is not supported via public API - `size`: `Int` - The size of the media in bytes - `source`: `IdeaMediaSourceInput` - Source information for the media #### IdeaMediaSourceInput Input type for the source information of media attached to an idea **Fields:** - `name`: `String!` - `id`: `String` - `trigger`: `String` - `author`: `String` - for unsplash only - `authorUrl`: `String` #### IdeasGroupFilter Selects which ideas to return by group membership. Exactly one field must be provided (enforced by @oneOf). To return ideas from all groups, omit `groupFilter` on IdeasInput rather than setting a field here. **Fields:** - `groups`: `[ID!]` - Return only ideas that belong to these specific groups (union/OR). - `membership`: `IdeaGroupMembership` - Return ideas by a group-membership bucket rather than by specific group IDs. #### IdeasInput Filtering criteria for the ideas list. **Fields:** - `groupFilter`: `IdeasGroupFilter` - Filter ideas by group membership. Omit this field entirely to return ideas across all groups. - `organizationId`: `OrganizationId!` - The organization to fetch ideas from. - `tagsFilter`: `TagComparator` - Filter ideas by tags using TagComparator. #### ImageAssetInput Image asset **Fields:** - `metadata`: `ImageMetadataInput` - Image specific metadata - `thumbnailUrl`: `String` - URL to the static thumbnail of the asset - `url`: `String!` - URL to the file source #### ImageDimensionsInput Image dimensions **Fields:** - `height`: `Int!` - Image height in pixels - `width`: `Int!` - Image width in pixels #### ImageMetadataInput Image metadata **Fields:** - `altText`: `String!` - Alternative text for accessibility - `animatedThumbnail`: `String` - Animated thumbnail URL - `dimensions`: `ImageDimensionsInput` - Ignored — the API resolves image dimensions itself. - `userTags`: `[UserTagInput!]` - Accounts to tag at specific points on the image. Each tag's x/y position uses normalized 0.0-1.0 coordinates - see UserTagInput. #### InstagramAudioInput Input for refreshing a single Instagram audio asset **Fields:** - `audioId`: `String!` - Meta audio asset ID - `channelId`: `ChannelId!` - Instagram channel used to authorize the refresh #### InstagramGeolocationInput Instagram Geolocation **Fields:** - `id`: `String` - The id of this location - `text`: `String` - The name of this location #### InstagramPostMetadataInput Instagram post metadata **Fields:** - `firstComment`: `String` - Instagram post's first comment - `geolocation`: `InstagramGeolocationInput` - Geolocation of the post - `isAiGenerated`: `Boolean` - Whether the post discloses AI-generated content - `link`: `String` - Shop Grid link for the post - `shouldShareToFeed`: `Boolean!` - Indicates whether post should be shared to feed - `stickerFields`: `InstagramStickerFieldsInput` - Sticker fields for reminder-based publishing - `type`: `PostType!` - The channel-specific type of the post, eg, post, story, reel for Instagram #### InstagramStickerFieldsInput Instagram fields for reminder-based publishing. Upon the reminder for publishing, the user is prompted to copy and paste these fields into the Instagram app to complete the post. **Fields:** - `music`: `String` - Placeholder text for the post's music - `other`: `String` - Additional field for any other post content - `products`: `String` - Placeholder text for the post's linked products - `text`: `String` - Text for the Story or Reel - `topics`: `String` - Placeholder text for the post's topics (Reels only) #### LinkAssetInput Link attached to the post **Fields:** - `description`: `String` - Description of the link - `thumbnailUrl`: `String` - Thumbnail URL of the link - `title`: `String` - Title of the link - `url`: `String!` - URL to the link #### LinkAttachmentInput Link attachment **Fields:** - `description`: `String` - Description shown on the link card - `thumbnail`: `LinkAttachmentThumbnailInput` - Thumbnail shown on the link card - `title`: `String` - Title shown on the link card - `url`: `String!` - URL that the link asset has been built from #### LinkAttachmentThumbnailInput Thumbnail of a link attachment **Fields:** - `url`: `String!` - URL of the thumbnail image #### LinkedInPostMetadataInput LinkedIn post metadata **Fields:** - `annotations`: `[AnnotationInputLinkedIn!]` - Annotations representing entities in the text - `firstComment`: `String` - LinkedIn post's first comment - `linkAttachment`: `LinkAttachmentInput` - Link attachment. Mutually exclusive with a non-empty `assets` array — input providing both is rejected. #### MastodonPostMetadataInput Mastodon post metadata **Fields:** - `spoilerText`: `String` - Spoiler text hiding the root text of this post - `thread`: `[ThreadedPostInput!]` - The ordered list of posts that make up the thread (not paginated). This array is the source of truth for what gets published: every post in the thread, including the root post, must be provided here. Posts are published in order, each replying to the previous one. The first item is the root post and should match the top-level `text` on the post input. #### MovePostInQueueInput movePostInQueue mutation moves a queued post to the top or bottom of its channel's queue. **Fields:** - `id`: `PostId!` - ID of the post to move. - `position`: `QueuePosition!` - Target position within the channel's queue. #### OrganizationFilterInput Allow retrieving a specific Organization **Fields:** - `organizationId`: `String!` #### PinterestPostMetadataInput Pinterest post metadata **Fields:** - `boardServiceId`: `String` - The board ID of the Pin, can be obtained when fetching the channel details with the following query: ``` query GetChannelWithSubprofiles { channel(input: { id: "[CHANNEL_ID_HERE]" }) { metadata { ... on PinterestMetadata { boards { serviceId } } } } } ``` Required on create; optional on edit (omitted preserves existing board). - `title`: `String` - The title of the Pin - `url`: `String` - The Pin destination link #### PostInput Input for the post query **Fields:** - `id`: `PostId!` - The ID of the post to be retrieved #### PostInputMetaData Metadata of the post which differs based on the social network/service **Fields:** - `bluesky`: `BlueskyPostMetadataInput` - Metadata for Bluesky post - `facebook`: `FacebookPostMetadataInput` - Metadata for Facebook post - `google`: `GoogleBusinessPostMetadataInput` - Metadata for Google Business Profile post - `instagram`: `InstagramPostMetadataInput` - Metadata for Instagram post - `linkedin`: `LinkedInPostMetadataInput` - Metadata for LinkedIn post - `mastodon`: `MastodonPostMetadataInput` - Metadata for Mastodon post - `pinterest`: `PinterestPostMetadataInput` - Metadata for Pinterest post - `substack`: `SubstackPostMetadataInput` - Metadata for Substack post - `threads`: `ThreadsPostMetadataInput` - Metadata for Threads post - `tiktok`: `TikTokPostMetadataInput` - Metadata for TikTok post - `twitter`: `TwitterPostMetadataInput` - Metadata for Twitter post - `youtube`: `YoutubePostMetadataInput` - Metadata for Youtube post #### PostsFiltersInput Filter to apply to the posts query **Fields:** - `channelIds`: `[ChannelId!]` - When set, it will filter posts by channel - `dueAt`: `DateTimeComparator` - When set, it will filter posts by their scheduled posting date - `dueAtPresence`: `DateTimePresence` - When set, it will filter posts by whether their scheduled posting date exists. `absent` cannot be combined with `dueAt`, because absent dates cannot also match a date range. - `endDate`: `DateTime` - When set, it will return posts with createdAt or dueAt date before endDate - `postTypes`: `[PostType!]` - When set, it will filter posts by format. `post` is a fallback bucket rather than one stored format: it matches every post the other formats do not claim, which is what `Post.metadata.type` reports for the same post. `carousel` and `thread` are rejected, because no stored value resolves to them. - `startDate`: `DateTime` - When set, it will return posts with createdAt or dueAt date after startDate - `status`: `[PostStatus!]` - When set, it will filter posts by status - `tagIds`: `[TagId!]` - When set, it will filter posts by tag - `tags`: `TagComparator` - Filter posts by tags. Supports specific tags, untagged posts, or union of both. - `createdAt`: `DateTimeComparator` - When set, it will filter posts by the date they were created #### PostsInput Input for the posts query **Fields:** - `filter`: `PostsFiltersInput` - The filters to apply to the posts query - `organizationId`: `OrganizationId!` - The Organization id to fetch posts for - `sort`: `[PostSortInput!]` - The sort to apply to the posts results #### PostSortInput Sort order of post results. List multiple to create tie-breaking order. **Fields:** - `direction`: `SortDirection!` - The direction to sort by. - `field`: `PostSortableKey!` - The field to sort by. #### PostTemplateInput Input for fetching a single post template by ID. **Fields:** - `id`: `PostTemplateId!` - The unique identifier of the template to fetch. #### PostTemplatesFilterInput Filters for the template library. Visibility narrows the actor-scoped result set; it can never widen it. **Fields:** - `visibility`: `PostTemplateVisibility` - Narrow the result to a single visibility scope. Omit to receive the union of: public templates, internal templates from the supplied organization, and private templates from the actor's account. #### PostTemplatesInput Input for fetching templates accessible to the current actor for the template library. A caller can never reach templates outside their own scope — the server pins `private` to the actor's account and `internal` to the supplied `organizationId` regardless of input. **Fields:** - `filter`: `PostTemplatesFilterInput` - Optional filters to narrow the template list. - `organizationId`: `OrganizationId!` - Organization to scope `internal`-visibility templates to. The caller must be a member of this organization. #### PromoteContentItemDraftToPostsInput Input for promoteContentItemDraftToPosts. **Fields:** - `id`: `ContentItemId!` - The content item to promote. - `posts`: `[CreatePostInput!]!` - The channel-specific posts to create, one per channel. Provide at least one post, and at most one post per channel. - `tagIds`: `[TagId!]` - Tags to apply to this content item. Omit to keep the current tags. An empty list or null removes them all. #### RetweetMetadataInput Information about the initial Tweet that was retweeted **Fields:** - `id`: `String!` - Retweet ID - `comment`: `String` - Optional user comment shown above the embedded retweet #### SearchInstagramAudioInput Input for searching Instagram audio for one channel. **Fields:** - `audioType`: `InstagramAudioType!` - Music or original sound catalog - `channelId`: `ChannelId!` - Instagram channel to search audio for - `query`: `String!` - Search text. Required. Use trendingInstagramAudio for trending results. #### SubstackPostMetadataInput Substack post metadata **Fields:** - `linkAttachment`: `LinkAttachmentInput` - Link attachment. Mutually exclusive with a non-empty `assets` array — input providing both is rejected. #### TagComparator Comparator for filtering by tags **Fields:** - `in`: `[TagId!]!` - Include results that have any of the specified tags (union/OR). - `isEmpty`: `Boolean!` (default: false) - When true, include results that have no tags assigned. Can be combined with 'in' for union filtering. Defaults to false if not specified. #### TagInput Input type for tag information used in idea creation **Fields:** - `id`: `ID!` - `name`: `String!` - `color`: `String!` #### ThreadedPostInput A post authored by the user which is posted to a thread. This is commonly used for long-format twitter and meta threads posts to allow authored content to span multiple threads. Threads are represented as a list of replies, each replying to the previous one. The first item in the list is the root post of the thread and should match the top-level `text` on the post input; the remaining items are replies. **Fields:** - `assets`: `[AssetInput!]!` (default: []) - Ordered list of assets on this threaded post - `metadata`: `ThreadItemMetadataInput` - Service-specific fields for this threaded post - `text`: `String` - The text body content of the threaded post #### ThreadItemBlueskyMetadataInput Bluesky fields on a single item of a thread **Fields:** - `linkAttachment`: `LinkAttachmentInput` - Link attachment. Mutually exclusive with a non-empty `assets` array — input providing both is rejected. #### ThreadItemMetadataInput Service-specific fields on a single item of a thread. The key must match the channel's service; input under any other key is rejected. **Fields:** - `bluesky`: `ThreadItemBlueskyMetadataInput` - Bluesky fields for this thread item - `threads`: `ThreadItemThreadsMetadataInput` - Threads fields for this thread item #### ThreadItemThreadsMetadataInput Threads fields on a single item of a thread **Fields:** - `linkAttachment`: `LinkAttachmentInput` - Link attachment. Mutually exclusive with a non-empty `assets` array — input providing both is rejected. #### ThreadsPostMetadataInput Threads post metadata **Fields:** - `linkAttachment`: `LinkAttachmentInput` - Link attachment. Mutually exclusive with a non-empty `assets` array — input providing both is rejected. - `locationId`: `String` - LocationId associated with the post - `locationName`: `String` - Location name associated with the post - `thread`: `[ThreadedPostInput!]` - The ordered list of posts that make up the thread (not paginated). This array is the source of truth for what gets published: every post in the thread, including the root post, must be provided here. Posts are published in order, each replying to the previous one. The first item is the root post and should match the top-level `text` on the post input. - `topic`: `String` - Topic associated with the post - `type`: `PostType` - The type of the post #### TikTokPostMetadataInput TikTok post metadata **Fields:** - `isAiGenerated`: `Boolean` - Whether the post discloses AI-generated content (TikTok video only) - `title`: `String` - The title of the TikTok post (for photo posts) #### TrendingInstagramAudioInput Input for trending Instagram audio for one channel. **Fields:** - `audioType`: `InstagramAudioType!` - Music or original sound catalog - `channelId`: `ChannelId!` - Instagram channel to load trending audio for #### TwitterPostMetadataInput Twitter post metadata **Fields:** - `isAiGenerated`: `Boolean` - Whether the post discloses AI-generated content (original tweets only, never retweets) - `retweet`: `RetweetMetadataInput` - The details of the tweet being retweeted - `thread`: `[ThreadedPostInput!]` - The ordered list of posts that make up the thread (not paginated). This array is the source of truth for what gets published: every post in the thread, including the root post, must be provided here. Posts are published in order, each replying to the previous one. The first item is the root post and should match the top-level `text` on the post input. #### UpdateContentItemDraftInput Input for updateContentItemDraft. **Fields:** - `id`: `ContentItemId!` - The content item whose channel-less draft is replaced. - `draft`: `DraftContentInput!` - The new draft content. Replaces the current draft in full. - `tagIds`: `[TagId!]` - Tags to apply to this content item. Omit to keep the current tags. An empty list or null removes them all. - `targetDate`: `DateTime` - Date indicating when this piece of content should go out. This is a planning aid only and does not schedule any posts. Omit to preserve the existing target date. Null clears it. #### UpdateContentItemInput Input for updateContentItem. Only the fields provided are changed. Omitted fields keep their current value. **Fields:** - `id`: `ContentItemId!` - The content item to update. - `targetDate`: `DateTime` - Omit to preserve the existing target date. Null clears it. - `title`: `String` - Omit to preserve the existing title. Null clears it. #### UpdatePostTemplateInput Input for an end user updating a post template. Buffer-curated taxonomy fields are ignored; setting them is only available to official Buffer clients. **Fields:** - `id`: `PostTemplateId!` - The ID of the template to update. - `body`: `String` - The main content body of the template, may contain {{placeholders}}. - `description`: `String` - A short user-facing description of the template. - `emoji`: `String` - The emoji associated with the template. - `title`: `String` - The title of the template. - `visibility`: `PostTemplateVisibility` - `public` is rejected — it is only available to official Buffer clients. #### UserTagInput User tag in the image **Fields:** - `handle`: `String!` - The handle (username) of the account to tag, without the leading @. - `x`: `Float!` - Horizontal position of the tag as a normalized decimal float between 0.0 and 1.0 - the fraction of the image width from the left edge (0.5 is the horizontal center). Pass a number, not a string, and do not use pixel coordinates; to convert, divide the pixel X by the image width. - `y`: `Float!` - Vertical position of the tag as a normalized decimal float between 0.0 and 1.0 - the fraction of the image height from the top edge (0.5 is the vertical center). Pass a number, not a string, and do not use pixel coordinates; to convert, divide the pixel Y by the image height. #### VideoAssetInput Video asset **Fields:** - `metadata`: `VideoMetadataInput` - Video specific metadata - `thumbnailUrl`: `String` - Do not use: social networks do not accept custom video thumbnail images, and the API rejects video assets that set this field. To choose the video thumbnail, set `metadata.thumbnailOffset` to select a frame from the video (supported for Instagram, TikTok, and Pinterest only). - `url`: `String!` - URL to the file source #### VideoMetadataInput Video metadata **Fields:** - `thumbnailOffset`: `Int` - Offset of the thumbnail chosen for the video, in ms - `title`: `String` - Video title #### YoutubePostMetadataInput Youtube post metadata **Fields:** - `categoryId`: `String` - Youtube Category ID, one ID of this list: ID: 1 -> Film & Animation ID: 2 -> Autos & Vehicles ID: 10 -> Music ID: 15 -> Pets & Animals ID: 17 -> Sports ID: 19 -> Travel & Events ID: 20 -> Gaming ID: 22 -> People & Blogs ID: 23 -> Comedy ID: 24 -> Entertainment ID: 25 -> News & Politics ID: 26 -> Howto & Style ID: 27 -> Education ID: 28 -> Science & Technology ID: 29 -> Nonprofits & Activism Required on create; optional on edit (omitted preserves existing value). - `embeddable`: `Boolean` - Indicates whether the video allows embedding (default: true) - `isAiGenerated`: `Boolean` - Whether the post discloses AI-generated content - `license`: `YoutubeLicense` - Video license (default: youtube) - `madeForKids`: `Boolean` - Indicates whether the video is suitable for kids (default: false) - `notifySubscribers`: `Boolean` - Indicates whether to notify subscribers on publish video (default: true) - `privacy`: `YoutubePrivacy` - Privacy setting for post (default: public) - `title`: `String` - Title of the Youtube post. Required on create; optional on edit (omitted preserves existing value). ### Interfaces #### Asset Asset interface with common fields **Fields:** - `id`: `ID` - The ID of the asset in the database - `mimeType`: `String!` - The MIME type of the asset - `source`: `String!` - URL to the file source - `thumbnail`: `String!` - URL to the static thumbnail of the asset - `type`: `AssetType!` - The type of the asset #### CommonPostMetadata Common properties for all post metadata types **Fields:** - `annotations`: `[Annotation!]!` - Annotations representing entities in the text - `type`: `PostType!` - The channel-specific type of the post, eg, post, story, reel for Instagram #### MutationError Base Mutation Error type **Fields:** - `message`: `String!` - Error message #### MutationSuccess Base Mutation Success type Used when we have a success response with no data, we return this type with empty string **Fields:** - `_empty`: `String!` - The value is alwaus an empty string '' Note: GraphQL doesn't allow types with no fields, so we have to add this field #### ScrapedLink Link data for link preview **Fields:** - `text`: `String!` - Description for the scraped link - `thumbnails`: `[String!]!` - Thumbnails of media available in the link - `url`: `String!` - URL that the link asset has been built from #### ThreadedPostMetadata Common properties for all posts that support threaded replies. See ThreadedPost for more details. **Fields:** - `thread`: `[ThreadedPost!]!` - The list of threaded posts (not paginated) - `threadCount`: `Int!` - The number of threaded posts #### ValidationRule A validation rule constraining how a content property may be used. Each concrete rule carries only the fields it needs; new constraint kinds are additive (a new type implementing this interface). Clients switch on `__typename`. **Status:** ⚠️ Experimental **Fields:** - `property`: `ContentProperty!` - The content property this rule constrains. ### Unions #### ChannelMetadata Metadata or settings about the channel depending on the service type **Possible types:** InstagramMetadata | TiktokMetadata | YoutubeMetadata | PinterestMetadata | MastodonMetadata | BlueskyMetadata | GoogleBusinessMetadata | FacebookMetadata | TwitterMetadata | LinkedInMetadata #### ContentItemBody A content item contains either a channel-less draft or channel-specific posts, but never both. **Possible types:** DraftContent | PostContent #### CreateContentItemDraftPayload All possible responses for createContentItemDraft. **Possible types:** CreateContentItemDraftSuccess | CreateContentItemDraftFailure #### CreateContentItemError A recoverable error that prevented a content item from being created. Variant errors identify the specific channel they apply to; the others apply to the request as a whole. **Possible types:** InvalidInputError | NotFoundError | LimitReachedError | CreateContentItemVariantInvalidInputError | CreateContentItemVariantLimitReachedError | CreateContentItemVariantNotFoundError #### CreateContentItemPayload All possible responses for createContentItem. **Possible types:** CreateContentItemSuccess | CreateContentItemFailure #### CreateIdeaPayload createIdea response (including errors) **Possible types:** Idea | IdeaResponse | InvalidInputError | UnauthorizedError | UnexpectedError | LimitReachedError #### CreatePostTemplatePayload Result of an end user creating a post template. **Possible types:** CreatePostTemplateSuccess | VoidMutationError #### DeleteContentItemPayload All possible responses for deleteContentItem. **Possible types:** EmptySuccess | DeleteContentItemFailure #### DeletePostPayload All possible response types for the deletePost mutation. **Possible types:** DeletePostSuccess | VoidMutationError #### DeletePostTemplatePayload Result of an end user deleting a post template. **Possible types:** EmptySuccess | VoidMutationError #### EngagementMetadata Network-specific engagement metadata. Single-member union for now; grows additively as more networks contribute their own shape. **Possible types:** GoogleBusinessEngagementMetadata #### GoogleBusinessPostDetails GoogleBusiness Metadata details **Possible types:** GoogleBusinessWhatsNewMetaData | GoogleBusinessOfferMetaData | GoogleBusinessEventMetaData #### InstagramAudioPayload Payload returned when refreshing Instagram audio metadata **Possible types:** InstagramAudioSuccess | ChannelRefreshRequired #### MovePostInQueuePayload All possible response types that can be returned by movePostInQueue mutation. **Possible types:** PostActionSuccess | VoidMutationError #### PostActionPayload Create post's request response payload. **Possible types:** PostActionSuccess | NotFoundError | UnauthorizedError | UnexpectedError | RestProxyError | LimitReachedError | InvalidInputError #### PostMetadata Post metadata union type. Contains all possible types of post metadata. **Possible types:** InstagramPostMetadata | FacebookPostMetadata | LinkedInPostMetadata | TwitterPostMetadata | PinterestPostMetadata | GoogleBusinessPostMetadata | YoutubePostMetadata | MastodonPostMetadata | TiktokPostMetadata | ThreadsPostMetadata | BlueskyPostMetadata | SubstackPostMetadata #### PromoteContentItemDraftToPostsError A recoverable error that prevented a content item draft from being promoted. Post errors identify the specific channel they apply to; the others apply to the request as a whole. **Possible types:** ContentItemStateError | InvalidInputError | PostChannelNotFoundError | PostInvalidInputError | PostLimitReachedError #### PromoteContentItemDraftToPostsPayload All possible responses for promoteContentItemDraftToPosts. **Possible types:** PromoteContentItemDraftToPostsSuccess | PromoteContentItemDraftToPostsFailure #### ReactionVisual How to render a reaction. The client switches on `__typename` purely as a render strategy (built-in icon vs network image) — never on the network or the reaction identity. **Possible types:** IconVisual | ImageVisual #### SearchInstagramAudioPayload Payload returned when searching Instagram audio **Possible types:** SearchInstagramAudioSuccess | ChannelRefreshRequired #### UpdateContentItemDraftError A recoverable error that prevented a content item draft from being updated. **Possible types:** InvalidInputError | ContentItemStateError #### UpdateContentItemDraftPayload All possible responses for updateContentItemDraft. **Possible types:** UpdateContentItemDraftSuccess | UpdateContentItemDraftFailure #### UpdateContentItemPayload All possible responses for updateContentItem. **Possible types:** UpdateContentItemSuccess | UpdateContentItemFailure #### UpdatePostTemplatePayload Result of an end user updating a post template. **Possible types:** UpdatePostTemplateSuccess | VoidMutationError ### Enums #### AiFeature AI features supported for an engagement type. Grows additively. **Status:** ⚠️ Experimental **Values:** - `insights` - `suggestions` #### AnnotationType List of possible types for an annotation **Values:** - `annotation` - `cashtag` - `hashtag` - `mention` - `url` #### AssetType Asset types **Values:** - `document` - `image` - `video` #### AuthorizationStatus Represents the authorization evaluation result for a feature. **Status:** ⚠️ Experimental **Values:** - `needsRefresh` - Authorization exists but requires token refresh - `needsUpgrade` - The client version is below the minimum required to access this feature. The client should prompt the user to upgrade. - `notEnoughData` - Insufficient data to determine authorization state. Typically indicates missing scopes or webhook metadata. - `notEnoughRights` - The connected platform user's role/permissions are insufficient for this feature, so reconnecting with the same user cannot fix it. Clients should prompt the user to contact an administrator of the platform account (or have one connect the channel) instead of offering a reconnect. - `ok` - Authorization is valid and the feature can be used. #### ChannelAction List of possible actions that can be performed on a Channel **Values:** - `backfillChannel` - `exportInsights` - `manageCapabilities` - `manageComments` - `manageIntegrations` - `managePostingSchedule` - `manageUpdates` - `publishStartPage` - `readUpdates` - `reconnectChannel` - `removeChannel` - `viewCapabilities` - `viewChannel` - `viewComments` - `viewInsights` - `viewPublish` - `viewUpdates` *(Deprecated: Renamed to `viewPublish`. Still emitted for backward-compat with clients that gate on `viewUpdates`; use `viewPublish` to gate the Publish UI or `readUpdates` to gate reading post data. Will be removed once no client reads it.)* #### ChannelType Channel is a representation of a social media account or page that can be connected to Buffer. **Values:** - `account` - `business` - `channel` - `group` - `page` - `profile` #### ConfigurationContentType The kinds of content a channel can compose. Grows additively. **Status:** ⚠️ Experimental **Values:** - `comment` - `post` - `reel` - `story` #### ConnectedAppCategory The category of a connected app. **Values:** - `mcp` - An MCP client connection. #### ContentItemAction Actions the calling actor can take on a `ContentItem`. The server computes this set from the same rules the content item mutations enforce. A client reads it to drive UI affordances instead of deriving the rules itself. The set carries an action only when a client can offer it on this one item. An action whose answer is the same for every item in a response drives no affordance, so it stays out. Read access is one of those: the API returns only a content item the actor may read. **Status:** ⚠️ Experimental **Values:** - `deleteContentItem` - The actor can delete this content item. An item that holds channel-specific posts gets a second check. The mutation checks each post, so a delete can still fail. - `updateContentItem` - The actor can change this content item's title and target date. The actor can also change the tags and the draft text while the item holds a channel-less draft. #### ContentItemSortableKey Field to sort content items by. **Status:** ⚠️ Experimental **Values:** - `targetDate` - Sort by the content item's target date, the date it is planned for. Content items with no target date sort first in ascending order. - `createdAt` - Sort by the date the content item was created. #### ContentItemStatus The status of a content item's body. **Status:** ⚠️ Experimental **Values:** - `draftContent` - The content item holds a channel-less draft. - `postContent` - The content item holds channel-specific posts. #### ContentProperty A property a content type can carry. Support is expressed by inclusion in `supportedProperties` — there are no exclusion lists. Grows additively. **Status:** ⚠️ Experimental **Values:** - `board` - `document` - `firstComment` - `gif` - `image` - `imageAltText` - `linkAttachment` - `retweetAttachment` - `text` - `thread` - `video` #### DateTimePresence Presence filter for nullable date fields. When filtering the same field, absent dates cannot also match a date comparator range. **Values:** - `absent` - Include results where the date field is missing or null - `present` - Include results where the date field exists and is not null #### DayOfWeek Day of the week. **Values:** - `fri` - `mon` - `sat` - `sun` - `thu` - `tue` - `wed` #### EngagementType Discriminator used in inputs/filters to scope a query to specific types. **Status:** ⚠️ Experimental **Values:** - `comment` - Fetch comments (direct replies to Buffer-managed posts). - `mention` - Fetch mentions (references to the account across the network). #### Feature A capability a channel can expose, spanning every domain. The shared key for per-channel `authorizationStatus`. Grows additively. **Status:** ⚠️ Experimental **Values:** - `comment` - `engagementReaction` - `insights` - `mention` - `posting` #### GoogleBusinessPostActionType List of possible types for GBP cta **Values:** - `book` - `call` - `learn_more` - `none` - `order` - `shop` - `signup` #### IdeaGroupMembership Named buckets for filtering ideas by their group membership. **Values:** - `grouped` - Only ideas that are assigned to a group. - `ungrouped` - Only ideas that are not assigned to any group. #### InstagramAudioType Instagram audio catalog type from Meta's Audio API. **Status:** ⚠️ Experimental **Values:** - `music` - Licensed music from Meta's audio catalog. - `originalSound` - Audio created by an Instagram account. #### MediaFormat Media formats a `FormatRule` can allow-list. Grows additively. **Status:** ⚠️ Experimental **Values:** - `gif` - `jpeg` - `mov` - `mp4` - `pdf` - `png` - `webm` #### MediaType The type of media attached to a post **Values:** - `image` - `gif` - `video` - `link` - `document` - `unsupported` #### NoteAction List of possible actions that can be performed on a note **Values:** - `deleteNote` - The user can delete the note. - `updateNote` - The user can update the note. #### NoteType The type of a note. **Values:** - `aiGenerated` - A note that was generated by our AI system. - `bufferGenerated` - A note that was generated by our internal system. Can be used for approval flows notifications or other automated processes. - `userGenerated` - A note that was manually written by a user. #### NotificationStatus List of possible statuses for a notification **Values:** - `markedAsPublished` - `notified` #### PostAction List of possible actions that can be performed on a Post **Values:** - `addPostNote` - `addPostToQueue` - `approvePost` - `cancelPostRecurrence` - `copyPostLink` - `createPostRecurrence` - `deletePost` - `duplicatePost` - `editPostRecurrence` - `movePostToDraft` - `publishPostNext` - `publishPostNow` - `rejectPost` - `removePostScheduledTime` - `requestPostApproval` - `revertPostApprovalRequest` - `sharePostLink` - `updatePost` - `updatePostSchedule` - `updatePostTags` - `updateShopGridLink` - `viewPost` #### PostApprovalChange A change to a post's approval state, for a post that is already a draft. **Values:** - `request` - Submit the draft for approval. - `revert` - Withdraw a pending approval request, returning the post to a plain draft. #### PostingGoalStatus PostingGoalStatus is used to track the status of a posting goal. **Values:** - `AtRisk` - `Hit` - `OnTrack` #### PostMetricType List of possible metrics available for a Post. Values fall into three groups: - **Cross-network normalized** (reactions, comments, shares, reposts, reach, impressions, views, clicks, engagementRate): Used wherever a concept maps cleanly across networks. Per-network adapters normalize their native names (e.g., Instagram `likes` → `reactions`, Twitter `retweets` → `reposts`). - **Network-specific** (saves, follows, quotes, viewers, totalTimeWatched, averageTimeWatched, likes, freeSubscriptions, paidSubscriptions): Real metrics that don't have a cross-network equivalent. `likes` is intentionally distinct from `reactions` on Facebook — Facebook's Graph API surfaces them separately. `freeSubscriptions` and `paidSubscriptions` are Substack's per-Note subscription attribution; no other network reports it. - **Aggregation-only** (postCount): Meaningful only on aggregate endpoints; never emitted per-post. Deprecated values are pre-normalization legacy or tied to features being removed. They're kept in the enum for backwards compatibility until clients migrate. **Values:** - `averageTimeWatched` - Average time watched, in seconds, for video-style posts (TikTok, Instagram Reels). - `clicks` - How many times people clicked on your post. - `comments` - The count of comments and replies on your post. Unified across networks (Threads `replies` maps here). - `engagementRate` - The percentage of people who interacted with your post compared to how many saw it. Unit: percentage. - `favorites` - Deprecated: not emitted by any per-network definition. Use `reactions` instead — Twitter and Mastodon favorites normalize into `reactions`. Will be removed on 2026-12-01. *(Deprecated: Not emitted by any per-network definition; use `reactions` instead. Will be removed on 2026-12-01.)* - `follows` - The number of new followers gained from this post (Instagram). - `freeSubscriptions` - The number of free subscriptions Substack attributes to this Note. *(⚠️ Experimental)* - `impressions` - How many times your post was shown on screen. May include multiple views by the same person — useful for spotting how often the content gets surfaced. - `likes` - The Like-reaction subcount on Facebook. Distinct from `reactions` (which is the total of all reaction types — Like, Love, Care, Haha, Wow, Sad, Angry); Facebook's Graph API reports them separately and we mirror that. - `link_clicks` - Deprecated: StartPage link-clicks metric. StartPage is being deprecated as a product. Will be removed on 2026-12-01. *(Deprecated: StartPage is being deprecated as a product. Will be removed on 2026-12-01.)* - `other` - Deprecated catch-all from pre-normalization. Never emitted. Will be removed on 2026-12-01. *(Deprecated: Catch-all from pre-normalization; never emitted. Will be removed on 2026-12-01.)* - `paidSubscriptions` - The number of paid subscriptions Substack attributes to this Note. *(⚠️ Experimental)* - `postCount` - The count of posts included in an aggregated response. Only meaningful on aggregate endpoints — never emitted per-post. - `quotes` - How many times your post was quoted (Threads). - `reach` - The number of unique people who saw your post. - `reactions` - How many people reacted to your post. Unified across networks: Instagram/Twitter `likes`, Mastodon `favorites`, etc. all map to this value. - `reblogs` - Deprecated: not emitted by any per-network definition. Use `reposts` instead — Mastodon reblogs normalize into `reposts`. Will be removed on 2026-12-01. *(Deprecated: Not emitted by any per-network definition; use `reposts` instead. Will be removed on 2026-12-01.)* - `repins` - Deprecated: not emitted by any per-network definition. Pre-normalization legacy from the Pinterest era. Will be removed on 2026-12-01. *(Deprecated: Not emitted by any per-network definition. Will be removed on 2026-12-01.)* - `replies` - Deprecated: not emitted by any per-network definition. Use `comments` instead — replies are normalized into `comments` on the networks that distinguish them (Threads). Will be removed on 2026-12-01. *(Deprecated: Not emitted by any per-network definition; use `comments` instead. Will be removed on 2026-12-01.)* - `reposts` - How many times your post was reposted by others. Twitter `retweets`, Mastodon `reblogs`, Threads `reposts` all normalize to this value. - `retweets` - Deprecated: not emitted by any per-network definition. Use `reposts` instead — Twitter retweets normalize into `reposts`. Will be removed on 2026-12-01. *(Deprecated: Not emitted by any per-network definition; use `reposts` instead. Will be removed on 2026-12-01.)* - `saves` - How many times people saved your post (Instagram, Pinterest). A strong signal that the content is worth revisiting. - `shares` - How many times your post was shared or forwarded by others. - `totalTimeWatched` - Total time watched, in minutes, for video-style posts (LinkedIn, TikTok, Instagram Reels). - `viewers` - Unique viewer count for video-style posts (LinkedIn). - `views` - How many times your post was viewed. Used for video-style posts and on networks that report views distinctly from impressions. #### PostMetricUnit The unit representing the value of a PostMetric. **Values:** - `count` - An integer count (e.g. number of reactions, impressions, reach). - `percentage` - A percentage value between 0 and 100 (e.g. engagement rate). #### PostSortableKey Key of collection to use for sorting **Values:** - `dueAt` - Sort by the post's dueAt field. Due at is the date when the post is scheduled to be published. - `createdAt` - Sort by the post's createdAt field. Created at is the date when the post was created. #### PostStatus List of possible statuses for a Post **Values:** - `draft` - `error` - `needs_approval` - `scheduled` - `sending` - `sent` #### PostTemplateVisibility The visibility level of a post template. `public` is reserved for Buffer-curated templates; setting it is only available to official Buffer clients. **Status:** 🧪 Preview **Values:** - `internal` - All members of the template's organization can access this template. - `private` - Only the creator can access this template. - `public` - Anyone across all organizations can access this template. Reserved for Buffer-curated templates — `createPostTemplate` and `updatePostTemplate` reject this value; setting it is only available to official Buffer clients. It appears in read results. #### PostType List of possible types for a Post. Some services may have different types (e.g., Instagram has story, reel, post but Twitter has only post) **Values:** - `carousel` - `event` - `ghost_post` - `offer` - `post` - `reel` - `short` - `story` - `thread` - `whats_new` #### PostTypeFacebook List of specific post types available for Facebook **Values:** - `post` - `reel` - `story` #### PostTypeGoogleBusiness List of specific post types available for Google Business profiles **Values:** - `event` - `offer` - `whats_new` #### PostVia List of possible ways to create a Post **Values:** - `api` - `buffer` - `network` #### Product Buffer products, buffer is used as all products **Values:** - `analyze` - `buffer` - `comments` - `engage` - `publish` - `startPage` #### QueuePosition Target position within a channel's queue that a post can be moved to. **Status:** ⚠️ Experimental **Values:** - `bottom` - Move the post to the bottom of the queue, taking the last slot. - `top` - Move the post to the top of the queue, taking the next available slot. #### ReactionIcon Standard reaction icon set used by `IconVisual`. Grows additively. **Status:** ⚠️ Experimental **Values:** - `heart` - `star` - `thumbsUp` #### ReactionType Types of reactions that can be applied to a comment. **Status:** ⚠️ Experimental **Values:** - `celebrate` - A celebrate reaction. Supported only for LinkedIn comments. - `external` - An external reaction on the comment, using an external link. - `funny` - A funny reaction. Supported only for LinkedIn comments. - `insightful` - An insightful reaction. Supported only for LinkedIn comments. - `like` - A like reaction on the comment. - `love` - A love reaction. Supported only for LinkedIn comments. - `support` - A support reaction. Supported only for LinkedIn comments. #### ScheduleOption **Values:** - `Queue` - `Prioritize` - `FixedTime` - `Now` #### SchedulingType Indicates whether the post was scheduled for notification publishing or automatic publishing **Values:** - `automatic` - Buffer's publishing workers send the post, with nobody having to act - `notification` - Buffer reminds someone to publish the post by hand #### Service The list of services that can be authorized. **Values:** - `bluesky` - `facebook` - `googlebusiness` - `instagram` - `linkedin` - `mastodon` - `pinterest` - `startPage` - `substack` - `threads` - `tiktok` - `twitter` - `whatsapp` - `youtube` #### ShareMode How the post is being scheduled. **Values:** - `addToQueue` - `customScheduled` - `shareNext` - `shareNow` #### SortDirection Direction to sort the results by. **Values:** - `asc` - Sort records in ascending order. - `desc` - Sort records in descending order. #### SyncDataMechanism How items of an engagement type are pulled / pushed. `realTime` covers push (webhook / firehose); `polling` is pull. **Status:** ⚠️ Experimental **Values:** - `polling` - `realTime` #### TagColorName Stable Buffer identifiers for the supported tag color palette. Clients map these values to platform- and theme-specific presentation. Values ending in `Light` are lighter palette variants, not UI theme modes. **Values:** - `blue` - The standard blue palette color. - `blueLight` - The lighter blue palette color. - `gray` - The standard gray palette color. - `grayLight` - The lighter gray palette color. - `green` - The standard green palette color. - `greenLight` - The lighter green palette color. - `orange` - The standard orange palette color. - `orangeLight` - The lighter orange palette color. - `pink` - The standard pink palette color. - `pinkLight` - The lighter pink palette color. - `purple` - The standard purple palette color. - `purpleLight` - The lighter purple palette color. - `red` - The standard red palette color. - `redLight` - The lighter red palette color. - `teal` - The standard teal palette color. - `tealLight` - The lighter teal palette color. - `yellow` - The standard yellow palette color. - `yellowLight` - The lighter yellow palette color. #### YoutubeLicense List of license types **Values:** - `creativeCommon` - `youtube` #### YoutubePrivacy List of privacy types **Values:** - `private` - `public` - `unlisted` ### Scalars #### AccountId The `AccountId` scalar represents the MongoDB ObjectId of a Buffer Account #### ChannelId The `ChannelId` scalar represents the MongoDB ObjectId of a Buffer Channel #### ContentItemId A unique identifier for a content item. #### DateTime The `DateTime` scalar represents a date and time following the ISO 8601 standard. #### DraftContentId A unique identifier for a channel-less draft's content. #### DraftId The `DraftId` scalar represents the MongoDB ObjectId of a Buffer Draft #### Email The `Email` scalar represents a valid, normalized email address. Input is trimmed and lowercased before validation. #### IdeaId The `IdeaId` scalar represents the MongoDB ObjectId of a Buffer Idea #### InvitationId The `InvitationId` scalar represents the MongoDB ObjectId of a pending team invitation #### NoteId The `NoteId` scalar represents the MongoDB ObjectId of a Buffer Note #### OrganizationId The `OrganizationId` scalar represents the MongoDB ObjectId of a Buffer Organization #### PostId The `PostId` scalar represents the MongoDB ObjectId of a Buffer Post #### PostTemplateId The `PostTemplateId` scalar represents the MongoDB ObjectId of a Post Template. #### TagId The `TagId` scalar represents the MongoDB ObjectId of a Buffer Tag #### URL The `URL` scalar represents a valid URL #### Uuid The `Uuid` scalar represents an RFC 4122 v4 UUID, e.g. `550e8400-e29b-41d4-a716-446655440000`.