API reference
Version 0.1.0. Generated from the schemas the routes parse, so this page and the API cannot disagree — a change to one that is not reflected in the other fails the build.
Authentication
Send your workspace key as a bearer token. A key beginning pn_test_ resolves to the sandbox environment, where
posts travel the whole pipeline and reach no timeline.
Authorization: Bearer pn_live_…
GET /v1/posts
Posts, newest first
Paged by creation instant rather than by offset, so publishing during paging cannot show a reader the same post twice while hiding another.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| status | query | string | |
| platform | query | string | |
| from | query | string | |
| to | query | string | |
| before | query | string | The `nextCursor` of the previous page. |
| limit | query | integer |
Responses
| 200 | A page of posts. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 500 | An internal error occurred. |
POST /v1/posts
Publish or schedule a post
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| Idempotency-Key | header | string | Replaying this key with the same body returns the original post. |
Request body
application/json. The schema is in the specification, which is the same
document the SDKs are generated from.
Responses
| 202 | Accepted, with per-destination status. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 409 | The request conflicts with what is already recorded. |
| 413 | The request body is larger than this route accepts. |
| 500 | An internal error occurred. |
POST /v1/media
Announce an upload and get a URL to put the bytes at
Three steps: announce, upload straight to storage with the URL returned here, then confirm. The URL is signed for exactly this size and type and expires in minutes.
Request body
application/json. The schema is in the specification, which is the same
document the SDKs are generated from.
Responses
| 201 | The asset, and where to put the bytes. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 413 | The request body is larger than this route accepts. |
| 500 | An internal error occurred. |
POST /v1/media/{assetId}/confirmation
Confirm an upload
Reads the file’s own header back and records what it actually is. An asset is not publishable until this succeeds, and a file that is not what it was declared to be is refused here rather than by a platform hours later.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| assetId* | path | string |
Responses
| 200 | The asset, now publishable. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 409 | Nothing was uploaded, or the asset was already settled. |
| 500 | An internal error occurred. |
GET /v1/media/{assetId}
Read an asset back
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| assetId* | path | string |
Responses
| 200 | The asset and what was found in it. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 500 | An internal error occurred. |
GET /v1/accounts
Where this workspace can post, and what each will accept
Call this before composing. The limits travel with the account because on a federated network two accounts on the same platform genuinely differ. An account that needs reconnecting is still listed, marked unavailable with the reason.
Responses
| 200 | Connected accounts with their live capabilities. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 500 | An internal error occurred. |
GET /v1/accounts/{accountId}
One connected account
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| accountId* | path | string |
Responses
| 200 | The account and its capabilities. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 500 | An internal error occurred. |
GET /v1/accounts/{accountId}/boards
The boards a Pinterest account may Pin to
Every Pin names a board, so this is what a caller asks before naming one. Refused with not_supported_on_platform for an account whose platform has no such list. An alias of PostPeer’s GET /v1/pinterest/boards.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| accountId* | path | string |
Responses
| 200 | The boards this account may Pin to. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 422 | This account’s platform does not offer this list. |
| 500 | An internal error occurred. |
GET /v1/accounts/{accountId}/creator-info
What a TikTok creator’s application audit currently permits
Privacy levels on offer, duet/stitch/comment state and the duration ceiling, read from the account rather than assumed. Refused with not_supported_on_platform for an account whose platform has no such audit. An alias of PostPeer’s GET /v1/tiktok/creator-info.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| accountId* | path | string |
Responses
| 200 | What this account may currently post. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 422 | This account’s platform does not offer this list. |
| 500 | An internal error occurred. |
GET /v1/subjects/{externalUserRef}
What PostNode holds about one of your end users
Row counts per table for one `external_user_ref`. Erasure is irreversible, so seeing what will go — and that it is the right subject — is worth one extra call.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| externalUserRef* | path | string |
Responses
| 200 | What erasure would remove. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 500 | An internal error occurred. |
DELETE /v1/subjects/{externalUserRef}
Erase everything PostNode holds about one end user
Removes the connected account and its tokens, the inbox conversations and messages, the media files and their rows, the request logs and webhook deliveries, and any post whose only destination was theirs. Files go before the rows that name them, and every table is re-counted afterwards: the receipt reports zero residual rows or the call fails. Irreversible, and safe to repeat.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| externalUserRef* | path | string |
Responses
| 200 | What was removed. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 409 | This deployment has no object storage and the subject owns files, so erasing the rows would leave the files behind. |
| 500 | An internal error occurred. |
GET /v1/posts/{postId}
One post, with each destination’s own status
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| postId* | path | string |
Responses
| 200 | The post and every destination. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 500 | An internal error occurred. |
POST /v1/posts/{postId}/retry
Send a dead-lettered post again
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| postId* | path | string |
Responses
| 200 | Per destination: reopened, or the reason it was left alone. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 409 | The request conflicts with what is already recorded. |
| 500 | An internal error occurred. |
POST /v1/posts/{postId}/schedule
Move a scheduled post
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| postId* | path | string |
Request body
application/json. The schema is in the specification, which is the same
document the SDKs are generated from.
Responses
| 200 | Per destination: moved, or why it was not. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 500 | An internal error occurred. |
DELETE /v1/posts/{postId}/schedule
Cancel a scheduled post
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| postId* | path | string |
Responses
| 200 | Per destination: cancelled, or why it was not. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 500 | An internal error occurred. |
GET /v1/analytics
Normalized metrics across platforms
Posts are dated by when they were published, and every post in the window is counted. Totals are built from each post’s latest reading, and every total states how many platforms reported it. A figure a platform does not measure is null, never zero.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| from | query | string | Defaults to thirty days before `to`. |
| to | query | string | Defaults to now. |
| platform | query | string | |
| accountId | query | string | |
| granularity | query | string | day · week · month |
Responses
| 200 | Totals and a series, both null-aware. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 500 | An internal error occurred. |
GET /v1/analytics/posts
Posts with their numbers, sortable by any of them
One row per post with its destinations beneath it. `source` is `postnode` for a post published here and `platform` for one found on the account, which is how a post made in the platform’s own app appears at all. Every figure is a stored reading: sorting never becomes a platform call. A figure no platform reported is null, never zero — so an unmeasured post ranks last rather than alongside one nobody engaged with.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| sortBy | query | string | Defaults to `date`. `engagement` is likes + comments + shares + saves. date · likes · comments · impressions · views · shares · saves · clicks · engagement |
| order | query | string | asc · desc |
| platform | query | string | |
| accountId | query | string | |
| fromDate | query | string | On the post’s own date, not the reading’s. |
| toDate | query | string | |
| source | query | string | postnode · platform |
| limit | query | integer | |
| page | query | integer | Exclusive with `cursor`; sending both is refused. |
| cursor | query | string | The `nextCursor` of the previous page. Preferred over `page`: a reading arriving mid-paging shifts every offset. |
Responses
| 200 | A page of posts, ranked. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 500 | An internal error occurred. |
GET /v1/analytics/instagram/account-insights
An Instagram account’s own daily figures
Reach, profile views and website clicks, one point per day, from the readings the daily account job already takes. An account with nothing captured yet falls back to one live call, at most once an hour. Answered for any account whose adapter publishes account insights — the route dispatches on that, not on the platform’s name; an account whose platform does not answers `409`.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| accountId* | query | string | |
| from | query | string | |
| to | query | string |
Responses
| 200 | A day series, and when it was captured. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 409 | The request conflicts with what is already recorded. |
| 500 | An internal error occurred. |
GET /v1/analytics/instagram/demographics
Who follows an Instagram account
Age, gender, country and city, as the platform cuts them. A cut the platform withholds — it does so below a hundred followers — comes back empty rather than failing the others.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| accountId* | query | string |
Responses
| 200 | Four breakdowns, and when they were captured. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 409 | The request conflicts with what is already recorded. |
| 500 | An internal error occurred. |
GET /v1/analytics/instagram/follower-stats
Follower count, and how it moved
The count answers “how many now” and never “how did that change”, so the daily deltas come with it, built from the readings the account job has been taking all along.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| accountId* | query | string | |
| from | query | string | |
| to | query | string |
Responses
| 200 | The latest count and the daily series. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 409 | The request conflicts with what is already recorded. |
| 500 | An internal error occurred. |
GET /v1/analytics/deliveries/{deliveryId}
One post’s readings over time
Every capture is kept, so growth is visible rather than a single figure being overwritten.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| deliveryId* | path | string |
Responses
| 200 | The capture history, oldest first. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 500 | An internal error occurred. |
GET /v1/conversations
Comment threads, most recent activity first
Comment threads only. Direct messages are their own surface under `/v1/messages` and their own scopes: a key that may moderate comments has not been told it may read somebody private conversation. An id of the wrong kind answers 404 rather than 403, so it cannot be used to discover that a private thread exists.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| platform | query | string | |
| accountId | query | string | |
| before | query | string | The `nextCursor` of the previous page. |
| limit | query | integer |
Responses
| 200 | A page of threads. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 500 | An internal error occurred. |
PATCH /v1/conversations/{id}
Archive or reopen a thread
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| id* | path | string |
Request body
application/json. The schema is in the specification, which is the same
document the SDKs are generated from.
Responses
| 200 | The thread, as it now stands. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 500 | An internal error occurred. |
GET /v1/conversations/{id}/messages
One thread, oldest message first
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| id* | path | string | |
| after | query | string | |
| before | query | string | Reach back past what PostNode keeps. Messages are partitioned by month and retained for a bounded window; a `before` earlier than that window is answered from the platform instead of the database and nothing is stored. The response `source` says which it was — a live read costs a platform call, is subject to that platform’s rate limits, and can disagree with what was stored if a message has since been deleted. |
| cursor | query | string | The platform’s own cursor, for paging a live read. |
| limit | query | integer |
Responses
| 200 | A page of messages, with `source` naming where they came from. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 500 | An internal error occurred. |
POST /v1/conversations/{id}/reply
Reply in a thread
A platform that does not let an application reply is refused with not_supported_on_platform at the moment of asking, rather than accepting a reply that could never be sent.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| id* | path | string |
Request body
application/json. The schema is in the specification, which is the same
document the SDKs are generated from.
Responses
| 201 | The reply was recorded and sent. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 413 | The request body is larger than this route accepts. |
| 422 | This platform does not accept replies from an application. |
| 500 | An internal error occurred. |
POST /v1/connect/bluesky/auth
Connect a Bluesky account with an app password
OAuth is the default and is what GET /v1/connect/bluesky starts; this exists because many Bluesky accounts live on servers whose operators have never registered this deployment's OAuth client. The password is used once and never stored: what is stored is the session it buys, which the person can revoke from their own settings without changing their password. An app password cannot be scoped and grants everything the account can do.
Request body
application/json. The schema is in the specification, which is the same
document the SDKs are generated from.
Responses
| 201 | The account is connected. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No Bluesky account answers to that handle. |
| 413 | The request body is larger than this route accepts. |
| 500 | An internal error occurred. |
| 502 | The server holding that account did not answer. |
GET /v1/comments
One post's comments, oldest first
Flat rather than nested: each comment carries its parent id, so a caller that wants the tree can build it and one that wants to read through in order is not made to walk it.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| accountId* | query | string | |
| postId* | query | string | The post as the platform identifies it. |
| after | query | string | |
| limit | query | integer |
Responses
| 200 | The post's comments. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 500 | An internal error occurred. |
POST /v1/comments
Comment on a post
Starts a thread under a post, which is not the same as replying inside one — a reply goes to /v1/conversations/{id}/reply, which has the thread the comment belongs to. A platform with no comment endpoint an application may use is refused at the moment of asking.
Request body
application/json. The schema is in the specification, which is the same
document the SDKs are generated from.
Responses
| 201 | The comment was created. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 409 | This platform has no comment endpoint PostNode can use. |
| 413 | The request body is larger than this route accepts. |
| 500 | An internal error occurred. |
DELETE /v1/comments/{commentId}
Delete a comment by its own id
The same action as the thread-shaped route, for a caller that holds a comment id and nothing else. A direct message is never found here.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| commentId* | path | string |
Responses
| 204 | Deleted. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 409 | This platform does not let an application delete comments. |
| 500 | An internal error occurred. |
POST /v1/comments/{commentId}/hide
Hide a comment by its own id
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| commentId* | path | string |
Responses
| 204 | Hidden. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 409 | This platform does not let an application hide comments. |
| 500 | An internal error occurred. |
GET /v1/messages
Direct threads, most recent first
Behind `messages:read`, which is not `inbox:read`: a comment is public and a message is not. Instagram only, through a connected Page.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| accountId | query | string | |
| platform | query | string | |
| before | query | string | |
| limit | query | integer |
Responses
| 200 | A page of threads. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 500 | An internal error occurred. |
POST /v1/messages
Send a direct message
The platform allows a reply within 24 hours of the last message received. A thread whose stored last inbound message has aged past that is refused here with the instant named; silence is never read as a refusal, and the platform own answer is the authority in every other case.
Request body
application/json. The schema is in the specification, which is the same
document the SDKs are generated from.
Responses
| 201 | The message was sent. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 409 | The reply window has closed, or this platform has no messaging PostNode can use. |
| 413 | The request body is larger than this route accepts. |
| 500 | An internal error occurred. |
GET /v1/messages/{conversationId}
One direct thread, oldest message first
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| conversationId* | path | string | |
| after | query | string | |
| limit | query | integer |
Responses
| 200 | A page of messages. |
| 400 | The request was not valid. |
| 401 | The bearer key was missing, unknown or malformed. |
| 403 | The key lacks the scope this route requires. |
| 404 | No such post, or none in this workspace. |
| 500 | An internal error occurred. |
GET /health
Liveness
Responses
| 200 | The service is running. |
GET /ready
Readiness, including the database
Responses
| 200 | The service and its database are reachable. |
| 503 | A dependency is unavailable. |