Skip to content
Social SDK megaphone markSocial SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Buffer

Configure the Buffer managed backend with an API key or OAuth access token, GraphQL publishing and scheduling, public media URLs, and post status.

Buffer is an optional managed backend. Buffer holds the social channels connected to a Buffer organization and publishes or queues posts through its GraphQL API at https://api.buffer.com. Create a personal API key under Settings → API, or let other people approve your Buffer OAuth app. The key or access token stays on the server and is sent as Authorization: Bearer.

import {
  createSocial,
  connectedAccountRef,
  type ConnectedAccountRef,
  type DeliveryOutcome,
  type Platform,
} from "@opencoredev/social-sdk";
import { buffer } from "@opencoredev/social-sdk/cloud/buffer";

// Get tenantId from your authenticated server session and enforce account grants.
const authorization = { tenantId: "tenant-from-session" };
const social = createSocial({
  backend: buffer({
    apiKey: process.env.BUFFER_API_KEY!,
  }),
});
const accounts = await social.accounts.list({ backend: "default", authorization });

function selectAccount<P extends Platform>(platform: P): ConnectedAccountRef<P> {
  const match = accounts.items.find((item) => item.ref.platform === platform);
  if (!match) throw new Error(`Connect a ${platform} channel first.`);
  return connectedAccountRef({
    backend: match.ref.backend,
    platform,
    accountId: match.ref.accountId,
  });
}

const account = selectAccount("x");
const youtubeAccount = selectAccount("youtube");
const at = new Date(Date.now() + 7 * 86_400_000).toISOString();

A Buffer API key acts for the Buffer account that created it and can read every organization on that account. Pass organizationId when you want the adapter to query only one organization. Connected channels are Buffer records; your application must map them to tenants before any write.

The adapter lists channels, publishes now or at schedule.at, reads posts and delivery status, cancels a future scheduled post, deletes draft, approval, or failed Buffer records, and reads post metrics after a post is sent. It does not upload files, verify webhooks, comment, message, or remove a post from the destination network. See capabilities. Related server helpers are in examples/snippets/backend-buffer.ts.

Publish now or schedule

Omit schedule to send mode: shareNow. Set a future schedule.at to send mode: customScheduled with that UTC timestamp. Both paths use schedulingType: automatic. Buffer’s addToQueue slot picker is not mapped.

const result = await social.posts.publish(
  {
    targets: [{ account }],
    content: { text: "Office hours start at 10:00." },
    schedule: { at, timeZone: "Europe/Berlin" },
  },
  { authorization },
);

timeZone is application metadata only; Buffer’s dueAt is UTC. Store result.outcomes[0]?.delivery and read it later with getDelivery. Buffer statuses map as scheduled → scheduled, sending → processing, draft or needs_approval → accepted, sent → published, and error → failed. A published outcome uses the Buffer post ID. Buffer’s public schema does not return the destination network’s post ID.

Media is a public HTTPS URL

Buffer has no upload mutation. Host the file yourself and pass a stable public HTTPS URL. Buffer fetches the file when the post publishes, which can be hours or days later, so expiring or signed URLs fail after create. This adapter maps image and video assets. It does not map Buffer document assets, because those require a title and thumbnail the SDK does not collect.

const scheduled = await social.posts.publish(
  {
    targets: [
      {
        account: youtubeAccount,
        options: {
          title: "Release walkthrough",
          categoryId: "22",
          visibility: "unlisted",
          madeForKids: false,
        },
      },
    ],
    content: {
      text: "A two-minute tour of the new release.",
      media: [
        {
          kind: "video",
          mimeType: "video/mp4",
          source: { kind: "https-url", url: "https://cdn.example/release-walkthrough.mp4" },
        },
      ],
    },
  },
  { authorization },
);

YouTube requires title, categoryId, visibility, and madeForKids; these are sent as Buffer metadata.youtube. Instagram requires an explicit boolean shareToFeed, sent as metadata.instagram.shouldShareToFeed with type: post; stories and reels are not mapped. TikTok’s Buffer metadata documents only optional isAiGenerated, so other TikTok options are rejected while Buffer’s media shape rules still apply. X reply settings have no Buffer Twitter field.

