Skip to content

Bulk update posts

PATCH
/v1/accounts/{accountId}/posts
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.

accountId
required
string

Numeric Tailwind account ID returned by GET /v1/accounts

Media typeapplication/json
object
dryRun

True previews the batch: nothing is written, and the response carries each Pin’s previous and proposed values and a previewToken.

boolean
previewToken

Required to save (when dryRun is absent or false): the previewToken a dryRun of these exact posts returned, within 30 minutes.

string
posts
required

One entry per Pin, each naming its postId and the fields to change on that Pin. A postId may appear only once.

Array
>= 1 items <= 50 items
object
title

Pin title

string
>= 1 characters <= 100 characters
description

Pin description

string
>= 1 characters <= 800 characters
url

Destination link for the Pin. At most 1000 bytes in UTF-8.

string format: uri
boardId

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.

string
boardSectionId

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.

string
altText

Accessibility alt text

string
<= 500 characters
productTagPinIds

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.

Array<string>
<= 24 items
isSimplifiedPin

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.

boolean
postId
required

ID of the draft or scheduled Pin to edit

string
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"
}
]
}

Per-Pin results, in request order

Media typeapplication/json
object
data
object
results

One result per request entry, in request order.

Array<object>
object
postId
required
string
status
required

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.

string
Allowed values: will_update updated failed not_attempted
previous

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
key
additional properties
any
proposed

will_update only. The same fields as the save would write them.

object
key
additional properties
any
current

updated only. The same fields as saved. Absent if the Pin could not be read back after saving; the edit still landed.

object
key
additional properties
any
error

failed only. Same codes and reasons as the single-Pin endpoint.

object
code
string
message
string
reason
string
summary

willUpdate on a dryRun, updated on a save.

object
willUpdate
integer
updated
integer
failed
integer
notAttempted
integer
previewToken

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.

string
expiresAt

DryRun only. When previewToken stops being accepted (ISO 8601).

string
meta
object
requestId

Unique request ID for debugging

string
Example
{
"data": {
"results": [
{
"status": "will_update"
}
]
}
}

Invalid request

Media typeapplication/json
object
error
object
code

Error code

string
message

Error message

string
reason

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.

string
resolve_url

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.

string
meta
object
requestId

Unique request ID for debugging

string
Example
{
"error": {
"code": "BAD_REQUEST",
"message": "Invalid request body"
},
"meta": {
"requestId": "abc123"
}
}

Authentication required or invalid bearer credentials

Media typeapplication/json
object
error
object
code

Error code

string
message

Error message

string
reason

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.

string
resolve_url

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.

string
meta
object
requestId

Unique request ID for debugging

string
Example
{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key"
},
"meta": {
"requestId": "abc123"
}
}

Resource not found

Media typeapplication/json
object
error
object
code

Error code

string
message

Error message

string
reason

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.

string
resolve_url

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.

string
meta
object
requestId

Unique request ID for debugging

string
Example
{
"error": {
"code": "NOT_FOUND",
"message": "Account not found"
},
"meta": {
"requestId": "abc123"
}
}

Rate limit exceeded

Media typeapplication/json
object
error
object
code

Error code

string
message

Error message

string
reason

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.

string
resolve_url

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.

string
meta
object
requestId

Unique request ID for debugging

string
Example
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Daily rate limit of 10000 requests for this organization exceeded. Resets at midnight UTC."
},
"meta": {
"requestId": "abc123"
}
}
Retry-After
integer

Seconds until the daily limit resets at midnight UTC.