# Pinterest

Flaghand posts to Pinterest business accounts. Switching is free in Pinterest's settings. Agents draft, upload media,
check and request approval. Only the owner approves each exact Pin with a passkey. Pinterest is available only to enabled
workspaces.

## Connect

All API paths below start with `/api/v1`.

| Route | Who | Shape |
| --- | --- | --- |
| `POST /workspaces/{workspace}/connections/pinterest/start` | `connections` scope | Body `{}` or `{ "connection_id": "con_…" }` to reconnect the same account. Returns `{ url, for_person, attempt_id, platform, expires_at }`. Relay `for_person`, then wait on the attempt. |
| `GET /workspaces/{workspace}/connection-attempts/{id}` | `read` scope | The consent's outcome. A personal account ends with `pinterest_business_account` and connects nothing. CLI: `connections wait`. |
| `GET /workspaces/{workspace}/connections/{id}/pinterest/boards` | `read` | `listPinterestBoards`: `{ boards: [{ id, name, privacy, owner, group }], next }`, read live each time. |
| `GET /workspaces/{workspace}/connections/{id}/pinterest/boards/{board_id}/sections` | `read` | `listPinterestBoardSections`: `{ sections: [{ id, name }], next }`, read live each time. |
| `POST /workspaces/{workspace}/connections/{id}/verify` | `read` | `{ at, ok, account_matches }`. Reads the account; never posts. |
| `DELETE /workspaces/{workspace}/connections/{id}` | owner | Stops unsent jobs and erases everything kept from Pinterest. |

Boards are made in Pinterest. Flaghand never creates, changes or deletes a board.

## Draft, check and approve

An agent's flow: `listPinterestBoards`, then `createDraft` with `connection_id` and `board_id`, then `checkDraft`, then
`requestApproval`. There is no default board, no last-used board and no first board: `board_id` is required on every Pin.
One draft is one Pin on one board. To put a Pin on two boards, make one draft per board in a `createPost` plan.

| Kind | Body |
| --- | --- |
| `pinterest_image` | `{ "board_id": "…", "section_id": "…", "title": "…", "description": "…", "link": "https://…", "media": ["med_…"], "alt": "…", "ai": [] }` |
| `pinterest_carousel` | The same, with `media` holding 2–5 images in display order and no `alt`. |
| `pinterest_video` | The same, with `media` holding one video and no `alt`, plus `cover` (`med_…`, a JPEG or PNG) or `thumb_offset`. |

- `section_id` is optional and has to be a section of that board. `title`, `description`, `link`, `alt` and `ai` are optional.
- Text goes out exactly as saved. Omitted text is empty. Nothing is trimmed, rewritten or shortened.
- `board_id` and `section_id` are kept sealed under the connection's own key, so a draft reads them back as opaque values.
  Send those back unchanged, or send a board id. `checkDraft` returns `pinterest.destination`, such as `Board: Recipes › Autumn`.
  Changing `connection_id` means choosing the board again.
- `ai` lists Pinterest's disclosures: `ai_modified`, `synthetic_performer`. They show on the approval sheet.
- `thumb_offset` is milliseconds from the start, in whole seconds (`3000`, not `3500`), because Pinterest picks the cover
  frame in seconds. With neither `cover` nor `thumb_offset` the cover is the first frame, and the approval sheet says so.
- `pinterest_image` and `pinterest_carousel` can change into each other on the same draft with the complete body and
  current `rev`. A video doesn't convert. Changing what would be posted voids the approval.
- Media limits are Pinterest's: images are JPEG, PNG or WebP; video is MP4, M4V or MOV, 4 seconds to 5 minutes.

Check reads the board and section live with the connection's login. It answers with these stable codes:
`pinterest_board_required`, `pinterest_board_missing`, `pinterest_section_missing`, `pinterest_title_length` (100),
`pinterest_description_length` (800), `pinterest_alt_length` (500), `pinterest_link` (2,048), `pinterest_media`,
`pinterest_image_count`, `pinterest_video_format`, `pinterest_video_duration`, `pinterest_cover_seconds`,
`pinterest_cover_past_end`, `pinterest_business_account`, `pinterest_disabled`.

The snapshot the owner signs freezes the account, board and section (sealed under the connection's key), the kind, the
exact text, ordered media hashes, the cover hash or frame, and `ai`. The approval sheet shows each Pin, its board and who
will see it. Each Pin is shown and approved on its own: Flaghand has no repeat, automation or feed-to-Pin publishing
for Pinterest, and reshare doesn't apply.

## Schedule, post and read the result

Pinterest has no publish time, so a scheduled Pin waits in Flaghand's queue and goes out at the approved instant. Cancel,
pause and disconnect work until then. Slots and `next_slot` work as on other networks.

A video goes to Pinterest first and runs again each minute while Pinterest processes it (job state `processing`). Then the Pin
is created once, with the approved cover. A create with no clear answer is never sent again.

`result.pinterest` holds `step` (`upload`, `processing`, `created`), `media_type`, `pin_id`, `media_id`, `board_id`,
`permalink`, `visibility` (`public` or `creator_only`), `video`, `media_status`, `create_requested_at`, `candidates` and
`pinterest_said`. `result.ids` and `result.urls` hold the Pin's id and address. `creator_only` means Flaghand's Pinterest
access is still in Trial, so Pinterest shows the Pin only to its owner. A video Pin's `video` stays `processing` until a
read-back finds Pinterest finished it.

| Outcome | Job | What to do |
| --- | --- | --- |
| Pinterest's request limit | `scheduled`, `platform_rate_limited` | Nothing. It goes out after Pinterest's reset, as approved. |
| Login refused | `held`, `connection_login_expired` | Reconnect, then the owner retries. |
| Board deleted or not writable | `held`, `pinterest_board_missing` | Cancel the job, choose another board, request approval again. It never moves to another board by itself. |
| Personal account | `held`, `pinterest_business_account` | Switch the account to business in Pinterest. |
| Video not processed | `failed`, `pinterest_processing` | Upload the file again and request approval again. |
| Media, text or link refused | `failed`, `pinterest_media` or `pinterest_link` | Read `pinterest_said`, change the draft, request approval again. |
| Unclear answer | `failed`, `pinterest_uncertain` | Check the board on Pinterest. `candidates` lists Pins made on it since the request: possibilities, not proof. To send it anyway, change the draft and get a new approval. |

`POST /workspaces/{workspace}/jobs/{id}/verify` reads the Pin back and compares title, description, link, board, section,
alt text and media type. `result.check.fields` says `matches`, `different` or `not_verifiable` for each. Only that status is
kept, not Pinterest's copy.

## What's kept

Board lists, section lists and numbers are read live and never kept. Flaghand keeps only what the person chose and what it
created: the account, the chosen board and section, and the Pin's ids. All of it is sealed under the connection's own key.
Disconnecting destroys that key, so nothing readable from Pinterest is left, in backups either. The person's own title,
description, link, alt text and media stay. Preview links, calendar feeds, chat alerts, webhooks and notifications say
"Pinterest", never the username, a board name or a Pinterest id.

Pin analytics, secret boards, creating boards, per-slide carousel text, comments and messages are not offered here.
