Skip to content

It’s Monday morning and someone asks how Pinterest went last week. How many pins went out, how many failed, and whether that’s better or worse than the week before.

The usual answer is a script. It pages through every pin, groups them by status, then does the whole thing again for the previous week so there’s something to compare against. That’s a pagination loop run twice, and it usually counts the wrong thing. Group pins by the day they were created and a pin submitted late on Sunday that went live on Monday lands in the wrong week. A pin submitted on Tuesday that failed, got retried and went out on Thursday shows up under Tuesday, not the day it was actually published.

PinBridge now answers the question with one request: GET /v1/dashboard/summary. You get pin outcomes for the period, a success rate, the same figures for the period before, a daily series, what’s waiting in the queue right now and what’s scheduled next.

One request, one week

Pass a start, an end and a time zone. Timestamps without an offset are read in that zone, so 2026-09-14 means midnight in Paris, not midnight UTC:

curl "https://api.pinbridge.io/v1/dashboard/summary?start=2026-09-14&end=2026-09-21&tz=Europe/Paris" \
  -H "x-api-key: $PINBRIDGE_API_KEY"
{
  "start": "2026-09-14T00:00:00+02:00",
  "end": "2026-09-21T00:00:00+02:00",
  "timezone": "Europe/Paris",
  "granularity": "day",
  "previous_start": "2026-09-07T00:00:00+02:00",
  "previous_end": "2026-09-14T00:00:00+02:00",
  "pins": {
    "total": 147,
    "submitted": 139,
    "by_status": {"queued": 0, "deferred": 2, "publishing": 0, "published": 141, "failed": 4},
    "success_rate": 0.9724
  },
  "previous_pins": {
    "total": 128,
    "submitted": 131,
    "by_status": {"queued": 0, "deferred": 0, "publishing": 0, "published": 117, "failed": 11},
    "success_rate": 0.9141
  },
  "series": [
    {"start": "2026-09-14T00:00:00+02:00", "created": 22, "published": 25, "failed": 1},
    {"start": "2026-09-15T00:00:00+02:00", "created": 19, "published": 18, "failed": 0}
  ],
  "published_by_account": [
    {"account_id": "0b7c…", "published": 88},
    {"account_id": "5e21…", "published": 41},
    {"account_id": "c9a4…", "published": 12}
  ],
  "queue": {"queued": 5, "deferred": 2, "publishing": 0},
  "schedules": {
    "by_status": {"scheduled": 0, "queued": 0, "deferred": 0, "running": 0,
                  "done": 96, "failed": 3, "canceled": 2},
    "upcoming": 58
  },
  "import_jobs": {
    "by_status": {"queued": 0, "processing": 0, "completed": 2,
                  "completed_with_errors": 1, "failed": 0}
  }
}

The series is trimmed here. The real one has a row for every day in the range, zero-filled, oldest first. Ranges of 48 hours or less come back hourly instead, so “the last 24 hours” gets a row per hour. A range can be up to 366 days long. Leave out start and end and you get the last 30 days.

Outcomes count on the day they happened

This is where a hand-rolled script usually goes wrong. A pin counts as published in the period its published_at falls in. A pin submitted on Sunday the 13th that sat deferred behind a rate limit and went live on Monday the 14th is a success for this week, not last week. That’s why published (141) can be higher than submitted (139): a few of this week’s successes were submitted the week before.

A pin counts as failed in the period it failed in, and only while it’s still failed. Retry it, it publishes, and it moves from failed to published. So on Monday morning the failed number is the failures nobody has fixed yet. That’s the one you can do something about.

queued, deferred and publishing count pins submitted in the period that are still waiting. total is the sum of by_status, so the numbers always add up. submitted counts every pin created in the period, whatever happened to it afterwards.

success_rate is published / (published + failed), from 0 to 1. It’s null when nothing published or failed, so a quiet week doesn’t show up as 0%.

Scheduled pins follow the same rule. A schedule creates its pin when it runs, so a pin scheduled on the 2nd for the 15th counts on the 15th.

Week over week, without a second request

previous_pins holds the same figures for the period of equal length just before start, and previous_start / previous_end tell you exactly which period that was. A week gets compared with the week before, a 30-day range with the 30 days before it. Here, published went from 117 to 141 and unfixed failures dropped from 11 to 4.

Two parts of the response ignore the range on purpose. queue is what’s waiting right now, and schedules.upcoming counts the schedules due from now on. They describe the state of things when you make the call, so a Monday report can say both “here’s last week” and “here’s what’s lined up.”

Add account_id to narrow everything to one Pinterest account. import_jobs is then null, because a single bulk import can target several accounts.

Post it to Slack every Monday

