Works with all AI agents

Connect your AI agent

Choose your AI agent and connect in seconds.

Add to Claude

Works on every Claude plan, including free. Full Claude guide

  1. 1

    Add to Claude

    One click from Claude’s connector directory.

  2. 2

    Sign up or sign in

    Takes seconds, then you’re sent straight back.

  3. 3

    Ask Claude

    “Plan and schedule my posts for next week.”

How to Migrate from the Buffer API to PostEverywhere (2026)

Step-by-step guide to switching from the Buffer API to PostEverywhere. Side-by-side SDK code, an endpoint mapping table, and the gotchas (timezones, media uploads, error shapes) you will hit on the way.

Jamie Partridge
Jamie Partridge
Founder··Updated May 2026·13 min read
Migrating a Node.js codebase from the Buffer API to the PostEverywhere API

If you have built anything on the Buffer API over the last few years, you know the feeling: a stable product, a quiet roadmap, and the slow realisation that the integration you depend on is no longer the focus.

Buffer pivoted away from heavy API investment a long time ago. Their developer docs have not seen meaningful new endpoints in years, the free Buffer plan dropped from generous to capped, and the API tier sits behind specific business plans.

Teams who originally chose Buffer because it was cheap and simple are migrating to APIs with active development, modern auth, public OpenAPI specs, and SDK support.

This guide is the playbook. We will walk you through exporting your Buffer data, installing the PostEverywhere SDK, reauthorizing your social platforms, mapping Buffer profile IDs to PostEverywhere account_ids, and porting your scheduling code piece by piece. With code samples for each step.

Edited by Jamie Partridge, Founder.

Who Switches and Why

The teams migrating off Buffer in 2026 fall into a few buckets:

  • Agencies hitting Buffer's profile cap. Buffer's pricing scales with social channels per workspace and API access requires the higher tiers. PostEverywhere's Scale plan at $39/mo covers 20 accounts with the API included by default.
  • Developers tired of the GraphQL-ish update format. Buffer's API has historical baggage from their move between platforms. The shape of an update is different from a post, profiles have multiple ID flavors, and getting the exact ISO timestamp right is fiddlier than it should be.
  • Teams that need cross-posting in one call. Buffer requires one update per profile. PostEverywhere takes an account_ids array and fans out internally. For cross-posting workflows the difference is dozens of lines of code.
  • Anyone building AI agents. PostEverywhere ships a native MCP package (source on GitHub), AI image generation on the API, and a typed Node.js SDK. Buffer does not.
  • Teams concerned about Buffer's API stability. Buffer has historically deprecated entire products (Reply, Analyze, the original Pablo). The core API has been stable, but the trajectory is clear: Buffer is investing in their dashboard, not their developer ecosystem.

If any of those describe you, the rest of this post is a working migration playbook.

What You'll Need to Adjust

Before you write code, set expectations. The biggest delta between the two APIs:

  • Auth model. Buffer uses OAuth 2.0 with access tokens scoped to a user. PostEverywhere uses static Bearer tokens issued from your dashboard. For server-side use this is faster.
  • Posting model. Buffer posts one update per profile_id. PostEverywhere posts one post to many account_ids.
  • Media flow. Buffer accepts a media object directly in the update. PostEverywhere uses a presigned upload URL flow (POST /media/upload then POST /media/{id}/complete) before referencing the media in a post.
  • Timezone model. Buffer's scheduled_at is a Unix timestamp. PostEverywhere's scheduled_for is an ISO 8601 UTC string with an optional timezone IANA field for display.
  • Response envelope. Buffer returns the resource at the top level. PostEverywhere wraps every response in { data, error, meta }.
  • Error shape. Buffer returns { success: false, message: "..." }. PostEverywhere returns standard HTTP status codes plus { error: { code, message } } in the envelope.

Plan a couple of focused engineering hours, not a quarter. Most migrations land in a single afternoon.

Step 1: Export Your Buffer Data

Before you touch any code, get your data out.

In the Buffer dashboard, open each connected channel and export your scheduled queue. For programmatic export, hit the Buffer API and dump everything you currently rely on:

