Home / Docs / Bulk imports

Bulk imports (CSV and JSON)

Bulk imports let you queue many pins in a single request instead of calling the publish endpoint once per pin. This page covers both import formats, the exact schema, and how to read the results.

Prerequisites

  • A paid plan. Bulk imports use the bulk_publishing feature, which is off on the Free (Playground) plan. A Free-plan key gets a 403 feature_unavailable. Bulk imports are available on Starter, Growth, Agency, and Enterprise.
  • A connected Pinterest account (you need its account_id) and a board_id to publish to.
  • Your API key in the X-API-Key header.

For a single pin, use POST /v1/pins instead. See Publish your first pin.

The two import endpoints

Format Endpoint Body Response
JSON POST /v1/pins/imports/json A raw JSON array of row objects 202 Accepted with a job
CSV POST /v1/pins/imports/csv multipart/form-data with a file field 202 Accepted with a job

Both endpoints return 202 Accepted and process asynchronously. You get back an import job, then poll it for per-row results.

  • Check one job: GET /v1/pins/imports/{job_id}
  • List jobs: GET /v1/pins/imports

Per-row schema

Each row is a pin. The required and optional fields are the same as a normal pin create, plus one import-only field (run_at).

Required in every row:

  • account_id — the Pinterest account UUID to publish with.
  • board_id — the destination board.
  • title — the pin title, max 100 characters.
  • idempotency_key — a string you choose, max 255 characters. This is required and is never auto-generated. It is what dedupe keys on (see below).

Each row also needs exactly one media source: either image_url or asset_id. Supplying both, or neither, fails that row.

Optional per row:

  • description (max 800), related_terms, alt_text (max 500), dominant_color (6-digit hex), link_url (max 2048).
  • cover_image_url or cover_image_asset_id (video pins only; provide at most one).
  • run_at — an ISO 8601 timestamp with a timezone offset. If set, it must be in the future, and the row becomes a schedule instead of an immediate publish. Omit it (or leave the CSV cell blank) to publish right away.

CSV format

The CSV must be UTF-8 (a BOM is accepted) and have a header row.

Required columns: account_id, board_id, title, idempotency_key. Missing any of these returns 400 “CSV is missing required columns”.

Allowed columns (anything outside this set returns 400 “CSV contains unsupported columns”):

account_id, board_id, title, description, related_terms, alt_text, dominant_color, cover_image_url, cover_image_asset_id, link_url, image_url, asset_id, idempotency_key, run_at.

Blank cells are treated as empty (no value). All-blank rows are skipped. Duplicate column names, a whitespace-only header, or an empty file each return 400.

Downloadable sample

Start from this valid 3-row example: sample-import.csv. Replace 6f1c2e4a-3b7d-4c9e-8a21-5d0f9b3e7c41 and 987654321098765432 with your real IDs.

account_id,board_id,title,idempotency_key,image_url,description,link_url,run_at
6f1c2e4a-3b7d-4c9e-8a21-5d0f9b3e7c41,987654321098765432,Autumn table setting,import-2026-001,https://example.com/images/autumn-table.jpg,Warm autumn table styling ideas,https://example.com/blog/autumn-table,
6f1c2e4a-3b7d-4c9e-8a21-5d0f9b3e7c41,987654321098765432,Minimalist desk setup,import-2026-002,https://example.com/images/desk-setup.jpg,Clean desk inspiration for a focused workspace,https://example.com/blog/desk-setup,
6f1c2e4a-3b7d-4c9e-8a21-5d0f9b3e7c41,987654321098765432,Weekend brunch spread,import-2026-003,https://example.com/images/brunch.jpg,Easy brunch recipes for the weekend,https://example.com/blog/brunch,2026-12-01T09:00:00+00:00

The first two rows publish immediately. The third has a future run_at, so it becomes a schedule.

Limits

  • Maximum 500 rows per import, for both CSV and JSON. Over 500 returns 400 “Import exceeds the maximum of 500 rows”. An empty import also returns 400.
  • There is no documented byte-size limit for the CSV upload in the API itself. Any file-size cap is enforced at the reverse-proxy or server layer, not in the app.

Upload a CSV (curl)

curl -X POST https://api.pinbridge.io/v1/pins/imports/csv \
  -H "X-API-Key: $PINBRIDGE_API_KEY" \
  -F "[email protected];type=text/csv"

Expected response (trimmed):

{
  "id": "4c8f2a6e-9b1d-4f3a-b5c7-0e2d6a8f4b92",
  "status": "queued",
  "total_rows": 3,
  "processed_rows": 0,
  "created_rows": 0,
  "existing_rows": 0,
  "failed_rows": 0
}

Upload JSON (curl)

The JSON body is a raw array of row objects.