Cancel or delete a Buffer record

posts.cancelScheduled deletes a still-future scheduled post. posts.deleteBackendRecord deletes a draft, needs_approval, or error record. A sent or in-flight post is rejected. Buffer’s deletePost removes the Buffer record; it is not advertised as native network removal.

Connect other Buffer accounts with OAuth

A personal API key is enough for your own automations. For an app that other people sign in to, register an OAuth client under Settings → API. Buffer requires the authorization-code flow with PKCE for every client. Confidential clients also send client_secret. Request offline_access if you need a refresh token. Refresh tokens are single-use: store the replacement and discard the old one.

import {
  bufferAuthorizationUrl,
  createBufferPkce,
  exchangeBufferCode,
  refreshBufferToken,
} from "@opencoredev/social-sdk/cloud/buffer";

interface OAuthSession {
  bufferState?: string | undefined;
  bufferVerifier?: string | undefined;
}

export function startOAuth(session: OAuthSession): Response {
  const pkce = createBufferPkce();
  const state = crypto.randomUUID();
  session.bufferState = state;
  session.bufferVerifier = pkce.codeVerifier;

  return Response.redirect(
    bufferAuthorizationUrl({
      clientId: process.env.BUFFER_CLIENT_ID!,
      redirectUri: "https://app.example/buffer/callback",
      state,
      codeChallenge: pkce.codeChallenge,
    }),
  );
}

export async function finishOAuth(session: OAuthSession, url: URL) {
  if (url.searchParams.get("state") !== session.bufferState) throw new Error("Invalid state.");
  const code = url.searchParams.get("code");
  const verifier = session.bufferVerifier;
  if (!code || !verifier) throw new Error("Missing authorization code or PKCE verifier.");

  const tokens = await exchangeBufferCode({
    clientId: process.env.BUFFER_CLIENT_ID!,
    clientSecret: process.env.BUFFER_CLIENT_SECRET,
    code,
    redirectUri: "https://app.example/buffer/callback",
    codeVerifier: verifier,
  });

  return {
    backend: buffer({ apiKey: tokens.accessToken }),
    tokens,
  };
}

export async function refresh(tokens: { refreshToken: string }) {
  const refreshed = await refreshBufferToken({
    clientId: process.env.BUFFER_CLIENT_ID!,
    clientSecret: process.env.BUFFER_CLIENT_SECRET,
    refreshToken: tokens.refreshToken,
  });

  // Persist `refreshed.refreshToken` before replacing the old token.
  return refreshed;
}

Default scopes are posts:read, posts:write, account:read, and offline_access. Ideas, snippets, and account writes are not used by this adapter.

Errors, rate limits, and metrics

GraphQL usually returns HTTP 200. Non-recoverable failures arrive in errors with codes such as UNAUTHORIZED, FORBIDDEN, NOT_FOUND, UNEXPECTED, and RATE_LIMIT_EXCEEDED. Recoverable create/delete failures arrive as MutationError.message on HTTP 200 and become invalid_input. HTTP 429 includes Retry-After. Limits are per API key or app client and depend on the Buffer plan; the documented windows are 15 minutes, 24 hours, and 30 days. Query complexity, depth, alias, directive, and token limits also apply.

analytics.getPostMetrics reads Post.metrics after status sent. Buffer documents that field for personal API keys and automations. OAuth app clients may not receive metrics. Values refresh about once a day. The adapter keeps only metrics whose unit is count, percentage, milliseconds, or seconds.

Buffer documents additional services (Pinterest, Mastodon, Google Business Profile, Start Page, Substack, and WhatsApp) and ideas, tags, and templates. Those are outside this adapter’s selected platform coverage. Live publication, plan eligibility, and channel reconnects remain Buffer-side constraints.

Run the setup on your server with your own assets and connected channels. In a multi-tenant app, configure an authorization policy backed by your account grants; listing channels is not a tenant grant.

Last updated on October 11, 2026