API Reference
PinFlow exposes a REST API that its own web interface uses. Call these endpoints directly to build integrations, automation scripts, or external dashboards. All requests and responses use JSON.
Quick start
sk_pinflow_… secret — it is shown only once.
curl https://pinflow.mirajpatterns.com/auth/me \
-H "Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY"
Authentication
PinFlow uses API key authentication for programmatic access. Every API request must carry your secret key in the Authorization header. Session cookies (set by the browser OAuth flow) are also accepted as a fallback for browser-based usage.
pk_pinflow_…) and a secret key (sk_pinflow_…). Only the secret key is used for authentication. It is shown once at creation and never retrievable again — store it in a password manager or secret vault.
Key management
Create, disable, and revoke API keys from the dashboard: click the ⚙ gear icon in the sidebar → API Keys tab. You can have up to 10 active keys per account.
403 Account pending approval, contact the admin.
GET /auth/login HTTP/1.1 Host: pinflow.mirajpatterns.com
No body required. Redirects browser to Pinterest's OAuth consent page.
HTTP/1.1 302 Found Location: https://www.pinterest.com/oauth/?client_id=...&scope=pins:write+boards:read+user_accounts:read
POST /auth/logout HTTP/1.1 Host: pinflow.mirajpatterns.com Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY
HTTP/1.1 303 See Other Location: /
GET /auth/me HTTP/1.1 Host: pinflow.mirajpatterns.com Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY
HTTP/1.1 200 OK Content-Type: application/json { "id": 1, "pinterest_user_id": "12345678", "username": "your_username", "timezone": "UTC", "daily_pin_limit": 2, "created_at": "2026-01-15T10:00:00Z" }
Required headers
The server checks authentication in this order: API key → session cookie. Use the API key for all programmatic access.
| Header | Value | When |
|---|---|---|
| Authorization | Bearer sk_pinflow_YOUR_SECRET_KEY | All authenticated endpoints — preferred method |
| X-API-Key | sk_pinflow_YOUR_SECRET_KEY | Alternative to Authorization header |
| Content-Type | application/json | POST / PUT requests with a JSON body |
| Content-Type | multipart/form-data | POST /pins/upload-image only |
Using both auth methods
# API key (recommended for scripts and automation) curl https://pinflow.mirajpatterns.com/pins \ -H "Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY" # Alternative header name curl https://pinflow.mirajpatterns.com/pins \ -H "X-API-Key: sk_pinflow_YOUR_SECRET_KEY" # Session cookie (browser / fallback — cookie extracted from DevTools) curl https://pinflow.mirajpatterns.com/pins \ -b "session=YOUR_SESSION_COOKIE"
403 Forbidden means your account is pending admin approval, or the key's owner was suspended.
Rate limits
Rate limits are per IP address and reset every minute. Exceeding a limit returns 429 Too Many Requests.
| Endpoint | Limit |
|---|---|
| POST /pins | 30 / min |
| POST /pins/bulk | 10 / min |
| POST /pins/upload-image | 30 / min |
| All other endpoints | 200 / min (global) |
POST /pins returns 429 with a message indicating when the limit resets (midnight UTC).
Pins
The core resource. A pin is one Pinterest post managed through PinFlow.
Status lifecycle
| Status | Meaning |
|---|---|
| draft | Saved but will not publish automatically |
| review | Flagged for manual review before queuing |
| queued | Will publish as soon as the queue processes it |
| scheduled | Will publish at the specified scheduled_at time |
| publishing | Currently being sent to Pinterest — cannot edit |
| published | Live on Pinterest — cannot edit or delete |
| failed | Exhausted all retry attempts |
Query parameters
| Param | Type | Description |
|---|---|---|
| status | string | Filter by status value |
| title | string | Case-insensitive partial match |
| board_id | string | Exact Pinterest board ID |
| collection_id | integer | Only pins in this collection |
GET /pins?status=scheduled&title=summer HTTP/1.1 Host: pinflow.mirajpatterns.com Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY
HTTP/1.1 200 OK Content-Type: application/json [ { "id": 42, "profile_id": 12, "pinterest_user_id": "12345678", "pinterest_username": "my_pinterest_profile", "board_id": "123456789", "title": "Summer Recipe Ideas", "description": "Quick and easy summer meals", "media_url": "https://example.com/image.jpg", "link": "https://myblog.com/summer-recipes", "alt_text": null, "status": "scheduled", "scheduled_at": "2026-05-01T19:00:00Z", "published_at": null, "pinterest_pin_id": null, "error_message": null, "retry_count": 0, "sort_order": 0, "created_at": "2026-04-26T12:00:00Z" } ]
Body fields
| Field | Type | Notes |
|---|---|---|
| board_id | string | required — Pinterest board ID from GET /boards |
| media_url | string | required — Image URL, or a URL returned by POST /pins/upload-image |
| title | string | optional — Max 256 characters |
| description | string | optional |
| link | string | optional — Destination URL users see when they click the pin |
| alt_text | string | optional — Max 500 characters |
| scheduled_at | datetime | optional — ISO 8601 UTC, must be in the future. Sets status to scheduled. |
| status | string | optional — Only "draft" is accepted; omit to queue immediately |
POST /pins HTTP/1.1 Host: pinflow.mirajpatterns.com Content-Type: application/json Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY { "board_id": "123456789012345678", "media_url": "https://example.com/image.jpg", "title": "Summer Recipe Ideas", "description": "Quick and easy summer meals", "link": "https://myblog.com/summer-recipes", "scheduled_at":"2026-05-01T19:00:00Z" }
HTTP/1.1 201 Created Content-Type: application/json { "id": 42, "profile_id": 12, "pinterest_user_id": "12345678", "pinterest_username": "my_pinterest_profile", "board_id": "123456789012345678", "title": "Summer Recipe Ideas", "status": "scheduled", "scheduled_at": "2026-05-01T19:00:00Z", "created_at": "2026-04-26T12:00:00Z", ... }
Also fires a pin.created webhook event.
429 if monthly plan quota is reached: { "detail": "Monthly pin limit of 200 reached for plan 'starter'." }
POST /pins/bulk HTTP/1.1 Host: pinflow.mirajpatterns.com Content-Type: application/json Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY [ { "board_id": "...", "media_url": "...", "title": "Pin 1" }, { "board_id": "...", "media_url": "...", "title": "Pin 2" } ]
Each item has the same shape as POST /pins. Requires Growth plan or higher, and the entire batch is rejected if it exceeds your monthly plan quota.
HTTP/1.1 201 Created Content-Type: application/json [ { "id": 43, ... }, { "id": 44, ... } ]
Each pin object includes profile_id, pinterest_user_id, and pinterest_username.
GET /pins/42 HTTP/1.1 Host: pinflow.mirajpatterns.com Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY
HTTP/1.1 200 OK Content-Type: application/json { "id": 42, "profile_id": 12, "pinterest_user_id": "12345678", "pinterest_username": "my_pinterest_profile", "status": "published", ... }
All body fields are optional — send only what you want to change. Pins with status publishing or published cannot be updated. status can be set to draft, review, queued, or scheduled.
PUT /pins/42 HTTP/1.1 Host: pinflow.mirajpatterns.com Content-Type: application/json Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY { "title": "Updated Title", "scheduled_at": "2026-06-01T08:00:00Z" }
HTTP/1.1 200 OK Content-Type: application/json { "id": 42, "profile_id": 12, "pinterest_user_id": "12345678", "pinterest_username": "my_pinterest_profile", "title": "Updated Title", ... }
DELETE /pins/42 HTTP/1.1 Host: pinflow.mirajpatterns.com Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY
Pins with status publishing or published cannot be deleted.
HTTP/1.1 204 No ContentPOST /pins/42/publish HTTP/1.1 Host: pinflow.mirajpatterns.com Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY
No body required. Overrides scheduled_at and pushes the pin to Pinterest right now.
HTTP/1.1 200 OK Content-Type: application/json { "id": 42, "profile_id": 12, "pinterest_user_id": "12345678", "pinterest_username": "my_pinterest_profile", "status": "published", ... }
PUT /pins/reorder HTTP/1.1 Host: pinflow.mirajpatterns.com Content-Type: application/json Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY { "pin_ids": [42, 17, 88, 3] }
The array position becomes sort_order. Pins with lower sort_order publish first.
HTTP/1.1 200 OK { "updated": 4 }
Send as multipart/form-data. Field name must be file. Accepted types: JPEG, PNG, GIF, WebP, AVIF. Max: 10 MB.
POST /pins/upload-image HTTP/1.1 Host: pinflow.mirajpatterns.com Content-Type: multipart/form-data; boundary=----FormBoundary Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY ------FormBoundary Content-Disposition: form-data; name="file"; filename="photo.jpg" Content-Type: image/jpeg <binary image data> ------FormBoundary-- # curl shorthand: curl -X POST https://pinflow.mirajpatterns.com/pins/upload-image \ -H "Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY" \ -F "file=@/path/to/photo.jpg"
HTTP/1.1 200 OK Content-Type: application/json { "url": "https://pinflow.mirajpatterns.com/static/uploads/a1b2c3d4.jpg" }
Use the returned url as the media_url when creating a pin.
GET /pins/calendar?year=2026&month=5 HTTP/1.1 Host: pinflow.mirajpatterns.com Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY
HTTP/1.1 200 OK [ { "id": 42, "profile_id": 12, "pinterest_user_id": "12345678", "pinterest_username": "my_pinterest_profile", "scheduled_at": "2026-05-01T19:00:00Z", ... } ]
DELETE /pins/bulk — delete multiple pins
DELETE /pins/bulk HTTP/1.1 Content-Type: application/json Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY { "pin_ids": [1, 2, 3] } # Returns 204 · skips published/publishing pins silently
PUT /pins/bulk/status — change status of many pins
PUT /pins/bulk/status HTTP/1.1 Content-Type: application/json Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY { "pin_ids": [1, 2, 3], "status": "draft" } # Returns { "updated": N } · cannot set to publishing or published
PUT /pins/bulk/schedule — schedule many pins to the same time
PUT /pins/bulk/schedule HTTP/1.1 Content-Type: application/json Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY { "pin_ids": [1, 2, 3], "scheduled_at": "2026-05-01T19:00:00Z" } # Returns { "updated": N }
Boards
GET /boards HTTP/1.1 Host: pinflow.mirajpatterns.com Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY
Proxied live from the Pinterest API. Use the id values as board_id when creating pins.
HTTP/1.1 200 OK Content-Type: application/json { "pinterest_user_id": "12345678", "pinterest_username": "my_pinterest_profile", "items": [ { "id": "123456789012345678", "name": "My Travel Board", "description": "Places I want to visit" } ] }
GET /boards/analytics HTTP/1.1 Host: pinflow.mirajpatterns.com Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY
HTTP/1.1 200 OK Content-Type: application/json { "pinterest_user_id": "12345678", "pinterest_username": "my_pinterest_profile", "boards": [ { "board_id": "123456789012345678", "total_pins": 42, "published_pins": 31, "scheduled_pins": 7, "draft_pins": 3, "failed_pins": 1 } ] }
Collections
Local groupings of pins — not Pinterest boards. Use them to organise pins by campaign, season, or theme.
GET — list collections
GET /collections HTTP/1.1 Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY → 200 [ { "id": 1, "name": "Summer", "pin_count": 5, ... } ]
POST — create a collection
POST /collections HTTP/1.1 Content-Type: application/json Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY { "name": "Summer Campaign" } → 201 { "id": 1, "name": "Summer Campaign", "pin_count": 0, ... }
GET — collection detail including pin IDs
GET /collections/1 HTTP/1.1 Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY → 200 { "id": 1, "name": "Summer", "pin_count": 3, "pin_ids": [12, 34, 56], ... }
PUT — rename
PUT /collections/1 HTTP/1.1 Content-Type: application/json Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY { "name": "Autumn Campaign" } → 200
DELETE — remove collection (pins are NOT deleted)
DELETE /collections/1 HTTP/1.1 Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY → 204 No Content
POST — add a pin (idempotent)
POST /collections/1/pins HTTP/1.1 Content-Type: application/json Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY { "pin_id": 42 } → 204
DELETE — remove a pin from collection
DELETE /collections/1/pins/42 HTTP/1.1 Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY → 204 No Content
Webhooks
Webhooks push real-time event notifications to a URL you control. PinFlow calls your endpoint each time a pin is created, scheduled, published, or fails.
Events
| Event | Fires when |
|---|---|
| pin.created | A new pin is created in PinFlow |
| pin.scheduled | A pin's scheduled_at is set |
| pin.published | A pin goes live on Pinterest |
| pin.failed | A pin exhausts all retries |
| Field | Type | Notes |
|---|---|---|
| url | string | required — must resolve to a public internet host (no localhost, no private IPs) |
| event | string | required — one of the four event names |
| secret | string | optional — used to sign deliveries with HMAC-SHA256 |
POST /webhooks HTTP/1.1 Host: pinflow.mirajpatterns.com Content-Type: application/json Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY { "url": "https://your-server.com/hooks/pinflow", "event": "pin.published", "secret":"a-random-string-you-pick" }
PinFlow sends a POST request to your URL with this shape:
POST /hooks/pinflow HTTP/1.1 Host: your-server.com Content-Type: application/json X-Webhook-Signature: sha256=a1b2c3d4e5f6... { "event": "pin.published", "timestamp": "2026-04-26T14:32:00Z", "data": { "pin_id": 42, "title": "Summer Recipe Ideas", "board_id": "123456789", "status": "published", "pinterest_pin_id": "987654321098765432" } }
Verify the signature
# Python import hmac, hashlib def verify(body: bytes, secret: str, header: str) -> bool: expected = "sha256=" + hmac.new( secret.encode(), body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, header)
GET /webhooks → 200 [ { "id": 1, "profile_id": 12, "pinterest_user_id": "...", "pinterest_username": "...", "url": "...", "event": "pin.published", "is_active": true } ] GET /webhooks/1 → 200 { single object } DELETE /webhooks/1 → 204 # Pause a webhook: PUT /webhooks/1 HTTP/1.1 Content-Type: application/json Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY { "is_active": false } → 200
API Keys
Manage the API keys used to authenticate requests. Keys are scoped to the authenticated user — you can only see and manage your own keys.
GET /api-keys HTTP/1.1 Host: pinflow.mirajpatterns.com Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY
Returns metadata only — secret keys are never returned after creation.
HTTP/1.1 200 OK Content-Type: application/json [ { "id": 1, "name": "n8n integration", "display_id": "pk_pinflow_a1b2c3d4e5f6g7h8", "is_active": true, "last_used_at": "2026-04-26T10:00:00Z", "created_at": "2026-04-20T08:00:00Z" } ]
| Field | Type | Notes |
|---|---|---|
| name | string | required — a label to identify this key (e.g. "n8n", "Zapier") |
POST /api-keys HTTP/1.1 Host: pinflow.mirajpatterns.com Content-Type: application/json Authorization: Bearer sk_pinflow_YOUR_EXISTING_KEY { "name": "n8n integration" }
HTTP/1.1 201 Created Content-Type: application/json { "id": 2, "name": "n8n integration", "display_id": "pk_pinflow_a1b2c3d4e5f6g7h8", "secret_key": "sk_pinflow_64hexcharactershere...", "is_active": true, "created_at": "2026-04-26T12:00:00Z" }
The secret_key is only returned here — copy it immediately. It cannot be retrieved again.
PATCH /api-keys/{id}/toggle — enable or disable a key
PATCH /api-keys/1/toggle HTTP/1.1 Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY → 200 { "id": 1, "is_active": false, ... }
DELETE /api-keys/{id} — permanently revoke a key
DELETE /api-keys/1 HTTP/1.1 Authorization: Bearer sk_pinflow_YOUR_SECRET_KEY → 204 No Content
Object schemas
Pin
| Field | Type | Description |
|---|---|---|
| id | integer | PinFlow ID |
| user_id | integer | Owner's PinFlow ID |
| board_id | string | Pinterest board ID |
| title | string | null | |
| description | string | null | |
| link | string | null | Destination URL |
| alt_text | string | null | |
| media_url | string | Image URL |
| status | string | See status lifecycle above |
| scheduled_at | datetime | null | ISO 8601 UTC |
| published_at | datetime | null | Set on successful publish |
| pinterest_pin_id | string | null | Pinterest's own ID, set after publish |
| error_message | string | null | Last error from Pinterest API |
| retry_count | integer | 0–3; at 3 → status becomes failed |
| sort_order | integer | Queue position; lower publishes first |
| created_at | datetime | ISO 8601 UTC |
Collection
| Field | Type | Description |
|---|---|---|
| id | integer | |
| name | string | Max 128 characters |
| pin_count | integer | Number of pins in this collection |
| pin_ids | integer[] | Only present on GET /collections/{id} |
| created_at | datetime |
Errors
All error responses return JSON. The detail field is either a plain string or an array of validation objects.
| Status | Meaning |
|---|---|
| 400 | Bad request — invalid input (e.g. scheduled_at in the past, private webhook URL) |
| 401 | Not authenticated — missing, invalid, or revoked API key (or no auth header sent) |
| 403 | Forbidden — account pending approval, or admin access required |
| 404 | Not found — resource doesn't exist or belongs to another user |
| 422 | Validation error — detail is an array (see below) |
| 429 | Rate limited — IP limit or monthly plan pin quota reached |
| 500 | Server error |
# Plain string error: { "detail": "Daily pin limit of 5 reached. Resets at midnight UTC." } # Validation error array (422): { "detail": [ { "loc": ["body", "board_id"], "msg": "Field required", "type": "missing" } ] }