# All profiles
curl "https://api.bufferapp.com/1/profiles.json?access_token=$BUFFER_TOKEN" \
  > buffer-profiles.json

# Pending updates per profile
curl "https://api.bufferapp.com/1/profiles/$PROFILE_ID/updates/pending.json?access_token=$BUFFER_TOKEN" \
  > buffer-pending-${PROFILE_ID}.json

Save those JSON files. You will use them to:

  1. Map Buffer profile IDs to PostEverywhere account IDs.
  2. Replay any scheduled-but-not-yet-published posts after you cut over.

If you have a database of post history that you want to retain (analytics, reporting), this is also the moment to snapshot it. Once the cutover is done, your historical Buffer queue stays in Buffer but your new posts go to PostEverywhere.

Step 2: Install the PostEverywhere SDK

npm install @posteverywhere/sdk

The SDK is published on npm with TypeScript types out of the box. Source on GitHub.

import { PostEverywhere } from "@posteverywhere/sdk";

const client = new PostEverywhere({
  apiKey: process.env.PE_API_KEY,
});

If you are in Python, Go, or another language, the public OpenAPI spec lets you generate a typed client with openapi-generator-cli. The endpoints below are language-agnostic.

Step 3: Authenticate

Generate your API key at your developer settings. Keys look like:

pe_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Add it to your environment:

export PE_API_KEY=pe_live_...

That is the entire auth dance. No callback URL, no token refresh, no OAuth scopes to negotiate. The token is yours until you rotate it.

If you want to gate AI generation calls separately (for example, restrict CI keys from spending AI credits), generate a key without the optional ai scope.

Step 4: Reauthorize Platforms in the PostEverywhere Dashboard

Buffer's OAuth tokens are scoped to Buffer. They do not transfer. You will need to reconnect each social platform inside the PostEverywhere dashboard — Instagram, TikTok, LinkedIn, Facebook, X, YouTube, Threads, Pinterest.

This takes 60 seconds per platform. The dashboard handles each platform's OAuth flow for you. Once connected, the API can publish to that account.

There is no way around this step — every social platform requires fresh authorization for a new app — but it only happens once per account.

Step 5: Map Buffer Profile IDs to PostEverywhere Account IDs

You exported buffer-profiles.json in step 1. Now fetch your PostEverywhere accounts and build a mapping table.

import { PostEverywhere } from "@posteverywhere/sdk";
import fs from "node:fs";

const client = new PostEverywhere({ apiKey: process.env.PE_API_KEY! });

const bufferProfiles = JSON.parse(fs.readFileSync("buffer-profiles.json", "utf8"));
const peAccounts = await client.accounts.list();

const mapping: Record<string, number> = {};

for (const bp of bufferProfiles) {
  // Buffer profiles have service ("instagram", "twitter", etc.) and service_username
  const match = peAccounts.data.find(
    (a) =>
      a.platform === bp.service.replace("twitter", "x") &&
      a.username?.toLowerCase() === bp.service_username?.toLowerCase()
  );

  if (match) {
    mapping[bp.id] = match.id;
  } else {
    console.warn(`No PE account found for Buffer profile ${bp.service_username}`);
  }
}

fs.writeFileSync("profile-mapping.json", JSON.stringify(mapping, null, 2));

Two notes on naming:

  • Buffer calls X "twitter". PostEverywhere uses "x". The mapping above handles that.
  • Buffer's id is a 24-character ObjectId string. PostEverywhere's account.id is an integer. Your mapping needs to be string-to-int.

Now you have a profile-mapping.json you can reference when porting scheduling code.

Step 6: Port Your Scheduling Code

Here is where the real work happens. We will go through four of the most common Buffer API operations side by side: posting, scheduling, fetching scheduled posts, and deleting.

Posting Now

Before — Buffer:

async function postWithBuffer(text, profileIds) {
  const formData = new URLSearchParams();
  formData.append("text", text);
  formData.append("now", "true");
  for (const id of profileIds) {
    formData.append("profile_ids[]", id);
  }
  formData.append("access_token", process.env.BUFFER_TOKEN);

  const res = await fetch("https://api.bufferapp.com/1/updates/create.json", {
    method: "POST",
    body: formData,
  });

  return res.json();
}

