{"openapi":"3.0.3","info":{"title":"Tailwind Public API","version":"1.0.0","description":"The Tailwind Public API allows you to programmatically schedule pins to Pinterest. Use an API key or OAuth access token to authenticate requests.","contact":{"name":"Tailwind Support","url":"https://www.tailwindapp.com"}},"servers":[{"url":"https://api-v1.tailwind.ai","description":"Production"},{"url":"https://{stage}-api-v1.dev.tailwindapp.net","description":"Development","variables":{"stage":{"default":"dev","description":"Development stage name"}}}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Health","description":"Health check endpoints"},{"name":"Identity","description":"Authenticated caller identity"},{"name":"Accounts","description":"Pinterest account management"},{"name":"Boards","description":"Pinterest boards"},{"name":"Posts","description":"Scheduled post management"},{"name":"Generations","description":"AI Pin generation from a URL (SmartPin)"},{"name":"Group boards","description":"Public Pinterest group boards, as benchmark context"},{"name":"Analytics","description":"Pinterest performance: per Pin, per account, and best performers"},{"name":"Pins","description":"Every Pin on a connected Pinterest account"},{"name":"Media","description":"Uploading your own file bytes"},{"name":"Keywords","description":"The organization's saved keyword list"},{"name":"Feeds","description":"RSS feed subscriptions that automatically generate SmartPins"},{"name":"Capability requests","description":"Telling Tailwind what this API cannot do yet"},{"name":"Issue reports","description":"Telling Tailwind that a call answered wrongly rather than failing"}],"paths":{"/health":{"get":{"tags":["Health"],"operationId":"health-check","summary":"Health check","description":"Check if the API is running. No authentication required.","security":[],"responses":{"200":{"description":"API is healthy","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"ok"}}}}}}}}},"/v1/health":{"get":{"tags":["Health"],"operationId":"health-check-v1","summary":"Health check (alias)","description":"Alias of /health. Check if the API is running. No authentication required.","security":[],"responses":{"200":{"description":"API is healthy","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"ok"}}}}}}}}},"/v1/me":{"get":{"tags":["Identity"],"operationId":"get-identity","summary":"Get authenticated identity","description":"Return the organization ID, customer ID, and scopes associated with the credentials making the request. Useful as a first call to verify authentication.","responses":{"200":{"description":"Authenticated caller identity","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"orgId":{"type":"string","description":"Organization ID"},"custId":{"type":"string","description":"Customer ID"},"scopes":{"type":"array","items":{"type":"string"},"description":"Scopes granted to the credentials"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts":{"get":{"tags":["Accounts"],"operationId":"list-accounts","summary":"List accounts","description":"List all Pinterest accounts for the authenticated organization.","responses":{"200":{"description":"List of accounts","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"accounts":{"type":"array","items":{"$ref":"#/components/schemas/Account"}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts/connect-link":{"get":{"tags":["Accounts"],"operationId":"get-account-connect-link","summary":"Get account connect link","description":"Get a browser link that connects a Pinterest account to the organization. With `accountId`, the link re-authorizes that existing account (use it when `tokenAuthorized` is false). Without it, the link adds a new Pinterest account, and is returned only if the caller is an organization admin and the plan has room for another account; otherwise `url` is null and `blocked` says why. Nothing is connected or billed until the user opens the link in a browser signed in to this Tailwind organization and completes Pinterest's authorization there. The user must also be signed in to the right Pinterest account in that browser, since Pinterest authorizes whichever account is signed in.","parameters":[{"name":"accountId","in":"query","required":false,"description":"Tailwind account ID (from `GET /v1/accounts`) to re-authorize. Omit to add a new account.","schema":{"type":"string"}}],"responses":{"200":{"description":"Connect link, or the reason a new account cannot be added","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"action":{"type":"string","enum":["add","reconnect"],"description":"Whether the link adds a new account or re-authorizes `accountId`"},"url":{"type":"string","nullable":true,"description":"Link for the user to open in a browser signed in to Tailwind. Null when `blocked` is set."},"accountLimit":{"type":"integer","nullable":true,"description":"Pinterest accounts the plan allows. Null when the plan has no account cap, and on reconnect links."},"blocked":{"type":"object","nullable":true,"description":"Why a new account cannot be added; null when it can","properties":{"reason":{"type":"string","enum":["not_admin","account_limit"]},"message":{"type":"string","description":"User-facing explanation"}},"required":["reason","message"]}},"required":["action","url","accountLimit","blocked"]},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts/{accountId}":{"get":{"tags":["Accounts"],"operationId":"get-account","summary":"Get account","description":"Get a single connected Pinterest account. A 404 means the account is unknown, is not yours (the two are deliberately indistinguishable, so that account IDs cannot be enumerated), or is one of your own accounts that is no longer connected to Tailwind — the last case says so in the message.","parameters":[{"$ref":"#/components/parameters/accountId"}],"responses":{"200":{"description":"Account details","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"account":{"$ref":"#/components/schemas/AccountDetail"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts/{accountId}/boards":{"get":{"tags":["Boards"],"operationId":"list-boards","summary":"List boards","description":"List all Pinterest boards for an account.","parameters":[{"$ref":"#/components/parameters/accountId"}],"responses":{"200":{"description":"List of boards","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"boards":{"type":"array","items":{"$ref":"#/components/schemas/Board"}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"The account is not connected to Pinterest. The account holder needs to reconnect it in Tailwind before its boards can be listed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"BAD_REQUEST","message":"This account is not connected to Pinterest. Reconnect it in Tailwind, then retry."},"meta":{"requestId":"abc123"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/PinterestReconnectRequired"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"description":"Pinterest boards could not be loaded. Listing changes nothing, so the request can be retried; contact Tailwind support if it keeps failing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"BAD_GATEWAY","message":"Pinterest Boards could not be loaded right now. Nothing was changed. Retry in a few minutes, and contact Tailwind support if this keeps happening.","reason":"list_boards_upstream_failed"},"meta":{"requestId":"abc123"}}}}},"503":{"$ref":"#/components/responses/PinterestRateLimited"},"504":{"description":"Pinterest did not respond in time. Listing changes nothing, so the request can be retried.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"GATEWAY_TIMEOUT","message":"Pinterest did not respond in time. Nothing was changed, so this request can be retried.","reason":"pinterest_timeout"},"meta":{"requestId":"abc123"}}}}}}},"post":{"tags":["Boards"],"operationId":"create-board","summary":"Create board","description":"Create a board on the account's Pinterest profile. Creating a board does not consume credits. Use the board ID returned here rather than re-listing: GET /v1/accounts/{accountId}/boards is served from a cache, so a just-created board may not appear in it yet.","parameters":[{"$ref":"#/components/parameters/accountId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBoardRequest"}}}},"responses":{"201":{"description":"Board created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"board":{"$ref":"#/components/schemas/ManagedBoard"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/PinterestReconnectRequired"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/PinterestRequestFailed"},"503":{"$ref":"#/components/responses/PinterestRateLimited"},"504":{"$ref":"#/components/responses/PinterestTimeout"}}}},"/v1/accounts/{accountId}/boards/{boardId}":{"patch":{"tags":["Boards"],"operationId":"update-board","summary":"Update board","description":"Rename a board or change its description. Supply at least one of `name` or `description`. Privacy cannot be changed here — switching a public board to secret would delist every Pin on it.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/boardId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBoardRequest"}}}},"responses":{"200":{"description":"Board updated","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"board":{"$ref":"#/components/schemas/ManagedBoard"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/PinterestForbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/PinterestReconnectRequired"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/PinterestRequestFailed"},"503":{"$ref":"#/components/responses/PinterestRateLimited"},"504":{"$ref":"#/components/responses/PinterestTimeout"}}},"delete":{"tags":["Boards"],"operationId":"delete-board","summary":"Delete board","description":"Permanently delete a board on Pinterest, **including every Pin saved to it**. This cannot be undone and is not a soft delete. The response reports the deleted board's name and pin count so the caller can record what was removed.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/boardId"}],"responses":{"200":{"description":"Board deleted","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"board":{"$ref":"#/components/schemas/DeletedBoard"},"deleted":{"type":"boolean","example":true}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/PinterestForbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/PinterestReconnectRequired"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/PinterestRequestFailed"},"503":{"$ref":"#/components/responses/PinterestRateLimited"},"504":{"$ref":"#/components/responses/PinterestTimeout"}}}},"/v1/accounts/{accountId}/boards/{boardId}/pins":{"get":{"tags":["Boards"],"operationId":"list-board-pins","summary":"List the Pins on a board","description":"The Pins saved on one of the account's boards, secret boards included, in Pinterest's board order: mostly newest first, but not strictly. Each Pin carries its Pinterest URL, image URL, destination link, saved time and, when it sits in a section, the section's id. This lists what is on the board now on Pinterest, however each Pin got there, unlike GET /v1/accounts/{accountId}/posts, which lists only Tailwind's own post records.\n\nEach Pin's `pinId` is the Pinterest Pin id, which GET /v1/accounts/{accountId}/pins/{pinId}/analytics takes. It is not a Tailwind postId.\n\nstartDate and endDate filter on when the Pin was created, which for a save is when it was saved, and either may be given alone. Because board order is not strictly by date, every Pin is checked and the walk does not stop early: a response can hold fewer than `limit` Pins, or none, while `cursor` is still present. Keep paging until `cursor` is absent: only then has the whole board been read. To sort by date, sort the collected Pins by `createdAt`.\n\nReading a board is free and consumes no credits.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/boardId"},{"name":"cursor","in":"query","required":false,"description":"Pagination cursor from the previous response","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Pins to read from Pinterest per page, 1 to 250. Defaults to 100.","schema":{"type":"integer","minimum":1,"maximum":250,"default":100}},{"name":"startDate","in":"query","required":false,"description":"Only Pins created or saved at or after this instant (ISO 8601 date-time in UTC, e.g. 2026-10-01T00:00:00Z)","schema":{"type":"string","format":"date-time"}},{"name":"endDate","in":"query","required":false,"description":"Only Pins created or saved at or before this instant (ISO 8601 date-time in UTC, e.g. 2026-10-07T23:59:59Z)","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"One page of the board's Pins, in board order","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"accountId":{"type":"string"},"boardId":{"type":"string"},"pins":{"type":"array","items":{"$ref":"#/components/schemas/BoardPin"}},"cursor":{"type":"string","description":"Pass back as `cursor` for the next page. Absent once the whole board has been read."}},"required":["accountId","boardId","pins"]},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/PinterestReconnectRequired"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/PinterestRequestFailed"},"503":{"$ref":"#/components/responses/PinterestRateLimited"}}},"post":{"tags":["Boards"],"operationId":"save-pins-to-board","summary":"Save Pins to a board","description":"Save existing Pins (repins) onto a board. This is the seeding write behind \"build me a board about X\": create a board, call `get-recommended-pins` for it with your keyword, then pass those Pin IDs here.\n\n**Billing.** Saving onto a board created through Tailwind in the last hour is free for up to 50 Pins in total, across calls. Any other save is billed like scheduling a Pin: one `pin_schedule` credit per saved Pin on modular billing, or one post from the post allowance on legacy and Shopify billing. A save the plan cannot cover returns `402` with nothing saved.\n\nThree things to plan around:\n\n- **Per-Pin failures are counted, not raised.** A deleted or unreachable Pin lands in `failedCount` and the rest still save. A board ID that does not exist therefore returns `200` with every Pin failed rather than a `404`, and the response does not say which Pins failed.\n- **This is not idempotent.** Pinterest treats a repeat save as a second Pin, so retrying — or passing the same ID twice in one batch — pins twice. Re-running `get-recommended-pins` is safe: a save stores a *copy* under a new Pin ID, and that endpoint matches the copy back to the Pin it was saved from, so a second pass returns what is still missing rather than what you just saved.\n- **A large batch can time out.** Saves run four at a time against a 10-second-per-Pin Pinterest budget, so a big batch of slow saves can exceed the request budget and return a `504`. Saves already in flight are not cancelled, so an unknown number of Pins may still have saved — re-read the board rather than blind-retrying, since a retry can save a Pin twice. Prefer batches in the tens.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/boardId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavePinsToBoardRequest"}}}},"responses":{"200":{"description":"Save attempted for every Pin. Check both counts — a `200` with `savedCount: 0` is a complete failure, not a success.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SavePinsToBoardResult"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/PinterestForbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/PinterestReconnectRequired"},"429":{"$ref":"#/components/responses/RateLimited"},"504":{"$ref":"#/components/responses/PinterestTimeout"}}}},"/v1/accounts/{accountId}/boards/{boardId}/recommended-pins":{"get":{"tags":["Boards"],"operationId":"get-recommended-pins","summary":"Recommend Pins to add to a board","description":"**Paused:** returns `503` with `reason: \"recommended_pins_awaiting_pinterest_access\"` while Tailwind waits for Pinterest to approve access to its Pin search API. Pinterest's public search stopped returning Pins that can be saved, so recommendations resume once access is granted.\n\nPins from Pinterest's public search worth adding to this board for a keyword, strongest first, each carrying the relevance tier that earned its place. Excludes Pins already on the board, promoted/ad content, and anything that does not clear the relevance threshold — so `relevance` is never `poor`, and an empty list means nothing on-topic was found rather than that the board is bad. These are public Pins from across Pinterest, not the account's own. Free: no credit is checked or spent.\n\nTo act on the result, pass the IDs you want to `save-pins-to-board` — that saves the existing Pins onto the board. Creating your own new Pin from one instead is `create-post`.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/boardId"},{"name":"keyword","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":255},"description":"The keyword or topic to find Pins for"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":25,"default":10},"description":"How many recommendations to return. Bounds the response only — every candidate is scored regardless, so the top result is the best of the whole candidate set."}],"responses":{"200":{"description":"Recommended Pins, strongest first. Empty when nothing cleared the threshold.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"pins":{"type":"array","items":{"$ref":"#/components/schemas/RecommendedPin"}}},"required":["pins"]},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/PinterestReconnectRequired"},"429":{"$ref":"#/components/responses/RecommendedPinsRateLimited"},"502":{"$ref":"#/components/responses/RecommendedPinsSearchFailed"},"503":{"$ref":"#/components/responses/RecommendedPinsAwaitingPinterestAccess"},"504":{"$ref":"#/components/responses/RecommendedPinsTimeout"}}}},"/v1/accounts/{accountId}/boards/{boardId}/relevance":{"get":{"tags":["Boards"],"operationId":"get-board-keyword-relevance","summary":"Score a board against a keyword","description":"How well a board's own live Pinterest content covers a keyword, with the Pins that earned the score. Reads the board's real Pins from Pinterest, so it includes Pins added outside Tailwind. Costs one credit per scored request; an `absent` result is not charged. Large boards are scored from a capped sample — check `truncated`.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/boardId"},{"name":"keyword","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":255},"description":"The keyword or topic to score the board against"}],"responses":{"200":{"description":"Board relevance, or an absent state when the board had no readable Pins","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"relevance":{"$ref":"#/components/schemas/BoardKeywordRelevance"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/PinterestReconnectRequired"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/PinterestRateLimited"},"504":{"$ref":"#/components/responses/PinterestTimeout"}}}},"/v1/accounts/{accountId}/boards/{boardId}/sections":{"get":{"tags":["Boards"],"operationId":"list-board-sections","summary":"List board sections","description":"List the sections on a board. Returns an empty array when the board has no sections.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/boardId"}],"responses":{"200":{"description":"List of board sections","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"sections":{"type":"array","items":{"$ref":"#/components/schemas/BoardSection"}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/PinterestReconnectRequired"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/PinterestRequestFailed"},"503":{"$ref":"#/components/responses/PinterestRateLimited"},"504":{"$ref":"#/components/responses/PinterestTimeout"}}},"post":{"tags":["Boards"],"operationId":"create-board-section","summary":"Create board section","description":"Create a section on a board. Creating a section does not consume credits.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/boardId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BoardSectionRequest"}}}},"responses":{"201":{"description":"Board section created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"section":{"$ref":"#/components/schemas/BoardSection"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/PinterestForbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/PinterestReconnectRequired"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/PinterestRequestFailed"},"503":{"$ref":"#/components/responses/PinterestRateLimited"},"504":{"$ref":"#/components/responses/PinterestTimeout"}}}},"/v1/accounts/{accountId}/boards/{boardId}/sections/{sectionId}":{"patch":{"tags":["Boards"],"operationId":"update-board-section","summary":"Update board section","description":"Rename a section on a board.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/boardId"},{"$ref":"#/components/parameters/sectionId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BoardSectionRequest"}}}},"responses":{"200":{"description":"Board section updated","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"section":{"$ref":"#/components/schemas/BoardSection"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/PinterestForbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/PinterestReconnectRequired"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/PinterestRequestFailed"},"503":{"$ref":"#/components/responses/PinterestRateLimited"},"504":{"$ref":"#/components/responses/PinterestTimeout"}}},"delete":{"tags":["Boards"],"operationId":"delete-board-section","summary":"Delete board section","description":"Delete a section from a board. Pins in the section return to the board root — they are not deleted.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/boardId"},{"$ref":"#/components/parameters/sectionId"}],"responses":{"200":{"description":"Board section deleted","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"deleted":{"type":"boolean","example":true}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/PinterestForbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/PinterestReconnectRequired"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/PinterestRequestFailed"},"503":{"$ref":"#/components/responses/PinterestRateLimited"},"504":{"$ref":"#/components/responses/PinterestTimeout"}}}},"/v1/accounts/{accountId}/board-lists":{"get":{"tags":["Boards"],"operationId":"list-board-lists","summary":"List board lists","description":"List all board lists (collections of boards) for an account.","parameters":[{"$ref":"#/components/parameters/accountId"}],"responses":{"200":{"description":"List of board lists","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"boardLists":{"type":"array","items":{"$ref":"#/components/schemas/BoardList"}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Boards"],"operationId":"create-board-list","summary":"Create board list","description":"Save a new named board list: an ordered grouping of the account's boards, used to publish one Pin across several boards with intervalBoards. The order of boards in the request is the publishing order. Board lists are Tailwind's own groupings; creating one changes nothing on Pinterest and does not consume credits. Not idempotent: a retried create saves a second list.","parameters":[{"$ref":"#/components/parameters/accountId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BoardListRequest"}}}},"responses":{"201":{"description":"Board list created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"boardList":{"$ref":"#/components/schemas/BoardList"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The account is no longer active in Tailwind, so its board lists cannot be changed (reason forbidden).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts/{accountId}/board-lists/{boardListId}":{"put":{"tags":["Boards"],"operationId":"update-board-list","summary":"Update board list","description":"Replace a board list's name and its full ordered set of boards. Send every board the list should hold, in publishing order: boards left out are removed from the list, and reordering is this same call with the boards in their new order. Removing a board from a list never changes the board or its Pins on Pinterest. Resubmitting the same body is safe and gives the same result.","parameters":[{"$ref":"#/components/parameters/accountId"},{"name":"boardListId","in":"path","required":true,"description":"Numeric board list ID returned by GET /v1/accounts/{accountId}/board-lists","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BoardListRequest"}}}},"responses":{"200":{"description":"Board list updated","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"boardList":{"$ref":"#/components/schemas/BoardList"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The account is no longer active in Tailwind, so its board lists cannot be changed (reason forbidden).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The account or the board list was not found. A list that was deleted, or that belongs to another account, reads as not found (reason board_list_not_found).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"NOT_FOUND","message":"Board list not found. Check the boardListId belongs to this account, from GET /v1/accounts/:accountId/board-lists."},"meta":{"requestId":"abc123"}}}}},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Boards"],"operationId":"delete-board-list","summary":"Delete board list","description":"Delete a saved board list. Only Tailwind's grouping is removed: the boards in it and every Pin on them stay as they are on Pinterest.","parameters":[{"$ref":"#/components/parameters/accountId"},{"name":"boardListId","in":"path","required":true,"description":"Numeric board list ID returned by GET /v1/accounts/{accountId}/board-lists","schema":{"type":"string"}}],"responses":{"200":{"description":"Board list deleted","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"boardList":{"type":"object","properties":{"id":{"type":"integer","description":"ID of the deleted board list"},"name":{"type":"string","description":"Name of the deleted board list; omitted when unnamed"}},"required":["id"]},"deleted":{"type":"boolean"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"The account or the board list was not found. A list that was deleted, or that belongs to another account, reads as not found (reason board_list_not_found).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"NOT_FOUND","message":"Board list not found. Check the boardListId belongs to this account, from GET /v1/accounts/:accountId/board-lists."},"meta":{"requestId":"abc123"}}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts/{accountId}/analytics":{"get":{"tags":["Analytics"],"operationId":"get-account-analytics","summary":"Get account analytics","description":"Pinterest performance for the whole account over a date window: summary totals plus a per-day series. The same numbers Pinterest Insights shows in the Tailwind dashboard, covering claimed and unclaimed content alike. Totals combine paid (promoted) and organic activity unless contentType says otherwise: ORGANIC reports unpromoted activity only, PAID promoted only. The per-Pin analytics endpoint is always organic-only, so its figures compare against ORGANIC totals, not the default blend. The filter actually applied is echoed back as `contentType`.\n\nThis is the account-wide rollup. For one Pin use GET /v1/accounts/{accountId}/pins/{pinId}/analytics; for the best performers use GET /v1/accounts/{accountId}/top-pins.\n\nOmit startDate and endDate for the last 30 days (today and the 29 days before it, UTC). Supply both to choose a window; supplying only one is a 400. The window actually used is echoed back as `startDate`/`endDate` inside `data`, so a caller never has to infer the period a number covers. Pinterest's own limits apply: startDate no more than 90 days before today, endDate no more than 90 days after startDate.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/analyticsContentType"},{"$ref":"#/components/parameters/analyticsStartDate"},{"$ref":"#/components/parameters/analyticsEndDate"}],"responses":{"200":{"description":"Account analytics over the resolved window","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"accountId":{"type":"string"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"contentType":{"type":"string","enum":["ALL","PAID","ORGANIC"]},"summaryMetrics":{"$ref":"#/components/schemas/AnalyticsMetrics"},"dailyMetrics":{"type":"array","items":{"$ref":"#/components/schemas/AnalyticsDailyMetric"}}},"required":["accountId","startDate","endDate","contentType","summaryMetrics","dailyMetrics"]},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsForbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/PinterestReconnectRequired"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/PinterestUpstreamError"},"503":{"$ref":"#/components/responses/PinterestRateLimited"}}}},"/v1/accounts/{accountId}/top-pins":{"get":{"tags":["Analytics"],"operationId":"list-top-pins","summary":"List top-performing Pins","description":"The account's best-performing Pins over a date window, ranked by one metric and returned in Pinterest's ranked order. Metrics and ranking combine paid (promoted) and organic activity unless contentType says otherwise, so by default ad spend can crowd organic Pins out of the list and a promoted Pin can rank far higher than its organic-only per-Pin analytics suggest. contentType ORGANIC ranks on unpromoted activity alone, PAID on promoted only; the filter applied is echoed back as `contentType`.\n\nRanks by ENGAGEMENT unless sortBy says otherwise, and echoes the metric used back as `sortBy`. Engagement is the default rather than impressions because it counts what people did with a Pin rather than how often Pinterest showed it.\n\nEach entry carries the Pinterest pinId, which is what GET /v1/accounts/{accountId}/pins/{pinId}/analytics takes — this endpoint is the usual way to find Pins worth reading in detail.\n\nOmit startDate and endDate for the last 30 days (today and the 29 days before it, UTC). Supply both to choose a window; supplying only one is a 400. The window actually used is echoed back as `startDate`/`endDate` inside `data`, so a caller never has to infer the period a number covers. Pinterest's own limits apply: startDate no more than 90 days before today, endDate no more than 90 days after startDate.","parameters":[{"$ref":"#/components/parameters/accountId"},{"name":"sortBy","in":"query","required":false,"description":"Metric to rank by. Defaults to ENGAGEMENT. Only these five are accepted — Pinterest's documented sort vocabulary.","schema":{"type":"string","enum":["ENGAGEMENT","SAVE","IMPRESSION","OUTBOUND_CLICK","PIN_CLICK"]}},{"$ref":"#/components/parameters/analyticsContentType"},{"$ref":"#/components/parameters/analyticsStartDate"},{"$ref":"#/components/parameters/analyticsEndDate"}],"responses":{"200":{"description":"Top-performing Pins over the resolved window, ranked by the resolved metric","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"accountId":{"type":"string"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"sortBy":{"type":"string","enum":["ENGAGEMENT","SAVE","IMPRESSION","OUTBOUND_CLICK","PIN_CLICK"]},"contentType":{"type":"string","enum":["ALL","PAID","ORGANIC"]},"pins":{"type":"array","items":{"$ref":"#/components/schemas/TopPin"}},"isPartial":{"type":"boolean","description":"True when some Pins Pinterest ranked are missing from `pins` because Pinterest limited the lookups of their details. Retrying shortly should return the full list. Pins Pinterest reports as deleted or no longer visible are left out without setting it, since a retry would not bring them back."},"omittedPinIds":{"type":"array","items":{"type":"string"},"description":"The Pinterest Pin ids missing from `pins` for that reason, in ranking order. Empty when `isPartial` is false. Where they ranked among `pins` is not known."}},"required":["accountId","startDate","endDate","sortBy","contentType","pins","isPartial","omittedPinIds"]},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsForbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/PinterestReconnectRequired"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/PinterestUpstreamError"},"503":{"$ref":"#/components/responses/PinterestRateLimited"}}}},"/v1/accounts/{accountId}/pins":{"get":{"tags":["Pins"],"operationId":"list-pins","summary":"List every Pin on the account","description":"Every Pin on the connected Pinterest account, newest first, however it was published: through Tailwind, directly on Pinterest, or by another tool. This is the complete inventory, unlike GET /v1/accounts/{accountId}/posts, which lists only Tailwind's own post records, and GET /v1/accounts/{accountId}/top-pins, which returns a ranked subset. Pins on secret boards are included.\n\nEach Pin's `pinId` is the Pinterest Pin id, which GET /v1/accounts/{accountId}/pins/{pinId}/analytics takes. It is not a Tailwind postId.\n\nstartDate and endDate filter on when the Pin was created (or saved, for a repin), and either may be given alone. Pinterest itself has no date filter, so pages newer than endDate are read and skipped server-side; a response can therefore hold fewer than `limit` Pins, or none, while `cursor` is still present. Keep paging until `cursor` is absent: only then is the inventory complete. pinType ORIGINAL keeps Pins the account created, SAVED keeps saves of other people's Pins. A save of the account's own Pin to another board (isRepin true) is in neither, so ORIGINAL and SAVED can sum to less than ALL: to count every save, use ALL and isRepin.\n\nReading the inventory is free and consumes no credits.","parameters":[{"$ref":"#/components/parameters/accountId"},{"name":"cursor","in":"query","required":false,"description":"Pagination cursor from the previous response","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Pins to read from Pinterest per page, 1 to 250. Defaults to 100.","schema":{"type":"integer","minimum":1,"maximum":250,"default":100}},{"name":"startDate","in":"query","required":false,"description":"Only Pins created at or after this instant (ISO 8601 date-time in UTC, e.g. 2026-10-01T00:00:00Z)","schema":{"type":"string","format":"date-time"}},{"name":"endDate","in":"query","required":false,"description":"Only Pins created at or before this instant (ISO 8601 date-time in UTC, e.g. 2026-10-07T23:59:59Z)","schema":{"type":"string","format":"date-time"}},{"name":"pinType","in":"query","required":false,"description":"ORIGINAL for Pins the account created (isRepin false), SAVED for saves of other people's Pins, ALL for everything. Defaults to ALL. A save of the account's own Pin to another board is in neither ORIGINAL nor SAVED, so the two can sum to less than ALL.","schema":{"type":"string","enum":["ALL","ORIGINAL","SAVED"],"default":"ALL"}}],"responses":{"200":{"description":"One page of the account's Pins, newest first","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"accountId":{"type":"string"},"pinType":{"type":"string","enum":["ALL","ORIGINAL","SAVED"]},"pins":{"type":"array","items":{"$ref":"#/components/schemas/InventoryPin"}},"cursor":{"type":"string","description":"Pass back as `cursor` for the next page. Absent once the inventory, or the requested window, is exhausted."}},"required":["accountId","pinType","pins"]},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Pinterest refused to list this account's Pins (reason `pinterest_forbidden`). Permanent: retrying returns the same answer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/PinterestReconnectRequired"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/PinterestUpstreamError"},"503":{"$ref":"#/components/responses/PinterestRateLimited"}}}},"/v1/accounts/{accountId}/pins/{pinId}/analytics":{"get":{"tags":["Analytics"],"operationId":"get-pin-analytics","summary":"Get Pin analytics","description":"One Pin's Pinterest performance over a date window: summary totals, a per-day series, and lifetime totals that ignore the window. Figures are organic-only: Pinterest's per-Pin analytics exclude paid (promoted) distribution, so a promoted Pin reads far lower here than in GET /v1/accounts/{accountId}/top-pins or GET /v1/accounts/{accountId}/analytics — that gap is expected, not an error.\n\npinId is the PINTEREST Pin id, not the Tailwind postId returned by the posts endpoints. A published post carries its Pinterest pinId; GET /v1/accounts/{accountId}/pins and GET /v1/accounts/{accountId}/top-pins return them directly. Passing a Tailwind postId here returns 404.\n\nOnly a Pin this account OWNS can be read. Pinterest itself would also report on Pins sitting on group boards this account collaborates on; this endpoint deliberately does not, because holding the account is not evidence of owning the Pin and this read does not verify board collaboration. Any other Pin id — absent, someone else's, on a shared board, or unreadable — returns the same 404, so this endpoint cannot be used to probe which Pins exist.\n\nOmit startDate and endDate for the last 30 days (today and the 29 days before it, UTC). Supply both to choose a window; supplying only one is a 400. The window actually used is echoed back as `startDate`/`endDate` inside `data`, so a caller never has to infer the period a number covers. Pinterest's own limits apply: startDate no more than 90 days before today, endDate no more than 90 days after startDate.","parameters":[{"$ref":"#/components/parameters/accountId"},{"name":"pinId","in":"path","required":true,"description":"The Pinterest Pin id — a numeric string. NOT the Tailwind postId from GET /v1/accounts/{accountId}/posts.","schema":{"type":"string"}},{"$ref":"#/components/parameters/analyticsStartDate"},{"$ref":"#/components/parameters/analyticsEndDate"}],"responses":{"200":{"description":"The Pin's analytics over the resolved window","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"pinId":{"type":"string"},"accountId":{"type":"string"},"boardId":{"type":"string","description":"The Pinterest board the Pin is saved to — the same id GET /v1/accounts/{accountId}/boards returns."},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"summaryMetrics":{"allOf":[{"$ref":"#/components/schemas/AnalyticsMetrics"}],"description":"This Pin's totals over the window; dailyMetrics carries the same fields per day. Comment and reaction counts are not windowed: see lifetimeMetrics."},"dailyMetrics":{"type":"array","items":{"$ref":"#/components/schemas/AnalyticsDailyMetric"}},"lifetimeMetrics":{"allOf":[{"$ref":"#/components/schemas/AnalyticsMetrics"}],"description":"Totals since the Pin was created, ignoring the requested window. The only place Pinterest reports totalComments (the comment count) and totalReactions; absent when Pinterest reports none."}},"required":["pinId","accountId","boardId","startDate","endDate","summaryMetrics","dailyMetrics"]},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AnalyticsForbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/PinterestReconnectRequired"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/PinterestUpstreamError"},"503":{"$ref":"#/components/responses/PinterestRateLimited"}}}},"/v1/accounts/{accountId}/timeslots":{"get":{"tags":["Accounts"],"operationId":"list-timeslots","summary":"List timeslots","description":"List all smart schedule timeslots for an account. These are RECURRING WEEKLY rules (day of week + time of day + timezone), not dated occurrences, and they carry no occupancy information. For which specific upcoming times are actually free, use GET /v1/accounts/{accountId}/schedule.","parameters":[{"$ref":"#/components/parameters/accountId"}],"responses":{"200":{"description":"List of timeslots","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"timeslots":{"type":"array","items":{"$ref":"#/components/schemas/Timeslot"}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Accounts"],"operationId":"create-timeslot","summary":"Create timeslot","description":"Add one recurring weekly slot to the account's smart schedule: a day of week, a 24-hour HH:MM time, and optionally the IANA timezone the time is expressed in. Without `timezone` the account's own timezone is used, and the request is rejected with a 400 if the account has none set. The slot is created with type `manual`, the same type the dashboard gives a user-placed slot. Free: managing the schedule costs no credits, in the dashboard or here.\n\nThere is no duplicate check, matching the dashboard: two identical calls create two slots at the same time, so read GET /v1/accounts/{accountId}/timeslots first when adding to an existing schedule. Evergreen (SmartLoop) slots cannot be created here.","parameters":[{"$ref":"#/components/parameters/accountId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTimeslotRequest"}}}},"responses":{"201":{"description":"Timeslot created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"timeslot":{"$ref":"#/components/schemas/Timeslot"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/ScheduleLocked"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts/{accountId}/timeslots/rebuild":{"post":{"tags":["Accounts"],"operationId":"rebuild-schedule","summary":"Rebuild smart schedule","description":"Replace the account's smart schedule with a freshly generated one: **every non-evergreen slot is deleted**, including manual and recommended ones, and `slotsPerDay` new `auto-created` slots are spread across each of the 7 days between 5am and 11pm in the account's timezone. This is the dashboard's \"Generate new SmartSchedule\". Only call it when the user has explicitly asked to rebuild the entire schedule; to move, add or remove individual slots use the create, update and delete endpoints instead. There is no day-part preference: the generator cannot be asked for mornings only.\n\nPins already scheduled keep their times and become off-slot. Evergreen (SmartLoop) slots survive. Rebuilds are serialized per account: a second one arriving while the first is still running is rejected with a 409 rather than racing it, so a client that retries after a timeout cannot end up with a doubled schedule. Within a single call the delete and the insert are still separate writes, so a failure between them can leave the account with no schedule; re-running the rebuild is the recovery. `timezoneSource` reports `fallback` when the account has no timezone set and the schedule was generated in America/New_York instead.","parameters":[{"$ref":"#/components/parameters/accountId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RebuildScheduleRequest"}}}},"responses":{"200":{"description":"Schedule rebuilt","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/RebuildScheduleResult"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/ScheduleLocked"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts/{accountId}/timeslots/{timeslotId}":{"patch":{"tags":["Accounts"],"operationId":"update-timeslot","summary":"Update timeslot","description":"Move a recurring slot to another day, time or timezone. Supply at least one of `dayPreference`, `time` or `timezone`; omitted fields keep the slot's stored values, including its timezone. Any edit converts the slot to type `manual`, as the dashboard's edit does. Pins already placed on the slot keep their scheduled time and become off-slot; nothing is rescheduled. Evergreen (SmartLoop) slots are not addressable here and return a 404.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/timeslotId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTimeslotRequest"}}}},"responses":{"200":{"description":"Timeslot updated","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"timeslot":{"$ref":"#/components/schemas/Timeslot"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/ScheduleLocked"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Accounts"],"operationId":"delete-timeslot","summary":"Delete timeslot","description":"Remove one recurring slot from the smart schedule. Pins already placed on it keep their scheduled time and become off-slot; nothing is unscheduled or deleted on Pinterest. The response carries the slot as it was, so the caller can record what was removed. Evergreen (SmartLoop) slots are not addressable here and return a 404.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/timeslotId"}],"responses":{"200":{"description":"Timeslot deleted","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"timeslot":{"$ref":"#/components/schemas/Timeslot"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/ScheduleLocked"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts/{accountId}/pin-spacing/rules":{"get":{"tags":["Accounts"],"operationId":"list-pin-spacing-rules","summary":"List Pin Spacing rules","description":"Every Pin Spacing rule on the account: the account-wide default, the URL-specific custom rules, and the linked URLs that follow another URL's rule. Pin Spacing is the minimum gap the scheduler keeps between Pins that share a destination URL. To find the rule a particular URL gets, call GET /v1/accounts/{accountId}/pin-spacing/applied-rule instead of matching URLs against this list yourself: URLs match after normalization and linked URLs inherit their base URL's rule. Read-only: rules are managed in the Tailwind dashboard.","parameters":[{"$ref":"#/components/parameters/accountId"}],"responses":{"200":{"description":"The account's Pin Spacing rules","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PinSpacingRules"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts/{accountId}/pin-spacing/applied-rule":{"get":{"tags":["Accounts"],"operationId":"get-applied-pin-spacing-rule","summary":"Get the Pin Spacing rule for a URL","description":"The Pin Spacing rule the scheduler applies to Pins linking to `url`, resolved the same way useNextOpenSlot resolves it: a linked URL follows its base URL's rule, a custom rule for the URL beats the account default, and URLs match regardless of http/https and trailing slashes. `rule` is null when no rule applies, meaning Pin Spacing does not constrain that URL. useNextOpenSlot on POST /v1/accounts/{accountId}/posts honors this rule. An explicit sendAt sets only the Pin's initial time and is not an exemption from Pin Spacing: a later Pin Spacing respace or schedule shuffle can still move the Pin, and the API has no way to lock it in place. Read-only: rules are managed in the Tailwind dashboard.","parameters":[{"$ref":"#/components/parameters/accountId"},{"name":"url","in":"query","required":true,"description":"The Pin destination URL to resolve","schema":{"type":"string","maxLength":2048}}],"responses":{"200":{"description":"The rule that applies to the URL, or null","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AppliedPinSpacingRule"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts/{accountId}/schedule":{"get":{"tags":["Accounts"],"operationId":"get-schedule","summary":"Get schedule","description":"The account's schedule over a date window, as dated entries: which smart schedule slots are open, and what occupies the rest. The companion read to GET /v1/accounts/{accountId}/timeslots, which returns the recurring weekly rules and says nothing about occupancy.\n\nEntries are ordered earliest first. `open` marks an unused smart schedule slot; `scheduled`, `publishing` and `published` each name the Pin holding that time. `timeslotId` is absent on a Pin that sits on no slot at all — scheduled at an arbitrary time, or left behind by a slot that has since moved.\n\nThis is a read, not a placement primitive: a slot that is open here can be taken before a subsequent write lands. To put a Pin on the schedule, use `useNextOpenSlot` on the create or schedule endpoints, which resolves and claims a slot under the account's schedule lock — do not copy a time from this response into `sendAt`.","parameters":[{"$ref":"#/components/parameters/accountId"},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Start of the window (ISO 8601). Defaults to now."},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"End of the window (ISO 8601). Defaults to 14 days after the start, and cannot be more than 60 days after it."}],"responses":{"200":{"description":"The account's schedule over the requested window","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"schedule":{"type":"array","items":{"$ref":"#/components/schemas/ScheduleEntry"}},"window":{"type":"object","description":"The window actually read, as Unix timestamps.","properties":{"from":{"type":"integer"},"to":{"type":"integer"}}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"description":"Timed out building the schedule. Nothing is wrong with the request; retry, or ask for a shorter window."}}}},"/v1/accounts/{accountId}/subscriptions":{"get":{"tags":["Accounts"],"operationId":"list-subscriptions","summary":"Get subscription status","description":"Whether a Pinterest account currently has active, paying access, and what is backing it — normalized across Tailwind's legacy and modular billing systems so callers never need to know which one the org is on.\n\n`entitled` is the answer to \"can this account use paid features right now\". Legacy orgs are billed org-wide and always report a single entry describing the org's plan; modular orgs report one entry per entitlement the account currently holds, which may be several (e.g. Publishing and SEO), or none. A modular entitlement can come from a subscription or from a Tailwind-issued grant — `source` says which.\n\nOnly current entitlements are reported: ended subscriptions and expired grants are omitted rather than returned with `entitled: false`.","parameters":[{"$ref":"#/components/parameters/accountId"}],"responses":{"200":{"description":"Subscription status","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AccountSubscriptionStatus"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts/{accountId}/usage":{"get":{"tags":["Accounts"],"operationId":"get-account-usage","summary":"Get credit and post usage","description":"How much a Pinterest account can still spend before a credit- or post-consuming action, and when it refills. Works the same on every billing system; read `billingSystem` and `scope` to interpret the numbers. Free: this read spends nothing.\n\nWhen `unlimitedScheduling` is true (the modular winback plan), scheduling Pins spends no credits, whatever the credit balance says. On modular billing, credits belong to this account and pay for scheduling (one credit per Pin, unless `unlimitedScheduling` is true) as well as AI generation; there is no post allowance, so `postLimit` is null. On legacy and Shopify billing, `scope` is `organization`: one credit pool and one post allowance are shared by every account in the org, so each account reports the same numbers. That pool pays for credit-priced features such as AI generation (including SmartPins from a connected feed) and Turbo, while ordinary scheduling spends the post allowance instead of credits.\n\nThis is a snapshot for planning, not an authorization. Trial allowances on modular billing are tracked separately, so zero credits does not mean nothing can be done and a positive balance does not guarantee every action. Concurrent spends can also change the numbers before your next call.","parameters":[{"$ref":"#/components/parameters/accountId"}],"responses":{"200":{"description":"Credit and post usage","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AccountUsage"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/group-boards":{"get":{"tags":["Group boards"],"operationId":"list-group-boards","summary":"List public group boards for a keyword","description":"Public Pinterest group boards matching a keyword, from Tailwind's Group Board Finder scrape cache. This is public benchmark context, not the authenticated user's own boards — use list-boards for those.\n\nResults are cached and refreshed in the background, so the response carries a status: `ready` means the data below is what we have (including an empty list, when a completed search found no group boards), `warming` means a search is running and no results are cached yet, and `unavailable` means the most recent search failed. On `warming`, retry after `retryAfterSeconds`.","parameters":[{"name":"keyword","in":"query","required":true,"description":"Search keyword. Trimmed, whitespace-collapsed and lowercased before lookup; 100 characters maximum.","schema":{"type":"string","maxLength":100}}],"responses":{"200":{"description":"Group boards for the keyword, with the scrape status","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"keyword":{"type":"string","description":"The normalized keyword the results are keyed on"},"status":{"type":"string","enum":["warming","ready","unavailable"],"description":"State of the underlying scrape for this keyword"},"boards":{"type":"array","items":{"$ref":"#/components/schemas/GroupBoard"}},"lastCompletedAt":{"type":"string","format":"date-time","description":"When a search for this keyword last completed successfully"},"retryAfterSeconds":{"type":"integer","description":"On `warming`, how long to wait before asking again"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/keywords":{"get":{"tags":["Keywords"],"operationId":"list-keywords","summary":"List saved keywords","description":"The authenticated organization's saved keywords, newest first. This is the same list the Tailwind dashboard's Saved Keywords page shows — it is not keyword research; these are terms the organization has already chosen to track.\n\nPass `url` to read only the keywords saved for one destination URL, for example to check what a Pin's link already has before saving more. `search` and `url` cannot be combined; a request with both returns 400.","parameters":[{"name":"search","in":"query","description":"Only return keywords containing this text. Cannot be combined with `url`.","schema":{"type":"string","maxLength":255}},{"name":"url","in":"query","description":"Only return keywords associated with this destination URL. Must be a full `http` or `https` URL. It is matched exactly against the URL the keyword was saved with, trailing slash and query string included, except that `http` and `https` are treated as the same. A bare domain such as `https://example.com` also matches keywords saved for any page on that domain. Cannot be combined with `search`.","schema":{"type":"string","format":"uri","maxLength":2048}},{"name":"limit","in":"query","description":"Keywords per page.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","description":"Opaque cursor from a previous response's `pagination.nextCursor`.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of saved keywords","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"keywords":{"type":"array","items":{"$ref":"#/components/schemas/SavedKeyword"}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"hasMore":{"type":"boolean"},"nextCursor":{"type":"string","description":"Pass as `cursor` to fetch the next page"},"totalCount":{"type":"integer","description":"Total matching keywords across all pages"}}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Keywords"],"operationId":"save-keywords","summary":"Save keywords","description":"Add keywords to the organization's saved list. Matching is case-insensitive: a term already on the list is returned as it is stored rather than duplicated, so an overlapping batch is safe to re-send. Saving keywords does not consume credits.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveKeywordsRequest"}}}},"responses":{"201":{"description":"The saved keywords, including any that already existed","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"keywords":{"type":"array","items":{"$ref":"#/components/schemas/SavedKeyword"}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/feeds":{"get":{"tags":["Feeds"],"operationId":"list-feeds","summary":"List feed subscriptions","description":"The organization's connected RSS feeds, newest first. Each one generates a SmartPin draft for every new post the daily watcher finds.\n\nFeeds are org-scoped, not per-account: every feed names the Pinterest account its Pins are assigned to, and `accountId` filters by it. Results are paginated; pass the returned `nextCursor` to read further.","parameters":[{"name":"accountId","in":"query","required":false,"description":"Only return feeds assigned to this Pinterest account.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Feeds per page, 1-100. Defaults to 50.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","required":false,"description":"nextCursor from a previous call, to read the next page.","schema":{"type":"string"}}],"responses":{"200":{"description":"The organization's feed subscriptions","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"feeds":{"type":"array","items":{"$ref":"#/components/schemas/Feed"}},"pagination":{"type":"object","properties":{"limit":{"type":"number"},"hasMore":{"type":"boolean"},"nextCursor":{"type":"string","description":"Pass as `cursor` to read the next page. Absent on the last page."}},"required":["limit","hasMore"]}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Feeds"],"operationId":"connect-feed","summary":"Connect a feed","description":"Subscribe to an RSS feed. The feed's current posts are snapshotted as the baseline, so nothing already published generates a Pin — only posts appearing on a later watcher run do.\n\nThe watcher runs once a day and creates each Pin as a draft for review; it never schedules or publishes. Generated Pins consume SmartPin credits at the same rate as manual ones, so connecting many feeds can drive credit use.\n\n`feedUrl` must be an actual feed. Use preview-feed first when you only have a site URL.\n\n`accountId` must name a currently connected Pinterest account: a feed assigned to a disconnected one would be skipped by the watcher every day without any error, so this is refused with 400 rather than accepted silently.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectFeedRequest"}}}},"responses":{"201":{"description":"The feed was connected","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"feed":{"$ref":"#/components/schemas/Feed"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/FeedConflict"},"413":{"$ref":"#/components/responses/FeedTooLarge"},"422":{"$ref":"#/components/responses/FeedUnprocessable"},"424":{"$ref":"#/components/responses/FeedUnreachable"},"429":{"$ref":"#/components/responses/RateLimited"},"504":{"$ref":"#/components/responses/FeedTimeout"}}}},"/v1/feeds/preview":{"post":{"tags":["Feeds"],"operationId":"preview-feed","summary":"Preview a feed before connecting it","description":"Resolve a site or feed URL and report what would be subscribed to. Accepts a plain website URL and discovers its feed.\n\nReturns `kind: \"feed\"` with a bounded sample of current entries when there is one answer, or `kind: \"candidates\"` when a site exposes several feeds — pick one and pass it as `feedUrl` to connect-feed. Nothing is saved and no credits are spent.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","description":"A feed URL, or a website URL whose feed should be discovered.","maxLength":2048}}}}}},"responses":{"200":{"description":"The resolved feed, or the candidates to choose between","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/FeedPreview"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"$ref":"#/components/responses/FeedTooLarge"},"422":{"$ref":"#/components/responses/FeedUnprocessable"},"424":{"$ref":"#/components/responses/FeedUnreachable"},"429":{"$ref":"#/components/responses/RateLimited"},"504":{"$ref":"#/components/responses/FeedTimeout"}}}},"/v1/feeds/{feedId}":{"get":{"tags":["Feeds"],"operationId":"get-feed","summary":"Get a feed subscription","description":"One feed subscription, including how many SmartPins it has generated and why its last poll failed, if it did.","parameters":[{"name":"feedId","in":"path","required":true,"description":"Feed ID, from list-feeds or connect-feed","schema":{"type":"string"}}],"responses":{"200":{"description":"The feed subscription","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"feed":{"$ref":"#/components/schemas/Feed"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"tags":["Feeds"],"operationId":"update-feed","summary":"Update or pause a feed subscription","description":"Change the defaults applied to Pins this feed generates, or pause and resume it with `status`.\n\nPausing preserves the subscription, but the watcher does not poll a paused feed, so its record of already-seen posts stops advancing. Anything the site publishes during the pause therefore counts as new when you resume, and will generate Pins (up to 25 per feed per daily run) until the backlog clears. To stop a feed permanently without that catch-up, delete it instead.\n\nResuming re-checks the feed's Pinterest account and is refused with 400 if it is no longer connected, since the watcher would skip the feed and generate nothing. Pausing, editing and deleting are always allowed, which is what you need when an account has gone away.\n\nAt least one field is required. The Pinterest account a feed is assigned to cannot be changed.","parameters":[{"name":"feedId","in":"path","required":true,"description":"Feed ID, from list-feeds or connect-feed","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFeedRequest"}}}},"responses":{"200":{"description":"The updated feed subscription","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"feed":{"$ref":"#/components/schemas/Feed"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Feeds"],"operationId":"delete-feed","summary":"Remove a feed subscription","description":"Stop watching a feed. SmartPins it already generated are left alone — this only stops future ones.\n\nReturns 404 if the feed is not this organization's, so a successful response always means the subscription was removed. To stop generation temporarily instead, pause it with update-feed.","parameters":[{"name":"feedId","in":"path","required":true,"description":"Feed ID, from list-feeds or connect-feed","schema":{"type":"string"}}],"responses":{"200":{"description":"The subscription was removed","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"deleted":{"type":"boolean"},"feedId":{"type":"string"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/keywords/{keywordId}":{"delete":{"tags":["Keywords"],"operationId":"delete-keyword","summary":"Delete a saved keyword","description":"Remove one keyword from the organization's saved list. Returns 404 if the keyword is not on this organization's list, so a successful response always means the keyword was removed.","parameters":[{"name":"keywordId","in":"path","required":true,"description":"Saved keyword ID, from list-keywords","schema":{"type":"string"}}],"responses":{"200":{"description":"The keyword was removed","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"deleted":{"type":"boolean"},"keywordId":{"type":"string"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/keywords/bulk-delete":{"post":{"tags":["Keywords"],"operationId":"bulk-delete-keywords","summary":"Delete several saved keywords","description":"Remove up to 100 keywords from the organization's saved list in one call.\n\nAll-or-nothing: if any id is not on this organization's list, nothing is deleted and the unknown ids are named in the error message. Re-sending a batch after a successful delete therefore returns 404, not a silent success.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkDeleteKeywordsRequest"}}}},"responses":{"200":{"description":"Every keyword in the request was removed","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"deleted":{"type":"boolean"},"deletedCount":{"type":"integer"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/keywords/tag":{"post":{"tags":["Keywords"],"operationId":"tag-keywords","summary":"Tag saved keywords","description":"Apply tags, by name, to up to 100 saved keywords. Every keyword gets every tag in the request. A name the organization has no tag for yet creates that tag; an existing tag is reused rather than duplicated, and re-applying a tag a keyword already has changes nothing, so the call is safe to retry.\n\nIf any keyword id is not on this organization's list, nothing is changed and the unknown ids are named in the error message. Tagging is free and does not consume credits.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagKeywordsRequest"}}}},"responses":{"200":{"description":"Every keyword in the request, with its tags after the change","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"keywords":{"type":"array","items":{"$ref":"#/components/schemas/SavedKeyword"}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/keywords/untag":{"post":{"tags":["Keywords"],"operationId":"untag-keywords","summary":"Remove tags from saved keywords","description":"Remove tags, by name, from up to 100 saved keywords. The tags themselves are kept for reuse, even when no keyword carries them any more. Removing a tag a keyword does not have changes nothing.\n\nIf any keyword id is not on this organization's list, or any tag name matches no tag, nothing is changed and the error message names what was not found.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TagKeywordsRequest"}}}},"responses":{"200":{"description":"Every keyword in the request, with its tags after the change","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"keywords":{"type":"array","items":{"$ref":"#/components/schemas/SavedKeyword"}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts/{accountId}/posts":{"get":{"tags":["Posts"],"operationId":"list-posts","summary":"List posts","description":"List scheduled posts for an account. Filter by status to get drafts, queued, sent, uploading, failed, or missed posts. status=draft omits drafts not yet assigned to any Pinterest account (such as some Made For You drafts), which the Pin Scheduler counts in its \"drafts that aren't assigned to a Pinterest account yet\" banner. The draft count can be lower than the dashboard's by that number: that gap is expected, not missing drafts, but a larger one is worth reporting. Assigning them to the account from that banner makes them list. A missed post is a queued post whose send time has passed without publishing; it is listed with status missed. Failed and missed posts are windowed by their send time; a failed post that had already published is windowed by when it published. Queued posts are returned in send-time order, soonest first, and without a startDate begin at the current time, so overdue ones are listed only as missed; every other status is returned newest first. A draft carrying a generationId was generated by Tailwind's AI, from POST /v1/accounts/{accountId}/generations or a connected feed, and is waiting for review: it never publishes on its own until it is scheduled, and it can be edited, re-designed through the design endpoints, or deleted instead.","parameters":[{"$ref":"#/components/parameters/accountId"},{"name":"status","in":"query","description":"Filter by post status","schema":{"type":"string","enum":["draft","queued","sent","uploading","failed","missed"],"default":"queued"}},{"name":"cursor","in":"query","description":"Pagination cursor from the previous response. Pass it unchanged with the same status, startDate and endDate; a cursor that cannot be used for this list is a 400.","schema":{"type":"string"}},{"name":"limit","in":"query","description":"Number of results to return (1-100)","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"startDate","in":"query","description":"Filter posts scheduled after this date (ISO 8601). Required for sent, uploading and missed status; for failed, give both startDate and endDate or neither.","schema":{"type":"string","format":"date-time"}},{"name":"endDate","in":"query","description":"Filter posts scheduled before this date (ISO 8601). Required for sent, uploading and missed status; for failed, give both startDate and endDate or neither. For missed, a date after now is treated as now.","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"List of posts","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"posts":{"type":"array","items":{"$ref":"#/components/schemas/Post"}},"cursor":{"type":"string","nullable":true,"description":"Cursor for next page, null if no more results"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Posts"],"operationId":"create-post","summary":"Create post","description":"Create a new post. Pass useNextOpenSlot or sendAt to schedule it; with neither, it is saved as a draft. A draft is a Tailwind state, not a Pinterest draft: nothing is created on Pinterest, the Pin does not appear in Pinterest's own drafts, and it stays unpublished until it is scheduled with POST /v1/accounts/{accountId}/posts/{postId}/schedule. useNextOpenSlot places the Pin in the account's next open smart schedule slot, honoring the account's Pin Spacing rules for the Pin's destination url — the same calculation the Pin Scheduler runs, and the reason it is preferable to computing a time from GET /v1/accounts/{accountId}/timeslots yourself. GET /v1/accounts/{accountId}/pin-spacing/applied-rule shows which rule applies to a URL. An explicit sendAt sets only the Pin's initial time and is not an exemption from Pin Spacing: a later Pin Spacing respace or schedule shuffle can still move the Pin, and the API has no way to lock it in place. Scheduling either way goes through the same pin_schedule charge, and what it costs depends on the organization's billing system: modular-billing accounts with Publishing access, whether subscribed or granted, spend one credit per Pin, except on winback plans, which schedule without charge; modular accounts without Publishing draw on a limited free-trial allowance; and legacy and Shopify accounts spend no credits at all — their plan's post limit for the billing period governs instead. A 422 means the account's schedule has no open slot; a 503 without a reason means resolving one timed out. Neither writes anything, and neither falls back to an invented time. A 503 with reason pinterest_rate_limited is different: Pinterest rate-limited the boardSectionId check, which runs for drafts and explicit sendAt too. Nothing was written; retry in a few minutes.","parameters":[{"$ref":"#/components/parameters/accountId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePostRequest"}}}},"responses":{"201":{"description":"Post created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"post":{"$ref":"#/components/schemas/Post"},"intervalPosts":{"type":"array","items":{"$ref":"#/components/schemas/Post"},"description":"Every Pin the intervalBoards fan-out created, in Board order and including `post` itself. Present only when intervalBoards produced more than one Pin."}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The uploadId has already been attached to a Pin. An upload is single-use — request a new one with POST /v1/media/uploads.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"CONFLICT","message":"This upload has already been used. An uploadId can only be attached to one Pin — request a new one with POST /v1/media/uploads."},"meta":{"requestId":"abc123"}}}}},"410":{"description":"The uploadId expired before it was attached. Staged media is kept for a limited window — request a new upload and attach it sooner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"GONE","message":"This upload expired before it was used. Staged media is kept for a limited window — request a new one with POST /v1/media/uploads and attach it sooner."},"meta":{"requestId":"abc123"}}}}},"422":{"description":"Media could not be processed, or (useNextOpenSlot only) the account's smart schedule has no open slot to publish into.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNPROCESSABLE_ENTITY","message":"Failed to process media URL. Ensure the URL is publicly accessible and points to a valid media file."},"meta":{"requestId":"abc123"}}}}},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/PinterestRequestFailed"},"503":{"description":"Either useNextOpenSlot timed out resolving the next open slot (nothing was scheduled; retry, or pass an explicit sendAt instead), or, with reason pinterest_rate_limited, Pinterest rate-limited the boardSectionId check (nothing was written; retry in a few minutes).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Posts"],"operationId":"bulk-update-posts","summary":"Bulk update posts","description":"Edit up to 50 unpublished Pins in one call, each with its own fields — for example, a find-and-replace where you read the Pins, compute each new description, and send them back. Every entry follows the same rules as PATCH /v1/accounts/{accountId}/posts/{postId}: drafts and scheduled Pins only, `sendAt`, `status` and media rejected, free of credits. Saving takes two requests. First send the batch with `dryRun: true`: nothing is written, each result has `status` `will_update` with the Pin's `previous` and `proposed` values (or `failed`, with the reason the save would refuse it), and the response carries a `previewToken` valid for 30 minutes, but only when every entry is `will_update`. If any entry would be refused or was not reached in time, the response carries no token: drop or fix those entries and preview again. Then send the same `posts` with that `previewToken` to save. A save without a token, with a token for a different batch, account or org, or with an expired token is a `400` (`reason` `edit_preview_required`, `edit_preview_mismatch`, `edit_preview_expired` or `edit_preview_invalid`) and nothing is edited. The preview exists so the person whose Pins these are can review the changes before they are saved: there is no version history to restore from afterwards. Product-tag rules that depend on the stored Pin are enforced only by the save. The request is validated as a whole first: more than 50 entries, a postId repeated, a forbidden field or an invalid entry is a `400` and nothing is edited. After that the batch is not atomic. Each Pin is saved on its own and reported in `results`, in request order, with `status` `updated`, `failed` (with an `error` using the codes the single-Pin endpoint returns) or `not_attempted` (the request ran out of time before reaching it; send those again). The response is a `200` however many Pins failed — read `summary`. `previous` on each updated Pin holds the prior values of the fields you edited, in request form, so sending `{ postId, ...previous }` entries back (previewed and saved the same way) undoes the edit. An empty title, description or url cannot be emptied again through this API, so `previous` omits it; an empty alt text is reported as `\"\"`, which clears it. A board move also reports the board section it cleared, and a product-tag edit reports `isSimplifiedPin`. A Pin changed by another edit after the save read it fails with a `409` `CONFLICT` error, `reason` `post_changed_since_read`, rather than saving with a `previous` that would undo that other edit: read it again and preview the edit again. Re-sending the same edits is safe.","parameters":[{"$ref":"#/components/parameters/accountId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkUpdatePostsRequest"}}}},"responses":{"200":{"description":"Per-Pin results, in request order","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/BulkUpdatePostsResult"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts/{accountId}/posts/{postId}":{"get":{"tags":["Posts"],"operationId":"get-post","summary":"Get post","description":"Get a specific post by ID. A 404 means either that the account is unknown or is not yours — the two are deliberately indistinguishable, so that account IDs cannot be enumerated — or that the post itself does not exist. Deleting a post is a soft delete: it stays readable with status `deleted` for at least 28 days, after which it becomes eligible for automatic purge — eviction is asynchronous, so a deleted post may remain readable somewhat longer. Once purged it reads as a 404 like an ID that never existed.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/postId"}],"responses":{"200":{"description":"Post details","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"post":{"$ref":"#/components/schemas/Post"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"tags":["Posts"],"operationId":"update-post","summary":"Update post","description":"Edit an unpublished Pin's copy. Fields absent from the body are left unchanged. Only drafts and scheduled (queued) Pins can be edited — a Pin already published to Pinterest is terminal in Tailwind and must be edited in the Pinterest app. Editing does not consume credits. `sendAt`, `status` and the Pin's media are deliberately not accepted here and are rejected with a message naming the operation that does handle them: scheduling goes through POST /v1/accounts/{accountId}/posts/{postId}/schedule, which is where putting a Pin on the schedule is charged — one credit on modular billing with Publishing access (winback plans excepted), and no credits on legacy and Shopify accounts, which count it against the plan's post limit instead. `title` and `description` cannot be set to an empty string — a scheduled Pin left without them could not be published. Moving a Pin to a different board clears any board section it was in, since a section belongs to one board. `productTagPinIds` replaces the Pin's tags wholesale — an empty array clears them, and omitting the field leaves them alone; tagging forces a simplified Pin, and tags are refused on a carousel Pin because Pinterest will not publish that combination. `isSimplifiedPin` switches a draft or scheduled Pin between a simplified and a standard Pin; `false` is refused on a Pin that keeps product tags, so send `productTagPinIds: []` alongside it to make a tagged Pin standard. A `409` means the Pin changed status while the edit was being applied and the edit was not saved.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/postId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePostRequest"}}}},"responses":{"200":{"description":"Updated post","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"post":{"$ref":"#/components/schemas/Post"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The Pin changed status while the edit was being applied — it is no longer a draft or scheduled Pin, and the edit was not saved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/PinterestRequestFailed"},"503":{"$ref":"#/components/responses/PinterestRateLimited"}}},"delete":{"tags":["Posts"],"operationId":"delete-post","summary":"Delete post","description":"Delete a post that has not been sent yet. Posts already published to Pinterest cannot be deleted through Tailwind. A 404 means either that the account is unknown or is not yours — the two are deliberately indistinguishable, so that account IDs cannot be enumerated — or that the post itself does not exist, which includes a deleted post that has since been purged — deletion is a soft delete readable for at least 28 days before it becomes eligible for automatic purge.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/postId"}],"responses":{"200":{"description":"Post deleted","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"deleted":{"type":"boolean","example":true}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts/{accountId}/posts/{postId}/image":{"get":{"tags":["Posts"],"operationId":"get-post-image","summary":"Get post image","description":"The picture a Pin shows, as raw image bytes: the cover image of an image or carousel Pin, or a video Pin's cover (never the video itself). A video on Tailwind's CDN with no saved cover returns its first-second frame. Images on Tailwind's CDN come back resized to 1000px wide. The response's Content-Type is the image's real format (JPEG, PNG, GIF or WebP), whatever the host declared. Use it when you cannot fetch the post's `mediaUrl` yourself. Every fetch failure message names the image's URL (the post's `mediaUrl` for an image Pin) so you can try it directly: a `422` means the image is over 4MB or the video Pin has no cover image, and a `502` means the image's host refused or failed the fetch. A 404 means either that the account is unknown or is not yours, or that the post does not exist.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/postId"}],"responses":{"200":{"description":"The image bytes","content":{"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/png":{"schema":{"type":"string","format":"binary"}},"image/gif":{"schema":{"type":"string","format":"binary"}},"image/webp":{"schema":{"type":"string","format":"binary"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"The image is larger than 4MB, or the video Pin has no cover image. For an oversized image, the message names the image's URL.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"description":"The image's host refused or failed the fetch, or returned something that is not an image. The message names the image's URL.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/accounts/{accountId}/posts/{postId}/schedule":{"post":{"tags":["Posts"],"operationId":"schedule-post","summary":"Schedule post","description":"Schedule a draft post or reschedule a queued post, passing exactly one of sendAt or useNextOpenSlot. A draft exists only in Tailwind, not on Pinterest, until this call: scheduling is what puts it into Tailwind's publishing flow. useNextOpenSlot places the Pin in the account's next open smart schedule slot, honoring the account's Pin Spacing rules for the Pin's destination url — the same calculation the Pin Scheduler runs. An explicit sendAt sets only the Pin's initial time and is not an exemption from Pin Spacing: a later Pin Spacing respace or schedule shuffle can still move the Pin, and the API has no way to lock it in place. The post must have title, description, and url set before it can be scheduled. Cannot reschedule posts that have already been sent or are uploading. A 422 means the account's schedule has no open slot; a 503 without a reason means resolving one timed out. Neither writes anything, and any pin_schedule charge already taken is refunded. A 503 with reason pinterest_rate_limited is different: Pinterest rate-limited the boardSectionId check, which runs with sendAt too. Nothing was written or charged; retry in a few minutes. A draft saved with multiple interval Boards cannot be scheduled here and is refused with a 400; to publish one Pin across several Boards, create it with intervalBoards on POST /v1/accounts/{accountId}/posts instead. A 400 writes nothing, refunds any charge, and will fail the same way if retried unchanged.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/postId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SchedulePostRequest"}}}},"responses":{"200":{"description":"Post scheduled","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"post":{"$ref":"#/components/schemas/Post"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid request, or the post cannot be scheduled as it stands — for example a draft saved with multiple interval Boards. Nothing was scheduled; retrying unchanged fails the same way.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Nothing was scheduled. Either the account's schedule is being updated by another request (retry shortly), or the post changed status while it was being scheduled (read it back with GET before retrying).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"useNextOpenSlot only: the account's smart schedule has no open slot to publish into.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/PinterestRequestFailed"},"503":{"description":"Either useNextOpenSlot timed out resolving the next open slot (nothing was scheduled; retry, or pass an explicit sendAt instead), or, with reason pinterest_rate_limited, Pinterest rate-limited the boardSectionId check (nothing was written; retry in a few minutes).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/accounts/{accountId}/posts/{postId}/unschedule":{"post":{"tags":["Posts"],"operationId":"unschedule-post","summary":"Unschedule post","description":"Move a scheduled (queued) Pin back to draft, for example to revise a time-sensitive Pin before it publishes. The Pin keeps its postId and everything stored on it: media, title, description, destination url, board and board section, alt text, product tags and carousel images. Only its status and scheduled time change, so the returned Pin has status draft and no sendAt. Works on any queued Pin on the account, including one created in the Tailwind dashboard and a missed Pin whose scheduled time has already passed. Any pin_schedule credit charged for putting the Pin on the schedule is returned, as in the Tailwind dashboard; a Pin scheduled on a free-trial allowance, scheduled in an earlier billing period, or on a legacy or Shopify plan has nothing to return. Scheduling the draft again with POST /v1/accounts/{accountId}/posts/{postId}/schedule charges again. A Pin that is not queued is rejected with a 400: `post_already_published` for a Pin that has been published or is publishing now (edit or delete it in the Pinterest app), and `post_not_queued` for drafts, failed and deleted Pins. A 409 means the Pin's status changed while it was being unscheduled, or the account's schedule was busy; nothing was changed, so read the Pin back with GET before retrying.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/postId"}],"responses":{"200":{"description":"Pin moved back to draft","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"post":{"$ref":"#/components/schemas/Post"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The Pin changed status while it was being unscheduled, or the account's schedule was being updated elsewhere. Nothing was changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts/{accountId}/publishing/pause":{"post":{"tags":["Posts"],"operationId":"pause-publishing","summary":"Pause publishing","description":"Stop every Pin on the account from publishing, for example to correct many scheduled Pins before any of them goes out. Nothing about the Pins themselves changes: scheduled Pins stay queued with their existing times, and drafts stay drafts. While publishing is paused, any Pin whose scheduled time arrives fails instead of publishing, and it stays failed after publishing resumes: it has lost its slot and needs rescheduling with POST /v1/accounts/{accountId}/posts/{postId}/schedule. The rest of the schedule does not shift forward to fill the gap. This is a switch for the whole account, not an edit to any Pin: to change Pins, use PATCH /v1/accounts/{accountId}/posts/{postId} on each one, and to take a single Pin off the schedule, use POST /v1/accounts/{accountId}/posts/{postId}/unschedule. The pause applies to the Pinterest account itself, so it also covers any other Tailwind account connected to the same Pinterest account. Pausing an account that is already paused succeeds and changes nothing. Resume with POST /v1/accounts/{accountId}/publishing/resume.","parameters":[{"$ref":"#/components/parameters/accountId"}],"responses":{"200":{"description":"Publishing paused","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingStatusResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/PublishingStatusUpdateFailed"}}}},"/v1/accounts/{accountId}/publishing/resume":{"post":{"tags":["Posts"],"operationId":"resume-publishing","summary":"Resume publishing","description":"Let the account's scheduled Pins publish again after POST /v1/accounts/{accountId}/publishing/pause. Queued Pins whose scheduled time is still ahead publish at that time, unchanged. Pins whose time arrived during the pause have already failed and are not retried: list them with GET /v1/accounts/{accountId}/posts?status=failed and reschedule the ones that should still go out. A Pin failed by a pause reports failureReason publishing_paused. One failed by a pause before October 15, 2026 reports max_pin_limit instead; its failure time falls inside the pause, and it does not mean the account hit a limit. Resuming an account that is not paused succeeds and changes nothing.","parameters":[{"$ref":"#/components/parameters/accountId"}],"responses":{"200":{"description":"Publishing resumed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingStatusResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/PublishingStatusUpdateFailed"}}}},"/v1/accounts/{accountId}/generations":{"post":{"tags":["Generations"],"operationId":"generate-pins","summary":"Generate Pins from a URL","description":"Start a one-shot AI Pin generation from a source URL (SmartPin). Asynchronous: the response returns a generation id to poll with GET /v1/generations/{generationId}; resulting drafts appear in the posts list carrying a matching generationId. Each generation spends plan credits by design tier (resolved server-side). If a generation for the same URL is already in flight, the response returns that generation with deduplicated: true and NO additional credit is charged by this call. If the URL already has a SmartPin on this account, it is reused as-is (its stored tier is billed; caller tier parameters are ignored); a SmartPin for the URL on a different account is a 409 conflict.","parameters":[{"$ref":"#/components/parameters/accountId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGenerationRequest"}}}},"responses":{"202":{"description":"Generation accepted and processing","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"generation":{"$ref":"#/components/schemas/GenerationAccepted"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Conflict: the URL's SmartPin belongs to a different account in the organization, or a concurrent request for the same URL holds the generation lock (retry shortly)"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/generations/{generationId}":{"get":{"tags":["Generations"],"operationId":"get-generation","summary":"Get generation status","description":"Poll the status of a Pin generation. Generation typically completes in 1-3 minutes; poll every ~15 seconds and give up after ~30 minutes (retryable failures can hold processing for several minutes, and a hung request can stay processing for ~25 minutes before the terminal generation-request-timed-out status lands). A 404 within ~10 seconds of creating the generation is a transient index-lag artifact — retry, don't stop. Once status is success, list the account's draft posts and filter by generationId to find the generated drafts.","parameters":[{"$ref":"#/components/parameters/generationId"}],"responses":{"200":{"description":"Generation status","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"generation":{"$ref":"#/components/schemas/Generation"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Generations"],"operationId":"cancel-generation","summary":"Cancel a queued generation","description":"Cancel a Pin generation that is still queued, before Tailwind has started generating it. A cancelled generation is never charged, and it reads back as status failed with failureCode generation-cancelled. Once generation has started, its credits are committed and the cancel is refused with a 409; poll GET /v1/generations/{generationId} until it finishes instead. Most generations start within seconds, so a cancel usually only succeeds right after the generation was created. A 404 within ~10 seconds of creating the generation is a transient index-lag artifact, not a missing generation: retry the cancel.","parameters":[{"$ref":"#/components/parameters/generationId"}],"responses":{"200":{"description":"Generation cancelled","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"generation":{"$ref":"#/components/schemas/Generation"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The generation has already started, so its credits are committed, or it has already finished. Nothing was changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts/{accountId}/posts/{postId}/design":{"get":{"tags":["Designs"],"operationId":"get-pin-design","summary":"Get a Pin's design","description":"Read the design of a Pin that SmartPin generated: its tier, every variation already rendered, and supportedOperations - the operations this particular draft accepts. Read this before any design write and follow supportedOperations, because the available operations differ by tier and calling an unsupported one is rejected. workspaceStatus reports whether the design metadata and the Pin agree; pending_apply means an earlier edit rendered a design the Pin never received, and pendingMediaUrl is what to pass to the variation endpoint to finish applying it, with no re-render. sourceImageCandidates lists the other source photos stored on the SmartPin behind this Pin; pass one as mediaUrl to the update endpoint (or to regenerate on a premium Pin) to rebuild the design around it.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/postId"}],"responses":{"200":{"description":"The Pin's design","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"design":{"$ref":"#/components/schemas/PinDesign"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"tags":["Designs"],"operationId":"update-pin-design","summary":"Re-render a template design","description":"Re-render a template-tier Pin's design with a new overlay title, new colors, new fonts, and/or a new base image. Free, but synchronous and server-rendered, so it can take several seconds. Only valid when supportedOperations includes update; AI-generated (premium) designs change through the regenerate endpoint instead. Send every change in ONE call: each call is a separate render, and omitted fields keep their stored values. Supply a new image as either mediaUrl or uploadId, never both; video uploads are rejected because design editing is image-only.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/postId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePinDesignRequest"}}}},"responses":{"200":{"description":"The updated design","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"design":{"$ref":"#/components/schemas/PinDesign"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The draft's tier does not support this operation, or the Pin changed status mid-edit"},"422":{"description":"The supplied image could not be processed"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts/{accountId}/posts/{postId}/design/variation":{"post":{"tags":["Designs"],"operationId":"select-pin-design-variation","summary":"Select a design variation","description":"Make one of the Pin's already-rendered variations the active design. Free and instant: no rendering and no credits. The URL must be one this draft already carries - a templateImageUrl from the design read, the variationUrl a finished regeneration produced, or the pendingMediaUrl reported when workspaceStatus is pending_apply.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/postId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["templateImageUrl"],"properties":{"templateImageUrl":{"type":"string","maxLength":2048,"description":"A variation URL this draft already carries"}}}}}},"responses":{"200":{"description":"The updated design","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"design":{"$ref":"#/components/schemas/PinDesign"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The Pin changed status mid-edit"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts/{accountId}/posts/{postId}/design/revert":{"post":{"tags":["Designs"],"operationId":"revert-pin-design","summary":"Revert to the original photo","description":"Put the Pin back to the original photo its design was built from, discarding the generated design. Free and instant. Only valid when supportedOperations includes revert, which requires the draft to still carry an original photo. The generated variations stay listed, so this is reversible through the variation endpoint.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/postId"}],"responses":{"200":{"description":"The updated design","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"design":{"$ref":"#/components/schemas/PinDesign"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The draft has no original photo, or the Pin changed status mid-edit"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts/{accountId}/posts/{postId}/design/regenerate":{"post":{"tags":["Designs"],"operationId":"regenerate-pin-design","summary":"Regenerate an AI design","description":"Have Tailwind's AI generate a new design for a premium (AI-designed) Pin. Asynchronous and metered: it spends plan credits, takes a few minutes, and returns a job id to poll with GET /v1/design-regenerations/{jobId}. Only valid when supportedOperations includes regenerate. The result does NOT replace the Pin's design: a finished regeneration returns a variationUrl that must be applied through the variation endpoint. To revise the design the Pin already has rather than replace it, pass selectedVariationUrl - the generator only enters its edit path when that URL matches an existing premium variation, so omitting it spends the same credits on an unrelated design. Billing is charged to the account in the path, never to another account in the organization. An Idempotency-Key header is required so a retried request returns the original job instead of charging again.","parameters":[{"$ref":"#/components/parameters/accountId"},{"$ref":"#/components/parameters/postId"},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":191},"description":"Caller-generated key. The same key with the same post and body replays the original job; the same key with a different post or body is a 409."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegeneratePinDesignRequest"}}}},"responses":{"202":{"description":"Regeneration accepted and processing","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"regeneration":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["processing"]},"creditCost":{"type":"number"}},"required":["id","status"]}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"The path account cannot be billed for this regeneration"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The draft's tier does not support regeneration, or the Idempotency-Key was already used for a different request"},"422":{"description":"The supplied image could not be processed"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/design-regenerations/{jobId}":{"get":{"tags":["Designs"],"operationId":"get-design-regeneration","summary":"Get design regeneration status","description":"Poll a design regeneration. It usually completes in a few minutes; poll every ~15 seconds. On success the response carries variationUrl - the new design, which is NOT applied to the Pin. Apply it through the variation endpoint. On failure the charge is refunded; billing.refund reports whether that refund has landed yet. This endpoint reads design regenerations only; other SmartPin generation jobs are 404.","parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"The id returned when the regeneration was accepted"}],"responses":{"200":{"description":"Regeneration status","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"regeneration":{"$ref":"#/components/schemas/DesignRegeneration"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/copy-generations":{"post":{"tags":["Generations"],"operationId":"generate-copy","summary":"Generate Pin copy","description":"Generate Pinterest Pin titles and descriptions with Tailwind's Ghostwriter, using the organization's saved brand context so the copy matches how this account already writes, and saving the result to the account's Ghostwriter history. Asynchronous: the response returns a job id to poll with GET /v1/copy-generations/{jobId}. Each generation spends plan credits and is charged when the request is accepted; on legacy (non-credit) plans an exhausted AI-credit balance is instead rejected synchronously as a 400. Org-scoped — no accountId is needed. This endpoint generates Pinterest Pin copy only; Tailwind's other Ghostwriter generators are not exposed here.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCopyGenerationRequest"}}}},"responses":{"202":{"description":"Copy generation accepted and processing","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"generation":{"$ref":"#/components/schemas/CopyGenerationAccepted"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"description":"No Pinterest account exists to gate Ghostwriter against for this modular-billing org — connect one before generating."},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/copy-generations/{jobId}":{"get":{"tags":["Generations"],"operationId":"get-copy-generation","summary":"Get copy generation status","description":"Poll a copy generation started by POST /v1/copy-generations. Generation usually completes in under a minute; poll every ~5 seconds. Once status is success, the result holds the generated title and description pairs.","parameters":[{"$ref":"#/components/parameters/copyGenerationId"}],"responses":{"200":{"description":"Copy generation status","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"generation":{"$ref":"#/components/schemas/CopyGeneration"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/accounts/{accountId}/post-batches":{"post":{"tags":["Posts"],"operationId":"create-post-batch","summary":"Create Pins in bulk","description":"Create up to 175 Pins in one request, each entry its own post with its own image, title, description, link, Board and schedule. Each entry takes the same fields as POST /v1/accounts/{accountId}/posts, except intervalBoards and carouselItems, which a batch does not accept; create those Pins one at a time. Asynchronous: the response returns a batch id to poll with GET /v1/post-batches/{batchId}, which reports every entry as created, failed or pending, in request order. Entries run one after another, so a full batch can take a few minutes. A draft entry (no sendAt and no useNextOpenSlot) may leave out mediaUrl and uploadId if it has a title, description or url: it is saved with Tailwind's placeholder image, cannot be scheduled until a real image replaces it with Change media in Tailwind's Pin Scheduler, and the API cannot replace it. Billing matches single creates: drafts are free and each scheduled entry is charged when it is created. The request is refused with a 402, and nothing is created, when the account cannot cover every scheduled entry; credits that run out part way through fail only the entries after that point. Not idempotent unless you send an Idempotency-Key header: without one, resending a batch creates its Pins again, so after a failure resend only the failed entries. With one, repeating the same request returns the original batch. Results are kept for 7 days.","parameters":[{"$ref":"#/components/parameters/accountId"},{"name":"Idempotency-Key","in":"header","required":false,"description":"Up to 255 characters. Repeating a request with the same key and body returns the original batch instead of creating its Pins again; the same key with a different body is a 409.","schema":{"type":"string","maxLength":255}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePostBatchRequest"}}}},"responses":{"202":{"description":"Batch accepted. Entries refused at submit are counted in summary.rejected and reported by the status poll.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"batch":{"$ref":"#/components/schemas/PostBatchAccepted"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The Idempotency-Key was already used for a different batch."},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/post-batches/{batchId}":{"get":{"tags":["Posts"],"operationId":"get-post-batch","summary":"Get bulk create status","description":"Poll a batch started by POST /v1/accounts/{accountId}/post-batches. Every entry is reported in request order as created (with its postId), failed (with the error a single create would have returned) or pending. The batch is completed once no entry is pending. Poll every ~10 seconds. To retry, resend only the failed entries in a new batch; an entry that failed with a note that the Pin may exist should be checked with GET /v1/accounts/{accountId}/posts first.","parameters":[{"$ref":"#/components/parameters/postBatchId"}],"responses":{"200":{"description":"Batch status","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"batch":{"$ref":"#/components/schemas/PostBatch"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/media/uploads":{"post":{"tags":["Media"],"operationId":"create-media-upload","summary":"Create a media upload","description":"Get a short-lived, write-only URL for uploading your own file bytes, for media that has no public URL (a Canva export, a photo, a video on disk). Three steps: call this endpoint with the file's extension; PUT the raw bytes to the returned uploadUrl with a Content-Type header exactly matching the returned contentType; then pass the uploadId to POST /v1/accounts/{accountId}/posts instead of mediaUrl. Bytes go straight to storage and never pass through this API. The uploadId is single-use, scoped to your organization, and expires if it is never attached to a Pin — note that uploadUrl expires much sooner than uploadId does, so upload promptly. Free: staging bytes costs no credits; only scheduling a Pin is billed, and what that costs depends on the organization's billing system.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMediaUploadRequest"}}}},"responses":{"201":{"description":"Upload slot created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"upload":{"$ref":"#/components/schemas/MediaUpload"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/capability-requests":{"post":{"tags":["Capability requests"],"operationId":"create-capability-request","summary":"Request a capability","description":"Tell Tailwind about something this API cannot do yet — an endpoint you expected and could not find, a missing field or filter, or a step you had to work around. Requests go to the product team, who use them to decide what to build next. Your description is recorded in Tailwind's internal analytics and posted to an internal Tailwind channel for staff to read, so send only what you would be comfortable sharing with Tailwind — do not include credentials or personal data. No request resource is created: there is nothing to read back, no ticket to follow up on, and no timeline, so this returns only an acknowledgement. Report gaps as you hit them rather than working around them silently. Not for reporting errors: a rejected argument, an expired token, or an out-of-credits response are not missing capabilities. Free — this consumes no credits.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCapabilityRequestRequest"}}}},"responses":{"201":{"description":"Capability request received","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CapabilityRequestReceipt"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/issue-reports":{"post":{"tags":["Issue reports"],"operationId":"create-issue-report","summary":"Report a wrong result","description":"Tell Tailwind that a call answered wrongly rather than failing — a result that contradicts another result, a result you had to work around, or an output your user says is wrong. Reports go to the people who maintain this API, who read them to find what is broken. Your description and tool are recorded in Tailwind's internal analytics and posted to an internal Tailwind channel for staff to read, so send only what you would be comfortable sharing with Tailwind — describe the problem rather than quoting the result, and do not include credentials or personal data. No report resource is created: there is nothing to read back and no ticket to follow up on, so this returns only an acknowledgement, and nothing about the result that prompted it changes. Not for reporting errors: a rejected argument, an expired token, or an out-of-credits response are errors to handle, not wrong answers. If the API simply cannot do what you need, use POST /v1/capability-requests instead. Free — this consumes no credits.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateIssueReportRequest"}}}},"responses":{"201":{"description":"Issue report received","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/IssueReportReceipt"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Bearer authentication. Use a Tailwind API key (tw_pk_...) or OAuth access token."}},"parameters":{"accountId":{"name":"accountId","in":"path","required":true,"description":"Numeric Tailwind account ID returned by GET /v1/accounts","schema":{"type":"string"}},"analyticsContentType":{"name":"contentType","in":"query","required":false,"description":"Which activity to count: ORGANIC for unpromoted activity only, PAID for promoted (ad) activity only, ALL for both blended. Defaults to ALL.","schema":{"type":"string","enum":["ALL","PAID","ORGANIC"],"default":"ALL"}},"analyticsStartDate":{"name":"startDate","in":"query","required":false,"description":"First day of the reporting window, as a UTC calendar date (YYYY-MM-DD). Must be supplied together with endDate; omit both for the last 30 days. Pinterest does not report further back than 90 days.","schema":{"type":"string","format":"date"}},"analyticsEndDate":{"name":"endDate","in":"query","required":false,"description":"Last day of the reporting window, inclusive, as a UTC calendar date (YYYY-MM-DD). Must be supplied together with startDate. Cannot be more than 90 days after startDate.","schema":{"type":"string","format":"date"}},"boardId":{"name":"boardId","in":"path","required":true,"description":"Numeric Pinterest board ID returned by GET /v1/accounts/{accountId}/boards. Tailwind's own `b_`-prefixed CSV export format is accepted and normalized; board names and board URLs are rejected with a 400.","schema":{"type":"string"}},"sectionId":{"name":"sectionId","in":"path","required":true,"description":"Board section ID returned by GET /v1/accounts/{accountId}/boards/{boardId}/sections","schema":{"type":"string"}},"timeslotId":{"name":"timeslotId","in":"path","required":true,"description":"Numeric timeslot ID returned by GET /v1/accounts/{accountId}/timeslots","schema":{"type":"string"}},"postId":{"name":"postId","in":"path","required":true,"description":"Post ID","schema":{"type":"string"}},"generationId":{"name":"generationId","in":"path","required":true,"description":"Generation ID returned by POST /v1/accounts/{accountId}/generations","schema":{"type":"string"}},"copyGenerationId":{"name":"jobId","in":"path","required":true,"description":"Copy generation ID returned by POST /v1/copy-generations","schema":{"type":"string"}},"postBatchId":{"name":"batchId","in":"path","required":true,"description":"Batch ID returned by POST /v1/accounts/{accountId}/post-batches","schema":{"type":"string"}}},"schemas":{"AnalyticsMetrics":{"type":"object","description":"Pinterest metrics, camelCased (impression, save, saveRate, pinClick, outboundClick, engagement, engagementRate, ...). A metric Pinterest does not report for this subject is ABSENT; a metric it reports as not-yet-computed is present and NULL. The two mean different things — absent will never arrive, null may.","additionalProperties":{"type":"number","nullable":true}},"AnalyticsDailyMetric":{"type":"object","properties":{"date":{"type":"string","format":"date","description":"The UTC day these metrics cover"},"dataStatus":{"type":"string","description":"Pinterest's readiness marker for this day — READY, PROCESSING, BEFORE_PIN_CREATED and similar. Anything other than READY explains why the day's metrics are null or zero; PROCESSING is the only one worth re-reading later."},"metrics":{"$ref":"#/components/schemas/AnalyticsMetrics"}},"required":["date","metrics"]},"TopPin":{"type":"object","description":"One of the account's best-performing Pins. A deliberately narrow projection of Pinterest's Pin payload — tracking parameters, per-metric status flags, promotion state and internal identifiers are not passed through.","properties":{"pinId":{"type":"string","description":"The Pinterest Pin id. This is the id GET /v1/accounts/{accountId}/pins/{pinId}/analytics takes — not a Tailwind postId."},"title":{"type":"string"},"description":{"type":"string"},"destinationUrl":{"type":"string","description":"Where the Pin links to"},"pinUrl":{"type":"string","description":"The Pin's own page on Pinterest"},"imageUrl":{"type":"string"},"boardId":{"type":"string","description":"The Pinterest board the Pin is saved to — the same id GET /v1/accounts/{accountId}/boards returns. Group by it to compare performance across boards."},"creativeType":{"type":"string","nullable":true,"description":"Pinterest's creative format for the Pin, verbatim — for example REGULAR, VIDEO, CAROUSEL, IDEA or COLLAGE. Pinterest adds values over time, so treat it as an open set. Null when Pinterest did not report one."},"createdAt":{"type":"string","format":"date-time"},"metrics":{"$ref":"#/components/schemas/AnalyticsMetrics"}},"required":["pinId","boardId","metrics"]},"InventoryPin":{"type":"object","description":"One Pin on the account, however it was published.","properties":{"pinId":{"type":"string","description":"The Pinterest Pin id. This is the id GET /v1/accounts/{accountId}/pins/{pinId}/analytics takes, not a Tailwind postId."},"boardId":{"type":"string","description":"The Pinterest board the Pin is saved to, the same id GET /v1/accounts/{accountId}/boards returns"},"title":{"type":"string"},"description":{"type":"string"},"destinationUrl":{"type":"string","description":"Where the Pin links to, verbatim, including any UTM parameters"},"pinUrl":{"type":"string","description":"The Pin's own page on Pinterest"},"imageUrl":{"type":"string"},"createdAt":{"type":"string","format":"date-time","description":"When the Pin was created, or saved for a repin"},"isRepin":{"type":"boolean","description":"True when the Pin was saved from another Pin, including the account's own Pins saved to another board"},"creativeType":{"type":"string","nullable":true,"description":"Pinterest's creative format for the Pin, verbatim, for example REGULAR, VIDEO, CAROUSEL, IDEA or SHOPPING. Pinterest adds values over time, so treat it as an open set. Null when Pinterest did not report one."}},"required":["pinId","boardId","createdAt","isRepin","creativeType"]},"BoardPin":{"description":"One Pin saved on a board.","allOf":[{"$ref":"#/components/schemas/InventoryPin"},{"type":"object","properties":{"boardSectionId":{"type":"string","nullable":true,"description":"The board section the Pin sits in, the same id GET /v1/accounts/{accountId}/boards/{boardId}/sections returns. Null when the Pin sits on the board itself."}},"required":["boardSectionId"]}]},"Meta":{"type":"object","properties":{"requestId":{"type":"string","description":"Unique request ID for debugging"}}},"PublishingStatusResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"publishing":{"type":"object","properties":{"status":{"type":"string","enum":["active","paused"],"description":"active: scheduled Pins publish at their times. paused: Pins whose time arrives fail instead of publishing."}},"required":["status"]}},"required":["publishing"]},"meta":{"$ref":"#/components/schemas/Meta"}}},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Error code"},"message":{"type":"string","description":"Error message"},"reason":{"type":"string","description":"Machine-readable reason. On PAYMENT_REQUIRED errors it is the denial reason: insufficient_credits, trial_exhausted, no_access, account_not_connected, legacy_post_limit_exceeded. Other errors may carry an operation-specific reason, such as pinterest_reconnect_required, pinterest_rate_limited, pinterest_timeout, pinterest_request_failed or list_boards_upstream_failed; each operation's documented error responses show the values it returns. Branch on reason rather than parsing message."},"resolve_url":{"type":"string","description":"Present on PAYMENT_REQUIRED errors: where the denial can be resolved. Reason-specific, because buying credits does not clear a trial, access or connection denial. Requests sent with X-Request-Source: mcp receive an informational plans page instead, so agent surfaces never present a purchase link."}}},"meta":{"$ref":"#/components/schemas/Meta"}}},"Account":{"type":"object","properties":{"id":{"type":"string","description":"Account ID"},"userId":{"type":"string","description":"Pinterest user ID"},"displayName":{"type":"string","nullable":true,"description":"Display name"},"username":{"type":"string","nullable":true,"description":"Pinterest username"},"avatarUrl":{"type":"string","nullable":true,"description":"Profile image URL"},"tokenAuthorized":{"type":"boolean","description":"Whether the Pinterest token is confirmed valid. False both when the token is revoked and when its status could not be checked; `tokenStatus` tells the two apart."},"tokenStatus":{"type":"string","enum":["authorized","unauthorized","unknown"],"description":"The Pinterest token's status. `unauthorized` means the account must be reconnected. `unknown` means Tailwind could not check the token just now, usually a temporary failure; it does not mean the account needs reconnecting, so retry later."},"isDomainVerified":{"type":"boolean","description":"Whether the domain is verified"},"timezone":{"type":"string","nullable":true,"description":"IANA time zone Tailwind schedules this account in, such as America/New_York — the zone its timeslots and the dashboard calendar use. Set per organization. Null when none is set."},"createdAt":{"type":"integer","nullable":true,"description":"When the account was connected (Unix timestamp)"}}},"AccountDetail":{"type":"object","description":"A single account. Same fields as an entry in GET /v1/accounts, plus the verified domain.","properties":{"id":{"type":"string","description":"Account ID"},"userId":{"type":"string","description":"Pinterest user ID"},"displayName":{"type":"string","nullable":true,"description":"Display name"},"username":{"type":"string","nullable":true,"description":"Pinterest username"},"avatarUrl":{"type":"string","nullable":true,"description":"Profile image URL"},"tokenAuthorized":{"type":"boolean","description":"Whether the Pinterest token is confirmed valid. False both when the token is revoked and when its status could not be checked; `tokenStatus` tells the two apart."},"tokenStatus":{"type":"string","enum":["authorized","unauthorized","unknown"],"description":"The Pinterest token's status. `unauthorized` means the account must be reconnected. `unknown` means Tailwind could not check the token just now, usually a temporary failure; it does not mean the account needs reconnecting, so retry later."},"isDomainVerified":{"type":"boolean","description":"Whether the domain is verified"},"domainUrl":{"type":"string","description":"The account's claimed domain. Omitted when no domain is set."},"timezone":{"type":"string","nullable":true,"description":"IANA time zone Tailwind schedules this account in, such as America/New_York — the zone its timeslots and the dashboard calendar use. Set per organization. Null when none is set."},"createdAt":{"type":"integer","nullable":true,"description":"When the account was connected (Unix timestamp)"}}},"Board":{"type":"object","description":"A board as returned by the cached list endpoint. The write endpoints return a `ManagedBoard` instead — reads and writes reach Pinterest by different paths and carry different fields.","properties":{"id":{"type":"string","description":"Board ID"},"name":{"type":"string","description":"Board name"},"description":{"type":"string","nullable":true,"description":"Board description, or null when the board has none"},"followerCount":{"type":"integer","description":"Number of accounts following the board"},"isCollaborator":{"type":"boolean","description":"Whether the user is a collaborator"},"isSecret":{"type":"boolean","description":"Whether the board is secret"},"pinCount":{"type":"integer","description":"Number of Pins on the board, from a cached snapshot that can lag behind Pinterest. Treat it as a lower bound before DELETE, which destroys every Pin actually on the board."},"pinsModifiedAt":{"type":"string","format":"date-time","nullable":true,"description":"When Pinterest last recorded a change to the Pins on the board (Pinterest's `board_pins_modified_at`), as an ISO 8601 timestamp. Pinterest does not document which changes move it, so read it as a last-activity date for finding boards that have gone quiet, not as the date of the newest Pin. Null when unavailable."}}},"ManagedBoard":{"type":"object","description":"A board as returned by the create and update endpoints, read back from Pinterest at the moment of the write.","properties":{"id":{"type":"string","description":"Board ID","example":"1106196864631757445"},"name":{"type":"string","description":"Board name","example":"Weeknight dinners"},"description":{"type":"string","nullable":true,"description":"Board description, or null when the board has none"},"privacy":{"type":"string","enum":["public","protected","secret"],"description":"Board visibility. `protected` cannot be set through this API but can be read back from boards created elsewhere."},"pinCount":{"type":"integer","description":"Number of Pins on the board","example":12}}},"DeletedBoard":{"type":"object","description":"The board as it was immediately before deletion.","properties":{"id":{"type":"string","description":"Board ID"},"name":{"type":"string","description":"Board name"},"pinCount":{"type":"integer","description":"Number of Pins that were deleted along with the board"}}},"BoardKeywordRelevance":{"type":"object","required":["state"],"description":"Either a score, or `absent` when the board yielded no readable Pins. `absent` is not zero coverage — it means no evidence either way, and is not charged.","properties":{"state":{"type":"string","enum":["scored","absent"]},"relevance":{"type":"string","enum":["excellent","very-good","good","fair","poor"],"description":"How well the board covers the keyword. Absent when `state` is \"absent\"."},"relevantPinCount":{"type":"integer","description":"Pins on the board that cover the keyword"},"scoredPinCount":{"type":"integer","description":"Pins scored — the denominator behind the tier"},"truncated":{"type":"boolean","description":"The board held more Pins than one request reads, so this score covers a sample"},"contributingPins":{"type":"array","description":"The Pins that cover the keyword, strongest first","items":{"$ref":"#/components/schemas/ContributingPin"}}}},"RecommendedPin":{"type":"object","required":["id","title","description","link","thumbnailUrl","relevance"],"description":"A public Pinterest Pin worth adding to the board. Not one of the account's own Pins — creating it still requires a post.","properties":{"id":{"type":"string","description":"Pinterest Pin ID"},"title":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"link":{"type":"string","nullable":true,"description":"Destination URL, when set"},"thumbnailUrl":{"type":"string","nullable":true},"relevance":{"type":"string","enum":["excellent","very-good","good","fair"],"description":"How well this Pin matches the keyword. Never \"poor\" — weaker Pins are dropped rather than recommended."}}},"ContributingPin":{"type":"object","required":["id","relevance"],"properties":{"id":{"type":"string","description":"Pinterest Pin ID"},"title":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"link":{"type":"string","nullable":true,"description":"Destination URL, when set"},"relevance":{"type":"string","enum":["excellent","very-good","good","fair","poor"],"description":"This Pin's own relevance, on the same scale as the board's"}}},"BoardSection":{"type":"object","properties":{"id":{"type":"string","description":"Section ID"},"name":{"type":"string","description":"Section name"}}},"CreateBoardRequest":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":180,"description":"Board name"},"description":{"type":"string","maxLength":500,"description":"Board description"},"privacy":{"type":"string","enum":["public","secret"],"default":"public","description":"Whether the board is visible on the Pinterest profile"}}},"UpdateBoardRequest":{"type":"object","description":"At least one of `name` or `description` must be supplied.","properties":{"name":{"type":"string","maxLength":180,"description":"New board name"},"description":{"type":"string","maxLength":500,"description":"New board description"}}},"SavePinsToBoardRequest":{"type":"object","required":["pinIds"],"properties":{"pinIds":{"type":"array","minItems":1,"maxItems":50,"items":{"type":"string","pattern":"^\\d+$"},"description":"Numeric Pinterest Pin IDs to save onto the board, e.g. from `get-recommended-pins`. Pin URLs and names are rejected with a `400`. Duplicates are not removed — a repeated ID saves the Pin twice."}}},"SavePinsToBoardResult":{"type":"object","required":["savedCount","failedCount"],"properties":{"savedCount":{"type":"integer","description":"Pins saved onto the board","example":8},"failedCount":{"type":"integer","description":"Pins that could not be saved, most often because the Pin was deleted or the board ID does not exist. Which Pins failed is not reported.","example":2}}},"BoardSectionRequest":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":180,"description":"Section name"}}},"GroupBoard":{"type":"object","description":"A public Pinterest group board from the Group Board Finder scrape cache. Not one of the authenticated user's own boards, and not usable as a boardId when scheduling.","properties":{"boardId":{"type":"string","description":"Pinterest board ID"},"name":{"type":"string","description":"Board name"},"description":{"type":"string","description":"Board description"},"url":{"type":"string","description":"Board URL on Pinterest"},"ownerUsername":{"type":"string","description":"Pinterest username of the board owner"},"imageUrl":{"type":"string","description":"Board cover image URL"},"pinCount":{"type":"integer","description":"Pins on the board when last scraped"},"followerCount":{"type":"integer","description":"Followers of the board owner when last scraped"},"isCollaborative":{"type":"boolean","description":"Whether the board accepts collaborators"},"isJoinable":{"type":"boolean","description":"Whether the board was open to join"},"pinThumbnailUrls":{"type":"array","items":{"type":"string"},"description":"Up to four example Pin thumbnails from the board"}}},"SaveKeywordsRequest":{"type":"object","required":["keywords"],"properties":{"keywords":{"type":"array","minItems":1,"maxItems":50,"items":{"type":"object","required":["term"],"properties":{"term":{"type":"string","minLength":1,"maxLength":255,"description":"The keyword text to save"},"urls":{"type":"array","maxItems":25,"items":{"type":"string","format":"uri"},"description":"URLs of the organization's own to associate with this keyword. Any URL not already known is recorded."}}}}}},"Feed":{"type":"object","description":"A connected RSS feed subscription.","properties":{"id":{"type":"string","description":"Feed ID. Pass to get-feed, update-feed, delete-feed."},"accountId":{"type":"string","description":"Pinterest account the generated Pins are assigned to."},"feedUrl":{"type":"string"},"hostname":{"type":"string"},"title":{"type":"string","nullable":true,"description":"The feed's own title, when it declares one."},"keywords":{"type":"array","items":{"type":"string"},"description":"Keywords applied to every Pin this feed generates."},"designTier":{"type":"string","enum":["basic","premium","no-template","template-match"],"description":"Design treatment for generated Pins. `template-match` is readable but cannot be set through this API — it is not generally available."},"premiumStyle":{"type":"string","nullable":true,"enum":["image-highlight","text-highlight","minimalist","artistic","professional","natural"],"description":"Set only on the premium tier; null otherwise."},"generationFrequency":{"type":"string","enum":["every-3-days","weekly","every-2-weeks","monthly"],"description":"How often a generated Pin is redesigned, when autoRegenerate is on. Not how often the feed is checked — that is daily."},"autoRegenerate":{"type":"boolean","description":"Whether generated Pins are periodically redesigned."},"status":{"type":"string","enum":["active","paused"],"description":"A paused feed generates nothing, but is also not polled — so posts published while it is paused are treated as new on resume."},"lastPolledAt":{"type":"string","nullable":true,"description":"ISO 8601 timestamp of the last watcher run over this feed, or null if it has not run yet."},"lastFailureReason":{"type":"string","nullable":true,"description":"Why the last poll failed, or null. A feed that keeps failing here is usually unreachable rather than empty."},"smartPinCount":{"type":"number","description":"SmartPins generated from this feed so far."},"createdAt":{"type":"string","description":"ISO 8601"},"updatedAt":{"type":"string","description":"ISO 8601"}},"required":["id","accountId","feedUrl","hostname","title","keywords","designTier","premiumStyle","generationFrequency","autoRegenerate","status","lastPolledAt","lastFailureReason","smartPinCount","createdAt","updatedAt"]},"FeedPreview":{"oneOf":[{"type":"object","description":"A single resolved feed.","properties":{"kind":{"type":"string","enum":["feed"]},"feedUrl":{"type":"string","description":"Pass this to connect-feed."},"hostname":{"type":"string"},"title":{"type":"string","nullable":true},"totalPages":{"type":"number","description":"Entries currently in the feed. All of them form the baseline on connect; none generate Pins."},"sectionCount":{"type":"number","description":"How many sections the feed has in total, before the list below is capped."},"sections":{"type":"array","description":"How the feed's entries group by URL path. Capped at 25; sectionCount has the true total.","items":{"type":"object","properties":{"name":{"type":"string"},"pageCount":{"type":"number"}}}},"sampleEntries":{"type":"array","description":"Up to 10 entries, enough to confirm this is the right feed. Not the full list — totalPages has the count.","items":{"type":"object","properties":{"url":{"type":"string"},"title":{"type":"string","nullable":true},"publishedAt":{"type":"string","nullable":true,"description":"ISO 8601"}}}}},"required":["kind","feedUrl","hostname","title","totalPages","sections","sampleEntries"]},{"type":"object","description":"Several feeds were found; choose one.","properties":{"kind":{"type":"string","enum":["candidates"]},"feeds":{"type":"array","items":{"type":"object","properties":{"feedUrl":{"type":"string"},"title":{"type":"string","nullable":true}}}}},"required":["kind","feeds"]}]},"ConnectFeedRequest":{"type":"object","required":["feedUrl","accountId"],"properties":{"feedUrl":{"type":"string","maxLength":2048,"description":"A resolved RSS or Atom feed URL. Use preview-feed to turn a site URL into one."},"accountId":{"type":"string","description":"Pinterest account the generated Pins are assigned to, from list-accounts."},"keywords":{"type":"array","items":{"type":"string","maxLength":255},"maxItems":50,"description":"Applied to every Pin this feed generates."},"designTier":{"type":"string","enum":["basic","premium","no-template"],"description":"Defaults to basic. premium requires premiumStyle."},"premiumStyle":{"type":"string","nullable":true,"enum":["image-highlight","text-highlight","minimalist","artistic","professional","natural"],"description":"Required when designTier is premium, and rejected otherwise."},"generationFrequency":{"type":"string","enum":["every-3-days","weekly","every-2-weeks","monthly"]},"autoRegenerate":{"type":"boolean"}}},"UpdateFeedRequest":{"type":"object","description":"At least one field is required. Unknown fields are rejected rather than ignored.","properties":{"status":{"type":"string","enum":["active","paused"],"description":"Pause or resume the watcher. Resuming backfills posts published during the pause; see the operation description."},"keywords":{"type":"array","items":{"type":"string","maxLength":255},"maxItems":50},"designTier":{"type":"string","enum":["basic","premium","no-template"]},"premiumStyle":{"type":"string","nullable":true,"enum":["image-highlight","text-highlight","minimalist","artistic","professional","natural"],"description":"Send null when moving off the premium tier, so the stored style is cleared."},"generationFrequency":{"type":"string","enum":["every-3-days","weekly","every-2-weeks","monthly"]},"autoRegenerate":{"type":"boolean"}}},"BulkDeleteKeywordsRequest":{"type":"object","required":["keywordIds"],"properties":{"keywordIds":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string"},"description":"Saved keyword IDs, from list-keywords"}}},"TagKeywordsRequest":{"type":"object","required":["keywordIds","tags"],"properties":{"keywordIds":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string"},"description":"Saved keyword IDs, from list-keywords"},"tags":{"type":"array","minItems":1,"maxItems":10,"items":{"type":"string","minLength":1,"maxLength":100},"description":"Tag names. Leading and trailing whitespace is trimmed."}}},"SavedKeyword":{"type":"object","description":"A keyword on the organization's saved list — the same list the Tailwind dashboard's Saved Keywords page shows.","properties":{"id":{"type":"string","description":"Saved keyword ID"},"term":{"type":"string","description":"The keyword text"},"savedAt":{"type":"string","format":"date-time","description":"When the keyword was added to the list"},"urls":{"type":"array","items":{"type":"string"},"description":"URLs of the organization's own that this keyword is associated with"},"tags":{"type":"array","description":"Tags the organization has applied to this keyword","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}}}},"pinterestSearchVolume":{"type":"integer","description":"Estimated monthly Pinterest search volume. Absent when Tailwind has no volume data for the term; treat a missing value as unknown, not as zero demand."},"commercialIntentScore":{"type":"number","description":"How strongly the term signals buying intent. Absent when Tailwind has no interest data for it."},"detectedLanguage":{"type":"string","description":"Language detected for the term, e.g. \"english\". Absent when Tailwind has no interest data for it."}}},"BoardList":{"type":"object","properties":{"id":{"type":"integer","description":"Board list ID"},"name":{"type":"string","description":"Board list name; omitted when unnamed"},"accountId":{"type":"string","description":"Account ID"},"boards":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Board ID"},"order":{"type":"integer","description":"Order in the list"},"boardSectionId":{"type":"string","description":"Pinterest board section ID, when the entry targets a section"}}}}}},"BoardListRequest":{"type":"object","required":["name","boards"],"properties":{"name":{"type":"string","minLength":1,"maxLength":255,"description":"Board list name"},"boards":{"type":"array","minItems":1,"maxItems":100,"description":"Every board the list holds, in publishing order. Each board may appear once. Interval Pinning takes at most 20 boards per Pin, so a longer list cannot be passed whole as intervalBoards.","items":{"type":"object","required":["boardId"],"properties":{"boardId":{"type":"string","description":"Numeric Pinterest board ID from GET /v1/accounts/{accountId}/boards"},"boardSectionId":{"type":"string","maxLength":255,"description":"Pinterest board section ID, to target a section of that board"}}}}}},"ScheduleEntry":{"type":"object","description":"One dated occurrence on the account's schedule. Either an open smart schedule slot, or the Pin holding that time.","properties":{"sendAt":{"type":"integer","description":"When this entry falls (Unix timestamp, seconds)"},"state":{"type":"string","enum":["open","scheduled","publishing","published"],"description":"open = an unused smart schedule slot; scheduled = a queued Pin; publishing = a Pin mid-publish; published = already on Pinterest"},"timeslotId":{"type":"string","description":"The smart schedule slot this entry sits on. Absent for a Pin scheduled off-slot."},"post":{"type":"object","description":"The Pin holding this time. Absent when state is open.","properties":{"postId":{"type":"string"},"title":{"type":"string"},"url":{"type":"string"},"boardId":{"type":"string"}}}}},"PinSpacingRules":{"type":"object","properties":{"defaultRule":{"type":"object","nullable":true,"description":"The account-wide rule for every URL without a custom rule. Null when the account has none.","properties":{"spacing":{"type":"integer","description":"Minimum gap between Pins to one URL"},"spacingUnits":{"type":"string","enum":["DAYS"]}},"required":["spacing","spacingUnits"]},"customRules":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","description":"The destination URL the rule is set for"},"spacing":{"type":"integer","description":"Minimum gap between Pins to this URL"},"spacingUnits":{"type":"string","enum":["DAYS"]}},"required":["url","spacing","spacingUnits"]}},"linkedUrls":{"type":"array","description":"URLs that follow another URL's rule. Pins to a linked URL and its base URL share one spacing window.","items":{"type":"object","properties":{"url":{"type":"string","description":"The linked URL"},"baseUrl":{"type":"string","description":"The URL whose rule it follows"}},"required":["url","baseUrl"]}}},"required":["defaultRule","customRules","linkedUrls"]},"AppliedPinSpacingRule":{"type":"object","properties":{"url":{"type":"string","description":"The URL that was resolved, as sent"},"linkedToUrl":{"type":"string","nullable":true,"description":"The base URL whose rule `url` follows when `url` is a linked URL; null otherwise."},"rule":{"type":"object","nullable":true,"description":"The applied rule, or null when Pin Spacing does not constrain the URL.","properties":{"ruleType":{"type":"string","enum":["DEFAULT","CUSTOM"],"description":"DEFAULT is the account-wide rule; CUSTOM is set for one URL"},"spacing":{"type":"integer","description":"Minimum gap between Pins to the URL"},"spacingUnits":{"type":"string","enum":["DAYS"]},"url":{"type":"string","description":"CUSTOM only: the URL the rule is set for"}},"required":["ruleType","spacing","spacingUnits"]}},"required":["url","linkedToUrl","rule"]},"Timeslot":{"type":"object","description":"A recurring weekly posting rule, not a dated occurrence. Carries no occupancy — see ScheduleEntry.","properties":{"id":{"type":"string","description":"Timeslot ID"},"accountId":{"type":"string","description":"Account ID"},"dayPreference":{"type":"integer","description":"Day of week (0-6, where 0 is Sunday)","minimum":0,"maximum":6},"timePreference":{"type":"string","description":"Time of day as a zero-padded 24-hour HHMM string (e.g. \"0930\"), in `timezone`"},"timezone":{"type":"string","description":"Timezone (e.g., America/New_York)"},"type":{"type":"string","enum":["auto-created","recommended","manual"],"description":"How the slot was added: generated by a rebuild, picked from Tailwind's suggestions, or placed by hand (including every slot created or edited through this API)"},"scheduleId":{"type":"integer","description":"Schedule ID, when associated with a schedule"},"pinUuid":{"type":"integer","description":"Pin UUID, when associated with a pin"},"sendAt":{"type":"integer","description":"Unix timestamp of one occurrence, predicted when the slot was written. Not maintained afterwards; use GET /v1/accounts/{accountId}/schedule for real upcoming times."}}},"CreateTimeslotRequest":{"type":"object","required":["dayPreference","time"],"properties":{"dayPreference":{"type":"integer","minimum":0,"maximum":6,"description":"Day of week (0 = Sunday through 6 = Saturday)"},"time":{"type":"string","pattern":"^(?:[01][0-9]|2[0-3]):[0-5][0-9]$","description":"Time of day as 24-hour HH:MM (e.g. \"09:30\"), in `timezone`"},"timezone":{"type":"string","description":"IANA timezone the time is expressed in (e.g. America/New_York). Defaults to the account's own timezone; required if the account has none set."}}},"UpdateTimeslotRequest":{"type":"object","description":"At least one field is required. Omitted fields keep the slot's stored values.","properties":{"dayPreference":{"type":"integer","minimum":0,"maximum":6,"description":"Day of week (0 = Sunday through 6 = Saturday)"},"time":{"type":"string","pattern":"^(?:[01][0-9]|2[0-3]):[0-5][0-9]$","description":"Time of day as 24-hour HH:MM (e.g. \"09:30\"), in the slot's timezone"},"timezone":{"type":"string","description":"IANA timezone the time is expressed in (e.g. America/New_York)"}}},"RebuildScheduleRequest":{"type":"object","required":["slotsPerDay"],"properties":{"slotsPerDay":{"type":"integer","minimum":1,"maximum":50,"description":"How many slots to generate on each of the 7 days. The same 1-50 range the dashboard allows."}}},"RebuildScheduleResult":{"type":"object","required":["rebuilt","slotsPerDay","timeslotCount","timezoneSource"],"properties":{"rebuilt":{"type":"boolean","example":true},"slotsPerDay":{"type":"integer","description":"The requested slots per day"},"timeslotCount":{"type":"integer","description":"Total non-evergreen slots the schedule now holds (slotsPerDay × 7)"},"timezoneSource":{"type":"string","enum":["user","fallback"],"description":"`user` when the schedule was generated in the account's own timezone; `fallback` when the account has none set and America/New_York was used"}}},"AccountSubscriptionStatus":{"type":"object","description":"A Pinterest account's current paying access, normalized across billing systems.","required":["entitled","billingSystem","subscriptions"],"properties":{"entitled":{"type":"boolean","description":"Whether the account currently has active, paying access. This is the field to check before relying on a paid capability."},"billingSystem":{"type":"string","enum":["legacy","modular","shopify"],"description":"Which billing system the owning organization is on. An org is on exactly one."},"subscriptions":{"type":"array","description":"The entitlements backing `entitled`. A legacy org always reports exactly one entry describing its org-wide plan. A modular org reports one entry per current entitlement — possibly several, possibly none.","items":{"$ref":"#/components/schemas/SubscriptionStatusEntry"}}}},"AccountUsage":{"type":"object","description":"Credit and post usage for a Pinterest account, normalized across billing systems.","required":["billingSystem","scope","credits","postLimit","unlimitedScheduling"],"properties":{"billingSystem":{"type":"string","enum":["legacy","modular","shopify"],"description":"Which billing system the owning organization is on."},"scope":{"type":"string","enum":["organization","account"],"description":"Who every value in this response belongs to. `organization`: one pool shared by all the org's accounts, reported identically on each, so never add them up across accounts. `account`: this account alone."},"credits":{"type":"object","required":["remaining","monthlyAllowance","resetsAt"],"properties":{"remaining":{"type":"integer","description":"Credits available to spend now: unused monthly allowance plus purchased (modular) or bonus (legacy) credits."},"monthlyAllowance":{"type":"integer","description":"Credits the plan grants each month."},"resetsAt":{"type":"integer","nullable":true,"description":"Unix timestamp the monthly allowance next refills. Null when there is no monthly allowance or no reliable upcoming reset is known."}}},"postLimit":{"type":"object","nullable":true,"description":"The post allowance ordinary scheduling spends on legacy and Shopify billing, for the current usage period only. Null when the billing system does not meter posts (modular).","required":["limit","used","periodEndsAt"],"properties":{"limit":{"type":"integer","nullable":true,"description":"Posts allowed in the current usage period. Null means unlimited."},"used":{"type":"integer","description":"Published plus scheduled posts in the current usage period, across every Pinterest, Instagram and Facebook account in the organization."},"periodEndsAt":{"type":"integer","description":"Unix timestamp the current usage period ends."}}},"unlimitedScheduling":{"type":"boolean","description":"True when the account is on the modular winback plan, which schedules Pins without spending credits while reporting zero monthly credits. Credits still pay for AI generation. Always false on legacy and Shopify billing: there, read `postLimit`, which covers the current usage period only."}}},"SubscriptionStatusEntry":{"type":"object","description":"One entitlement backing an account's access.","required":["source","state","pendingCancellation","currentPeriodEndsAt","productType","billingCycle"],"properties":{"source":{"type":"string","enum":["plan","subscription","grant"],"description":"What backs this entitlement: the org's plan (legacy), a Maxio subscription, or a Tailwind-issued grant."},"state":{"type":"string","description":"Billing-system state. Legacy reports the org's Chargify subscription state (e.g. \"active\", \"past_due\"); modular subscriptions report the Maxio state; grants report \"granted\"."},"pendingCancellation":{"type":"boolean","description":"Whether a cancellation is scheduled but access continues through the paid period"},"currentPeriodEndsAt":{"type":"integer","nullable":true,"description":"Unix timestamp the current paid period ends, when known. Null for ordinary legacy plans, whose renewal date is not stored locally."},"productType":{"type":"string","enum":["bundle","seo","engagement","publishing"],"nullable":true,"description":"Modular only: the product domain this entitlement covers. Null on the legacy rail, which is not domain-scoped."},"billingCycle":{"type":"string","enum":["monthly","annual"],"nullable":true,"description":"Modular subscriptions only. Null for legacy plans and for grants."}}},"IntervalBoard":{"type":"object","required":["boardId"],"properties":{"boardId":{"type":"string","description":"Target board ID"},"boardSectionId":{"type":"string","description":"Section of this Board to publish into. Omit to publish to the board root."}}},"Post":{"type":"object","properties":{"id":{"type":"string","description":"Post ID"},"status":{"type":"string","enum":["draft","queued","sent","uploading","failed","missed"],"description":"Post status. draft = saved in Tailwind only, not created on Pinterest; queued = scheduled; sent = published to Pinterest."},"mediaUrl":{"type":"string","description":"URL of the media file"},"mediaType":{"type":"string","enum":["image","video"],"description":"Type of media"},"carouselItems":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","description":"URL of this image"},"title":{"type":"string","description":"Per-slide title, when the Pin has one"},"description":{"type":"string","description":"Per-slide description, when the Pin has one"},"link":{"type":"string","description":"Per-slide destination, when the Pin has one"}}},"description":"Further images of a carousel Pin, in order after the cover in `mediaUrl`. Absent on a single-image Pin. Per-slide title, description and link are reported when the Pin carries them, but cannot be set through this API yet."},"title":{"type":"string","nullable":true,"description":"Pin title"},"description":{"type":"string","nullable":true,"description":"Pin description"},"url":{"type":"string","nullable":true,"description":"Destination URL"},"boardId":{"type":"string","nullable":true,"description":"Target board ID"},"boardSectionId":{"type":"string","nullable":true,"description":"Section of `boardId` the Pin publishes into. Null when it publishes to the board root."},"altText":{"type":"string","nullable":true,"description":"Alt text for accessibility"},"sendAt":{"type":"integer","nullable":true,"description":"Scheduled send time (Unix timestamp)"},"sentAt":{"type":"integer","nullable":true,"description":"Actual send time (Unix timestamp)"},"createdAt":{"type":"integer","description":"Creation time (Unix timestamp)"},"pinId":{"type":"string","nullable":true,"description":"Pinterest pin ID (only present after publishing). This is the id GET /v1/accounts/{accountId}/pins/{pinId}/analytics takes."},"isSimplifiedPin":{"type":"boolean","description":"Whether this is a simplified pin (as opposed to a standard pin)"},"productTagPinIds":{"type":"array","items":{"type":"string"},"description":"Pinterest product-catalog pin IDs tagged on the pin (business accounts only)."},"generationId":{"type":"string","nullable":true,"description":"ID of the AI generation that produced this post, when Tailwind's AI generated it from a URL (matches the id returned by POST /v1/accounts/{accountId}/generations) or from a connected feed. Absent for posts created any other way. A draft with one is a generated draft waiting for review."},"failureReason":{"type":"string","description":"Why a failed Pin did not publish, as a stable code such as `board_not_found`, `expired_token` or `user_rate_limit`. Present only when `status` is `failed` and Tailwind recorded a reason. New codes are added over time, so treat an unrecognized code as a generic failure."},"failureMessage":{"type":"string","description":"A user-facing explanation of `failureReason` and what to do next, the same copy the Tailwind dashboard shows for a failed Pin. Present whenever `failureReason` is; a code with no specific copy gets a generic explanation."}}},"CreatePostRequest":{"type":"object","description":"Provide exactly one media source: mediaUrl (a publicly accessible URL) or uploadId (your own file bytes, uploaded first via POST /v1/media/uploads).","oneOf":[{"required":["mediaUrl"]},{"required":["uploadId"]}],"properties":{"mediaUrl":{"type":"string","format":"uri","description":"URL of the media file to pin. Can be any publicly accessible URL — external media is automatically uploaded and processed. A URL on a Tailwind image host is used as-is, so it must be a Tailwind image a Pin can use; any other path there is refused with a 422. Mutually exclusive with uploadId."},"uploadId":{"type":"string","description":"An upload from POST /v1/media/uploads whose bytes you have already PUT to its uploadUrl. Single-use: it can be attached to exactly one Pin, and belongs to the organization that created it. Mutually exclusive with mediaUrl. When provided, mediaType is taken from the upload itself and any mediaType in this request is ignored."},"mediaType":{"type":"string","enum":["image","video"],"default":"image","description":"Type of media. Set to 'video' for video files (.mp4, .mov, .qt). Defaults to 'image' if omitted. Ignored when uploadId is provided — the upload's own extension determines its type."},"carouselItems":{"type":"array","minItems":1,"maxItems":4,"items":{"type":"object","oneOf":[{"required":["mediaUrl"]},{"required":["uploadId"]}],"properties":{"mediaUrl":{"type":"string","format":"uri","description":"URL of this image. Mutually exclusive with uploadId on the same entry."},"uploadId":{"type":"string","description":"An upload from POST /v1/media/uploads for this image. Mutually exclusive with mediaUrl on the same entry, and no uploadId may appear twice in one request."}}},"description":"Turns this into a carousel Pin: further images in order after the cover, which stays in mediaUrl/uploadId. One to four entries, so a carousel holds the two to five images Pinterest allows. Images only — video is refused, as are product tags, and every image must share the same width-to-height ratio. Each entry takes a mediaUrl or an uploadId, and the two forms can be mixed across entries."},"title":{"type":"string","maxLength":100,"description":"Pin title. Required when scheduling."},"description":{"type":"string","maxLength":800,"description":"Pin description. Required when scheduling."},"url":{"type":"string","format":"uri","description":"Destination URL when pin is clicked. At most 1000 bytes in UTF-8. Required when scheduling — it is also what Pin Spacing rules are keyed on, so useNextOpenSlot needs it."},"boardId":{"type":"string","description":"Target board ID. Must be the numeric Pinterest board ID from GET /v1/accounts/{accountId}/boards — not a board name or URL. A leading \"b_\" (Tailwind CSV export format) is accepted and stripped. Required when scheduling, unless intervalBoards is provided instead. Mutually exclusive with intervalBoards."},"boardSectionId":{"type":"string","description":"Section of `boardId` to publish into. Requires boardId, and is rejected with a 400 if it is not a section of that board. Omit to publish to the board root. Use GET /v1/accounts/{accountId}/boards/{boardId}/sections to look up section IDs. Not for use with intervalBoards — put each Board's section on its own intervalBoards entry instead."},"intervalBoards":{"type":"array","items":{"$ref":"#/components/schemas/IntervalBoard"},"minItems":2,"maxItems":20,"description":"Publish one Pin across several Boards, spaced by intervalType/intervalDays/intervalMode, in the order given — the first entry is the Board the Pin itself lands on. Mutually exclusive with boardId. Requires sendAt or useNextOpenSlot (an interval spaces Boards relative to the Pin's send time, so it cannot be saved as a draft), and intervalType. No Board may repeat."},"intervalType":{"type":"string","enum":["daily","2_days","3_days","4_days","5_days","6_days","weekly","monthly"],"description":"Spacing preset between each Board in intervalBoards. Required when intervalBoards is provided; only valid with intervalBoards."},"intervalDays":{"type":"integer","minimum":1,"maximum":365,"description":"Overrides intervalType's day count with an exact number of days (e.g. intervalType \"weekly\" + intervalDays 14 for a real two-week gap, since the preset alone rounds to the nearest one). Only valid with intervalBoards."},"intervalMode":{"type":"string","enum":["slots","exact"],"default":"slots","description":"How each Board's send time is placed: \"slots\" (default) snaps to the account's smart-schedule slots, \"exact\" places it exactly intervalType/intervalDays after the previous Board regardless of slots. Only valid with intervalBoards."},"altText":{"type":"string","maxLength":500,"description":"Alt text for accessibility"},"sendAt":{"type":"string","format":"date-time","description":"Explicit publish time, in the future and within 10 years. Cannot be combined with useNextOpenSlot; omit both to save a draft. Requires boardId or intervalBoards, title, description, and url. Sets only the initial time, not an exemption from Pin Spacing: a later respace or schedule shuffle can still move the Pin."},"useNextOpenSlot":{"type":"boolean","default":false,"description":"Schedule into the account's next open smart schedule slot instead of naming a time, honoring the account's Pin Spacing rules for this Pin's url. Cannot be combined with sendAt; omit both to save a draft. Requires boardId or intervalBoards, title, description, and url."},"isSimplifiedPin":{"type":"boolean","default":true,"description":"Whether to create a simplified pin. Defaults to true. Set to false for a standard pin."},"productTagPinIds":{"type":"array","items":{"type":"string"},"maxItems":24,"description":"Pinterest product-catalog pin IDs to tag on the pin (business accounts only; max 24). Providing any forces a simplified pin, since Pinterest rejects tags on standard pins."}}},"CreateCapabilityRequestRequest":{"type":"object","required":["description"],"properties":{"description":{"type":"string","minLength":1,"maxLength":1000,"description":"What you were trying to do and what was missing, in plain sentences. Describe the outcome you wanted, not just an endpoint name — the concrete case is what makes a request actionable.","example":"No way to change a scheduled Pin's board after creating it — I had to delete the Pin and recreate it, which lost its scheduled time."}}},"CapabilityRequestReceipt":{"type":"object","properties":{"received":{"type":"boolean","description":"Always true — failures return an error status instead."}}},"CreateIssueReportRequest":{"type":"object","required":["description"],"properties":{"description":{"type":"string","minLength":1,"maxLength":1000,"description":"What you asked for, what came back, and why it is wrong. The concrete case is what makes a report actionable.","example":"GET /v1/accounts/123/boards returned 4 boards, but the same account reports 11 boards in GET /v1/accounts/123 — I used the larger number to continue."},"tool":{"type":"string","minLength":1,"maxLength":100,"description":"The endpoint or MCP tool you believe returned the wrong result. Recorded as your claim and never checked against the real roster — a name that does not exist is itself useful.","example":"GET /v1/accounts/{accountId}/boards"}}},"IssueReportReceipt":{"type":"object","properties":{"received":{"type":"boolean","description":"Always true — failures return an error status instead."}}},"CreateMediaUploadRequest":{"type":"object","required":["extension"],"properties":{"extension":{"type":"string","description":"The file's extension, with or without a leading dot and in any case (jpg, .PNG, mp4). It determines both the upload's media type and the Content-Type the signed URL is issued for. Accepted: jpg, jpeg, png, svg, gif, webp, heic, mp4, mov, qt.","example":"jpg"}}},"MediaUpload":{"type":"object","properties":{"uploadId":{"type":"string","description":"Opaque id to pass as uploadId on POST /v1/accounts/{accountId}/posts. Single-use."},"uploadUrl":{"type":"string","format":"uri","description":"Write-only URL to PUT the raw file bytes to. Send the bytes as the request body with a Content-Type header exactly matching contentType below — the URL's signature covers that header, so a mismatch is rejected."},"uploadUrlExpiresAt":{"type":"integer","description":"Unix timestamp after which uploadUrl stops accepting bytes. Much sooner than expiresAt — this is the deadline for the PUT itself, not for attaching the Pin."},"expiresAt":{"type":"integer","description":"Unix timestamp after which uploadId no longer resolves and the staged bytes are deleted. This is the deadline for attaching the upload to a Pin."},"mediaType":{"type":"string","enum":["image","video"],"description":"Media type derived from the extension. Used automatically on create."},"contentType":{"type":"string","description":"The exact Content-Type header value the PUT must send.","example":"image/jpeg"}}},"SchedulePostRequest":{"type":"object","description":"Provide exactly one of sendAt or useNextOpenSlot. A 422 means the account's schedule has no open slot; a 503 without a reason means resolving one timed out. Neither writes anything, and any pin_schedule charge is refunded.","oneOf":[{"required":["sendAt"]},{"required":["useNextOpenSlot"],"properties":{"useNextOpenSlot":{"enum":[true]}}}],"properties":{"sendAt":{"type":"string","format":"date-time","description":"Explicit publish time (ISO 8601), in the future and within 10 years. Exactly one of sendAt or useNextOpenSlot is required. Sets only the initial time, not an exemption from Pin Spacing: a later respace or schedule shuffle can still move the Pin."},"useNextOpenSlot":{"type":"boolean","default":false,"description":"Schedule into the account's next open smart schedule slot, honoring the account's Pin Spacing rules for the Pin's destination url. Exactly one of sendAt or useNextOpenSlot is required; must be true when provided."},"boardId":{"type":"string","description":"Target board ID. Must be the numeric Pinterest board ID from GET /v1/accounts/{accountId}/boards — not a board name or URL. A leading \"b_\" (Tailwind CSV export format) is accepted and stripped. Required when scheduling a draft that doesn't already have a board assigned."},"boardSectionId":{"type":"string","description":"Section to publish into, validated against the board this Pin ends up on. Send \"\" to publish to the board root. Supplying boardId WITHOUT this field clears any section the Pin already had, since a section is only valid on its own board. Use GET /v1/accounts/{accountId}/boards/{boardId}/sections to look up section IDs."}}},"BulkUpdatePostsRequest":{"type":"object","required":["posts"],"properties":{"dryRun":{"type":"boolean","description":"true previews the batch: nothing is written, and the response carries each Pin's previous and proposed values and a previewToken."},"previewToken":{"type":"string","description":"Required to save (when dryRun is absent or false): the previewToken a dryRun of these exact posts returned, within 30 minutes."},"posts":{"type":"array","minItems":1,"maxItems":50,"description":"One entry per Pin, each naming its postId and the fields to change on that Pin. A postId may appear only once.","items":{"allOf":[{"$ref":"#/components/schemas/UpdatePostRequest"},{"type":"object","required":["postId"],"minProperties":2,"properties":{"postId":{"type":"string","description":"ID of the draft or scheduled Pin to edit"}}}]}}}},"BulkUpdatePostsResult":{"type":"object","properties":{"results":{"type":"array","description":"One result per request entry, in request order.","items":{"type":"object","required":["postId","status"],"properties":{"postId":{"type":"string"},"status":{"type":"string","enum":["will_update","updated","failed","not_attempted"],"description":"`will_update` appears only on a dryRun. `not_attempted` means the request ran out of time before this Pin was reached; it was not changed."},"previous":{"type":"object","additionalProperties":true,"description":"`will_update` and `updated`. The edited fields' values before this edit, in request form, ready to send back as an undo. Omits fields that were empty, which cannot be emptied again here."},"proposed":{"type":"object","additionalProperties":true,"description":"`will_update` only. The same fields as the save would write them."},"current":{"type":"object","additionalProperties":true,"description":"`updated` only. The same fields as saved. Absent if the Pin could not be read back after saving; the edit still landed."},"error":{"type":"object","description":"`failed` only. Same codes and reasons as the single-Pin endpoint.","properties":{"code":{"type":"string"},"message":{"type":"string"},"reason":{"type":"string"}}}}}},"summary":{"type":"object","description":"`willUpdate` on a dryRun, `updated` on a save.","properties":{"willUpdate":{"type":"integer"},"updated":{"type":"integer"},"failed":{"type":"integer"},"notAttempted":{"type":"integer"}}},"previewToken":{"type":"string","description":"dryRun only. Send it with the same posts to save this batch. Absent when any entry is failed or not_attempted: drop or fix those entries and preview again."},"expiresAt":{"type":"string","description":"dryRun only. When previewToken stops being accepted (ISO 8601)."}}},"UpdatePostRequest":{"type":"object","description":"At least one field must be supplied. Fields you omit are left unchanged. `sendAt`, `status`, `mediaUrl` and `uploadId` are rejected rather than ignored — see the endpoint description.","minProperties":1,"properties":{"title":{"type":"string","minLength":1,"maxLength":100,"description":"Pin title"},"description":{"type":"string","minLength":1,"maxLength":800,"description":"Pin description"},"url":{"type":"string","format":"uri","description":"Destination link for the Pin. At most 1000 bytes in UTF-8."},"boardId":{"type":"string","description":"Target board ID. Must be the numeric Pinterest board ID from GET /v1/accounts/{accountId}/boards — not a board name or URL. A leading \"b_\" (Tailwind CSV export format) is accepted and stripped."},"boardSectionId":{"type":"string","description":"Section to publish into, validated against the board the Pin ends up on (boardId if you send one, otherwise its current board). Send \"\" to move the Pin to the board root. Supplying boardId WITHOUT this field clears any section the Pin already had, since a section is only valid on its own board. Use GET /v1/accounts/{accountId}/boards/{boardId}/sections to look up section IDs."},"altText":{"type":"string","maxLength":500,"description":"Accessibility alt text"},"productTagPinIds":{"type":"array","items":{"type":"string"},"maxItems":24,"description":"Pinterest product-catalog pin IDs to tag on the Pin, replacing any it already had (business accounts only; max 24). Send an empty array to remove every tag — omitting the field leaves the existing tags untouched. Tagging forces a simplified Pin, since Pinterest rejects tags on standard Pins. Rejected on carousel Pins, which Pinterest refuses to publish with tags; an empty array is still accepted on one, so tags can be cleared off it."},"isSimplifiedPin":{"type":"boolean","description":"true makes the Pin a simplified Pin, false a standard Pin. Applies to draft and scheduled Pins alike. false is rejected while the Pin has product tags, since Pinterest rejects tags on standard Pins; send productTagPinIds: [] in the same request to remove them."}}},"CreateGenerationRequest":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","maxLength":2048,"description":"Source page to generate Pins from (http/https). Query parameters and fragments are ignored for identity — variants of one page map to one SmartPin."},"title":{"type":"string","maxLength":256,"description":"Display name for the SmartPin in the Tailwind dashboard. Defaults to the URL (truncated to 256 characters)."},"designTier":{"type":"string","enum":["basic","premium","no-template"],"default":"basic","description":"Design tier for the generated Pins. Determines the credit cost (approximately: 3 basic / 7 premium / 1 no-template, resolved server-side). Ignored when the URL's existing SmartPin on this account is reused."},"premiumStyle":{"type":"string","enum":["image-highlight","text-highlight","minimalist","artistic","professional","natural"],"description":"Visual style for premium generations. Required when designTier is \"premium\"; must be absent otherwise."},"keywords":{"type":"array","items":{"type":"string","maxLength":100},"maxItems":10,"description":"Keywords to guide the generated Pin copy."}}},"GenerationAccepted":{"type":"object","properties":{"id":{"type":"string","description":"Generation ID. Poll GET /v1/generations/{generationId} with it."},"status":{"type":"string","enum":["processing"]},"smartPinId":{"type":"integer","description":"The SmartPin record this generation runs against."},"designTier":{"type":"string","enum":["basic","premium","template-match","no-template"],"description":"Effective design tier of the referenced generation (\"template-match\" can appear only when an existing record with that tier was reused)."},"creditCost":{"type":"integer","description":"Nominal per-generation credit cost of the referenced generation's tier. When deduplicated is true, the response refers to an already-charged in-flight generation — no additional credit is charged by this call."},"reusedExistingSmartPin":{"type":"boolean","description":"True when an existing SmartPin for this URL and account was reused (caller tier parameters ignored)."},"deduplicated":{"type":"boolean","description":"True when a generation for this URL was already in flight and its id is returned instead of starting (and charging) a new one."}}},"PinDesign":{"type":"object","description":"A generated Pin's design, plus the operations this particular draft accepts.","properties":{"postId":{"type":"string"},"tier":{"type":"string","nullable":true,"description":"How the Pin was generated. template and original are re-renderable; premium is AI-designed. Legacy values are normalized for display, which is why eligibility is reported separately in supportedOperations rather than inferred from this."},"supportedOperations":{"type":"array","items":{"type":"string","enum":["update","regenerate","select_variation","revert"]},"description":"The operations this draft actually accepts. Each entry mirrors the predicate its own endpoint enforces; anything absent here is rejected."},"activeMediaUrl":{"type":"string","description":"The image the Pin will publish with"},"originalPhotoUrl":{"type":"string","nullable":true},"title":{"type":"string","nullable":true,"description":"Text overlaid on the design, not the Pin's title"},"colors":{"type":"array","nullable":true,"items":{"type":"string"}},"variations":{"type":"array","items":{"type":"object","properties":{"templateImageUrl":{"type":"string"},"templateGroupId":{"type":"string"},"generationMethod":{"type":"string","nullable":true},"isActive":{"type":"boolean"}}}},"workspaceStatus":{"type":"string","enum":["in_sync","pending_apply"],"description":"pending_apply means a previous edit rendered a design the Pin never received. Apply pendingMediaUrl through the variation endpoint; no re-render is needed."},"pendingMediaUrl":{"type":"string","description":"Present only when workspaceStatus is pending_apply"},"sourceImageCandidates":{"type":"object","description":"Returned by GET only. Source photos stored on the SmartPin that generated this Pin, as the SmartPin stands now rather than when this draft was generated. These are not designs: pass a url as mediaUrl to the design update endpoint, or to regenerate on a premium Pin.","properties":{"status":{"type":"string","enum":["available","unavailable"]},"reason":{"type":"string","enum":["smart_pin_not_found","lookup_failed"],"description":"Present when status is unavailable. smart_pin_not_found: the SmartPin was deleted or the Pin predates the link. lookup_failed: temporary; read again."},"userPicked":{"type":"array","items":{"type":"string"},"description":"Images the user chose for the SmartPin. Empty when Tailwind picks images itself."},"ranked":{"type":"array","description":"Images Tailwind scraped from the source page and scored at its latest ranking. Hosted by Tailwind.","items":{"type":"object","properties":{"url":{"type":"string"},"quality":{"type":"string","enum":["great","good","poor"]},"aspectRatio":{"type":"number","description":"Width divided by height"}},"required":["url","quality","aspectRatio"]}}},"required":["status","userPicked","ranked"]}},"required":["postId","supportedOperations","activeMediaUrl","variations","workspaceStatus"]},"UpdatePinDesignRequest":{"type":"object","description":"At least one field is required. Omitted fields keep their stored values.","properties":{"title":{"type":"string","maxLength":256,"description":"Text overlaid on the design"},"colors":{"type":"array","maxItems":12,"items":{"type":"string","maxLength":64}},"fonts":{"type":"object","description":"Headline, and optionally subhead and website, font definitions"},"mediaUrl":{"type":"string","maxLength":2048,"description":"Use instead of uploadId, not with it"},"uploadId":{"type":"string","maxLength":191,"description":"An image staged through the media upload endpoint. Video is rejected."}}},"RegeneratePinDesignRequest":{"type":"object","properties":{"title":{"type":"string","maxLength":256},"premiumStyle":{"type":"string","enum":["image-highlight","text-highlight","minimalist","artistic","professional","natural"]},"userFeedback":{"type":"string","maxLength":2000,"description":"What to change, in the user's own words"},"selectedVariationUrl":{"type":"string","maxLength":2048,"description":"The variation to revise. Must be one of this draft's AI-generated variations; a URL it does not carry is rejected before anything is charged. Omit to generate a fresh, unrelated design."},"mediaUrl":{"type":"string","maxLength":2048,"description":"Use instead of uploadId, not with it"},"uploadId":{"type":"string","maxLength":191}}},"DesignRegeneration":{"type":"object","properties":{"id":{"type":"string"},"postId":{"type":"string"},"status":{"type":"string","enum":["processing","success","failed"]},"variationUrl":{"type":"string","description":"The new design, present on success. It is not applied to the Pin; apply it through the variation endpoint."},"appliesAutomatically":{"type":"boolean","description":"Always false"},"billing":{"type":"object","properties":{"charge":{"type":"string","enum":["charged","trial","not_charged"]},"refund":{"type":"string","enum":["none","pending","refunded"]}},"required":["charge","refund"]},"completedAt":{"type":"number","description":"Unix seconds; present once terminal"}},"required":["id","postId","status","billing"]},"Generation":{"type":"object","properties":{"id":{"type":"string","description":"Generation ID"},"status":{"type":"string","enum":["processing","success","failed"]},"siteUrl":{"type":"string","description":"Source URL the generation ran against"},"createdAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time","nullable":true,"description":"Present once status is success or failed."},"failureCode":{"type":"string","nullable":true,"description":"Stable failure code, present only when status is \"failed\". Known values include scrape-failed, no-images-on-page, not-enough-credits, smartpin-content-flagged, smartpin-lapsed-user, smartpin-no-account, smartpin-account-not-connected, failed-to-upload-source-images, generation-request-timed-out, generation-cancelled; \"unknown\" when no code was recorded."},"failureDetail":{"type":"string","enum":["blocked","no-content","fetch-failed"],"description":"Why a scrape-failed generation failed; present only when failureCode is scrape-failed and the cause is known. blocked: the site's firewall refuses Tailwind's scraper; retrying won't help until the site allows it. no-content: the page had no readable text, often because it was just published; retry once after a minute or two, and stop if it fails again. fetch-failed: the page couldn't be loaded; retry later."}}},"CreateCopyGenerationRequest":{"type":"object","required":["contentType","topic"],"properties":{"contentType":{"type":"string","enum":["lifestyle_content","featured_product","seasonal_promotion","behind_the_scenes","education_content"],"description":"The angle the copy should take."},"topic":{"type":"string","maxLength":500,"description":"What the Pin is about — a product, post, or idea in the user's words."},"keywords":{"type":"array","items":{"type":"string","maxLength":100},"maxItems":10,"description":"Keywords to work into the copy."},"cta":{"type":"string","maxLength":200,"description":"Call to action to end each Pin with."},"priorOutputs":{"type":"array","maxItems":10,"items":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"}}},"description":"Copy you already have, so this generation varies from it rather than repeating it."}}},"CreatePostBatchRequest":{"type":"object","required":["posts"],"properties":{"posts":{"type":"array","minItems":1,"maxItems":175,"description":"One entry per Pin, each with the fields of a single create. intervalBoards and carouselItems are refused per entry. A draft entry may omit mediaUrl and uploadId if it has a title, description or url.","items":{"$ref":"#/components/schemas/PostBatchEntry"}}}},"PostBatchEntry":{"type":"object","description":"One Pin in a batch. Each field means what it does on CreatePostRequest. Provide at most one of mediaUrl and uploadId; both may be left out only for a draft with a title, description or url, which gets the placeholder image.","properties":{"mediaUrl":{"type":"string","format":"uri"},"uploadId":{"type":"string"},"mediaType":{"type":"string","enum":["image","video"],"default":"image"},"title":{"type":"string","maxLength":100},"description":{"type":"string","maxLength":800},"url":{"type":"string","format":"uri"},"boardId":{"type":"string"},"boardSectionId":{"type":"string"},"altText":{"type":"string","maxLength":500},"isSimplifiedPin":{"type":"boolean","default":true},"productTagPinIds":{"type":"array","maxItems":24,"items":{"type":"string"}},"sendAt":{"type":"string","format":"date-time","description":"Schedule at this time. Omit both sendAt and useNextOpenSlot for a draft."},"useNextOpenSlot":{"type":"boolean","default":false,"description":"Schedule into the account's next open smart schedule slot. Entries fill slots in request order."}}},"PostBatchAccepted":{"type":"object","properties":{"id":{"type":"string","description":"Batch ID. Poll GET /v1/post-batches/{batchId} with it."},"accountId":{"type":"string"},"status":{"type":"string","enum":["queued","processing","completed"],"description":"completed straight away when every entry was refused at submit."},"createdAt":{"type":"string","format":"date-time"},"summary":{"type":"object","properties":{"total":{"type":"integer"},"accepted":{"type":"integer","description":"Entries that will be attempted."},"rejected":{"type":"integer","description":"Entries refused at submit; the status poll reports why."}}}}},"PostBatch":{"type":"object","properties":{"id":{"type":"string"},"accountId":{"type":"string"},"status":{"type":"string","enum":["queued","processing","completed"]},"createdAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"},"summary":{"type":"object","properties":{"total":{"type":"integer"},"created":{"type":"integer"},"failed":{"type":"integer"},"pending":{"type":"integer"}}},"results":{"type":"array","description":"One entry per submitted post, in request order.","items":{"$ref":"#/components/schemas/PostBatchResult"}}}},"PostBatchResult":{"type":"object","properties":{"index":{"type":"integer","description":"Position of the entry in the submitted posts array."},"status":{"type":"string","enum":["created","failed","pending"]},"postId":{"type":"string","description":"Present when created."},"postStatus":{"type":"string","description":"The new post's status when created: draft or queued."},"error":{"type":"object","description":"Present when failed: the error a single create would have returned.","properties":{"code":{"type":"string"},"message":{"type":"string"},"reason":{"type":"string"}}}}},"CopyGenerationAccepted":{"type":"object","properties":{"id":{"type":"string","description":"Copy generation ID. Poll GET /v1/copy-generations/{jobId} with it."},"status":{"type":"string","enum":["processing"]},"creditCost":{"type":"integer","description":"Credits this generation costs, charged when the request is accepted. 0 when the org's free trial allowance covered it instead of a credit."}}},"CopyGeneration":{"type":"object","properties":{"id":{"type":"string","description":"Copy generation ID"},"status":{"type":"string","enum":["processing","success","failed"]},"createdAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time","nullable":true,"description":"Present once status is success or failed."},"result":{"type":"object","nullable":true,"description":"The generated copy. Present only when status is success.","properties":{"pins":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"}}},"description":"Title and description pairs to choose from."}}}}}},"responses":{"PublishingStatusUpdateFailed":{"description":"The publishing status could not be updated. Retry: pausing and resuming are safe to repeat.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"AnalyticsForbidden":{"description":"Pinterest refused to report analytics for this subject. PERMANENT, and distinct from the collaborator-board 403 on the board endpoints: for a Pin, Pinterest has forbidden analytics on it and it will never start reporting; for an account, analytics require a Pinterest business account. Retrying will return the same answer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FORBIDDEN","message":"Pinterest does not permit analytics for this Pin. This is permanent for this Pin — it will not start reporting later, so do not retry.","reason":"pinterest_analytics_forbidden"},"meta":{"requestId":"abc123"}}}}},"FeedConflict":{"description":"This organization has already connected this feed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"CONFLICT","message":"You've already connected this feed."},"meta":{"requestId":"abc123"}}}}},"FeedTooLarge":{"description":"The feed was fetched and parsed, but the result is too large to process. Retrying returns the same feed, so this needs a smaller one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"PAYLOAD_TOO_LARGE","message":"That feed is too large for Tailwind to process. Try a feed with fewer or smaller entries."},"meta":{"requestId":"abc123"}}}}},"FeedUnprocessable":{"description":"The document was fetched but is not a usable RSS or Atom feed. Retrying will not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNPROCESSABLE_ENTITY","message":"The RSS feed could not be parsed. It may be malformed or in an unsupported format."},"meta":{"requestId":"abc123"}}}}},"FeedUnreachable":{"description":"The site could not be read. Either it did not respond, or its firewall refused us — the message says which, and a bot-protection refusal names the user agent to allowlist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"FAILED_DEPENDENCY","message":"This site's firewall or bot protection is blocking Tailwind from reading the feed. Ask your security provider to allow the TailwindApp-SiteIndexer user agent, then try again."},"meta":{"requestId":"abc123"}}}}},"FeedTimeout":{"description":"The site did not answer within the fetch budget. Safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"GATEWAY_TIMEOUT","message":"The site took too long to respond. Please try again."},"meta":{"requestId":"abc123"}}}}},"Unauthorized":{"description":"Authentication required or invalid bearer credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNAUTHORIZED","message":"Invalid API key"},"meta":{"requestId":"abc123"}}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"NOT_FOUND","message":"Account not found"},"meta":{"requestId":"abc123"}}}}},"ScheduleLocked":{"description":"Another write to this account's smart schedule holds the lock — in practice a rebuild, which replaces every slot and so cannot run alongside other changes. Nothing was written; retry in a moment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"CONFLICT","message":"This account's schedule is being updated elsewhere. Retry in a moment."},"meta":{"requestId":"abc123"}}}}},"BadRequest":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"BAD_REQUEST","message":"Invalid request body"},"meta":{"requestId":"abc123"}}}}},"PinterestForbidden":{"description":"Pinterest refused the operation. Distinct from a 422 reconnect: the account's authorization is fine. The usual cause is a board the account collaborates on but does not own — those cannot be renamed, deleted or given sections.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"PinterestRequestFailed":{"description":"Pinterest failed the request with an error of its own. A create may still have taken effect, so list Boards or sections before repeating one; other requests can be retried. If the same request keeps failing, Pinterest is refusing it rather than having an outage.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"BAD_GATEWAY","message":"Pinterest couldn't complete this request. Please try again.","reason":"pinterest_request_failed"},"meta":{"requestId":"abc123"}}}}},"PinterestUpstreamError":{"description":"Pinterest failed the read with an error on its side. Whether it is permanent is unknown, so this stays a 500, but retrying right away will not help; try again later.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"INTERNAL_ERROR","message":"Pinterest failed to return analytics for this Pin. The error is on Pinterest's side, so retrying right away will not help. Try again later.","reason":"pinterest_upstream_error"},"meta":{"requestId":"abc123"}}}}},"RecommendedPinsSearchFailed":{"description":"Pinterest's public Pin search could not be reached. Distinct from a Pinterest v5 fault on the board read: nothing about the board or the account is wrong, and the request can be retried.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"BAD_GATEWAY","message":"Could not search Pinterest for Pins right now. Nothing was changed, so this can be retried.","reason":"recommended_pins_search_failed"},"meta":{"requestId":"abc123"}}}}},"RecommendedPinsRateLimited":{"description":"Too many distinct keyword searches from this org in a short window. This is Tailwind's own limit protecting the upstream search, not Pinterest refusing — repeating the same keyword is served from cache and does not count against it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"TOO_MANY_REQUESTS","message":"Too many Pin searches in a short window. Wait a minute before requesting recommendations for another keyword.","reason":"recommended_pins_rate_limited"},"meta":{"requestId":"abc123"}}}}},"RecommendedPinsAwaitingPinterestAccess":{"description":"Recommendations are paused while Tailwind waits for Pinterest to approve access to its Pin search API. Pinterest's public search stopped returning Pins that can be saved, so no search ran and nothing was changed. Retrying will not help until access is granted; `save-pins-to-board` still works for Pin IDs you already have.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"SERVICE_UNAVAILABLE","message":"Pin recommendations are paused while Tailwind waits for Pinterest to approve access to its Pin search API. Pinterest's public search stopped returning Pins that can be saved, so nothing was searched or changed. This is not a problem with the board or keyword, and retrying will not help until access is granted. Pins whose ids are already known can still be saved to a board.","reason":"recommended_pins_awaiting_pinterest_access"},"meta":{"requestId":"abc123"}}}}},"RecommendedPinsTimeout":{"description":"The request ran out of budget. Nothing was changed. `reason: \"recommended_pins_board_too_large\"` means the board itself has more Pins than can be read completely within one request — the board must be read in full, because a partial read would recommend Pins the board already holds. `reason: \"recommended_pins_timeout\"` means the request as a whole (board read, search, and scoring together) ran past budget without any single step being identifiably at fault.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"GATEWAY_TIMEOUT","message":"This board holds too many Pins to read within one request. Nothing was changed.","reason":"recommended_pins_board_too_large"},"meta":{"requestId":"abc123"}}}}},"PinterestTimeout":{"description":"Pinterest did not respond before the request deadline. The operation may or may not have taken effect — re-read the account's boards before retrying, since a blind retry after a create that did land produces a duplicate.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"GATEWAY_TIMEOUT","message":"Pinterest did not respond in time, so this may or may not have taken effect. Check the account's boards before trying again rather than repeating the request.","reason":"pinterest_timeout"},"meta":{"requestId":"abc123"}}}}},"PinterestReconnectRequired":{"description":"The account's Pinterest connection is no longer usable. Your API key is fine — this is not an authentication failure on Tailwind's side, which is why it is not a 401. The account holder needs to reconnect Pinterest in Tailwind before this operation can succeed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"UNPROCESSABLE_ENTITY","message":"Reconnect your Pinterest account to manage Boards.","reason":"pinterest_reconnect_required"},"meta":{"requestId":"abc123"}}}}},"PinterestRateLimited":{"description":"Pinterest rate-limited the request. Pinterest refused it before acting, so nothing was changed. Distinct from a 429, which means the organization's shared daily request limit — retry this request in a few minutes rather than backing off across the API.","headers":{"Retry-After":{"description":"Seconds until Pinterest's limit for this account resets. Sent by the analytics and Pin inventory operations when Pinterest reports it; absent otherwise.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"SERVICE_UNAVAILABLE","message":"Pinterest is limiting requests for this account right now. Nothing was changed. Retry in a few minutes.","reason":"pinterest_rate_limited"},"meta":{"requestId":"abc123"}}}}},"PaymentRequired":{"description":"Billing denied — scheduling requires an available credit or trial allowance on the account, or would exceed the plan's post limit for the billing period","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"PAYMENT_REQUIRED","message":"Not enough credits to schedule this post.","reason":"insufficient_credits"},"meta":{"requestId":"abc123"}}}}},"RateLimited":{"description":"Rate limit exceeded","headers":{"Retry-After":{"description":"Seconds until the daily limit resets at midnight UTC.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"RATE_LIMIT_EXCEEDED","message":"Daily rate limit of 10000 requests for this organization exceeded. Resets at midnight UTC."},"meta":{"requestId":"abc123"}}}}}}}}