Most Pinterest pins that fail were going to fail before the request left your server. The board was archived last week. The token lost pins:write when the client reconnected. The monthly quota ran out at two in the morning. On 12 September we went through our own publish failures and found that 57% of them were board_access_denied, and every one of them surfaced after the pin had been accepted, queued and handed to a worker.
That is the wrong place to learn it, so PinBridge now has a dry-run endpoint. You send the exact request you are about to send, and you get back every check it would go through, the payload it would publish, and how much headroom you have left. Nothing is written.
What a dry run checks
POST /v1/pins/validate takes the same body as POST /v1/pins. It runs the full acceptance path and reports each step as a named check.
| Check | What it proves | Failure codes |
|---|---|---|
account |
The Pinterest account belongs to this workspace and your API key is allowed to use it | account_not_permitted, not found |
media |
The image_url or asset_id resolves to something PinBridge can hand to Pinterest |
asset not found, no media given |
cover_image |
A cover image is only attached to a video pin | cover_image_not_supported |
board_access |
The connected account can publish to this board right now | board_not_found, board_deleted, board_access_denied, token_expired, token_revoked, scope_missing |
idempotency |
The key is unused in this workspace | idempotency_key_exists (warning) |
billing |
Billing is active or trialing | billing_inactive |
quota |
Monthly pins remain, or credits do | quota_exceeded |
rate_headroom |
A Pinterest rate token is available right now | rate_paced (warning) |
Every check has one of four statuses: passed, failed, warning or skipped. The top-level valid is true when nothing failed. Warnings never block. They tell you what the real request would do differently: return the existing pin instead of creating one, or wait for a rate token instead of publishing at once.
Here is a request for a pin whose board was deleted through PinBridge a few days ago:
curl -X POST https://api.pinbridge.io/v1/pins/validate \
-H "x-api-key: $PINBRIDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"account_id": "3c1e7d2a-…",
"board_id": "1099344521867",
"title": "Autumn wreath, three ways",
"image_url": "https://cdn.example.com/wreath.png",
"link_url": "https://example.com/blog/autumn-wreath",
"idempotency_key": "blog-2026-09-24-autumn-wreath"
}'
And the answer, trimmed to the parts that matter:
{
"valid": false,
"dry_run": true,
"checks": [
{"name": "account", "status": "passed",
"message": "Pinterest account is connected to this workspace."},
{"name": "media", "status": "passed",
"message": "Resolved image media source."},
{"name": "cover_image", "status": "passed",
"message": "No cover image requested."},
{"name": "board_access", "status": "failed", "code": "board_deleted",
"message": "This board was deleted through PinBridge.",
"remediation": "This board was removed through PinBridge. Choose another board from GET /v1/pinterest/boards, or recreate it."},
{"name": "idempotency", "status": "passed",
"message": "Idempotency key is unused in this workspace."},
{"name": "billing", "status": "passed",
"message": "Billing status is active."},
{"name": "quota", "status": "passed",
"message": "212 of 300 monthly pins remaining."},
{"name": "rate_headroom", "status": "passed",
"message": "Pinterest rate headroom available; the publish would run now."}
],
"resolved": null,
"existing_pin_id": null,
"headroom": {
"quota": {"limit": 300, "used": 88, "remaining": 212},
"rate": {"tokens_available": 4.0, "capacity": 5, "refill_rate": 0.2}
}
}
The board check failed with a stable code and a sentence that says what to do about it. The other seven checks still ran, so you also know the quota and the token are fine. Fix the board and the next dry run passes.
Why the board check is the one that matters
Board access is where most publishes die, so the check is built to be cheap and to stay out of the way when Pinterest is having a bad minute. It runs cheapest first:
- Sandbox simulation. In a sandbox workspace only the simulated board is publishable, so the answer is local.
- Account health. A revoked token, a token that expired with no refresh token behind it, or a token missing
boards:readorpins:write. All local, all instant. - Boards you already removed. Anything hidden or deleted through PinBridge is remembered, so it fails without a network call.
- Pinterest itself.
GET /boards/{id}for existence and ownership. A positive answer is cached for 10 minutes per account and board, so a run of 200 pins to the same board costs one upstream call, not 200.
If Pinterest returns a 5xx, a 429, or the connection drops, the check reports skipped instead of failed. A transient upstream problem never fails your dry run, and it never blocks a real publish either. The worker’s own retry logic takes over from there.
The real POST /v1/pins runs the account and board checks too, and answers 422 with the same code when they fail. What the dry run adds is everything else at once, with no side effects, plus the resolved payload and the headroom.
What you get when it passes
When valid is true the response carries resolved: the exact payload PinBridge would publish. Account, board (the simulated board id if you are in sandbox), media type and URL, asset id, cover image, title, description, link, alt text, dominant color and idempotency key, after every reference has been resolved. Diff it against what you meant to send, log it, or put it in front of a human for approval. It is the last thing you will see before the pin exists.
headroom is the same data as the X-PinBridge-Quota-* and X-Pinterest-RateLimit-* headers, which the validate response also sets. If you already read those headers on publish responses, nothing changes.
Three places to put it
The first is before a bulk import. Do not push 400 rows and then read 400 error envelopes. Validate one row per account and board pair first. The board result is cached, so the rest of the batch pays nothing extra for it. If the dry run fails on quota, you know before you start that the batch will stop at row 212.
import requests
API = "https://api.pinbridge.io/v1"
def preflight(pin: dict) -> dict:
r = requests.post(f"{API}/pins/validate",
headers={"x-api-key": API_KEY}, json=pin, timeout=15)
r.raise_for_status()
report = r.json()
failed = [c for c in report["checks"] if c["status"] == "failed"]
if failed:
raise PreflightError("; ".join(
f'{c["name"]}: {c["code"]} ({c["remediation"]})' for c in failed
))
return report["resolved"]
# one dry run per (account, board) before the real loop
seen = set()
for row in rows:
key = (row["account_id"], row["board_id"])
if key not in seen:
preflight(row)
seen.add(key)
The second is an approval step or an agent loop. The PinBridge MCP server exposes the same thing as create_pin(..., dry_run=true). An agent runs the dry run, shows the checks and the resolved payload to a human, then calls create_pin again with dry_run=false and the same idempotency_key. That is the safe pattern from the MCP post, and the server’s built-in publish_pin prompt already walks through it.
The third is a health probe. The endpoint only needs a key with read scope, so a read-only key can run it on a timer. Once an hour, dry-run a fixed pin against a known board for each connected account. When account or board_access flips to failed, you find out hours before a scheduled pin does. That is the quiet-token-death problem from an earlier post, caught by a cron job instead of a customer.
Schedules have one too
POST /v1/schedules/validate runs the same checks and adds run_at, which must be in the future (run_at_in_past otherwise). A scheduled pin is the worst kind to lose, because it fails at 09:00 tomorrow when nobody is watching the queue. Validate it when it is created, when someone is.
What a dry run does not do
A few limits are worth knowing so you do not build on the wrong assumption.
It does not download your image. The media check confirms the asset exists or that a URL was supplied. Pinterest fetches the URL at publish time, so if the image lives on a host you do not fully trust, add a HEAD request in your own pipeline.
It does not reserve anything. Quota and rate headroom are a snapshot. Two dry runs and a real publish in the same second can still see rate_paced.
Board positives are cached for 10 minutes. A board archived on Pinterest three minutes after a passing dry run can still fail the real publish, so the recovery path (retry_pin and the activity log) still matters.
It never touches your monthly pin quota. It may spend one Pinterest read call on an uncached board, and that is all.
Dry run is not sandbox
They solve different problems. A sandbox workspace simulates publishing so you can build the pipeline without a real account on the line. A dry run is for the production workspace: real accounts, real boards, real quota, in the second before each publish. Use both. Start in sandbox. Move to production when the workflow is proven. Then dry-run every request that matters.
The field reference for both endpoints is in the pins and schedules API docs. If you would rather find out about a dead board before the pin is queued, start free.