After — PostEverywhere:

import { PostEverywhere } from "@posteverywhere/sdk";
const client = new PostEverywhere({ apiKey: process.env.PE_API_KEY! });

async function postWithPostEverywhere(content: string, accountIds: number[]) {
  return client.posts.create({
    content,
    account_ids: accountIds,
  });
}

Notice what disappeared: form-encoding, the profile_ids[] array syntax, the access_token parameter, the "now": "true" flag (PE infers immediate publish from the absence of scheduled_for).

Scheduling for Later

Before — Buffer:

async function scheduleWithBuffer(text, profileIds, unixTimestamp) {
  const formData = new URLSearchParams();
  formData.append("text", text);
  formData.append("scheduled_at", String(unixTimestamp));
  for (const id of profileIds) {
    formData.append("profile_ids[]", id);
  }
  formData.append("access_token", process.env.BUFFER_TOKEN);

  const res = await fetch("https://api.bufferapp.com/1/updates/create.json", {
    method: "POST",
    body: formData,
  });

  return res.json();
}

After — PostEverywhere:

async function scheduleWithPostEverywhere(
  content: string,
  accountIds: number[],
  isoTimestamp: string
) {
  return client.posts.create({
    content,
    account_ids: accountIds,
    scheduled_for: isoTimestamp,
    timezone: "Australia/Sydney",
  });
}

// Usage:
await scheduleWithPostEverywhere(
  "Launching tomorrow",
  [12, 17, 22],
  "2026-04-27T09:00:00Z"
);

Buffer wanted a Unix timestamp. PostEverywhere wants ISO 8601 UTC. Convert with new Date(unix * 1000).toISOString() if you have legacy timestamps to migrate.

Fetching Scheduled Posts

Before — Buffer:

async function getPendingFromBuffer(profileId) {
  const res = await fetch(
    `https://api.bufferapp.com/1/profiles/${profileId}/updates/pending.json?access_token=${process.env.BUFFER_TOKEN}`
  );
  const data = await res.json();
  return data.updates;
}

After — PostEverywhere:

async function getScheduledFromPostEverywhere(accountId: number) {
  const res = await client.posts.list({
    account_id: accountId,
    status: "scheduled",
  });
  return res.data;
}

Buffer required one request per profile. PostEverywhere lets you list across accounts in a single call by omitting the filter, with built-in pagination via meta.cursor.

Deleting a Scheduled Post

Before — Buffer:

async function deleteFromBuffer(updateId) {
  const formData = new URLSearchParams();
  formData.append("access_token", process.env.BUFFER_TOKEN);

  const res = await fetch(
    `https://api.bufferapp.com/1/updates/${updateId}/destroy.json`,
    { method: "POST", body: formData }
  );
  return res.json();
}

After — PostEverywhere:

async function deleteFromPostEverywhere(postId: string) {
  return client.posts.delete(postId);
}

Standard REST verbs replace Buffer's idiosyncratic /destroy.json action. Less code to remember.

Endpoint Mapping Table

Buffer Endpoint PostEverywhere Endpoint
GET /1/profiles.json GET /accounts
GET /1/profiles/{id}.json GET /accounts/{id}
POST /1/updates/create.json POST /posts
GET /1/profiles/{id}/updates/pending.json GET /posts?status=scheduled&account_id={id}
GET /1/profiles/{id}/updates/sent.json GET /posts?status=published&account_id={id}
GET /1/updates/{id}.json GET /posts/{id}
POST /1/updates/{id}/update.json PATCH /posts/{id}
POST /1/updates/{id}/destroy.json DELETE /posts/{id}
POST /1/updates/{id}/share.json POST /posts/{id}/retry
Buffer media object inside update POST /media/upload then POST /media/{id}/complete, then reference media_ids in the post
(none) POST /ai/generate-image

PostEverywhere also exposes endpoints Buffer never did: per-platform results (GET /posts/{id}/results), retry (POST /posts/{id}/retry), and AI image generation (POST /ai/generate-image).

Common Gotchas

