Appearance
Public API
The Public API provides programmatic access for third-party integrations and automation. It is split into two groups:
- Public API v1 —
/public/v1/*, authenticated with an API key or OAuth token. - Compatibility public routes —
/public/*outside/public/v1, kept for n8n/Zapier/Make compatibility where noted.
All v1 routes are org-scoped. Mutating routes support idempotency keys, and reads are rate-limited per org.
OpenAPI specification
The full machine-readable spec is committed at openapi.yml in the repository root, and a running instance serves the same document as browsable Swagger UI at /docs.
It is generated from the controllers — request and response schemas are inferred from the TypeScript types — so it tracks the code rather than drifting from it. CI fails the build if the committed file stops matching (.github/workflows/boot-guard.yml).
If you are contributing, do not hand-edit openapi.yml. Change the controllers or DTOs, then:
bash
pnpm run build:backend
pnpm run openapi:generateGenerating constructs the Nest application, so it needs DATABASE_URL, REDIS_URL, and JWT_SECRET in the environment. To improve a route's documentation, add @ApiOperation / @Api*Response decorators or JSDoc to the controller and regenerate.
Authentication
Pass the credential in the Authorization header as a raw string (no Bearer prefix):
http
Authorization: pm_live_xxxxxxxx...Two credential types are accepted:
| Type | Prefix | Where to create |
|---|---|---|
| API key | pm_live_ | Settings → Developers |
| OAuth access token | pos_ | OAuth app authorization flow |
API keys are per-user, per-org. The resolved org and user's role are attached to the request, so RBAC and billing gates still apply. OAuth tokens are constrained to the scopes the user approved (mcp:read, mcp:posts:write).
When billing is enabled (billingEnabled() — at least one payment provider configured), the org must have an active subscription; otherwise the request returns 401.
Rate limiting
Public v1 routes are rate-limited by API_LIMIT (default 600 requests/hour per org). Sensitive endpoints carry tighter @Throttle overrides, for example:
POST /oauth/token— 20/minGET /public/v1/analytics/overview— 60/minPOST /public/t— 60/min
Pagination
GET /public/v1/posts returns at most 100 posts per call (also the default page size). The response is always the paged shape { posts, cursor }; use the cursor offset for paging. cursor is null on the last page.
| Param | Description |
|---|---|
?limit= | 1–100; defaults to 100, hard-capped at 100. |
?cursor= | Opaque offset returned by the previous page. |
Idempotency
Mutating v1 endpoints accept an optional Idempotency-Key header. Repeating the same key within 24 hours replays the first response instead of re-running the mutation. Keys are scoped per-org, so the same string from a different org is independent. Supported on:
POST /public/v1/postsPOST /public/v1/uploadPOST /public/v1/upload-from-urlDELETE /public/v1/posts/:idDELETE /public/v1/posts/group/:groupDELETE /public/v1/integrations/:id
If Redis is unavailable the header is ignored and the request proceeds normally. A concurrent duplicate returns 409.
Posts
Base: /public/v1
| Method | Path | Purpose |
|---|---|---|
| POST | /public/v1/posts | Create a post |
| GET | /public/v1/posts | List posts |
| DELETE | /public/v1/posts/:id | Delete a post by post ID (resolves group) |
| DELETE | /public/v1/posts/group/:group | Delete a post by group ID |
| PUT | /public/v1/posts/:id/status | Change post status |
| PUT | /public/v1/posts/:id/release-id | Update external release ID |
| GET | /public/v1/posts/:id/missing | Get missing content to fill for a post |
| GET | /public/v1/find-slot/:id | Find next free posting time slot |
Creating or scheduling a post validates that every referenced channel is connected and does not need reauthentication. Drafts are allowed on disabled channels so they can be reconnected before promotion.
POST /public/v1/posts is gated on api:create and posts_per_month:create billing policies.
Media
Base: /public/v1
| Method | Path | Purpose |
|---|---|---|
| POST | /public/v1/upload | Upload a media file (multipart/form-data, field file) |
| POST | /public/v1/upload-from-url | Import media from a URL |
| POST | /public/v1/generate-video | Generate AI video |
| GET | /public/v1/generate-video/:id | Poll an async video generation job |
| POST | /public/v1/video/function | Call a provider tool, currently only loadVoices |
POST /public/v1/generate-video accepts type (text-to-video, image-to-video, video-to-video) and provider params in customParams. Both the POST and the polling GET return the same shape: { id, status, artifactUrl, provider, error }.
- If the artifact is returned synchronously,
idisnull,statusiscompleted, andartifactUrlis the URL. - If a job is queued,
idis the job id andstatusispending; pollGET /public/v1/generate-video/:idwith it. The poll normalizes any non-terminal provider status topending. - Terminal statuses are
completed(withartifactUrl) andfailed(witherrorset to the failure reason).
Integrations
Base: /public/v1
| Method | Path | Purpose |
|---|---|---|
| GET | /public/v1/integrations | List connected integrations |
| DELETE | /public/v1/integrations/:id | Delete a channel integration |
| GET | /public/v1/integration-settings/:id | Get integration rules, max length, settings schema, tools |
| GET | /public/v1/social/:integration | Get OAuth authorization URL for a provider |
| POST | /public/v1/integration-trigger/:id | Trigger a dynamic provider tool |
| GET | /public/v1/is-connected | Check whether the org has any active integration |
GET /public/v1/social/:integration requires an explicit provider version:
- Pass
providerId@versionas the path param (e.g.x@v1). - Or pass
?version=v1with a bare provider id. - A bare id without a version is rejected.
- An unknown or retired version returns
404or410.
Analytics
Base: /public/v1/analytics
The full analytics surface is public — the same routes serve API-key/OAuth clients (throttled 60/min per route, no RBAC/entitlement gate) and the dashboard (session cookie, RBAC + entitlement enforced, x-csrf-token required on mutations). See Analytics API for parameters and response details.
| Method | Path | Purpose |
|---|---|---|
| GET | /public/v1/analytics/overview | Org overview with optional date range |
| GET | /public/v1/analytics/channel/:integrationId | Single channel analytics |
| GET | /public/v1/analytics/channel/:integrationId/metric/:metric | Specific metric for a channel |
| GET | /public/v1/analytics/campaign/:id | Campaign-scoped analytics |
| GET | /public/v1/analytics/posts | Paginated post list with metrics |
| GET | /public/v1/analytics/post/:postId | Single post detail |
| GET | /public/v1/analytics/metric/:metric | Metric detail across date range |
| GET | /public/v1/analytics/day | Day-level detail |
| GET | /public/v1/analytics/best-time | Best-time-to-post heatmap |
| GET | /public/v1/analytics/recommendations | AI-powered recommendations |
| GET | /public/v1/analytics/health | Data-health panel |
| POST | /public/v1/analytics/refresh/:integrationId | On-demand live channel refresh (6/hour) |
| GET | /public/v1/analytics/content-insights | Content-attribute intelligence |
| POST | /public/v1/analytics/narrate | LLM-narrated summary |
| GET | /public/v1/analytics/export | CSV/JSON export |
| GET | /public/v1/analytics/anomalies | Detected anomalies for the org |
| POST | /public/v1/analytics/anomalies/:id/dismiss | Dismiss an anomaly |
| GET/POST/PUT/DELETE | /public/v1/analytics/alert-rules* | User-defined alert rules CRUD |
| GET/POST/DELETE | /public/v1/analytics/share | Org public share-link management |
| GET/POST/PUT/DELETE | /public/v1/analytics/watchlist* | Watched accounts CRUD + /:id/series |
| GET | /public/v1/analytics/shortlinks | Short-link aggregate stats |
| GET | /public/v1/analytics/shortlinks/timeseries | Short-link click time series |
GET /public/v1/analytics/overview validates from/to, requires to >= from, and caps the window at 400 days.
Notifications
Base: /public/v1
| Method | Path | Purpose |
|---|---|---|
| GET | /public/v1/notifications | Get paginated org notifications |
Compatibility public routes
Base: /public
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /public/t | None | Track analytics/behaviour event |
| POST | /public/modify-subscription | JWT | Modify subscription billing |
| GET | /public/stream | None | Proxy-stream an external MP4 (SSRF-safe) |
| GET | /public/integrations/list | None | Anonymous integrations catalogue (see below) |
Integrations catalogue
GET /public/integrations/list[?domain=<social|comms|ai|media|storage|shortlink|vpn>] is the source of truth for "what does Postmill integrate with": every registered provider in the seven product-facing domains, with counts, capability flags, descriptions, website and an absolute icon URL. The marketing site reads it instead of hardcoding provider tables. No session, API key or CSRF token is involved; the response is Cache-Control: public, max-age=300, stale-while-revalidate=3600 with a weak ETag and Access-Control-Allow-Origin: *, so it can be fetched from any origin (without credentials). Throttled at 120 requests/minute per IP.
json
{
"generatedAt": "2026-09-15T16:28:31.012Z",
"total": 166,
"domains": [
{
"id": "social",
"label": "Channels",
"count": 45,
"capabilityKeys": ["analytics", "comments", "firstComment", "video", "..."],
"providers": [
{
"id": "x",
"domain": "social",
"name": "X",
"description": { "en": "X (formerly Twitter) — real-time public posting and conversation." },
"website": "https://x.com",
"icon": "https://app.postmill.ai/icons/platforms/x.png",
"capabilities": ["analytics", "comments", "firstComment", "poll", "video", "linkPreview", "refreshToken", "watchlist"],
"maxMedia": 4,
"beta": false,
"featured": false,
"editor": "normal",
"authType": "oauth1",
"selfHosted": false,
"web3": false,
"chromeExtension": false,
"comments": { "read": true, "reply": true, "like": true },
"mentions": true,
"autoPlugs": true
}
]
}
]
}capabilitieslists the keys that aretruein the provider's manifest capability object; each domain'scapabilityKeysis the union in first-seen order (a ready-made matrix legend).idis the kernelproviderId(storage ids are lower-snake, e.g.backblaze_b2).kind(direct|hub|action) is set for AI/media providers,storageKind(built-in|proprietary|s3-compatible) for storage,mediaCategoriesfor media; theeditor…autoPlugsblock is channels-only.betamirrors the in-app Beta badge (built without live-key verification);featured/featuredSortOrderare the super-admin curated flags.- Icons: channels and comms apps point at the frontend's
/icons/platforms/<id>.pngset (YouTube is.svg); every other domain points at/icons/providers/<id>.svg, exported from the app'sProviderIconglyph map bynode tools/icons/export-provider-icons.mjs(a backend spec fails CI when a registered provider has no file). - Deliberately not included: provider versions, status, credential fields, setup notes and sunset dates — anything that fingerprints the deployment's exact release. That detail stays on the authenticated
GET /providers/catalog. Theauth,email,contentpackandpaymentsdomains are deployment plumbing and are not listed.
Internal integrations API
These routes sit under /integrations and are authenticated via cookie session. Mutating routes require CSRF protection and the appropriate RBAC permission.
Integration management
| Method | Path | Permission | Purpose |
|---|---|---|---|
| POST | /integrations/provider/:id/connect | channels:create | Save a provider page after two-step auth |
| GET | /integrations/:identifier/internal-plugs | — | Get internal plug definitions for a provider |
| GET | /integrations/list | — | Full integration list with capabilities |
| POST | /integrations/:id/settings | channels:update | Update provider additional settings |
| POST | /integrations/:id/nickname | channels:update | Set nickname and/or avatar |
| GET | /integrations/social/:integration | channels:create | Generate OAuth authorization URL |
| POST | /integrations/:id/time | channels:update | Configure posting time slots |
| POST | /integrations/mentions | channels:update | Search @mentions across providers |
| POST | /integrations/function | channels:update | Call a dynamic provider function |
| POST | /integrations/disable | channels:update | Disable a channel |
| POST | /integrations/enable | channels:update | Enable a channel |
| DELETE | /integrations/ | channels:delete | Delete a channel |
GET /integrations/social/:integration accepts ?campaign=<uuid> to auto-tag a newly connected channel onto that campaign.
Plugs
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET | /integrations/plug/list | — | List all available plug definitions |
| GET | /integrations/:id/plugs | — | Get plugs configured for an integration |
| POST | /integrations/:id/plugs | channels:create | Create or update plug configuration |
| PUT | /integrations/plugs/:id/activate | channels:update | Toggle plug active/inactive |
Provider-specific
| Method | Path | Purpose |
|---|---|---|
| GET | /integrations/telegram/updates | Poll Telegram bot for connect message |
| POST | /integrations/moltbook/register | Register a Moltbook agent |
| GET | /integrations/moltbook/status | Check Moltbook agent claim status |
No-auth integration endpoints
Base: /integrations
Used during OAuth callbacks and public provider-page connection.
| Method | Path | Purpose |
|---|---|---|
| GET | /integrations/ | All integration definitions (provider registry) |
| POST | /integrations/social-connect/:integration | OAuth callback handler |
| POST | /integrations/public/provider/:id/connect | Save provider page (public, state-gated) |
| POST | /integrations/extension-refresh | Refresh Chrome extension cookies |
Errors
Common status codes:
400— Validation error, missing fields, or malformed dates.401— Missing or invalid API key/OAuth token, or no active subscription.403— Insufficient OAuth scope or RBAC permission.404— Unknown resource.409— Idempotency conflict.410— Requested provider version has been retired.429— Rate limit exceeded.
Verified against v1.3.0