{ "info": { "_postman_id": "b0ffe4a1-2c3d-4e5f-8a9b-0c1d2e3f4a5b", "name": "Buffer API (GraphQL) — HRMS", "description": "Buffer's public GraphQL API — every operation the HRMS job-posting integration needs, plus the full read surface.\n\n**One endpoint for everything:** `POST https://api.buffer.com`. There are no REST paths; the operation is decided by the GraphQL document in the body.\n\n---\n\n### Setup\n1. Import `Buffer-API.postman_environment.json` and paste your key from `backend/.env` (`BUFFER_API`) into `buffer_token`.\n2. Run **01 · Get Organizations** → fills `{{org_id}}`.\n3. Run **02 · Get Channels** → fills `{{channel_id}}`.\n4. Everything else now works. Create/list requests fill `{{post_id}}` for you, so **Delete Post** always targets the last post you touched.\n\n### The three answers you were after\n| Need | Request | Field |\n|---|---|---|\n| `org_id` | 01 · Get Organizations | `account.organizations[].id` |\n| `channel_id` | 02 · Get Channels | `channels[].id` |\n| create post | 04 · Create Post · … | `createPost` → `PostActionSuccess.post.id` |\n| delete post | 04 · Delete Post | `deletePost` → `DeletePostSuccess.id` |\n| list posts | 03 · Get Posts | `posts.edges[].node` |\n\n### Gotchas that cost real time\n* Errors come back as **HTTP 200**. Check `errors[]` and `__typename`, not the status code.\n* Do **not** request `totalCount` on `posts` — API keys get `FORBIDDEN`.\n* The edit mutation is `editPost`, not `updatePost`.\n* `deletePost` returns `DeletePostSuccess`, *not* `PostActionSuccess`.\n* `schedulingType` is `automatic` | `notification` only — it is **not** the queue mode. The queue mode is `mode` (`addToQueue` | `shareNext` | `shareNow` | `customScheduled`).\n* `mode: customScheduled` requires `dueAt`; `mode: shareNow` publishes instantly.\n* `metadata..linkAttachment` and a non-empty `assets` array are mutually exclusive.\n* Sorting is only by `dueAt` or `createdAt` — there is no `sentAt` sort key.\n\n### Rate limits\nFree plan: **100 requests / 15 min**, 250 / day, 3000 / 30 days. A full Collection Runner pass over this collection is ~38 calls, so back-to-back runs will trip the 15-minute window (HTTP 429, `RATE_LIMIT_EXCEEDED`, `extensions.window: \"15m\"`). Every response carries `ratelimit` / `ratelimit-policy` headers — see **08 · Rate limit headers**.\n\n### Plan-gated operations\nThese are valid GraphQL but rejected on a Free account: LinkedIn `firstComment`, `needsApproval: true` (needs a posting policy), and Insights windows older than 31 days.\n\nDocs: https://developers.buffer.com/guides · Explorer: https://developers.buffer.com/explorer.html", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "auth": { "type": "bearer", "bearer": [ { "key": "token", "value": "{{buffer_token}}", "type": "string" } ] }, "item": [ { "name": "00 · Auth & Account", "description": "Verify the API key works and inspect the authenticated account. The key is account-scoped: it can reach every organization and channel on the account.", "item": [ { "name": "Ping / Whoami", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "pm.test('Token is valid', function () {", " pm.expect(res.data.account.id).to.be.a('string');", "});", "console.log('Rate limit:', pm.response.headers.get('ratelimit'));" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query Whoami {\\n account {\\n id\\n email\\n name\\n }\\n}\"\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "Cheapest possible call. 200 + an account id means the token is valid.\n401 / `UNAUTHORIZED` in `errors[]` means the token is wrong or revoked." }, "response": [] }, { "name": "Get Account (full)", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query GetAccount {\\n account {\\n id\\n email\\n backupEmail\\n name\\n avatar\\n timezone\\n createdAt\\n organizations {\\n id\\n name\\n ownerEmail\\n channelCount\\n }\\n connectedApps {\\n clientId\\n name\\n category\\n scopes\\n createdAt\\n }\\n }\\n}\"\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "Everything readable about the logged-in account in one call.\n\n`connectedApps[].clientId` is the OAuth **client id** — do not confuse it with `organizations[].id`." }, "response": [] } ] }, { "name": "01 · Organizations → org_id", "description": "**Run this first.** Almost every other query needs `organizationId`. The test script writes the first org id into the `org_id` collection variable automatically.", "item": [ { "name": "Get Organizations (captures org_id)", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "const orgs = res.data.account.organizations;", "pm.test('At least one organization', () => pm.expect(orgs).to.have.length.above(0));", "pm.collectionVariables.set('org_id', orgs[0].id);", "console.log('org_id =', orgs[0].id, '|', orgs[0].name);" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query GetOrganizations {\\n account {\\n organizations {\\n id\\n name\\n ownerEmail\\n channelCount\\n }\\n }\\n}\"\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "**This is the org_id endpoint.**\n\n`account.organizations[].id` is the `organizationId` every other call wants.\nThe test script stores `organizations[0].id` in `{{org_id}}`." }, "response": [] }, { "name": "Get Organization Limits", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query GetOrganizationLimits {\\n account {\\n organizations {\\n id\\n name\\n channelCount\\n limits {\\n channels\\n members\\n scheduledPosts\\n ideas\\n tags\\n postTemplates\\n }\\n }\\n }\\n}\"\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "Plan ceilings for the org (each field is the max, an `Int`) — compare `limits.channels` against `channelCount` before connecting another channel." }, "response": [] } ] }, { "name": "02 · Channels → channel_id", "description": "**Run `Get Channels` second.** `channel_id` is what `createPost` publishes to. The test script captures the first channel into `{{channel_id}}`.", "item": [ { "name": "Get Channels (captures channel_id)", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "const chans = res.data.channels;", "pm.test('At least one channel', () => pm.expect(chans).to.have.length.above(0));", "pm.collectionVariables.set('channel_id', chans[0].id);", "console.log('channel_id =', chans[0].id, '|', chans[0].service, '|', chans[0].name);", "chans.forEach(c => console.log(` ${c.id} ${c.service.padEnd(14)} ${c.name}`));" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query GetChannels($input: ChannelsInput!) {\\n channels(input: $input) {\\n id\\n name\\n displayName\\n service\\n type\\n serviceId\\n organizationId\\n avatar\\n externalLink\\n timezone\\n isDisconnected\\n isLocked\\n isQueuePaused\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"organizationId\": \"{{org_id}}\"\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "**This is the channel_id endpoint.**\n\nReturns every connected social profile in the organization. `id` → use as `channelId` in `createPost`. `service` is the network (`linkedin`, `twitter`, `instagram`, `facebook`, `tiktok`, `threads`, `youtube`, `pinterest`, `mastodon`, `bluesky`, `googlebusiness`, `startPage`).\n\nStore the id you actually want in `{{channel_id}}` — the script picks the first one." }, "response": [] }, { "name": "Get Channels (filtered)", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query GetFilteredChannels($input: ChannelsInput!) {\\n channels(input: $input) {\\n id\\n name\\n service\\n isLocked\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"organizationId\": \"{{org_id}}\",\n \"filter\": {\n \"isLocked\": false,\n \"product\": \"publish\"\n }\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "`filter.isLocked` — true/false/omit. `filter.product` — `publish` | `analyze` | `engage` | `comments` | `startPage` | `buffer`." }, "response": [] }, { "name": "Get Channel by ID", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query GetChannel($input: ChannelInput!) {\\n channel(input: $input) {\\n id\\n name\\n displayName\\n service\\n type\\n serviceId\\n organizationId\\n timezone\\n isDisconnected\\n isQueuePaused\\n allowedActions\\n scopes\\n postingSchedule {\\n day\\n times\\n paused\\n }\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"id\": \"{{channel_id}}\"\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "Single channel, including its weekly posting schedule (the slots `mode: addToQueue` will fill)." }, "response": [] }, { "name": "Get Daily Posting Limits", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query GetDailyPostingLimits($input: DailyPostingLimitsInput!) {\\n dailyPostingLimits(input: $input) {\\n channelId\\n limit\\n scheduled\\n sent\\n isAtLimit\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"channelIds\": [\n \"{{channel_id}}\"\n ]\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "Check before bulk-scheduling. `isAtLimit: true` means `createPost` will come back as `LimitReachedError`.\n\nOptional `input.date` (ISO 8601) checks a specific day." }, "response": [] } ] }, { "name": "03 · Posts — Read", "description": "Cursor-paginated. `first` = page size (20–50 recommended), `after` = `pageInfo.endCursor` from the previous page. Cursors are opaque — never parse them.\n\n⚠️ Do **not** add `totalCount` to the `posts` query — it returns `FORBIDDEN` on this API key.", "item": [ { "name": "Get Posts (paginated, captures post_id + cursor)", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "const conn = res.data.posts;", "if (conn.edges.length) {", " pm.collectionVariables.set('post_id', conn.edges[0].node.id);", " console.log('post_id =', conn.edges[0].node.id);", "}", "pm.collectionVariables.set('posts_cursor', conn.pageInfo.endCursor || '');", "console.log('hasNextPage =', conn.pageInfo.hasNextPage);", "conn.edges.forEach(e => console.log(` ${e.node.id} ${e.node.status.padEnd(14)} ${(e.node.text || '').slice(0, 60).replace(/\\n/g, ' ')}`));" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query GetPosts($first: Int, $after: String, $input: PostsInput!) {\\n posts(first: $first, after: $after, input: $input) {\\n edges {\\n cursor\\n node {\\n id\\n text\\n status\\n shareMode\\n schedulingType\\n dueAt\\n sentAt\\n createdAt\\n updatedAt\\n channelId\\n channelService\\n externalLink\\n isCustomScheduled\\n via\\n }\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n startCursor\\n hasPreviousPage\\n }\\n }\\n}\",\n \"variables\": {\n \"first\": 20,\n \"input\": {\n \"organizationId\": \"{{org_id}}\"\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "**This is the list-posts endpoint.**\n\nStores `edges[0].node.id` in `{{post_id}}` and `pageInfo.endCursor` in `{{posts_cursor}}` so *Get Posts — Next Page* and *Delete Post* just work." }, "response": [] }, { "name": "Get Posts — Next Page", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "pm.collectionVariables.set('posts_cursor', res.data.posts.pageInfo.endCursor || '');" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query GetPostsPage($first: Int, $after: String, $input: PostsInput!) {\\n posts(first: $first, after: $after, input: $input) {\\n edges {\\n cursor\\n node {\\n id\\n text\\n status\\n dueAt\\n channelId\\n }\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n}\",\n \"variables\": {\n \"first\": 20,\n \"after\": \"{{posts_cursor}}\",\n \"input\": {\n \"organizationId\": \"{{org_id}}\"\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "Run *Get Posts* first to populate `{{posts_cursor}}`. Re-run this request repeatedly — it rolls the cursor forward each time." }, "response": [] }, { "name": "Get Scheduled Posts (the queue)", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "const edges = res.data.posts.edges;", "const queued = edges.filter(e => !e.node.isCustomScheduled);", "if (queued.length) {", " pm.collectionVariables.set('queued_post_id', queued[0].node.id);", " console.log('queued_post_id =', queued[0].node.id);", "}" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query GetScheduledPosts($first: Int, $input: PostsInput!) {\\n posts(first: $first, input: $input) {\\n edges {\\n node {\\n id\\n text\\n status\\n shareMode\\n dueAt\\n isCustomScheduled\\n channelId\\n channelService\\n allowedActions\\n }\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n}\",\n \"variables\": {\n \"first\": 50,\n \"input\": {\n \"organizationId\": \"{{org_id}}\",\n \"filter\": {\n \"status\": [\n \"scheduled\"\n ],\n \"channelIds\": [\n \"{{channel_id}}\"\n ]\n },\n \"sort\": [\n {\n \"field\": \"dueAt\",\n \"direction\": \"asc\"\n }\n ]\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "Everything waiting to go out, soonest first. `allowedActions` tells you whether `deletePost` / `editPost` is permitted on each one.\n\n`sort.field` (`PostSortableKey`) is only `dueAt` or `createdAt`; `direction` is `asc` or `desc`.\n\nCaptures the first queued post into `{{queued_post_id}}` for **Move Post in Queue**." }, "response": [] }, { "name": "Get Sent Posts", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "const edges = res.data.posts.edges;", "if (edges.length) {", " pm.collectionVariables.set('sent_post_id', edges[0].node.id);", " console.log('sent_post_id =', edges[0].node.id);", "}" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query GetSentPosts($first: Int, $input: PostsInput!) {\\n posts(first: $first, input: $input) {\\n edges {\\n node {\\n id\\n text\\n sentAt\\n externalLink\\n channelService\\n metricsUpdatedAt\\n metrics {\\n name\\n type\\n unit\\n value\\n }\\n }\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n}\",\n \"variables\": {\n \"first\": 25,\n \"input\": {\n \"organizationId\": \"{{org_id}}\",\n \"filter\": {\n \"status\": [\n \"sent\"\n ],\n \"channelIds\": [\n \"{{channel_id}}\"\n ]\n },\n \"sort\": [\n {\n \"field\": \"dueAt\",\n \"direction\": \"desc\"\n }\n ]\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "Published posts with their live engagement metrics and the permalink (`externalLink`) on the network. `metrics` is null until the post is sent.\n\nCaptures the newest sent post into `{{sent_post_id}}` for the **06 · Analytics** folder." }, "response": [] }, { "name": "Get Drafts", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query GetDrafts($first: Int, $input: PostsInput!) {\\n posts(first: $first, input: $input) {\\n edges {\\n node {\\n id\\n text\\n status\\n createdAt\\n channelId\\n }\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n}\",\n \"variables\": {\n \"first\": 25,\n \"input\": {\n \"organizationId\": \"{{org_id}}\",\n \"filter\": {\n \"status\": [\n \"draft\",\n \"needs_approval\"\n ]\n }\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "`PostStatus` values: `draft`, `needs_approval`, `scheduled`, `sending`, `sent`, `error`." }, "response": [] }, { "name": "Get Failed Posts", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query GetFailedPosts($first: Int, $input: PostsInput!) {\\n posts(first: $first, input: $input) {\\n edges {\\n node {\\n id\\n text\\n status\\n dueAt\\n channelId\\n error {\\n message\\n }\\n }\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n}\",\n \"variables\": {\n \"first\": 25,\n \"input\": {\n \"organizationId\": \"{{org_id}}\",\n \"filter\": {\n \"status\": [\n \"error\"\n ]\n }\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "Posts the network rejected. `error.message` carries the reason (expired token, media rejected, duplicate content …)." }, "response": [] }, { "name": "Get Posts by Date Range", "event": [ { "listen": "prerequest", "script": { "type": "text/javascript", "exec": [ "pm.collectionVariables.set('range_end', new Date().toISOString());", "pm.collectionVariables.set('range_start', new Date(Date.now() - 30 * 864e5).toISOString());" ] } }, { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query GetPostsByDate($first: Int, $input: PostsInput!) {\\n posts(first: $first, input: $input) {\\n edges {\\n node {\\n id\\n text\\n status\\n dueAt\\n sentAt\\n createdAt\\n }\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n}\",\n \"variables\": {\n \"first\": 50,\n \"input\": {\n \"organizationId\": \"{{org_id}}\",\n \"filter\": {\n \"startDate\": \"{{range_start}}\",\n \"endDate\": \"{{range_end}}\"\n }\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "`startDate`/`endDate` match on `createdAt` **or** `dueAt`. The pre-request script sets a rolling 30-day window.\n\nFiner control: `dueAt` / `createdAt` accept a `DateTimeComparator` (`{ start, end }`), and `dueAtPresence` (`present` | `absent`) filters on whether a schedule exists at all. `absent` cannot be combined with a `dueAt` comparator." }, "response": [] }, { "name": "Get Post by ID", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query GetPost($input: PostInput!) {\\n post(input: $input) {\\n id\\n text\\n status\\n shareMode\\n schedulingType\\n dueAt\\n sentAt\\n createdAt\\n updatedAt\\n channelId\\n channelService\\n externalLink\\n isCustomScheduled\\n sharedNow\\n via\\n allowedActions\\n assets {\\n id\\n type\\n mimeType\\n source\\n thumbnail\\n }\\n tags {\\n id\\n name\\n }\\n author {\\n id\\n name\\n }\\n error {\\n message\\n }\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"id\": \"{{post_id}}\"\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "Full single post. `allowedActions` includes `deletePost` / `updatePost` when those mutations will be accepted." }, "response": [] } ] }, { "name": "04 · Posts — Create / Edit / Delete", "description": "Every create/edit response is a **union**. Always select `__typename` plus `... on PostActionSuccess` and `... on MutationError` — an HTTP 200 with `__typename: \"InvalidInputError\"` is still a failure.\n\n`ShareMode`: `addToQueue` · `shareNext` · `shareNow` · `customScheduled`.\n`SchedulingType`: `automatic` (Buffer publishes) · `notification` (Buffer reminds you).\n\nEach create request stores the new id in `{{post_id}}`, so **Delete Post** at the bottom of this folder cleans up whatever you just made.", "item": [ { "name": "Create Post · Add to Queue", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "const out = res.data.createPost;", "pm.test('createPost succeeded', function () {", " pm.expect(out.__typename, out.message || '').to.eql('PostActionSuccess');", "});", "if (out.__typename === 'PostActionSuccess') {", " pm.collectionVariables.set('post_id', out.post.id);", " console.log('post_id =', out.post.id, '| status =', out.post.status);", "}", "if (out.__typename === 'PostActionSuccess') {", " pm.collectionVariables.set('queued_post_id', out.post.id);", "}" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"mutation CreatePost($input: CreatePostInput!) {\\n createPost(input: $input) {\\n __typename\\n ... on PostActionSuccess {\\n post {\\n id\\n text\\n status\\n shareMode\\n dueAt\\n createdAt\\n channelId\\n channelService\\n }\\n }\\n ... on MutationError {\\n message\\n }\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"channelId\": \"{{channel_id}}\",\n \"text\": \"Posted from the Buffer API collection.\",\n \"schedulingType\": \"automatic\",\n \"mode\": \"addToQueue\",\n \"assets\": []\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "Drops the post into the next free slot of the channel's posting schedule. Buffer picks `dueAt` for you.\n\nThis is the mode the HRMS job-post flow uses by default.\n\nAlso stores the new id in `{{queued_post_id}}` so **Move Post in Queue** has a genuinely queued post to act on." }, "response": [] }, { "name": "Create Post · Draft (safe to test with)", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "const out = res.data.createPost;", "pm.test('createPost succeeded', function () {", " pm.expect(out.__typename, out.message || '').to.eql('PostActionSuccess');", "});", "if (out.__typename === 'PostActionSuccess') {", " pm.collectionVariables.set('post_id', out.post.id);", " console.log('post_id =', out.post.id, '| status =', out.post.status);", "}" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"mutation CreatePost($input: CreatePostInput!) {\\n createPost(input: $input) {\\n __typename\\n ... on PostActionSuccess {\\n post {\\n id\\n text\\n status\\n shareMode\\n dueAt\\n createdAt\\n channelId\\n channelService\\n }\\n }\\n ... on MutationError {\\n message\\n }\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"channelId\": \"{{channel_id}}\",\n \"text\": \"Draft from the Buffer API collection — not published.\",\n \"schedulingType\": \"automatic\",\n \"mode\": \"addToQueue\",\n \"saveToDraft\": true,\n \"assets\": []\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "`saveToDraft: true` creates the post with `status: draft`. Nothing is published and daily posting limits are not consumed.\n\n**Use this one when smoke-testing** — then run *Delete Post* to remove it." }, "response": [] }, { "name": "Create Post · Custom Scheduled", "event": [ { "listen": "prerequest", "script": { "type": "text/javascript", "exec": [ "pm.collectionVariables.set('due_at', new Date(Date.now() + 864e5).toISOString());" ] } }, { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "const out = res.data.createPost;", "pm.test('createPost succeeded', function () {", " pm.expect(out.__typename, out.message || '').to.eql('PostActionSuccess');", "});", "if (out.__typename === 'PostActionSuccess') {", " pm.collectionVariables.set('post_id', out.post.id);", " console.log('post_id =', out.post.id, '| status =', out.post.status);", "}" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"mutation CreatePost($input: CreatePostInput!) {\\n createPost(input: $input) {\\n __typename\\n ... on PostActionSuccess {\\n post {\\n id\\n text\\n status\\n shareMode\\n dueAt\\n createdAt\\n channelId\\n channelService\\n }\\n }\\n ... on MutationError {\\n message\\n }\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"channelId\": \"{{channel_id}}\",\n \"text\": \"Scheduled from the Buffer API collection.\",\n \"schedulingType\": \"automatic\",\n \"mode\": \"customScheduled\",\n \"dueAt\": \"{{due_at}}\",\n \"assets\": []\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "`mode: customScheduled` **requires** `dueAt` as an ISO 8601 UTC timestamp (`2026-08-06T09:00:00.000Z`). The pre-request script sets `{{due_at}}` to 24 hours from now." }, "response": [] }, { "name": "Create Post · Share Next (top of queue)", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "const out = res.data.createPost;", "pm.test('createPost succeeded', function () {", " pm.expect(out.__typename, out.message || '').to.eql('PostActionSuccess');", "});", "if (out.__typename === 'PostActionSuccess') {", " pm.collectionVariables.set('post_id', out.post.id);", " console.log('post_id =', out.post.id, '| status =', out.post.status);", "}" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"mutation CreatePost($input: CreatePostInput!) {\\n createPost(input: $input) {\\n __typename\\n ... on PostActionSuccess {\\n post {\\n id\\n text\\n status\\n shareMode\\n dueAt\\n createdAt\\n channelId\\n channelService\\n }\\n }\\n ... on MutationError {\\n message\\n }\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"channelId\": \"{{channel_id}}\",\n \"text\": \"Jumping the queue, via the Buffer API collection.\",\n \"schedulingType\": \"automatic\",\n \"mode\": \"shareNext\",\n \"assets\": []\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "Takes the *next* available slot, pushing everything else down." }, "response": [] }, { "name": "⚠️ Create Post · Share Now (PUBLISHES IMMEDIATELY)", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "const out = res.data.createPost;", "pm.test('createPost succeeded', function () {", " pm.expect(out.__typename, out.message || '').to.eql('PostActionSuccess');", "});", "if (out.__typename === 'PostActionSuccess') {", " pm.collectionVariables.set('post_id', out.post.id);", " console.log('post_id =', out.post.id, '| status =', out.post.status);", "}" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"mutation CreatePost($input: CreatePostInput!) {\\n createPost(input: $input) {\\n __typename\\n ... on PostActionSuccess {\\n post {\\n id\\n text\\n status\\n shareMode\\n dueAt\\n createdAt\\n channelId\\n channelService\\n }\\n }\\n ... on MutationError {\\n message\\n }\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"channelId\": \"{{channel_id}}\",\n \"text\": \"Published immediately from the Buffer API collection.\",\n \"schedulingType\": \"automatic\",\n \"mode\": \"shareNow\",\n \"assets\": []\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "**This goes live on the real social account the moment you hit Send.** There is no undo — `deletePost` removes it from Buffer but does not always retract it from the network.\n\nUse *Create Post · Draft* for testing instead." }, "response": [] }, { "name": "Create Post · Needs Approval", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "const out = res.data.createPost;", "pm.test('createPost succeeded', function () {", " pm.expect(out.__typename, out.message || '').to.eql('PostActionSuccess');", "});", "if (out.__typename === 'PostActionSuccess') {", " pm.collectionVariables.set('post_id', out.post.id);", " console.log('post_id =', out.post.id, '| status =', out.post.status);", "}" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"mutation CreatePost($input: CreatePostInput!) {\\n createPost(input: $input) {\\n __typename\\n ... on PostActionSuccess {\\n post {\\n id\\n text\\n status\\n shareMode\\n dueAt\\n createdAt\\n channelId\\n channelService\\n }\\n }\\n ... on MutationError {\\n message\\n }\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"channelId\": \"{{channel_id}}\",\n \"text\": \"Submitted for approval from the Buffer API collection.\",\n \"schedulingType\": \"automatic\",\n \"mode\": \"addToQueue\",\n \"needsApproval\": true,\n \"assets\": []\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "`needsApproval: true` parks the post at `status: needs_approval` instead of scheduling it.\n\n⚠️ Only accepted when the channel's posting policy actually requires approval (Buffer → Settings → posting policy, paid plans). Otherwise you get `InvalidInputError: needsApproval is only valid when your posting policy on this channel requires approval`." }, "response": [] }, { "name": "Create Post · With Image", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "const out = res.data.createPost;", "pm.test('createPost succeeded', function () {", " pm.expect(out.__typename, out.message || '').to.eql('PostActionSuccess');", "});", "if (out.__typename === 'PostActionSuccess') {", " pm.collectionVariables.set('post_id', out.post.id);", " console.log('post_id =', out.post.id, '| status =', out.post.status);", "}" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"mutation CreatePost($input: CreatePostInput!) {\\n createPost(input: $input) {\\n __typename\\n ... on PostActionSuccess {\\n post {\\n id\\n text\\n status\\n shareMode\\n dueAt\\n createdAt\\n channelId\\n channelService\\n }\\n }\\n ... on MutationError {\\n message\\n }\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"channelId\": \"{{channel_id}}\",\n \"text\": \"Image post from the Buffer API collection.\",\n \"schedulingType\": \"automatic\",\n \"mode\": \"addToQueue\",\n \"saveToDraft\": true,\n \"assets\": [\n {\n \"image\": {\n \"url\": \"https://picsum.photos/1200/630.jpg\",\n \"thumbnailUrl\": \"https://picsum.photos/1200/630.jpg\"\n }\n }\n ]\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "`assets` is an **ordered** list. Each entry is exactly one of `image` / `video` / `document` / `link`.\n\n* `image` → `{ url!, thumbnailUrl, metadata }`\n* `video` → `{ url!, thumbnailUrl, metadata }`\n* `document` → `{ url!, title!, thumbnailUrl! }`\n* `link` → `{ url!, title, description, thumbnailUrl }`\n\nURLs must be publicly reachable **and return the raw bytes** — Buffer fetches them server-side, so a page that redirects to a login or a CDN that blocks server-side fetches fails with `InvalidInputError: Image could not be read from its URL`. See the *Hosting Media* guide for Buffer's own upload endpoint.\n\nSet to `saveToDraft: true` here so you can run it safely." }, "response": [] }, { "name": "Create Post · LinkedIn (first comment + link attachment)", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "const out = res.data.createPost;", "pm.test('createPost succeeded', function () {", " pm.expect(out.__typename, out.message || '').to.eql('PostActionSuccess');", "});", "if (out.__typename === 'PostActionSuccess') {", " pm.collectionVariables.set('post_id', out.post.id);", " console.log('post_id =', out.post.id, '| status =', out.post.status);", "}" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"mutation CreatePost($input: CreatePostInput!) {\\n createPost(input: $input) {\\n __typename\\n ... on PostActionSuccess {\\n post {\\n id\\n text\\n status\\n shareMode\\n dueAt\\n createdAt\\n channelId\\n channelService\\n }\\n }\\n ... on MutationError {\\n message\\n }\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"channelId\": \"{{channel_id}}\",\n \"text\": \"LinkedIn post from the Buffer API collection.\",\n \"schedulingType\": \"automatic\",\n \"mode\": \"addToQueue\",\n \"saveToDraft\": true,\n \"assets\": [],\n \"metadata\": {\n \"linkedin\": {\n \"linkAttachment\": {\n \"url\": \"https://example.com/careers\"\n }\n }\n }\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "`metadata` is keyed by network: `linkedin`, `twitter`, `instagram`, `facebook`, `tiktok`, `threads`, `youtube`, `pinterest`, `mastodon`, `bluesky`, `google`.\n\nLinkedIn accepts `firstComment`, `linkAttachment` (`{ url }` only — no title/description override), and `annotations` (@-mentions).\n\n⚠️ `firstComment` is a **paid-plan feature** — on Free it comes back as `InvalidInputError: LinkedIn first comment requires a paid plan`. It is left out of the body below; add it back once the account is upgraded:\n```json\n\"linkedin\": { \"firstComment\": \"Full JD in the comments 👇\" }\n```\n\n⚠️ `metadata..linkAttachment` and a non-empty `assets` array are **mutually exclusive** — sending both is an `InvalidInputError`." }, "response": [] }, { "name": "Edit Post", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "pm.test('editPost succeeded', function () {", " pm.expect(res.data.editPost.__typename, res.data.editPost.message || '')", " .to.eql('PostActionSuccess');", "});" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"mutation EditPost($input: EditPostInput!) {\\n editPost(input: $input) {\\n __typename\\n ... on PostActionSuccess {\\n post {\\n id\\n text\\n status\\n dueAt\\n updatedAt\\n }\\n }\\n ... on MutationError {\\n message\\n }\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"id\": \"{{post_id}}\",\n \"text\": \"Edited via the Buffer API collection.\",\n \"schedulingType\": \"automatic\"\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "The mutation is `editPost` (not `updatePost`). `id` and `schedulingType` are required; every other field is optional and **omitting a field preserves its current value**.\n\nChange the schedule by sending `mode: \"customScheduled\"` together with a new `dueAt`." }, "response": [] }, { "name": "Move Post in Queue", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "pm.test('movePostInQueue succeeded', function () {", " pm.expect(res.data.movePostInQueue.__typename,", " res.data.movePostInQueue.message || '').to.eql('PostActionSuccess');", "});" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"mutation MovePostInQueue($input: MovePostInQueueInput!) {\\n movePostInQueue(input: $input) {\\n __typename\\n ... on PostActionSuccess {\\n post {\\n id\\n dueAt\\n shareMode\\n }\\n }\\n ... on MutationError {\\n message\\n }\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"id\": \"{{queued_post_id}}\",\n \"position\": \"top\"\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "`position` is `top` or `bottom`.\n\n⚠️ Only works on posts whose `shareMode` is `addToQueue`/`shareNext`. A draft or a `customScheduled` post gives `VoidMutationError: Only queued posts can be moved within the queue` — hence the separate `{{queued_post_id}}` variable, filled by *Get Scheduled Posts* or *Create Post · Add to Queue*." }, "response": [] }, { "name": "Delete Post", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "const out = res.data.deletePost;", "pm.test('deletePost succeeded', function () {", " pm.expect(out.__typename, out.message || '').to.eql('DeletePostSuccess');", "});", "if (out.__typename === 'DeletePostSuccess') {", " console.log('deleted', out.id);", " pm.collectionVariables.set('post_id', '');", "}" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"mutation DeletePost($input: DeletePostInput!) {\\n deletePost(input: $input) {\\n __typename\\n ... on DeletePostSuccess {\\n id\\n }\\n ... on MutationError {\\n message\\n }\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"id\": \"{{post_id}}\"\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "**This is the delete endpoint.**\n\nTakes only the post id. The payload union is `DeletePostSuccess { id }` | `VoidMutationError { message }` — note it is *not* `PostActionSuccess`.\n\nDeleting a `sent` post removes it from Buffer; it does not necessarily retract it from the social network. Check `allowedActions` on the post for `deletePost` first." }, "response": [] } ] }, { "name": "05 · Ideas", "description": "Ideas live on the **organization**, not a channel — drafts that are not yet committed to a network.", "item": [ { "name": "Get Ideas", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "const edges = res.data.ideas.edges;", "if (edges.length) pm.collectionVariables.set('idea_id', edges[0].node.id);" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query GetIdeas($first: Int, $after: String, $input: IdeasInput!) {\\n ideas(first: $first, after: $after, input: $input) {\\n edges {\\n cursor\\n node {\\n id\\n createdAt\\n content {\\n title\\n text\\n services\\n }\\n }\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n}\",\n \"variables\": {\n \"first\": 20,\n \"input\": {\n \"organizationId\": \"{{org_id}}\"\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "Cursor-paginated like posts. Optional `groupFilter` and `tagsFilter`." }, "response": [] }, { "name": "Create Idea", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"mutation CreateIdea($input: CreateIdeaInput!) {\\n createIdea(input: $input) {\\n __typename\\n ... on IdeaResponse {\\n refreshIdeas\\n idea {\\n id\\n organizationId\\n createdAt\\n content {\\n title\\n text\\n services\\n }\\n }\\n }\\n ... on Idea {\\n id\\n content {\\n title\\n text\\n }\\n }\\n ... on MutationError {\\n message\\n }\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"organizationId\": \"{{org_id}}\",\n \"content\": {\n \"title\": \"Idea from the Buffer API collection\",\n \"text\": \"Draft copy that is not tied to a channel yet.\",\n \"services\": [\n \"linkedin\"\n ]\n }\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "`content` accepts `title`, `text`, `services`, `media`, `tags`, `date`, `aiAssisted`.\n\nThe payload union is `IdeaResponse` | `Idea` | `InvalidInputError` | `UnauthorizedError` | `LimitReachedError` | `UnexpectedError` — this API returns `IdeaResponse`.\n\n⚠️ There is no `deleteIdea` mutation, so anything you create here has to be removed from the Buffer UI." }, "response": [] } ] }, { "name": "06 · Analytics", "description": "Metrics only exist for `sent` posts. On the Free plan, Insights history is capped at the **last 31 days** — a wider window returns `BAD_USER_INPUT`.", "item": [ { "name": "Get Post Metrics", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query GetPostMetrics($input: PostInput!) {\\n post(input: $input) {\\n id\\n sentAt\\n externalLink\\n metricsUpdatedAt\\n metrics {\\n name\\n description\\n type\\n unit\\n value\\n }\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"id\": \"{{sent_post_id}}\"\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "Run **03 · Get Sent Posts** first — it fills `{{sent_post_id}}`. (Pointing this at `{{post_id}}` right after a delete gives `BAD_USER_INPUT: Invalid PostId format`, because the variable is empty.)\n\n`metrics` is `null` until the post is sent. `type` is one of `impressions`, `reach`, `reactions`, `likes`, `comments`, `shares`, `reposts`, `quotes`, `clicks`, `saves`, `follows`, `views`, `viewers`, `totalTimeWatched`, `engagementRate`, `postCount`. `unit` is `count` or `percentage`." }, "response": [] }, { "name": "Get Aggregated Post Metrics (last 30 days)", "event": [ { "listen": "prerequest", "script": { "type": "text/javascript", "exec": [ "pm.collectionVariables.set('metrics_end', new Date().toISOString());", "pm.collectionVariables.set('metrics_start', new Date(Date.now() - 30 * 864e5).toISOString());" ] } }, { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query GetAggregatedPostMetrics($input: AggregatedPostMetricsInput!) {\\n aggregatedPostMetrics(input: $input) {\\n metricsUpdatedAt\\n metrics {\\n name\\n type\\n unit\\n value\\n }\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"organizationId\": \"{{org_id}}\",\n \"channelIds\": [\n \"{{channel_id}}\"\n ],\n \"startDateTime\": \"{{metrics_start}}\",\n \"endDateTime\": \"{{metrics_end}}\"\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "Totals across every sent post in the window. The pre-request script sets a 30-day range to stay inside the Free-plan 31-day cap." }, "response": [] } ] }, { "name": "07 · HRMS job-post flow", "description": "The exact calls `backend/job/job_post/plugins.py` makes, so you can reproduce a backend failure directly against Buffer.\n\n`.env` mapping: `BUFFER_API` → `{{buffer_token}}`, `BUFFER_API_URL` → `{{buffer_api_url}}`, `BUFFER_CHANNEL_ID` → `{{channel_id}}`.", "item": [ { "name": "1. list_buffer_channels — organizations", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "pm.collectionVariables.set('org_id', res.data.account.organizations[0].id);" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query { account { organizations { id name } } }\"\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "First half of `list_buffer_channels()` — mirrors the literal query string in `plugins.py`." }, "response": [] }, { "name": "2. list_buffer_channels — channels per org", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query GetChannels {\\n channels(input: { organizationId: \\\"{{org_id}}\\\" }) {\\n id\\n name\\n displayName\\n service\\n isQueuePaused\\n }\\n}\"\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "Second half of `list_buffer_channels()`, exposed by the backend at `GET /job/buffer/channels`. Note this one inlines the org id rather than using GraphQL variables — same as the Python." }, "response": [] }, { "name": "3. create_buffer_post — rendered job ad", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "const out = res.data.createPost;", "pm.test('createPost succeeded', function () {", " pm.expect(out.__typename, out.message || '').to.eql('PostActionSuccess');", "});", "if (out.__typename === 'PostActionSuccess') {", " pm.collectionVariables.set('post_id', out.post.id);", " console.log('post_id =', out.post.id, '| status =', out.post.status);", "}" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"mutation CreatePost($input: CreatePostInput!) {\\n createPost(input: $input) {\\n __typename\\n ... on PostActionSuccess {\\n post {\\n id\\n text\\n status\\n shareMode\\n dueAt\\n createdAt\\n channelId\\n channelService\\n }\\n }\\n ... on MutationError {\\n message\\n }\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"channelId\": \"{{channel_id}}\",\n \"text\": \"We're hiring: AI Engineer\\n\\nKarachi · Full-time\\n\\nExperience: 2–3 years\\n\\nRequirements:\\n• AWS\\n• FastAPI\\n• LangChain\\n\\nNice to have:\\n• Azure\\n\\nSalary: Anonymous\\n\\nInterested? Apply via our careers page or reply to this post.\\n\\n#AWS #FastAPI #LangChain\",\n \"schedulingType\": \"automatic\",\n \"mode\": \"addToQueue\",\n \"saveToDraft\": true,\n \"assets\": []\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "What `POST /job/post-job` ends up sending, using the output of `render_job_post()`. The backend supports `mode` of `addToQueue`, `shareNow`, or `customScheduled` (which then requires `due_at`).\n\nLinkedIn caps post text at 3000 characters — `render_job_post()` truncates to that.\n\n`saveToDraft: true` is added here so running it does not queue a real job ad; the backend does not send it." }, "response": [] }, { "name": "4. clean up — delete the post created above", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"mutation DeletePost($input: DeletePostInput!) {\\n deletePost(input: $input) {\\n __typename\\n ... on DeletePostSuccess {\\n id\\n }\\n ... on MutationError {\\n message\\n }\\n }\\n}\",\n \"variables\": {\n \"input\": {\n \"id\": \"{{post_id}}\"\n }\n }\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "Removes whatever step 3 created." }, "response": [] } ] }, { "name": "08 · Error shapes (reference)", "description": "Run these to see each failure mode. Buffer returns **HTTP 200** for almost everything — you must inspect the body.\n\n* Non-recoverable → top-level `errors[]` with `extensions.code`: `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `BAD_USER_INPUT`, `GRAPHQL_VALIDATION_FAILED`, `UNEXPECTED`, `RATE_LIMIT_EXCEEDED`.\n* Recoverable → `data..__typename` is a member of the error union (`InvalidInputError`, `LimitReachedError`, `NotFoundError`, `UnauthorizedError`, `RestProxyError`, `UnexpectedError`).", "item": [ { "name": "FORBIDDEN — totalCount on posts", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "console.log(JSON.stringify(res.errors, null, 2));", "pm.test('Returns a GraphQL error (expected)', function () {", " pm.expect(res.errors).to.be.an('array');", "});" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query {\\n posts(first: 1, input: { organizationId: \\\"{{org_id}}\\\" }) {\\n totalCount\\n }\\n}\"\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "`totalCount` is in the schema but rejected for API-key auth. This is the most common cause of a `posts` query failing after copy-pasting from the schema reference — leave it out." }, "response": [] }, { "name": "NOT_FOUND — bad post id", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "console.log(JSON.stringify(res.errors, null, 2));", "pm.test('Returns a GraphQL error (expected)', function () {", " pm.expect(res.errors).to.be.an('array');", "});" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query {\\n post(input: { id: \\\"000000000000000000000000\\\" }) {\\n id\\n }\\n}\"\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "Expect `errors[0].extensions.code === 'NOT_FOUND'`." }, "response": [] }, { "name": "Rate limit headers", "event": [ { "listen": "test", "script": { "type": "text/javascript", "exec": [ "const res = pm.response.json();", "pm.test('HTTP 200', () => pm.response.to.have.status(200));", "pm.test('No GraphQL errors', function () {", " pm.expect(res.errors, JSON.stringify(res.errors)).to.be.undefined;", "});", "console.log('ratelimit :', pm.response.headers.get('ratelimit'));", "console.log('ratelimit-policy:', pm.response.headers.get('ratelimit-policy'));" ] } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"query\": \"query { account { id } }\"\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{buffer_api_url}}", "host": [ "{{buffer_api_url}}" ] }, "description": "Every response carries three rolling windows. Free plan: 100 / 15 min, 250 / day, 3000 / 30 days. `r` = remaining, `t` = seconds to reset. Exceeding one gives HTTP 429 + `Retry-After`." }, "response": [] } ] } ], "variable": [ { "key": "buffer_api_url", "value": "https://api.buffer.com", "type": "string" }, { "key": "buffer_token", "value": "", "type": "string" }, { "key": "org_id", "value": "", "type": "string" }, { "key": "channel_id", "value": "", "type": "string" }, { "key": "post_id", "value": "", "type": "string" }, { "key": "sent_post_id", "value": "", "type": "string" }, { "key": "queued_post_id", "value": "", "type": "string" }, { "key": "idea_id", "value": "", "type": "string" }, { "key": "posts_cursor", "value": "", "type": "string" }, { "key": "due_at", "value": "", "type": "string" }, { "key": "range_start", "value": "", "type": "string" }, { "key": "range_end", "value": "", "type": "string" }, { "key": "metrics_start", "value": "", "type": "string" }, { "key": "metrics_end", "value": "", "type": "string" } ] }