We have walked dozens of teams through this migration. Here are the issues that come up reliably.

Timezone Handling

Buffer interprets scheduled_at as Unix UTC and uses the profile's saved timezone for display. PostEverywhere takes ISO 8601 UTC for scheduled_for and a separate IANA timezone (like Australia/Sydney or America/New_York) for display.

If you stored Buffer timestamps as Unix integers, convert them:

function bufferUnixToPeIso(unix: number): string {
  return new Date(unix * 1000).toISOString();
}

If you stored them as local times, you need to first localize back to UTC using your saved timezone. Get this wrong and posts publish at 9pm instead of 9am — test before cutover.

Media Upload Flow

Buffer's update accepts a media object containing a public image URL. PostEverywhere uses a three-step flow against POST /media/upload, the presigned URL, and POST /media/{id}/complete. Raw fetch here so the endpoints are visible:

import fs from 'fs';

const API_KEY = process.env.POSTEVERYWHERE_API_KEY;
const BASE = 'https://app.posteverywhere.ai/api/v1';

// 1. Request a presigned upload URL
const initRes = await fetch(`${BASE}/media/upload`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    filename: 'launch.jpg',
    content_type: 'image/jpeg',
    size: fs.statSync('launch.jpg').size,
  }),
});
const { data: { media_id, upload_url } } = await initRes.json();

// 2. PUT the file directly to the presigned URL
await fetch(upload_url, {
  method: 'PUT',
  headers: { 'Content-Type': 'image/jpeg' },
  body: fs.readFileSync('launch.jpg'),
});