Here’s the whole report with the Python SDK. It reads last week, maps account IDs to usernames, groups the week’s unfixed failures by error code, and posts the result to a Slack incoming webhook:

import os
from collections import Counter
from datetime import date, datetime, time, timedelta

import requests
from pinbridge_sdk import PinbridgeClient

TZ = "Europe/Paris"

this_monday = date.today() - timedelta(days=date.today().weekday())
last_monday = this_monday - timedelta(days=7)

with PinbridgeClient(api_key=os.environ["PINBRIDGE_API_KEY"]) as client:
    # Naive datetimes are read in TZ: Monday 00:00 to Monday 00:00, Paris time
    s = client.dashboard.summary(
        start=datetime.combine(last_monday, time()),
        end=datetime.combine(this_monday, time()),
        tz=TZ,
    )
    names = {a.id: a.username or a.display_name for a in client.pinterest.list_accounts()}
    failed = [
        p for p in client.pins.list(status="failed", limit=200)
        if p.failed_at and s.start <= p.failed_at < s.end
    ]

def pct(rate):
    return "n/a" if rate is None else f"{rate:.1%}"

published = s.pins.by_status["published"]
change = published - s.previous_pins.by_status["published"]
causes = ", ".join(f"{code} ×{n}" for code, n in Counter(p.error_code for p in failed).most_common(3))

lines = [
    f"*Pinterest, week of {last_monday:%d %b}*",
    f"Published: {published} ({change:+d} vs the week before)",
    f"Success rate: {pct(s.pins.success_rate)} (was {pct(s.previous_pins.success_rate)})",
    f"Failed, not yet fixed: {s.pins.by_status['failed']}" + (f" ({causes})" if causes else ""),
    f"Waiting now: {s.queue.queued} queued, {s.queue.deferred} deferred",
    f"Scheduled from now on: {s.schedules.upcoming}",
    "",
    *(f"• {names.get(a.account_id, a.account_id)}: {a.published}" for a in s.published_by_account),
]
requests.post(os.environ["SLACK_WEBHOOK_URL"], json={"text": "\n".join(lines)}, timeout=10)

Run it from cron at 09:00 on Mondays (0 9 * * 1) or from a scheduled workflow. The message reads like this:

Pinterest, week of 14 Sep
Published: 141 (+24 vs the week before)
Success rate: 97.2% (was 91.4%)
Failed, not yet fixed: 4 (board_access_denied ×3, invalid_image ×1)
Waiting now: 5 queued, 2 deferred
Scheduled from now on: 58

• shop_main: 88
• shop_recipes: 41
• shop_outlet: 12

Give the script its own API key with only the read scope. A report has no reason to publish anything. A key restricted to certain Pinterest accounts only sees those accounts, which is useful if you run Pinterest for clients: one read-only key per client, and each client’s Monday report covers only their accounts.

When a number looks wrong

The summary tells you something moved. Finding out why takes one more call:

  • If failed went up, list the failed pins with GET /v1/pins?status=failed, or filter by one cause with error_code=board_access_denied. My pin failed covers each error code and its fix, and retries and failures explains what PinBridge retries by itself.
  • If deferred won’t go down, remember what the status means. A deferred pin hit a retryable error (a rate limit, a Pinterest outage) several times in a row, so PinBridge parked it and will try again in about an hour. A few after a busy day is normal. A number that keeps growing usually means sustained rate limiting: you’re submitting faster than the account’s limit allows. See rate limits.
  • If upcoming is zero, nothing is scheduled. Either that’s on purpose, or a content pipeline upstream has stopped feeding you.
  • If an account dropped off published_by_account, check its health in GET /v1/pinterest/accounts. A reconnect_required account can’t publish until someone reconnects it.

Or ask your agent

The same endpoint is the get_dashboard_summary tool on the PinBridge MCP server. If Claude, ChatGPT or another MCP client is connected, ask it “How did Pinterest publishing go last week compared with the week before?” It works out the dates itself, and if anything failed it can list those pins with list_pins.

What it doesn’t cover

The summary reports publishing, not engagement. Impressions, saves and clicks live in the analytics endpoints; we covered those in measuring every pin past Pinterest’s 90-day window.

A pin that someone later deleted on Pinterest still counts as published in the week it went live, because it did. Pins you deleted in PinBridge don’t count at all.

It counts only pins that went through PinBridge. Pins someone created by hand in the Pinterest app aren’t in it.

The Dashboard page in the PinBridge app runs on the same numbers, with a range picker from the last 24 hours to 90 days or any custom range. If you’d like a Monday report on your own Pinterest accounts, start free and point the script at your key.

Leave a Reply

Your email address will not be published. Required fields are marked *

Skip the token plumbing

PinBridge is a hosted Pinterest publishing API. Connect an account once and we handle the refresh loop, the locking and the reconnect prompt.