# 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. **Source:** https://posteverywhere.ai/blog/migrate-from-buffer-api-to-posteverywhere **Author:** Jamie Partridge **Published:** 2026-04-26 --- If you have built anything on the [Buffer API](https://buffer.com/developers/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](https://buffer.com/pricing), 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](https://www.npmjs.com/package/@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. Reviewed 26 April 2026. ## Table of Contents 1. [Who Switches and Why](#who-switches-and-why) 2. [What You'll Need to Adjust](#what-youll-need-to-adjust) 3. [Step 1: Export Your Buffer Data](#step-1-export-your-buffer-data) 4. [Step 2: Install the PostEverywhere SDK](#step-2-install-the-posteverywhere-sdk) 5. [Step 3: Authenticate](#step-3-authenticate) 6. [Step 4: Reauthorize Platforms in the PostEverywhere Dashboard](#step-4-reauthorize-platforms-in-the-posteverywhere-dashboard) 7. [Step 5: Map Buffer Profile IDs to PostEverywhere Account IDs](#step-5-map-buffer-profile-ids-to-posteverywhere-account-ids) 8. [Step 6: Port Your Scheduling Code](#step-6-port-your-scheduling-code) 9. [Endpoint Mapping Table](#endpoint-mapping-table) 10. [Common Gotchas](#common-gotchas) 11. [Cost Savings: Quick Calculator](#cost-savings-quick-calculator) 12. [FAQs](#faqs) ## 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 [Growth plan at $39/mo](/pricing) covers 25 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](/cross-posting) the difference is dozens of lines of code. - **Anyone building [AI agents](/blog/build-social-media-agent-api).** PostEverywhere ships a [native MCP package](https://www.npmjs.com/package/@posteverywhere/mcp) (source on [GitHub](https://github.com/posteverywhere/mcp)), 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: ```bash # 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 ```bash npm install @posteverywhere/sdk ``` The SDK is published on [npm](https://www.npmjs.com/package/@posteverywhere/sdk) with TypeScript types out of the box. Source on [GitHub](https://github.com/posteverywhere/sdk). ```typescript 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](https://developers.posteverywhere.ai/) 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](https://developers.posteverywhere.ai/authentication). Keys look like: ``` pe_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 ``` Add it to your environment: ```bash 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](https://app.posteverywhere.ai/) — 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. ```typescript 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 = {}; 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:** ```javascript 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:** ```typescript 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:** ```javascript 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:** ```typescript 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:** ```javascript 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:** ```typescript 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:** ```javascript 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:** ```typescript 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: ```typescript 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: ```typescript 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: ```json { "success": false, "code": 1024, "message": "Profile not connected" } ``` PostEverywhere returns standard HTTP status codes (400, 401, 402, 404, 429, 500) with a structured envelope: ```json { "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: ```typescript 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 publishes flat numbers: 60 requests per minute and 1,000 per hour per API key. 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`: ```typescript 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](https://buffer.com/pricing). - **PostEverywhere Starter** is $29/mo flat for 10 accounts. [Pricing](/pricing). - **PostEverywhere Growth** is $39/mo for 25 accounts. Most agencies migrating from Buffer save $30-$200/mo on platform fees alone (Buffer Team at 10 channels = $100/mo vs PostEverywhere Starter 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](/blog/posteverywhere-vs-buffer) covers the user-facing differences. The list of [Buffer alternatives](/blog/buffer-alternatives) is useful if you are still shopping. For teams comparing all three big names, our [PostEverywhere API vs Hootsuite API](/blog/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](/agents) 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](/social-media-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](/blog/best-social-media-management-tools) or one of the leading [social media analytics tools](/blog/best-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](/pricing) (credit card required) so you can verify the API works for your stack before paying. Pricing is flat after that: $29, $39, or $79 monthly. ### What if my code is in Python or Go, not Node? The [PostEverywhere OpenAPI spec](https://developers.posteverywhere.ai/) is public. Generate a client with `openapi-generator-cli generate -i -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](https://www.npmjs.com/package/@posteverywhere/mcp) 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](https://github.com/posteverywhere/sdk). Migrations are common enough that we have seen most edge cases. The [PostEverywhere developer docs](https://developers.posteverywhere.ai/) include a full quickstart and authentication guide. --- That is the migration. If you are ready to start, [generate a PostEverywhere API key](https://developers.posteverywhere.ai/authentication), `npm install @posteverywhere/sdk`, and the rest is the code in this post. Most teams ship the cutover in a single sitting.