There is no Pinterest node in n8n, so I built one. Five things that were harder than expected

Disclosure up front: I built the thing in this post and I sell it. I am posting it here because the build details are the useful part, and a few of them cost me weeks.

If you want to publish to Pinterest from n8n today there is no core node. What exists on npm is a couple of community nodes that authenticate with a session cookie against Pinterest’s internal API. Those work until Pinterest changes something, and they mean putting your own logged-in session into a workflow. I wanted the official v5 API instead, which needs an OAuth app, token storage and a server to hold them, so I built that and wrapped it in a node.

What is published:

  • the node, n8n-nodes-pinbridge on npm
  • five templates on n8n.io under the pinbridge creator: bulk publish from Sheets, WordPress posts, Shopify products, an OpenAI pin writer, and multi-account scheduling
  • the API behind it, api.pinbridge.io

The parts I did not expect:

A video pin is not one call. An image pin is a single POST with a URL. Video is register the media, upload to the bucket you are handed, poll until Pinterest finishes processing, then create the pin with the media id and a cover image. Built by hand in n8n that is four nodes and a wait loop, and the polling step has no fixed duration you can plan around.

Pinterest hands you a new refresh token on every refresh. The access token lasts 30 days and the refresh token 60. Store the new access token but not the new refresh token and everything works for a month, then stops. I wrote that one up separately because it is not Pinterest-specific and it catches people on other providers too.

Most of my “auth” failures were not auth. Of 352 failures across the accounts I watch, 201 were board permission problems arriving as something that reads like a token error. Someone connects an account, picks a board they do not own or that has been deleted, and the message sends them off to reconnect instead of fixing the board. The node surfaces the real error code now with a remediation line.

Rate limits are per connected account, not per app. One global queue is the wrong shape. It has to be partitioned per account, each with its own in-flight count and backoff.

Idempotency belongs in the API, not the workflow. A retried execution re-sends the publish call, and with no key the second one is a duplicate pin. Every publish takes an idempotency key and the node sets one, so a retry is a no-op instead of a double post.

Beyond publishing, the node does schedules you can edit in place, batch publishing, CSV import, board access checks, asset uploads, analytics and webhooks for publish results. It also works as a tool for the AI Agent node.

Happy to answer anything on the Pinterest side. The thing I would most like to hear back: if you are publishing to Pinterest from n8n right now, what are you doing about the video flow? That is the part I still think is too hard, including in my own version.

1 Like

The idempotency point really resonates with me. I recently built and tested an n8n + FastAPI + PostgreSQL demo covering duplicate-request handling, idempotency, concurrent requests, and database-failure recovery.

Your point about surfacing the actual board-permission error instead of treating everything as an authentication failure is also valuable. Clear error classification makes workflows much easier to debug and recover.

For the video flow, I’d be interested to know how you handle retries if media processing takes longer than expected or temporarily fails.

The refresh token rotation point is a great catch. I’ve seen the same ‘works for a month, then dies’ pattern with other OAuth providers, and it’s painful because nothing fails until long after you’ve stopped looking. Storing the new refresh token on every refresh, and alerting when a refresh fails rather than when a publish fails, has saved me a lot of confusion.

On the video flow, how are you handling the polling step: a fixed Wait plus a retry counter, or a webhook/callback when processing finishes? With slow processing a capped loop that gives up and flags the item seems safer than letting the execution run forever.

1 Like

Good question, and it’s the part where the two failures look identical from outside but need opposite handling.

Processing still running and processing failed have to be separated, because a re-send doesn’t help the first and is the only fix for the second. Once the media is registered you can’t retry your way through processing: either it completes and you get a usable media id, or that id is dead and the asset has to go up again from the original file. So the asset upload and the pin creation are two steps with the asset’s own state in between, and pin creation waits on that state rather than on a timer.

The other half is making the retry safe. A slow video is the exact case where someone’s workflow times out and runs again, which is how one intent becomes two pins. Every publish takes an idempotency key, and in the node it defaults to {{$execution.id}}-{{$itemIndex}}, so an n8n retry of the same execution reuses the key and the second call is a no-op instead of a duplicate. Building this by hand, that’s the piece I’d put in before the polling loop rather than after.

Anything that doesn’t finish ends in a terminal failed state carrying the real reason, with an explicit retry, instead of sitting in a queue being re-attempted forever. A stuck item you can see beats a quiet one.

Your FastAPI and Postgres demo sounds like it covers the same ground from the server side. Is the duplicate handling in it a unique constraint on the key, or a read-then-insert?

Neither, from the workflow’s side. The polling happens server-side and the workflow doesn’t wait on it. The publish call returns a job, and the result comes back either by reading that job or by a webhook when the pin reaches a terminal state. That keeps a slow video out of execution time altogether, which matters because an execution sitting in a Wait loop is the thing most likely to get retried by something upstream and turn one pin into two.

If you’re building it by hand in n8n, your instinct is the right one. A capped loop that gives up and flags the item beats an unbounded one. Two things I’d add to it. Set the idempotency key before the loop rather than inside it, so giving up and retrying later doesn’t publish twice. And make the give-up state carry the reason, because “still processing after N tries” and “the media was rejected” want different handling: the first is worth retrying as-is, the second needs the file uploaded again.

Alerting on refresh failures rather than publish failures is the right read, and earlier than most people get to it. The refresh is where rotation drift shows up first, usually weeks before anything user-visible breaks.

Thanks for the follow-up. My demo uses FastAPI with PostgreSQL to handle duplicate events, payload conflicts, and concurrent requests.

I tested 100 concurrent requests with the same event ID: one was processed and the other 99 were handled as duplicates. I also tested the same event ID with a changed payload, which returns HTTP 409.

I want to be precise about the implementation detail, so I’ll verify the database constraint and insert logic in the code before confirming whether it relies on a unique constraint or a read-then-insert approach.

Your point about setting the idempotency key before the retry loop is valuable. It’s important that retries preserve the original intent rather than create duplicate side effects.