Core concepts
This page explains how PinBridge is structured and the words we use for each part of it. It is the terminology anchor for the rest of these docs, so if a term shows up elsewhere and you are not sure what it means, look it up here.
The object model
PinBridge nests a small set of objects. From the outside in:
organization → project → Pinterest account → board → asset → pin → schedule → import job → job
- An organization is your account and billing boundary. It owns projects and holds your plan, disk quota, and team members.
- A project is an isolated environment inside an organization. It holds its own Pinterest accounts, pins, schedules, API keys, and webhooks. A project is either
productionorsandbox(see Sandbox). You may see the field nameworkspacein older payloads. That is a legacy compatibility alias forproject. Use “project” everywhere. - A Pinterest account is a Pinterest profile you have connected through OAuth. It is the identity PinBridge publishes with. See Connect Pinterest.
- A board is a Pinterest board that belongs to a connected account. You publish pins onto a board.
- An asset is an image or video you upload to PinBridge (paid plans). A pin can reference an uploaded asset by
asset_idinstead of a publicimage_url. - A pin is a single publish request. It moves through a state machine (below) and, on success, gets a real
pinterest_pin_id. - A schedule is a pin queued to publish at a future
run_attime. - An import job is a bulk request (JSON or CSV) that creates many pins or schedules at once.
- A job is a generic async task record you can poll for status.
Two more objects sit alongside these:
- A webhook is an HTTPS endpoint PinBridge calls when a pin publishes or fails.
- An API key authenticates your requests. Send it in the
X-API-Keyheader. (User-session endpoints useAuthorization: Bearer <jwt>instead. API keys always go inX-API-Key.)
Terminology table
| Term | What it is | Key relationship |
|---|---|---|
| Organization | Account and billing boundary | Owns projects, plan, disk quota, team |
| Project | Isolated environment (production or sandbox) |
Belongs to an organization; owns everything below (legacy alias: workspace) |
| Pinterest account | A connected Pinterest profile | Belongs to a project; publishes pins |
| Board | A Pinterest board | Belongs to a Pinterest account |
| Asset | An uploaded image or video | Belongs to a project; referenced by a pin via asset_id |
| Pin | One publish request | Targets an account + board; carries an idempotency_key |
| Schedule | A pin queued for a future run_at |
Materializes into a pin when due |
| Import job | A bulk JSON or CSV request | Creates many pins or schedules |
| Job | A generic async task record | Poll it for status |
| Webhook | An HTTPS callback endpoint | Fires on pin.published and pin.failed |
| API key | Request credential | Sent in X-API-Key; scoped to one project |
The pin state machine
Every pin has a status. There are five values: queued, publishing, published, failed, and deferred.
created ──▶ queued ──▶ publishing ──▶ published (success)
│ │
│ └──▶ failed (non-retryable error)
│
└──▶ (retryable error) ──▶ queued ──▶ ...
│
└──▶ deferred ──▶ queued (auto-retry, up to 8 deferrals)
│
└──▶ failed (deferrals exhausted)
- A new pin starts in
queued. - A worker picks it up, sets
publishing, and calls Pinterest. - On success the pin becomes
published, getspinterest_pin_idandpublished_at, and (if you have one configured) fires apin.publishedwebhook. - A non-retryable error (for example a bad payload or a revoked token) sends the pin straight to
failedand fires apin.failedwebhook. - A retryable error (rate limits, transient upstream errors, temporarily unreachable media) requeues the pin. After a few quick retries it is parked as
deferredand retried about an hour later. PinBridge repeats this up to 8 deferrals before marking the pinfailed.
When a pin ends in failed, PinResponse.error_code tells you why. Common values:
error_code |
Meaning | Retryable |
|---|---|---|
rate_limited |
Pinterest rate limit hit | Yes |
transient_upstream |
Pinterest 5xx or temporary upstream error | Yes |
media_url_temporarily_unavailable |
PinBridge-hosted media was briefly unreachable | Yes |
media_url_unreachable |
The media URL could not be fetched | No |
token_expired / token_revoked |
The Pinterest token needs reconnecting | No |
scope_missing |
The connection is missing a required scope | No |
board_access_denied |
The account cannot publish to that board | No |
resource_not_found |
The board or account no longer exists | No |
invalid_payload |
Pinterest rejected the pin data | No |
publish_timeout |
The pin was stuck too long and was failed by a backstop | No |
api_error |
Fallback for an unclassified Pinterest error | Varies |
You can retry a failed pin yourself with POST /v1/pins/{pin_id}/retry (it returns 409 unless the pin is failed), or in bulk with POST /v1/pins/bulk-retry.
Idempotency
Every pin you create needs an idempotency_key. It is required. PinBridge never generates one for you when you call the API or SDK directly.
The key makes create requests safe to repeat. PinBridge deduplicates on the pair (project, idempotency_key). If you send the same key twice within the same project, the second call returns the existing pin instead of creating a duplicate. This is what lets you retry a network failure without publishing the same pin twice.
Pick keys that are stable for a given piece of content. A common pattern is a content ID or a hash of the row you are publishing.
See it in action
Send the same request twice with the same idempotency_key and you get the same pin back.
curl:
curl -sS -X POST https://api.pinbridge.io/v1/pins \
-H "X-API-Key: $PINBRIDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"account_id": "6f1c2e4a-3b7d-4c9e-8a21-5d0f9b3e7c41",
"board_id": "987654321098765432",
"title": "Autumn table setting",
"image_url": "https://placehold.co/1000x1500.png",
"idempotency_key": "recipe-2026-0187"
}'
Expected response (trimmed), same both times:
{
"id": "8a3f6c12-4d5e-4b7a-9c01-2e6f8d4b7a93",
"status": "queued",
"idempotency_key": "recipe-2026-0187"
}
Python SDK:
from pinbridge_sdk import PinbridgeClient
from pinbridge_sdk.models import PinCreate
with PinbridgeClient(api_key="$PINBRIDGE_API_KEY") as client:
payload = PinCreate(
account_id="6f1c2e4a-3b7d-4c9e-8a21-5d0f9b3e7c41",
board_id="987654321098765432",
title="Autumn table setting",
image_url="https://placehold.co/1000x1500.png",
idempotency_key="recipe-2026-0187", # required
)
first = client.pins.create(payload)
again = client.pins.create(payload)
assert first.id == again.id # same pin, no duplicate
Note the class name casing: PinbridgeClient (lowercase “b”). The SDK does not read environment variables, so pass api_key explicitly. There is no client.boards resource; boards live under client.pinterest.
Where terms map to auth
- Publishing and read endpoints accept an API key in the
X-API-Keyheader. - A few endpoints that act on the organization or on projects (for example
GET /v1/auth/meandGET /v1/projects) require a user-session JWT inAuthorization: Bearer <jwt>and reject API keys. - A quick way to test an API key is
GET /v1/pinterest/accounts. It works on any plan and returns[]if you have not connected an account yet.
Next steps
- Connect Pinterest to add the account you will publish with.
- Try the sandbox to publish without touching real Pinterest.
- Publish your first pin end to end.
- Bulk imports for publishing many pins at once.