curl -X POST https://api.pinbridge.io/v1/pins/imports/json \
  -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",
      "idempotency_key": "import-2026-001",
      "image_url": "https://example.com/images/autumn-table.jpg"
    },
    {
      "account_id": "6f1c2e4a-3b7d-4c9e-8a21-5d0f9b3e7c41",
      "board_id": "987654321098765432",
      "title": "Weekend brunch spread",
      "idempotency_key": "import-2026-003",
      "image_url": "https://example.com/images/brunch.jpg",
      "run_at": "2026-12-01T09:00:00+00:00"
    }
  ]'

Check job status and read results

curl https://api.pinbridge.io/v1/pins/imports/4c8f2a6e-9b1d-4f3a-b5c7-0e2d6a8f4b92 \
  -H "X-API-Key: $PINBRIDGE_API_KEY"

Expected response (trimmed):

{
  "id": "4c8f2a6e-9b1d-4f3a-b5c7-0e2d6a8f4b92",
  "status": "completed_with_errors",
  "total_rows": 3,
  "processed_rows": 3,
  "created_rows": 2,
  "existing_rows": 0,
  "failed_rows": 1,
  "results": [
    {"row_number": 1, "status": "created", "pin_id": "8a3f6c12-4d5e-4b7a-9c01-2e6f8d4b7a93", "idempotency_key": "import-2026-001"},
    {"row_number": 2, "status": "created", "pin_id": "1d9b4e7f-6a2c-4f8e-b5d3-9c0a7e2f4b16", "idempotency_key": "import-2026-002"},
    {"row_number": 3, "status": "failed", "idempotency_key": "import-2026-003",
     "error_code": "validation_error", "error_message": "run_at must be in the future"}
  ]
}

Per-row status

Each row result carries row_number, status, pin_id, schedule_id, idempotency_key, error_code, and error_message. The status is one of:

  • created — a new pin (or schedule, for a run_at row) was created.
  • existing — a matching pin or schedule already existed; nothing new was created (see dedupe).
  • failed — the row was rejected. Read error_code and error_message for the reason.

Job status

The job-level status is one of:

Job status Meaning
queued Accepted, not started yet.
processing Rows are being processed.
completed All rows processed, none failed.
completed_with_errors Finished, but at least one row failed.
failed The whole job crashed before finishing.

Processing is resumable and commits per row, so a job continues from where it left off rather than reprocessing rows.

Dedupe and idempotency

Dedupe keys on the pair (project, idempotency_key). Before creating a row’s pin, PinBridge looks for an existing pin in the same project with the same idempotency_key. If one exists, the row is marked existing and points at that pin. Scheduled rows dedupe the same way against existing schedules.

This means re-running the same import is safe: rows that already produced a pin come back as existing instead of creating duplicates. Choose stable, unique idempotency_key values (for example a product ID or a content slug) so the same source content always maps to the same key.

Retrying failed rows

There is no “retry the whole job” endpoint. To retry, act on the individual rows:

  • Validation failures (bad or missing fields, a past run_at): fix the source data and re-submit those rows as a new import. Rows that already succeeded return existing, so you can safely re-submit the full file.
  • Pins that were created but then failed to publish: retry them as pins. Use POST /v1/pins/{pin_id}/retry for one, or POST /v1/pins/bulk-retry for many. See Retries and failures.

Python SDK

from pinbridge_sdk import PinbridgeClient

with PinbridgeClient(api_key="pb_your_api_key") as client:
    # JSON import: pass a list of row dicts
    job = client.pins.import_json([
        {
            "account_id": "6f1c2e4a-3b7d-4c9e-8a21-5d0f9b3e7c41",
            "board_id": "987654321098765432",
            "title": "Autumn table setting",
            "idempotency_key": "import-2026-001",
            "image_url": "https://example.com/images/autumn-table.jpg",
        },
    ])

    # CSV import: pass a file handle
    with open("sample-import.csv", "rb") as f:
        job = client.pins.import_csv(f)

    # Poll for results
    result = client.pins.get_import(job["id"])
    print(result["status"], result["failed_rows"])

The SDK does not read environment variables, so pass api_key explicitly. idempotency_key is required on every row.

n8n

The PinBridge node covers imports under the Pin resource:

  • Pin → Import JSON (POST /v1/pins/imports/json) sends every incoming item’s JSON as one import array. Each item is a row.
  • Pin → Import CSV (POST /v1/pins/imports/csv) uploads a CSV from a binary property. Set Binary Property to the field holding the file (default data).
  • Pin → Get Import and Pin → List Imports poll job status. List Imports supports Import Status and Import Source filters.

For a full n8n walkthrough see Bulk import pins in n8n.

Common errors

Symptom Cause Fix
403 feature_unavailable Free plan Upgrade to a plan with bulk_publishing.
400 “missing required columns” CSV lacks a required column Include account_id, board_id, title, idempotency_key.
400 “unsupported columns” Column outside the allowed set Remove it or rename to an allowed column.
400 “exceeds the maximum of 500 rows” Too many rows Split into multiple imports.
Row failed with validation_error A field failed validation (e.g. past run_at, both/neither media source) Fix that row and re-submit.

Next steps

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