Home / Docs / Core concepts

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 production or sandbox (see Sandbox). You may see the field name workspace in older payloads. That is a legacy compatibility alias for project. 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_id instead of a public image_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_at time.
  • 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-Key header. (User-session endpoints use Authorization: Bearer <jwt> instead. API keys always go in X-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, gets pinterest_pin_id and published_at, and (if you have one configured) fires a pin.published webhook.
  • A non-retryable error (for example a bad payload or a revoked token) sends the pin straight to failed and fires a pin.failed webhook.
  • A retryable error (rate limits, transient upstream errors, temporarily unreachable media) requeues the pin. After a few quick retries it is parked as deferred and retried about an hour later. PinBridge repeats this up to 8 deferrals before marking the pin failed.

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-Key header.
  • A few endpoints that act on the organization or on projects (for example GET /v1/auth/me and GET /v1/projects) require a user-session JWT in Authorization: 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

Last updated September 13, 2026Was this page helpful? Tell us →