// 3. Finalize the upload
await fetch(`${BASE}/media/${media_id}/complete`, {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${API_KEY}` },
});

// 4. Reference the media in a post
await client.posts.create({
  content: 'We launched.',
  account_ids: [12, 17],
  media_ids: [media_id],
});

The trade-off: a couple more requests, but you can upload large videos, multiple files, and there is no public URL exposure. If you have a Buffer integration that hot-links from your CDN, you will rewrite this section. It is a one-time cost.

Error Response Shape

Buffer returns:

{
  "success": false,
  "code": 1024,
  "message": "Profile not connected"
}

PostEverywhere returns standard HTTP status codes (400, 401, 402, 404, 429, 500) with a structured envelope:

{
  "data": null,
  "error": {
    "code": "ACCOUNT_NOT_CONNECTED",
    "message": "The specified account is not currently connected"
  },
  "meta": { "request_id": "req_abc", "timestamp": "2026-04-26T12:00:00Z" }
}

If you have try/catch logic that checks response.success === false, swap it to check response.status >= 400 or use the SDK's typed errors:

import { PostEverywhereError } from "@posteverywhere/sdk";

try {
  await client.posts.create({ content, account_ids });
} catch (err) {
  if (err instanceof PostEverywhereError) {
    console.error(err.code, err.message, err.requestId);
  } else {
    throw err;
  }
}

Rate Limits

Buffer's rate limits varied by endpoint and were not always published clearly. PostEverywhere doesn't cap how much you post: platforms enforce their own limits, and very fast bursts of requests can be rate limited for a moment. Every response includes X-RateLimit-Remaining. On 429, respect the Retry-After header.

For bulk migrations where you replay 500 scheduled posts in one go, throttle to 50 per minute and you will not hit the limit.

platform_content for Per-Platform Variants

Buffer encouraged separate updates for per-platform copy. PostEverywhere supports this in a single request via platform_content:

await client.posts.create({
  content: "Default copy for any platform",
  account_ids: [12, 17, 22],
  platform_content: {
    instagram: { content: "Optimised for Instagram" },
    x: { content: "Short version for X (under 280 chars)" },
    linkedin: { content: "Long-form professional version for LinkedIn" },
  },
});

This is one of the migration wins — you delete a chunk of "compose three updates" code.

Cost Savings: Quick Calculator

A common reason teams migrate is cost. Roughly:

  • Buffer Team plan (the tier most agencies land on, given approval workflows and unlimited team seats) is $10/channel/month, so 10 channels = $100/mo, scaling linearly with each channel. The cheaper Essentials plan is $5/channel/month but is single-user and lacks team workflows. Source: Buffer's pricing page.
  • PostEverywhere Growth is $29/mo flat for 10 accounts. Pricing.
  • PostEverywhere Scale is $39/mo for 20 accounts.

Most agencies migrating from Buffer save $30-$200/mo on platform fees alone (Buffer Team at 10 channels = $100/mo vs PostEverywhere Growth at $29/mo for the same channel count), plus the saved engineering hours from a cleaner API.

Beyond the API: Product Switch

If you are also evaluating the dashboard side of the switch (or if non-technical teammates need to keep using a UI), the PostEverywhere vs Buffer comparison covers the user-facing differences.

The list of Buffer alternatives is useful if you are still shopping. Coming from Ayrshare rather than Buffer? The steps are the same (reconnect accounts, map IDs, port the post call), and our Ayrshare alternatives guide compares the API options on price per account, MCP and webhooks.

For teams comparing all three big names, our PostEverywhere API vs Hootsuite API breakdown sits next to this one. And if your end goal is agent-driven posting rather than a server integration, the agents page covers connecting Claude Code, Cursor, or Codex via MCP in 60 seconds.

FAQs

Will I lose any post data when I migrate?

No. Your historical Buffer posts stay in Buffer. PostEverywhere starts publishing fresh from your cutover date. If you need historical analytics imported, export them from Buffer first and feed them into your own data warehouse — neither API offers a direct cross-platform import.

Can I run both APIs in parallel during migration?

Yes, and we recommend it. For a few days, write the same content to both Buffer and PostEverywhere. Verify PostEverywhere is publishing correctly. Then turn off the Buffer half of the dual-write. Total downtime: zero.

How long does the full migration take?

For a single-developer team with one workspace and 5-15 social accounts: a half-day. For an agency with multiple clients and per-client integrations: 1-3 days, mostly spent reconnecting platforms inside the dashboard.

What about Buffer's analytics and Reply features?

Reply was already deprecated. Analyze is still in Buffer but only the dashboard, not the API. PostEverywhere ships analytics on every plan and exposes basic metrics through GET /posts/{id}/results.

For deep audience analytics most teams pair PE with a dedicated tool or one of the leading social media analytics tools.

Does PostEverywhere have a free tier like Buffer used to?

Not a permanent free tier — instead, every plan starts with a 7-day free trial (credit card required) so you can verify the API works for your stack before paying. Pricing is flat after that: $9, $19, $29, or $39 monthly.

What if my code is in Python or Go, not Node?

The PostEverywhere OpenAPI spec is public. Generate a client with openapi-generator-cli generate -i <spec-url> -g python -o ./pe-client and you have a typed client in any language openapi-generator supports.

Can my AI agent connect directly?

Yes — install the official MCP package with npx -y @posteverywhere/mcp and any MCP-compatible agent (Claude Code, Cursor, OpenAI Agents SDK with MCP) can post directly. Buffer has no equivalent.

What if I hit a problem mid-migration?

Email support, or open an issue on the SDK GitHub. Migrations are common enough that we have seen most edge cases. The PostEverywhere developer docs include a full quickstart and authentication guide.


That is the migration. If you are ready to start, generate a PostEverywhere API key, npm install @posteverywhere/sdk, and the rest is the code in this post. Most teams ship the cutover in a single sitting.

Jamie Partridge
Written by Jamie Partridge

Founder & CEO of PostEverywhere. Writing about social media strategy, publishing workflows, and analytics that help brands grow faster.

Related articles

Social media scheduler

Post everywhere from one dashboard

Schedule a month of posts in one sitting, tailor each one per platform, and let your AI agent handle the rest.

  • Unlimited posts to all top platforms
  • AI Studio for posts, images and video
  • Connect ChatGPT, Claude or any AI agent

7-day free trial on every plan. $0 due today, cancel anytime.

  • Lite

    2 social accounts

    $9/mo

  • Starter

    5 social accounts

    $19/mo

  • GrowthRecommended

    10 social accounts

    $29/mo

  • Scale

    20 social accounts

    $39/mo

Compare every plan