Bulk update posts
const url = 'https://api-v1.tailwind.ai/v1/accounts/example/posts';const options = { method: 'PATCH', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"dryRun":true,"previewToken":"example","posts":[{"title":"example","description":"example","url":"https://example.com","boardId":"example","boardSectionId":"example","altText":"example","productTagPinIds":["example"],"isSimplifiedPin":true,"postId":"example"}]}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request PATCH \ --url https://api-v1.tailwind.ai/v1/accounts/example/posts \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "dryRun": true, "previewToken": "example", "posts": [ { "title": "example", "description": "example", "url": "https://example.com", "boardId": "example", "boardSectionId": "example", "altText": "example", "productTagPinIds": [ "example" ], "isSimplifiedPin": true, "postId": "example" } ] }'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.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Numeric Tailwind account ID returned by GET /v1/accounts
Request Bodyrequired
Section titled “Request Bodyrequired”object
True previews the batch: nothing is written, and the response carries each Pin’s previous and proposed values and a previewToken.
Required to save (when dryRun is absent or false): the previewToken a dryRun of these exact posts returned, within 30 minutes.
One entry per Pin, each naming its postId and the fields to change on that Pin. A postId may appear only once.
object
Pin title
Pin description
Destination link for the Pin. At most 1000 bytes in UTF-8.
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.
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.
Accessibility alt text
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.
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.
ID of the draft or scheduled Pin to edit
Examplegenerated
{ "dryRun": true, "previewToken": "example", "posts": [ { "title": "example", "description": "example", "url": "https://example.com", "boardId": "example", "boardSectionId": "example", "altText": "example", "productTagPinIds": [ "example" ], "isSimplifiedPin": true, "postId": "example" } ]}Responses
Section titled “Responses”Per-Pin results, in request order
object
object
One result per request entry, in request order.
object
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.
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.
object
will_update only. The same fields as the save would write them.
object
updated only. The same fields as saved. Absent if the Pin could not be read back after saving; the edit still landed.
object
failed only. Same codes and reasons as the single-Pin endpoint.
object
willUpdate on a dryRun, updated on a save.
object
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.
DryRun only. When previewToken stops being accepted (ISO 8601).
object
Unique request ID for debugging
Example
{ "data": { "results": [ { "status": "will_update" } ] }}Invalid request
object
object
Error code
Error message
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.
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.
object
Unique request ID for debugging
Example
{ "error": { "code": "BAD_REQUEST", "message": "Invalid request body" }, "meta": { "requestId": "abc123" }}Authentication required or invalid bearer credentials
object
object
Error code
Error message
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.
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.
object
Unique request ID for debugging
Example
{ "error": { "code": "UNAUTHORIZED", "message": "Invalid API key" }, "meta": { "requestId": "abc123" }}Resource not found
object
object
Error code
Error message
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.
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.
object
Unique request ID for debugging
Example
{ "error": { "code": "NOT_FOUND", "message": "Account not found" }, "meta": { "requestId": "abc123" }}Rate limit exceeded
object
object
Error code
Error message
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.
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.
object
Unique request ID for debugging
Example
{ "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "Daily rate limit of 10000 requests for this organization exceeded. Resets at midnight UTC." }, "meta": { "requestId": "abc123" }}Headers
Section titled “Headers”Seconds until the daily limit resets at midnight UTC.
