diff --git a/AGENTS.md b/AGENTS.md index fea5a68..65e1293 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -113,11 +113,11 @@ The same applies to anything a self-hoster can switch off or raise: platform ava ### Endpoints, shapes and pagination -`openapi.json` is the source of truth for every operation, request field, response shape and error (hand-written from the app's `routes/api.php`, `app/Http/Requests/Api/**`, `app/Http/Resources/**`, `app/Http/Controllers/Api/**`). Public API = routes whose URI starts with `api/` (83 today: `php artisan route:list --path=api`). Edit the spec, not generated pages. `api-reference/introduction.mdx` covers auth, errors, pagination, rate limits and timestamps; `api-reference/guides/posting.mdx` covers `platforms[].meta` per network, scheduling modes and thread replies; `api-reference/guides/media-uploads.mdx` covers media. Update those, not this file. Facts to keep in mind: +`openapi.json` is the source of truth for every operation, request field, response shape and error (hand-written from the app's `routes/api.php`, `app/Http/Requests/Api/**`, `app/Http/Resources/**`, `app/Http/Controllers/Api/**`). Public API = routes whose URI starts with `api/` (83 today: `php artisan route:list --path=api`). Edit the spec, not generated pages. `api-reference/introduction.mdx` covers auth, errors, pagination, rate limits and timestamps; `api-reference/guides/posting.mdx` covers `meta` per network, scheduling modes and thread replies; `api-reference/guides/media-uploads.mdx` covers media. Update those, not this file. Facts to keep in mind: - Resources are unwrapped (`JsonResource::withoutWrapping()`); lists use the Laravel envelope `{ data, links, meta }` with the page size from `config('app.pagination.default')` (25 today). Always tell readers to read `meta.per_page`. - `PUT /posts/{post}` with `status: publishing` publishes now; there is no separate publish endpoint. -- `platforms[].meta` rules live only in the app's `App\Support\PostPlatformMetaRules`; document them from there. Unknown keys are dropped; on update `meta` is merged and `null` clears a nullable key. Boolean meta keys do not accept `null`; send `true` or `false`. +- Post `meta` rules live only in the app's `App\Support\PostPlatformMetaRules`; document them from there. Unknown keys are dropped; on update `meta` is merged and `null` clears a nullable key. Boolean meta keys do not accept `null`; send `true` or `false`. - There is no asset-library endpoint and no account toggle in the REST API. - Enum values (statuses, content types, webhook events, platforms) are documented in `openapi.json` schemas; copy them from the app enums under `app/Enums/**`, never from memory. @@ -157,6 +157,7 @@ The full table (17 entries, including analytics, retention and Google Business r - Upload limits: `upload_max_filesize=1G`, `post_max_size=1G` (matches the Docker image) - Bluesky and Mastodon work without API credentials; every other network needs developer app credentials (Google Business Profile has its own Google client, separate from YouTube) - Upgrading an existing install to 2.0 needs `php artisan release:trypost-2 --force --include-unsubscribed` after `migrate --force` (self-hosted has no Stripe subscription) +- The release after 2.0 (one channel per post) removes `release:trypost-2`; installs on v1.1.0 must upgrade to v2.0.0 and run it before upgrading further, or the first new migration stops ## Supported Languages diff --git a/ai/tools-reference.mdx b/ai/tools-reference.mdx index 8a00cb6..2c478d4 100644 --- a/ai/tools-reference.mdx +++ b/ai/tools-reference.mdx @@ -56,20 +56,20 @@ A member who needs approval can create and edit posts. When they schedule, queue | Tool | What it does | Key arguments | |------|--------------|---------------| -| `list-posts-tool` | List posts, newest scheduled date first. `published` includes partially published posts. Read-only. REST: [List posts](/api-reference/posts/list-posts) | `status` (`draft` \| `scheduled` \| `pending_approval` \| `published` \| `failed`), `search` (substring of the text), `channels[]`, `labels[]`, `untagged`, `page` | -| `get-post-tool` | Get one post: text, media, labels, status, `schedule_mode`, `scheduled_at`, `published_at`, recurrence, approval fields, `origin`, `post_group_id`, and its channel's `content_type`, `meta`, status, error and `platform_url`. Read-only. REST: [Get post](/api-reference/posts/get-post) | `post_id` (required) | -| `create-post-tool` | Create one post on one channel: a draft, a post at a custom time, a queue post (next or top, or one exact free slot), or a post published now. Returns the post. REST: [Create post](/api-reference/posts/create-post) | `platforms[]` (required, exactly one `{social_account_id, content_type?, meta?}`), `content`, `media[]`, `status` (`draft` default \| `scheduled` \| `publishing`), `scheduled_at`, `queue` (`next` \| `top`), `queue_slot`, `label_ids[]` | +| `list-posts-tool` | List posts, newest scheduled date first. Read-only. REST: [List posts](/api-reference/posts/list-posts) | `status` (`draft` \| `scheduled` \| `pending_approval` \| `published` \| `failed`), `search` (substring of the text), `channels[]`, `labels[]`, `untagged`, `page` | +| `get-post-tool` | Get one post: text, media, labels, status, `schedule_mode`, `scheduled_at`, `published_at`, recurrence, approval fields, `origin`, `post_group_id`, and its channel at the top level: `publish_status`, `platform`, `content_type`, `meta`, `error_message`, `platform_url` and `social_account`. Read-only. REST: [Get post](/api-reference/posts/get-post) | `post_id` (required) | +| `create-post-tool` | Create one post on one channel: a draft, a post at a custom time, a queue post (next or top, or one exact free slot), or a post published now. Returns the post. REST: [Create post](/api-reference/posts/create-post) | `social_account_id` (required), `content_type`, `meta`, `content`, `media[]`, `status` (`draft` default \| `scheduled` \| `publishing`), `scheduled_at`, `queue` (`next` \| `top`), `queue_slot`, `label_ids[]` | | `create-posts-tool` | Create one independent post per destination, as the composer does with several channels; the posts share a `post_group_id`. Each destination may override text, media, type and meta. No `queue_slot` here. Returns `{posts}`. REST: [Create posts in batch](/api-reference/posts/create-posts-in-batch) | `status` (required), `destinations[]` (required: `{social_account_id, content_type?, content?, media?, meta?}`), `content`, `media[]`, `scheduled_at`, `queue`, `label_ids[]` | | `update-post-tool` | Change a post's text, media (replaces the list), `content_type`, `meta` (merged; `null` clears a key, `thread_replies` replaces the list), status, schedule, queue or labels. The channel itself is fixed. On Pinterest and TikTok, new media re-decides the type when `content_type` is omitted. REST: [Update post](/api-reference/posts/update-post) | `post_id` (required), `content`, `media[]`, `content_type`, `meta`, `status` (`draft` \| `scheduled` \| `publishing`), `scheduled_at`, `queue`, `label_ids[]` | | `publish-post-tool` | Publish a draft or scheduled post now, at `scheduled_at`, or into the queue. Validated like a scheduled post (text limits, required meta, media rules, thread replies); a failure names the platform and field and nothing is published. Destructive. No single REST operation: REST uses [Update post](/api-reference/posts/update-post) with a status | `post_id` (required), `scheduled_at`, `queue` | | `approve-post-tool` | Approve a `pending_approval` post. By default it is scheduled as its author asked (queue or requested time); pass a new time or publish now instead. One of them is required when the requested time has passed or the author asked to publish now. Pass one of `scheduled_at` (future, before 2038-01-19) or `publish_now`, not both. Destructive. REST: [Approve post](/api-reference/approvals/approve-post) | `post_id` (required), `scheduled_at`, `publish_now` | | `reject-post-tool` | Reject a `pending_approval` post. It goes back to drafts and the member who asked is emailed; no reason is stored. REST: [Reject post](/api-reference/approvals/reject-post) | `post_id` (required) | -| `preview-post-tool` | Show the exact text each network gets (X links defused when that is on), next to the original, with length stats. Nothing is truncated. Read-only. REST: [Get post preview](/api-reference/posts/get-post-preview) | `post_id` (required) | +| `preview-post-tool` | Show the exact text the post's network gets (X links defused when that is on), next to the original, with length stats. One object, with `platform` and `content_type` at the top. Nothing is truncated. Read-only. REST: [Get post preview](/api-reference/posts/get-post-preview) | `post_id` (required) | | `delete-post-tool` | Delete a post from TryPost permanently. It is never removed from the network. Returns `{deleted: true}`. Destructive. REST: [Delete post](/api-reference/posts/delete-post) | `post_id` (required) | | `attach-media-from-url-tool` | Download files from public URLs and append them to the post. Returns `post`, `attached_count`, `failed_urls` and `failures[]` with a reason (`unreachable`, `type_not_allowed`, `too_large`, `host_not_allowed`). REST: [Attach media from URL](/api-reference/media-and-uploads/attach-media-from-url) | `post_id` (required), `urls[]` (required, 1–10 `{url, alt?}`) | | `request-media-upload-tool` | Issue a one-time signed URL to upload a local file. Returns `upload_token`, `upload_url`, `expires_at`, `max_bytes`, `max_bytes_by_type` and `field_name` (`media`). Upload with `curl -F media=@file ""`. REST: [Upload to signed URL](/api-reference/media-and-uploads/upload-to-signed-url) | none | | `attach-media-from-upload-tool` | Append an uploaded file to a post. Returns `{post}`. REST: [Attach media from upload](/api-reference/media-and-uploads/attach-media-from-upload) | `post_id` (required), `upload_token` (required), `alt` | -| `get-post-metrics-tool` | Read the latest saved analytics of a published post per platform (reactions, comments, saves, reach, views, watch time where supported). Values may lag the network until the next analytics run; excluded or unpublished platforms answer `unsupported`. Read-only. REST: [Get post metrics](/api-reference/posts/get-post-metrics) | `post_id` (required) | +| `get-post-metrics-tool` | Read the latest saved analytics of a published post (reactions, comments, saves, reach, views, watch time where supported). Returns one object: `post_id`, `platform`, `publish_status`, `platform_post_id`, `platform_url` and `metrics`. Values may lag the network until the next analytics run; an unpublished post or a network without analytics answers `unsupported`. Read-only. REST: [Get post metrics](/api-reference/posts/get-post-metrics) | `post_id` (required) | | `list-post-notes-tool` | List a post's notes (internal team comments, never published), newest first. Read-only. REST: [List post notes](/api-reference/post-notes/list-post-notes) | `post_id` (required), `page` | | `create-post-note-tool` | Add a note. Every other member who has note emails on is emailed; on a pending post, only the approvers and the member who asked. REST: [Create post note](/api-reference/post-notes/create-post-note) | `post_id` (required), `body` (required, max 2000) | | `update-post-note-tool` | Edit a note. Only its author can. No email is sent. REST: [Update post note](/api-reference/post-notes/update-post-note) | `post_id`, `note_id`, `body` (all required) | @@ -77,7 +77,7 @@ A member who needs approval can create and edit posts. When they schedule, queue | `set-post-recurrence-tool` | Make a scheduled post repeat every `interval` days, weeks, months or years, `times` more times, at the same local time in the channel's zone. The last occurrence must be on or before 2037-12-31 23:59:59 UTC. Members who publish directly only. REST: [Set post recurrence](/api-reference/recurrence/set-post-recurrence) | `post_id`, `interval` (1–365), `frequency` (`day` \| `week` \| `month` \| `year`), `times` (1–100), all required | | `clear-post-recurrence-tool` | Stop a post from repeating. The post and its next time stay. REST: [Remove post recurrence](/api-reference/recurrence/remove-post-recurrence) | `post_id` (required) | -Posts that are `publishing`, `published`, `partially_published` or `failed` cannot be changed or deleted. Notes and recurrence follow [Recurring posts](/knowledge-base/publish/recurring-posts). +Posts that are `publishing`, `published` or `failed` cannot be changed or deleted. Notes and recurrence follow [Recurring posts](/knowledge-base/publish/recurring-posts). ### Before a post can be scheduled or published @@ -220,7 +220,7 @@ Any member can manage signatures. A signature is never added to a post on its ow ## Webhooks -Admins only. Each delivery is a JSON `POST` signed with HMAC-SHA256 of the body; the hex digest is in `X-Webhook-Signature`. After 5 failed deliveries in a row the webhook is paused and the account owner is emailed. Events: `post.created`, `post.scheduled`, `post.unscheduled`, `post.published`, `post.partially_published`, `post.failed`, `post.deleted`. See [Webhooks](/knowledge-base/settings/webhooks). +Admins only. Each delivery is a JSON `POST` signed with HMAC-SHA256 of the body; the hex digest is in `X-Webhook-Signature`. After 5 failed deliveries in a row the webhook is paused and the account owner is emailed. Events: `post.created`, `post.scheduled`, `post.unscheduled`, `post.published`, `post.failed`, `post.deleted`. See [Webhooks](/knowledge-base/settings/webhooks). | Tool | What it does | Key arguments | |------|--------------|---------------| diff --git a/api-reference/guides/media-uploads.mdx b/api-reference/guides/media-uploads.mdx index 60a381e..075e221 100644 --- a/api-reference/guides/media-uploads.mdx +++ b/api-reference/guides/media-uploads.mdx @@ -47,9 +47,8 @@ curl -X POST https://app.trypost.it/api/posts \ { "upload_token": "4f6c2b1e-8d3a-4e5f-9a7b-1c2d3e4f5a6b" }, { "id": "9d3c1f30-8a9b-4c0d-9e1f-2a3b4c5d6e7f" } ], - "platforms": [ - { "social_account_id": "7a2e9c14-5b8d-4f3a-b6e1-0c9d8a7f6e52", "content_type": "instagram_feed" } - ] + "social_account_id": "7a2e9c14-5b8d-4f3a-b6e1-0c9d8a7f6e52", + "content_type": "instagram_feed" }' ``` @@ -186,4 +185,4 @@ Each media item takes optional settings: ## Which posts take media -Media can be added and replaced until the post goes out. Posts that are `publishing`, `published`, `partially_published` or `failed` cannot take media: the API answers `422` with `This post has already been processed and cannot be re-published. Duplicate it to try again.` +Media can be added and replaced until the post goes out. Posts that are `publishing`, `published` or `failed` cannot take media: the API answers `422` with `This post has already been processed and cannot be re-published. Duplicate it to try again.` diff --git a/api-reference/guides/posting.mdx b/api-reference/guides/posting.mdx index 3d8e7ec..5feaa88 100644 --- a/api-reference/guides/posting.mdx +++ b/api-reference/guides/posting.mdx @@ -57,7 +57,7 @@ The fields you need for posting: | Field | Use | |---|---| -| `id` | The `social_account_id` of the post's destination | +| `id` | The post's `social_account_id` | | `platform` | The network, which decides the content types and settings below | | `status` | Only `connected` accounts publish. `disconnected` and `token_expired` accounts need to be reconnected in the app | | `max_content_length` | The text limit of this account. X accounts with long posts (`long_posts: true`) get 25000 instead of 280 | @@ -91,7 +91,7 @@ When you omit `content_type`, it is picked like the app does: on Pinterest a vid ### Required settings -Each destination carries its network's settings in `meta`. A draft can be saved without them; scheduling or publishing needs them: +A post carries its network's settings in `meta`. A draft can be saved without them; scheduling or publishing needs them: | Network | Required in `meta` | Where to get the value | |---|---|---| @@ -101,11 +101,11 @@ Each destination carries its network's settings in `meta`. A draft can be saved | YouTube | A title: `title`, or the first line of the text | Up to 100 characters, without `<` or `>` | | Google Business | `event` with `title`, `start_date` and `end_date` when `topic_type` is `EVENT` or `OFFER`; a `call_to_action` needs a `url` unless its `action_type` is `NONE` or `CALL` | — | -Every other setting is optional: link previews, AI labels, YouTube privacy and category, Mastodon content warnings, Threads topics, and more. The full list per network is under `platforms[].meta` on [Create post](/api-reference/posts/create-post). Keys TryPost does not know are dropped; settings of another network are accepted but not used. +Every other setting is optional: link previews, AI labels, YouTube privacy and category, Mastodon content warnings, Threads topics, and more. The full list per network is under `meta` on [Create post](/api-reference/posts/create-post). Keys TryPost does not know are dropped; settings of another network are accepted but not used. ## 3. Create the post -[Create post](/api-reference/posts/create-post) creates one post on **one** channel: `platforms` takes exactly one entry. Without `status`, the post is saved as a draft. +[Create post](/api-reference/posts/create-post) creates one post on **one** channel: send its `social_account_id`, and its `content_type` and `meta` next to it at the top level. Without `status`, the post is saved as a draft. A `platforms` field is refused with `422`. ```bash curl -X POST https://app.trypost.it/api/posts \ @@ -117,20 +117,16 @@ curl -X POST https://app.trypost.it/api/posts \ "media": [ { "url": "https://example.com/autumn.jpg", "alt": "Three jackets on a rail" } ], - "platforms": [ - { - "social_account_id": "2b7e4c19-8f3a-4d6b-9c1e-5a0f7d2b8e63", - "content_type": "pinterest_pin", - "meta": { - "board_id": "1084231546543210987", - "link": "https://example.com/autumn" - } - } - ] + "social_account_id": "2b7e4c19-8f3a-4d6b-9c1e-5a0f7d2b8e63", + "content_type": "pinterest_pin", + "meta": { + "board_id": "1084231546543210987", + "link": "https://example.com/autumn" + } }' ``` -The response is the post (`201`), with `status: "draft"` and its destination under `platforms[]`. Media can come from a URL, an earlier upload or another post; see [Media uploads](/api-reference/guides/media-uploads). +The response is the post (`201`), with `status: "draft"`, `publish_status: "pending"` and its channel at the top level: `platform`, `content_type`, `meta` and `social_account`. Media can come from a URL, an earlier upload or another post; see [Media uploads](/api-reference/guides/media-uploads). ### Several channels at once @@ -214,7 +210,7 @@ curl -X PUT https://app.trypost.it/api/posts/9d3c1f2a-6b4e-4c8a-9f1e-2a7b5c8d0e4 -d '{ "status": "publishing" }' ``` -The response comes back with `status: "publishing"` while the network receives the post. It then becomes `published` or `failed`; `platforms[].platform_url` links to the live post and `platforms[].error_message` says what went wrong. Poll [Get post](/api-reference/posts/get-post), or subscribe a [webhook](/api-reference/webhooks/create-webhook) to `post.published` and `post.failed`. +The response comes back with `status: "publishing"` while the network receives the post. It then becomes `published` or `failed`. The network's side is in `publish_status` (`pending`, `publishing`, `retrying`, `pending_review`, `published`, `failed` or `rejected`); `platform_url` links to the live post and `error_message` says what went wrong. Poll [Get post](/api-reference/posts/get-post), or subscribe a [webhook](/api-reference/webhooks/create-webhook) to `post.published` and `post.failed`. Posts created through the API publish directly: API keys belong to workspace admins, so no approval step applies (see [Approvals](/api-reference/introduction#approvals)). @@ -227,21 +223,17 @@ X, Bluesky and Mastodon posts can carry up to **24** replies, published one unde "content": "1/ We rebuilt scheduling from scratch. Here is what changed.", "status": "scheduled", "queue": "next", - "platforms": [ - { - "social_account_id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", - "content_type": "x_post", - "meta": { - "thread_replies": [ - { "text": "2/ Every channel now has its own posting times." }, - { - "text": "3/ Full changelog below.", - "media": [{ "url": "https://example.com/changelog.png" }] - } - ] + "social_account_id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", + "content_type": "x_post", + "meta": { + "thread_replies": [ + { "text": "2/ Every channel now has its own posting times." }, + { + "text": "3/ Full changelog below.", + "media": [{ "url": "https://example.com/changelog.png" }] } - } - ] + ] + } } ``` @@ -256,18 +248,18 @@ If publishing stops halfway, replies already live are kept, and a retry continue Network limits are checked when a post is scheduled or published; drafts can go over them. The 25000-character ceiling applies to every post, drafts included. -- **Text per account.** The text must fit the destination account's `max_content_length`, measured on the text the network receives. An X account with long posts takes 25000 characters; other X accounts take 280. An emoji counts as one character. +- **Text per account.** The text must fit the account's `max_content_length`, measured on the text the network receives. An X account with long posts takes 25000 characters; other X accounts take 280. An emoji counts as one character. - **Instagram hashtags.** Instagram takes at most **5** hashtags per post. - **Stories are captionless.** `instagram_story` and `facebook_story` send no text, so their text is neither published nor measured, and the hashtag cap does not apply. - **Media.** Each content type has its own media count, size, duration and aspect ratio rules, listed by [List content types](/api-reference/platform/list-content-types) and on each network's page, for example [Instagram](/platforms/instagram), [X](/platforms/x-twitter) or [TikTok](/platforms/tiktok). - [Get post preview](/api-reference/posts/get-post-preview) returns the text each destination will receive (`sanitized_content`), its length and the account's limit. On X, TryPost Cloud writes links as `example(.)com` to keep the post out of X's link pricing (see [Links in posts](/platforms/x-twitter#links-in-posts)), and the preview shows that text. + [Get post preview](/api-reference/posts/get-post-preview) returns the text the post's channel will receive (`sanitized_content`), its length and the account's limit. On X, TryPost Cloud writes links as `example(.)com` to keep the post out of X's link pricing (see [Links in posts](/platforms/x-twitter#links-in-posts)), and the preview shows that text. ## Editing, deleting and errors -- [Update post](/api-reference/posts/update-post) changes only the fields you send. The channel is fixed; to post elsewhere, create a new post. Send the destination's settings as top-level `content_type` and `meta`, or as `platforms[]` with the destination `id`. `meta` is merged with the stored settings; send `null` for a key to clear it. Boolean settings cannot be cleared: send `true` or `false`. -- Posts that are `publishing`, `published`, `partially_published` or `failed` cannot be edited or deleted: the API answers `422`. +- [Update post](/api-reference/posts/update-post) changes only the fields you send. The channel is fixed; to post elsewhere, create a new post. Send the settings as top-level `content_type` and `meta` (`platforms` is refused). `meta` is merged with the stored settings; send `null` for a key to clear it. Boolean settings cannot be cleared: send `true` or `false`. +- Posts that are `publishing`, `published` or `failed` cannot be edited or deleted: the API answers `422`. - [Delete post](/api-reference/posts/delete-post) removes the post from TryPost only. It never deletes anything already published on a network. -- Validation errors are keyed by field. Errors on a destination's settings, text, or media fit may be reported under `destinations.0.…` (for example `destinations.0.meta.board_id`) even when you sent `platforms`. +- Validation errors are keyed by field: `meta.board_id`, `content_type`, `content`, `media` on [Create post](/api-reference/posts/create-post) and [Update post](/api-reference/posts/update-post); `destinations.N.…` on [Create posts in batch](/api-reference/posts/create-posts-in-batch). diff --git a/api-reference/introduction.mdx b/api-reference/introduction.mdx index 5a60647..71278ad 100644 --- a/api-reference/introduction.mdx +++ b/api-reference/introduction.mdx @@ -106,14 +106,14 @@ Validation errors list the messages of each field under `errors`, with the field { "message": "Select a Pinterest board to publish this post.", "errors": { - "platforms.0.meta.board_id": [ + "meta.board_id": [ "Select a Pinterest board to publish this post." ] } } ``` -The key follows the request you sent. A destination's settings, text and media checks on [Create post](/api-reference/posts/create-post) and [Create posts in batch](/api-reference/posts/create-posts-in-batch) are reported under `destinations.N.…` (for example `destinations.0.meta.board_id`), even when you sent `platforms`. +On [Create post](/api-reference/posts/create-post) and [Update post](/api-reference/posts/update-post) the key is the field itself: `meta.board_id`, `content_type`, `content`, `media`. On [Create posts in batch](/api-reference/posts/create-posts-in-batch) it names the destination, for example `destinations.0.meta.board_id`. Other errors carry only `message`: diff --git a/knowledge-base/publish/overview.mdx b/knowledge-base/publish/overview.mdx index ccac0ee..b2c918f 100644 --- a/knowledge-base/publish/overview.mdx +++ b/knowledge-base/publish/overview.mdx @@ -15,11 +15,11 @@ Each post belongs to one channel. When you save a post for several channels in t | **Queue** | Scheduled posts, and posts being published right now, in time order. | | **Approvals** | Posts waiting for approval. See [Approvals](/knowledge-base/publish/approvals). | | **Drafts** | Drafts. | -| **Sent** | Published, partially published and failed posts. | +| **Sent** | Published and failed posts. | Lists load more posts as you scroll. -Published and partially published posts are kept for 2 years, then deleted from TryPost with their media. They stay on the network. +Published posts are kept for 2 years, then deleted from TryPost with their media. They stay on the network. Self-hosting TryPost? Change this with `POST_HISTORY_RETENTION_DAYS`. See [Configuration](/self-hosting/configuration). @@ -62,7 +62,7 @@ Each post card has buttons and a menu. What you see depends on the post's status | **See post insights** | Open the post's metrics. See [Channel insights](/knowledge-base/insights/channel-insights). | | **Post Details** | Show the full post, its channels, who created it and, once sent, its metrics. | -Posts that are **Publishing**, **Published**, **Partially Published** or **Failed** cannot be edited or deleted. To try again, duplicate the post. Deleting a post never removes it from the network. +Posts that are **Publishing**, **Published** or **Failed** cannot be edited or deleted. To try again, duplicate the post. Deleting a post never removes it from the network. ## Statuses @@ -74,7 +74,6 @@ Posts that are **Publishing**, **Published**, **Partially Published** or **Faile | **Publishing** | Being sent to the network now. | | **Retrying** | The network had a temporary problem; TryPost tries again at the time shown. | | **Published** | Live on the network. | -| **Partially Published** | Some parts went out and some failed. | | **Failed** | Could not be published. | ### Why a post failed diff --git a/knowledge-base/settings/webhooks.mdx b/knowledge-base/settings/webhooks.mdx index 4022536..336c6f9 100644 --- a/knowledge-base/settings/webhooks.mdx +++ b/knowledge-base/settings/webhooks.mdx @@ -34,11 +34,14 @@ A new webhook starts **Enabled**. Creating or editing a webhook does not send a | `post.created` | Post created | A new post is created | [Post payload](#post-payload) | | `post.scheduled` | Post scheduled | An existing post changes to scheduled | Post payload | | `post.unscheduled` | Post unscheduled | A scheduled post goes back to drafts, or back to pending approval | Post payload | -| `post.published` | Post published | A post published on all its channels | Post payload | -| `post.partially_published` | Post partially published | Some channels published and others failed | Post payload | +| `post.published` | Post published | A post was published on its channel | Post payload | | `post.failed` | Post failed | A post failed to publish | Post payload | | `post.deleted` | Post deleted | A post is deleted | `{ "id", "workspace_id" }` only | + + Posts used to carry their channels in a `platforms` list, and there was a `post.partially_published` event. Both were removed: a post has one channel, and its fields are at the top of `data`. Webhooks subscribed to `post.partially_published` were moved to `post.published` and `post.failed`. + + Status events fire when the status changes, not on every save. A post created already scheduled or queued sends `post.created`, with `status` set to `scheduled`. Edits that keep the status (text, media, labels, a new time on a scheduled post) send nothing. There is no event while a post is publishing, and none for notes, approvals, channel changes or member changes. Posts deleted because their [channel was disconnected](/knowledge-base/channels/disconnecting) or their [workspace was deleted](/knowledge-base/settings/workspaces#delete-a-workspace) do not send `post.deleted`. @@ -123,15 +126,24 @@ For every event except `post.deleted`, `data` is the post at the moment of the e | `id` | Post id | | `workspace_id` | Workspace id | | `user_id` | Author's user id, or `null` | -| `status` | `draft`, `pending_approval`, `scheduled`, `publishing`, `published`, `partially_published` or `failed` | +| `status` | `draft`, `pending_approval`, `scheduled`, `publishing`, `published` or `failed` | | `created_via` | `web`, `api`, `mcp`, `repurpose`, or `null` | | `content` | Post text. May contain HTML from the editor, or be empty | -| `scheduled_at`, `published_at`, `created_at`, `updated_at` | ISO 8601, or `null` | +| `scheduled_at`, `published_at`, `created_at`, `updated_at` | ISO 8601, or `null`. `published_at` is when the network published the post; `updated_at` moves only when the post itself is edited, not when it publishes | | `author` | `{ id, name }`, or `null` | | `workspace` | `{ id, name }` | | `labels` | `{ id, name, color }[]` | | `media` | See [Media](#media) | -| `platforms` | See [Platforms](#platforms) | +| `publish_status` | Publishing state on the network: `pending`, `publishing`, `retrying`, `pending_review`, `published`, `failed` or `rejected` | +| `social_account_id` | Channel id | +| `platform` | For example `linkedin`, `x`, `instagram-facebook` | +| `content_type` | For example `linkedin_post`, `instagram_reel` | +| `platform_post_id` | Id on the network, or `null` | +| `platform_url` | Link to the live post, or `null` | +| `error_message` | Why publishing failed, or `null` | +| `display_name`, `display_username`, `display_avatar` | The channel as it looked when the post was saved | +| `meta` | Per-network settings of the post (see [Posting](/api-reference/guides/posting)) | +| `social_account` | `{ id, platform, display_name, username, status }`, or `null`. Never includes tokens | ### Media @@ -147,26 +159,6 @@ For every event except `post.deleted`, `data` is the post at the moment of the e | `source_meta` | Extra data from the source, or `null` | | `meta` | File metadata, such as `alt_text` and dimensions | -### Platforms - -One entry per channel of the post: - -| Field | Description | -|-------|-------------| -| `id` | Id of the post's entry for that channel | -| `social_account_id` | Channel id | -| `platform` | For example `linkedin`, `x`, `instagram-facebook` | -| `content_type` | For example `linkedin_post`, `instagram_reel` | -| `enabled` | Whether this channel is part of the publish | -| `status` | `pending`, `publishing`, `retrying`, `pending_review`, `published`, `failed` or `rejected` | -| `platform_post_id` | Id on the network, or `null` | -| `platform_url` | Link to the live post, or `null` | -| `published_at` | ISO 8601, or `null` | -| `error_message`, `error_context` | Details when that channel failed | -| `display_name`, `display_username`, `display_avatar` | The channel as it looked when the post was saved | -| `meta` | Per-network settings of the post (see [Posting](/api-reference/guides/posting)) | -| `social_account` | `{ id, platform, display_name, username, status }`, or `null`. Never includes tokens | - ## Test event From the webhook's page, open the actions menu and click **Send test event**. TryPost sends a signed `webhook.test` delivery and waits up to 5 seconds. Your endpoint must answer with `2xx`; otherwise the test fails with the reason (**The endpoint is not reachable.** or **The endpoint returned HTTP 500.**). @@ -189,7 +181,7 @@ Fix the endpoint, then click **Enable endpoint** to resume. Events that happened The webhook's page lists its **Deliveries**, newest first, with the event, **HTTP status**, **Attempts**, the **Response** your endpoint returned (first 2,000 characters) and the **Message payload**. New deliveries appear live. -**Replay** sends a delivery again with the same `data`, a new envelope `id` and a new row in **Deliveries**. Replays work even when the webhook is disabled or paused, but a successful replay does not re-enable it. +**Replay** sends a delivery again with the same `data`, a new envelope `id` and a new row in **Deliveries**. A delivery recorded before posts moved to one channel replays with its original `platforms` payload. Replays work even when the webhook is disabled or paused, but a successful replay does not re-enable it. Deliveries are kept for 7 days. diff --git a/openapi.json b/openapi.json index 8a83740..59b0a80 100644 --- a/openapi.json +++ b/openapi.json @@ -94,7 +94,7 @@ ], "summary": "List posts", "operationId": "listPosts", - "description": "Lists the workspace's posts: posts without a time first, then by scheduled time, latest first, with their destinations and labels. Pending approval requests of other members are included (API keys belong to admins, who can approve). Page size is set by the server: read `meta.per_page`.", + "description": "Lists the workspace's posts: posts without a time first, then by scheduled time, latest first, with their channel and labels. Pending approval requests of other members are included (API keys belong to admins, who can approve). Page size is set by the server: read `meta.per_page`.", "parameters": [ { "name": "channels[]", @@ -204,35 +204,28 @@ "approved_at": null, "recurrence": null, "origin": "trypost", - "platforms": [ - { - "id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", - "platform": "x", - "content_type": "x_post", - "meta": [], - "status": "pending", - "enabled": true, - "platform_url": null, - "published_at": null, - "error_message": null, - "display_name": "TryPost", - "display_username": "trypostit", - "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", - "social_account": { - "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", - "platform": "x", - "display_name": "TryPost", - "username": "trypostit", - "status": "connected", - "has_posting_schedule": true, - "timezone": "America/Sao_Paulo", - "posting_goal": 5, - "max_content_length": 280, - "long_posts": false, - "verified_badge": null - } - } - ], + "publish_status": "pending", + "platform": "x", + "content_type": "x_post", + "meta": [], + "platform_url": null, + "error_message": null, + "display_name": "TryPost", + "display_username": "trypostit", + "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", + "social_account": { + "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", + "platform": "x", + "display_name": "TryPost", + "username": "trypostit", + "status": "connected", + "has_posting_schedule": true, + "timezone": "America/Sao_Paulo", + "posting_goal": 5, + "max_content_length": 280, + "long_posts": false, + "verified_badge": null + }, "labels": [ { "id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f", @@ -308,7 +301,7 @@ ], "summary": "Create post", "operationId": "createPost", - "description": "Creates one post on one social account.\n\n- `status: draft` (default) saves a draft.\n- `status: scheduled` with `scheduled_at` schedules it at that time; with `queue` (`next` or `top`) it takes a slot of the channel's posting schedule; with `queue_slot` it takes that exact free slot (from `GET /channels/{account}/queue/slots`).\n- `status: publishing` publishes it now.\n\nOmitted `content_type` is chosen like the app does: on Pinterest a video makes `pinterest_video_pin` and several images `pinterest_carousel`, on TikTok images only make `tiktok_photo`, every other network takes its default format.\n\nTo schedule or publish, the destination needs its required settings in `platforms[].meta` (see `PlatformMeta`). API keys belong to workspace admins, who publish directly, so a post created through the API is never stored as `pending_approval`.\n\nTo post the same text on several channels at once, use `POST /posts/batch`.", + "description": "Creates one post on one social account.\n\n- `status: draft` (default) saves a draft.\n- `status: scheduled` with `scheduled_at` schedules it at that time; with `queue` (`next` or `top`) it takes a slot of the channel's posting schedule; with `queue_slot` it takes that exact free slot (from `GET /channels/{account}/queue/slots`).\n- `status: publishing` publishes it now.\n\nOmitted `content_type` is chosen like the app does: on Pinterest a video makes `pinterest_video_pin` and several images `pinterest_carousel`, on TikTok images only make `tiktok_photo`, every other network takes its default format.\n\nTo schedule or publish, the post needs its network's required settings in `meta` (see `PlatformMeta`). Sending `platforms` is refused with `422`; validation errors name the field directly (`meta.board_id`, `content_type`). API keys belong to workspace admins, who publish directly, so a post created through the API is never stored as `pending_approval`.\n\nTo post the same text on several channels at once, use `POST /posts/batch`.", "requestBody": { "required": true, "content": { @@ -316,7 +309,7 @@ "schema": { "type": "object", "required": [ - "platforms" + "social_account_id" ], "properties": { "content": { @@ -344,38 +337,24 @@ "description": "`draft` keeps the post editable. `scheduled` schedules it at `scheduled_at` or in the channel queue (`queue`). `publishing` publishes it now.", "default": "draft" }, - "platforms": { - "type": "array", - "minItems": 1, - "maxItems": 1, - "description": "Exactly one destination.", - "items": { - "type": "object", - "required": [ - "social_account_id" - ], - "properties": { - "social_account_id": { - "type": "string", - "format": "uuid", - "description": "Social account of this workspace." - }, - "content_type": { - "oneOf": [ - { - "$ref": "#/components/schemas/ContentType" - }, - { - "type": "null" - } - ], - "description": "Format; must belong to the account's network. Omitted, it is chosen from the media." - }, - "meta": { - "$ref": "#/components/schemas/PlatformMeta" - } + "social_account_id": { + "type": "string", + "format": "uuid", + "description": "Social account of this workspace. A post has exactly one; use `POST /posts/batch` for several." + }, + "content_type": { + "oneOf": [ + { + "$ref": "#/components/schemas/ContentType" + }, + { + "type": "null" } - } + ], + "description": "Format; must belong to the account's network. Omitted, it is chosen from the media." + }, + "meta": { + "$ref": "#/components/schemas/PlatformMeta" }, "scheduled_at": { "type": [ @@ -425,19 +404,15 @@ "alt": "Product launch banner" } ], - "platforms": [ - { - "social_account_id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", - "content_type": "x_post", - "meta": { - "thread_replies": [ - { - "text": "2/ Replies publish under the first post." - } - ] + "social_account_id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", + "content_type": "x_post", + "meta": { + "thread_replies": [ + { + "text": "2/ Replies publish under the first post." } - } - ], + ] + }, "label_ids": [ "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f" ] @@ -487,42 +462,35 @@ "approved_at": null, "recurrence": null, "origin": "trypost", - "platforms": [ - { - "id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", - "platform": "x", - "content_type": "x_post", - "meta": { - "thread_replies": [ - { - "text": "2/ Replies publish under the first post.", - "media": [] - } - ] - }, - "status": "pending", - "enabled": true, - "platform_url": null, - "published_at": null, - "error_message": null, - "display_name": "TryPost", - "display_username": "trypostit", - "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", - "social_account": { - "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", - "platform": "x", - "display_name": "TryPost", - "username": "trypostit", - "status": "connected", - "has_posting_schedule": true, - "timezone": "America/Sao_Paulo", - "posting_goal": 5, - "max_content_length": 280, - "long_posts": false, - "verified_badge": null + "publish_status": "pending", + "platform": "x", + "content_type": "x_post", + "meta": { + "thread_replies": [ + { + "text": "2/ Replies publish under the first post.", + "media": [] } - } - ], + ] + }, + "platform_url": null, + "error_message": null, + "display_name": "TryPost", + "display_username": "trypostit", + "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", + "social_account": { + "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", + "platform": "x", + "display_name": "TryPost", + "username": "trypostit", + "status": "connected", + "has_posting_schedule": true, + "timezone": "America/Sao_Paulo", + "posting_goal": 5, + "max_content_length": 280, + "long_posts": false, + "verified_badge": null + }, "labels": [ { "id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f", @@ -566,7 +534,7 @@ ], "summary": "Create posts in batch", "operationId": "createPostsInBatch", - "description": "Creates one independent post per destination, as the app does when several channels are selected. The posts share a `post_group_id`. Each destination may override the shared `content`, `media`, `content_type` and `meta`.\n\n`status: scheduled` schedules every post at `scheduled_at` or in each channel's queue (`queue: next` or `top`). One specific slot (`queue_slot`) is not taken here: use `POST /posts` or `PUT /channels/{account}/queue/slot`.\n\n`destinations[].meta` follows the same per-network rules as `platforms[].meta` (see `PlatformMeta`), checked with the destination's network; errors are reported on `destinations.N.meta.*`. To schedule or publish, every destination needs its required settings and text or media. API keys belong to workspace admins, so these posts are never stored as `pending_approval`.", + "description": "Creates one independent post per destination, as the app does when several channels are selected. The posts share a `post_group_id`. Each destination may override the shared `content`, `media`, `content_type` and `meta`.\n\n`status: scheduled` schedules every post at `scheduled_at` or in each channel's queue (`queue: next` or `top`). One specific slot (`queue_slot`) is not taken here: use `POST /posts` or `PUT /channels/{account}/queue/slot`.\n\n`destinations[].meta` follows the same per-network rules as a post's `meta` (see `PlatformMeta`), checked with the destination's network; errors are reported on `destinations.N.meta.*`. To schedule or publish, every destination needs its required settings and text or media. API keys belong to workspace admins, so these posts are never stored as `pending_approval`.", "requestBody": { "required": true, "content": { @@ -760,42 +728,35 @@ "approved_at": null, "recurrence": null, "origin": "trypost", - "platforms": [ - { - "id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", - "platform": "x", - "content_type": "x_post", - "meta": { - "thread_replies": [ - { - "text": "2/ Replies publish under the first post.", - "media": [] - } - ] - }, - "status": "pending", - "enabled": true, - "platform_url": null, - "published_at": null, - "error_message": null, - "display_name": "TryPost", - "display_username": "trypostit", - "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", - "social_account": { - "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", - "platform": "x", - "display_name": "TryPost", - "username": "trypostit", - "status": "connected", - "has_posting_schedule": true, - "timezone": "America/Sao_Paulo", - "posting_goal": 5, - "max_content_length": 280, - "long_posts": false, - "verified_badge": null + "publish_status": "pending", + "platform": "x", + "content_type": "x_post", + "meta": { + "thread_replies": [ + { + "text": "2/ Replies publish under the first post.", + "media": [] } - } - ], + ] + }, + "platform_url": null, + "error_message": null, + "display_name": "TryPost", + "display_username": "trypostit", + "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", + "social_account": { + "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", + "platform": "x", + "display_name": "TryPost", + "username": "trypostit", + "status": "connected", + "has_posting_schedule": true, + "timezone": "America/Sao_Paulo", + "posting_goal": 5, + "max_content_length": 280, + "long_posts": false, + "verified_badge": null + }, "labels": [ { "id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f", @@ -842,39 +803,32 @@ "approved_at": null, "recurrence": null, "origin": "trypost", - "platforms": [ - { - "id": "9d3c1f2b-2b3c-4d4e-9f5a-6b7c8d9e0f21", - "platform": "pinterest", - "content_type": "pinterest_pin", - "meta": { - "board_id": "1022106146620830917", - "title": "October changelog", - "link": "https://trypost.it/changelog" - }, - "status": "pending", - "enabled": true, - "platform_url": null, - "published_at": null, - "error_message": null, - "display_name": "TryPost", - "display_username": "trypostit", - "display_avatar": null, - "social_account": { - "id": "9c8b7a65-5e4f-4a3b-9c2d-1e0f9a8b7c65", - "platform": "pinterest", - "display_name": "TryPost", - "username": "trypostit", - "status": "connected", - "has_posting_schedule": true, - "timezone": "UTC", - "posting_goal": 5, - "max_content_length": 800, - "long_posts": false, - "verified_badge": null - } - } - ], + "publish_status": "pending", + "platform": "pinterest", + "content_type": "pinterest_pin", + "meta": { + "board_id": "1022106146620830917", + "title": "October changelog", + "link": "https://trypost.it/changelog" + }, + "platform_url": null, + "error_message": null, + "display_name": "TryPost", + "display_username": "trypostit", + "display_avatar": null, + "social_account": { + "id": "9c8b7a65-5e4f-4a3b-9c2d-1e0f9a8b7c65", + "platform": "pinterest", + "display_name": "TryPost", + "username": "trypostit", + "status": "connected", + "has_posting_schedule": true, + "timezone": "UTC", + "posting_goal": 5, + "max_content_length": 800, + "long_posts": false, + "verified_badge": null + }, "labels": [ { "id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f", @@ -925,7 +879,7 @@ ], "summary": "Get post", "operationId": "getPost", - "description": "Returns one post with its destinations and labels.", + "description": "Returns one post with its channel and labels.", "responses": { "200": { "description": "The post.", @@ -968,42 +922,35 @@ "approved_at": null, "recurrence": null, "origin": "trypost", - "platforms": [ - { - "id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", - "platform": "x", - "content_type": "x_post", - "meta": { - "thread_replies": [ - { - "text": "2/ Replies publish under the first post.", - "media": [] - } - ] - }, - "status": "pending", - "enabled": true, - "platform_url": null, - "published_at": null, - "error_message": null, - "display_name": "TryPost", - "display_username": "trypostit", - "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", - "social_account": { - "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", - "platform": "x", - "display_name": "TryPost", - "username": "trypostit", - "status": "connected", - "has_posting_schedule": true, - "timezone": "America/Sao_Paulo", - "posting_goal": 5, - "max_content_length": 280, - "long_posts": false, - "verified_badge": null + "publish_status": "pending", + "platform": "x", + "content_type": "x_post", + "meta": { + "thread_replies": [ + { + "text": "2/ Replies publish under the first post.", + "media": [] } - } - ], + ] + }, + "platform_url": null, + "error_message": null, + "display_name": "TryPost", + "display_username": "trypostit", + "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", + "social_account": { + "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", + "platform": "x", + "display_name": "TryPost", + "username": "trypostit", + "status": "connected", + "has_posting_schedule": true, + "timezone": "America/Sao_Paulo", + "posting_goal": 5, + "max_content_length": 280, + "long_posts": false, + "verified_badge": null + }, "labels": [ { "id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f", @@ -1042,7 +989,7 @@ ], "summary": "Update post", "operationId": "updatePost", - "description": "Updates a post. Only the fields you send change. The social account is fixed (`social_account_id` is refused).\n\nSend the destination's format and settings either as top-level `content_type` and `meta` (the post has one destination), or as `platforms[]` with the destination `id`. `meta` is merged with the stored settings: send `null` for a key to clear it (string, list and object settings; boolean settings cannot be cleared, so send `true` or `false`); `thread_replies` replaces the whole list. `media` replaces the post's media; `label_ids` replaces its labels.\n\n`status: publishing` publishes the post now; `scheduled` schedules it at `scheduled_at`, in the queue (`queue`), or keeps its future `scheduled_at`; `draft` unschedules it (it is no longer published; the last `scheduled_at` stays on the draft). When scheduling or publishing, the content type must fit the media and the required settings must be present.\n\nPosts that are publishing, published, partially published or failed cannot be edited: the request returns `422` with `This post has already been processed and cannot be re-published. Duplicate it to try again.`\n\nPosts with several destinations (created before each channel got its own post) cannot be queued; `platforms` there lists the destinations to keep, and the rest are turned off. On this endpoint an invalid body may answer `422` before the `404` check of a post of another workspace.", + "description": "Updates a post. Only the fields you send change. The social account is fixed (`social_account_id` is refused), and so is `platforms`.\n\n`content_type` and `meta` are top-level fields. `meta` is merged with the stored settings: send `null` for a key to clear it (string, list and object settings; boolean settings cannot be cleared, so send `true` or `false`); `thread_replies` replaces the whole list. Validation errors name the field directly (`meta.privacy_level`, `content_type`). `media` replaces the post's media; `label_ids` replaces its labels.\n\n`status: publishing` publishes the post now; `scheduled` schedules it at `scheduled_at`, in the queue (`queue`), or keeps its future `scheduled_at`; `draft` unschedules it (it is no longer published; the last `scheduled_at` stays on the draft). When scheduling or publishing, the content type must fit the media and the required settings must be present.\n\nPosts that are publishing, published or failed cannot be edited: the request returns `422` with `This post has already been processed and cannot be re-published. Duplicate it to try again.`\n\nOn this endpoint an invalid body may answer `422` before the `404` check of a post of another workspace.", "requestBody": { "required": true, "content": { @@ -1080,7 +1027,7 @@ "$ref": "#/components/schemas/ContentType" } ], - "description": "New format for the post's destination. Omitted, the stored format is kept, except on Pinterest and TikTok where new media decides it as on create." + "description": "New format for the post's channel. Omitted, the stored format is kept, except on Pinterest and TikTok where new media decides it as on create." }, "meta": { "allOf": [ @@ -1088,30 +1035,7 @@ "$ref": "#/components/schemas/PlatformMeta" } ], - "description": "Settings of the post's destination, merged with the stored ones." - }, - "platforms": { - "type": "array", - "description": "Alternative to top-level `content_type` / `meta`. On a post with several destinations, the destinations not listed are turned off.", - "items": { - "type": "object", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid", - "description": "An enabled destination of this post (`platforms[].id` in the post)." - }, - "content_type": { - "$ref": "#/components/schemas/ContentType" - }, - "meta": { - "$ref": "#/components/schemas/PlatformMeta" - } - } - } + "description": "Settings of the post's channel, merged with the stored ones." }, "scheduled_at": { "type": [ @@ -1208,61 +1132,54 @@ "approved_at": null, "recurrence": null, "origin": "trypost", - "platforms": [ - { - "id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", - "platform": "x", - "content_type": "x_post", - "meta": { - "thread_replies": [ - { - "text": "2/ Replies publish under the first post.", - "media": [] - }, + "publish_status": "pending", + "platform": "x", + "content_type": "x_post", + "meta": { + "thread_replies": [ + { + "text": "2/ Replies publish under the first post.", + "media": [] + }, + { + "text": "3/ Full changelog below.", + "media": [ { - "text": "3/ Full changelog below.", - "media": [ - { - "id": "9d3c1f30-8a9b-4c0d-9e1f-2a3b4c5d6e7f", - "path": "medias/9d3c1f30-8a9b-4c0d-9e1f-2a3b4c5d6e7f.jpg", - "url": "https://media.trypost.it/medias/9d3c1f30-8a9b-4c0d-9e1f-2a3b4c5d6e7f.jpg", - "type": "image", - "mime_type": "image/jpeg", - "original_filename": "launch.jpg", - "size": 482113, - "meta": { - "width": 1080, - "height": 1350, - "alt_text": "Product launch banner" - } - } - ] + "id": "9d3c1f30-8a9b-4c0d-9e1f-2a3b4c5d6e7f", + "path": "medias/9d3c1f30-8a9b-4c0d-9e1f-2a3b4c5d6e7f.jpg", + "url": "https://media.trypost.it/medias/9d3c1f30-8a9b-4c0d-9e1f-2a3b4c5d6e7f.jpg", + "type": "image", + "mime_type": "image/jpeg", + "original_filename": "launch.jpg", + "size": 482113, + "meta": { + "width": 1080, + "height": 1350, + "alt_text": "Product launch banner" + } } ] - }, - "status": "pending", - "enabled": true, - "platform_url": null, - "published_at": null, - "error_message": null, - "display_name": "TryPost", - "display_username": "trypostit", - "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", - "social_account": { - "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", - "platform": "x", - "display_name": "TryPost", - "username": "trypostit", - "status": "connected", - "has_posting_schedule": true, - "timezone": "America/Sao_Paulo", - "posting_goal": 5, - "max_content_length": 280, - "long_posts": false, - "verified_badge": null } - } - ], + ] + }, + "platform_url": null, + "error_message": null, + "display_name": "TryPost", + "display_username": "trypostit", + "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", + "social_account": { + "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", + "platform": "x", + "display_name": "TryPost", + "username": "trypostit", + "status": "connected", + "has_posting_schedule": true, + "timezone": "America/Sao_Paulo", + "posting_goal": 5, + "max_content_length": 280, + "long_posts": false, + "verified_badge": null + }, "labels": [ { "id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f", @@ -1307,7 +1224,7 @@ ], "summary": "Delete post", "operationId": "deletePost", - "description": "Deletes a post that has not been sent, with its media. Nothing is removed from the network. Posts that are publishing, published, partially published or failed cannot be deleted: the request returns `422` with `Sent posts cannot be deleted.` on `post`.", + "description": "Deletes a post that has not been sent, with its media. Nothing is removed from the network. Posts that are publishing, published or failed cannot be deleted: the request returns `422` with `Sent posts cannot be deleted.` on `post`.", "responses": { "204": { "description": "Deleted." @@ -1345,10 +1262,10 @@ ], "summary": "Get post metrics", "operationId": "getPostMetrics", - "description": "Returns the saved analytics of each enabled destination: the latest daily snapshot TryPost collected from the network. No call is made to the network. LinkedIn, LinkedIn Page, Telegram, Discord and Google Business are not in analytics.", + "description": "Returns the saved analytics of the post: the latest daily snapshot TryPost collected from the network. No call is made to the network. LinkedIn, LinkedIn Page, Telegram, Discord and Google Business are not in analytics.", "responses": { "200": { - "description": "Metrics per destination.", + "description": "The post's metrics.", "content": { "application/json": { "schema": { @@ -1356,64 +1273,59 @@ }, "example": { "post_id": "9d3c1f2a-6b4e-4c8a-9f1e-2a7b5c8d0e41", - "platforms": [ - { - "post_platform_id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", + "platform": "x", + "publish_status": "published", + "platform_post_id": "1843921066571337728", + "platform_url": "https://x.com/trypostit/status/1843921066571337728", + "metrics": { + "available": true, + "reason": null, + "publication": { + "id": "9d4a0b1c-2d3e-4f5a-8b6c-7d8e9f0a1b2c", + "post_id": "9d3c1f2a-6b4e-4c8a-9f1e-2a7b5c8d0e41", + "social_account_key": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", "platform": "x", - "status": "published", - "platform_post_id": "1843921066571337728", - "platform_url": "https://x.com/trypostit/status/1843921066571337728", - "metrics": { - "available": true, - "reason": null, - "publication": { - "id": "9d4a0b1c-2d3e-4f5a-8b6c-7d8e9f0a1b2c", - "post_platform_id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", - "social_account_key": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", - "platform": "x", - "origin": "trypost", - "content_type": "text", - "availability": "available", - "provider_published_at": "2026-10-12T14:00:03+00:00", - "permalink": "https://x.com/trypostit/status/1843921066571337728", - "excerpt": "We just shipped scheduled threads.", - "preview_metadata": null, - "account_display_name": "TryPost", - "account_username": "trypostit", - "account_avatar_url": null - }, - "snapshot": { - "date": "2026-10-14", - "collected_at": "2026-10-14T06:10:00+00:00", - "provider_observed_at": "2026-10-14T06:09:58+00:00", - "reactions_count": 42, - "comments_count": 5, - "shares_count": 7, - "saves_count": 3, - "views_count": null, - "impressions_count": 3120, - "reach_count": null, - "engagement_count": 57, - "exposure_count": 3120, - "exposure_kind": "impressions", - "watch_time_milliseconds": null, - "average_watch_time_milliseconds": null - }, - "metrics": { - "impressions": { - "value": 3120, - "unit": "count", - "time_basis": "lifetime", - "precision": "exact", - "availability": "available", - "provider_metric": "impression_count", - "period_start": null, - "period_end": null - } - } + "origin": "trypost", + "content_type": "text", + "availability": "available", + "provider_published_at": "2026-10-12T14:00:03+00:00", + "permalink": "https://x.com/trypostit/status/1843921066571337728", + "excerpt": "We just shipped scheduled threads.", + "preview_metadata": null, + "account_display_name": "TryPost", + "account_username": "trypostit", + "account_avatar_url": null + }, + "snapshot": { + "date": "2026-10-14", + "collected_at": "2026-10-14T06:10:00+00:00", + "provider_observed_at": "2026-10-14T06:09:58+00:00", + "reactions_count": 42, + "comments_count": 5, + "shares_count": 7, + "saves_count": 3, + "views_count": null, + "impressions_count": 3120, + "reach_count": null, + "engagement_count": 57, + "exposure_count": 3120, + "exposure_kind": "impressions", + "watch_time_milliseconds": null, + "average_watch_time_milliseconds": null + }, + "metrics": { + "impressions": { + "value": 3120, + "unit": "count", + "time_basis": "lifetime", + "precision": "exact", + "availability": "available", + "provider_metric": "impression_count", + "period_start": null, + "period_end": null } } - ] + } } } } @@ -1448,10 +1360,10 @@ ], "summary": "Get post preview", "operationId": "getPostPreview", - "description": "Shows the text each enabled destination will send, after the network's formatting rules, and its length against the account's limit. Nothing is published.", + "description": "Shows the text the post's channel will receive, after the network's formatting rules, and its length against the account's limit. Nothing is published.", "responses": { "200": { - "description": "Preview per destination.", + "description": "The post's preview.", "content": { "application/json": { "schema": { @@ -1461,21 +1373,16 @@ "post_id": "9d3c1f2a-6b4e-4c8a-9f1e-2a7b5c8d0e41", "original_content": "We just shipped scheduled threads. Here is how they work.", "original_length": 57, - "platforms": [ + "platform": "x", + "content_type": "x_post", + "sanitized_content": "We just shipped scheduled threads. Here is how they work.", + "sanitized_length": 57, + "max_content_length": 280, + "truncated": false, + "thread_replies": [ { - "post_platform_id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", - "platform": "x", - "content_type": "x_post", - "sanitized_content": "We just shipped scheduled threads. Here is how they work.", - "sanitized_length": 57, - "max_content_length": 280, - "truncated": false, - "thread_replies": [ - { - "text": "2/ Replies publish under the first post.", - "media": [] - } - ] + "text": "2/ Replies publish under the first post.", + "media": [] } ] } @@ -1889,42 +1796,35 @@ "approved_at": "2026-10-08 13:30:00", "recurrence": null, "origin": "trypost", - "platforms": [ - { - "id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", - "platform": "x", - "content_type": "x_post", - "meta": { - "thread_replies": [ - { - "text": "2/ Replies publish under the first post.", - "media": [] - } - ] - }, - "status": "pending", - "enabled": true, - "platform_url": null, - "published_at": null, - "error_message": null, - "display_name": "TryPost", - "display_username": "trypostit", - "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", - "social_account": { - "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", - "platform": "x", - "display_name": "TryPost", - "username": "trypostit", - "status": "connected", - "has_posting_schedule": true, - "timezone": "America/Sao_Paulo", - "posting_goal": 5, - "max_content_length": 280, - "long_posts": false, - "verified_badge": null + "publish_status": "pending", + "platform": "x", + "content_type": "x_post", + "meta": { + "thread_replies": [ + { + "text": "2/ Replies publish under the first post.", + "media": [] } - } - ], + ] + }, + "platform_url": null, + "error_message": null, + "display_name": "TryPost", + "display_username": "trypostit", + "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", + "social_account": { + "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", + "platform": "x", + "display_name": "TryPost", + "username": "trypostit", + "status": "connected", + "has_posting_schedule": true, + "timezone": "America/Sao_Paulo", + "posting_goal": 5, + "max_content_length": 280, + "long_posts": false, + "verified_badge": null + }, "labels": [ { "id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f", @@ -2019,42 +1919,35 @@ "approved_at": null, "recurrence": null, "origin": "trypost", - "platforms": [ - { - "id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", - "platform": "x", - "content_type": "x_post", - "meta": { - "thread_replies": [ - { - "text": "2/ Replies publish under the first post.", - "media": [] - } - ] - }, - "status": "pending", - "enabled": true, - "platform_url": null, - "published_at": null, - "error_message": null, - "display_name": "TryPost", - "display_username": "trypostit", - "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", - "social_account": { - "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", - "platform": "x", - "display_name": "TryPost", - "username": "trypostit", - "status": "connected", - "has_posting_schedule": true, - "timezone": "America/Sao_Paulo", - "posting_goal": 5, - "max_content_length": 280, - "long_posts": false, - "verified_badge": null + "publish_status": "pending", + "platform": "x", + "content_type": "x_post", + "meta": { + "thread_replies": [ + { + "text": "2/ Replies publish under the first post.", + "media": [] } - } - ], + ] + }, + "platform_url": null, + "error_message": null, + "display_name": "TryPost", + "display_username": "trypostit", + "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", + "social_account": { + "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", + "platform": "x", + "display_name": "TryPost", + "username": "trypostit", + "status": "connected", + "has_posting_schedule": true, + "timezone": "America/Sao_Paulo", + "posting_goal": 5, + "max_content_length": 280, + "long_posts": false, + "verified_badge": null + }, "labels": [ { "id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f", @@ -2196,42 +2089,35 @@ "remaining": 4 }, "origin": "trypost", - "platforms": [ - { - "id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", - "platform": "x", - "content_type": "x_post", - "meta": { - "thread_replies": [ - { - "text": "2/ Replies publish under the first post.", - "media": [] - } - ] - }, - "status": "pending", - "enabled": true, - "platform_url": null, - "published_at": null, - "error_message": null, - "display_name": "TryPost", - "display_username": "trypostit", - "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", - "social_account": { - "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", - "platform": "x", - "display_name": "TryPost", - "username": "trypostit", - "status": "connected", - "has_posting_schedule": true, - "timezone": "America/Sao_Paulo", - "posting_goal": 5, - "max_content_length": 280, - "long_posts": false, - "verified_badge": null + "publish_status": "pending", + "platform": "x", + "content_type": "x_post", + "meta": { + "thread_replies": [ + { + "text": "2/ Replies publish under the first post.", + "media": [] } - } - ], + ] + }, + "platform_url": null, + "error_message": null, + "display_name": "TryPost", + "display_username": "trypostit", + "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", + "social_account": { + "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", + "platform": "x", + "display_name": "TryPost", + "username": "trypostit", + "status": "connected", + "has_posting_schedule": true, + "timezone": "America/Sao_Paulo", + "posting_goal": 5, + "max_content_length": 280, + "long_posts": false, + "verified_badge": null + }, "labels": [ { "id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f", @@ -2316,42 +2202,35 @@ "approved_at": null, "recurrence": null, "origin": "trypost", - "platforms": [ - { - "id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", - "platform": "x", - "content_type": "x_post", - "meta": { - "thread_replies": [ - { - "text": "2/ Replies publish under the first post.", - "media": [] - } - ] - }, - "status": "pending", - "enabled": true, - "platform_url": null, - "published_at": null, - "error_message": null, - "display_name": "TryPost", - "display_username": "trypostit", - "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", - "social_account": { - "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", - "platform": "x", - "display_name": "TryPost", - "username": "trypostit", - "status": "connected", - "has_posting_schedule": true, - "timezone": "America/Sao_Paulo", - "posting_goal": 5, - "max_content_length": 280, - "long_posts": false, - "verified_badge": null + "publish_status": "pending", + "platform": "x", + "content_type": "x_post", + "meta": { + "thread_replies": [ + { + "text": "2/ Replies publish under the first post.", + "media": [] } - } - ], + ] + }, + "platform_url": null, + "error_message": null, + "display_name": "TryPost", + "display_username": "trypostit", + "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", + "social_account": { + "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", + "platform": "x", + "display_name": "TryPost", + "username": "trypostit", + "status": "connected", + "has_posting_schedule": true, + "timezone": "America/Sao_Paulo", + "posting_goal": 5, + "max_content_length": 280, + "long_posts": false, + "verified_badge": null + }, "labels": [ { "id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f", @@ -2397,7 +2276,7 @@ ], "summary": "Upload media", "operationId": "uploadPostMedia", - "description": "Uploads one file as `multipart/form-data` and appends it to the post's media. Accepted files: JPEG, PNG, GIF and WebP images (HEIC and HEIF too when the server can convert them, stored as JPEG), MP4 and MOV videos, and PDF documents. Size caps: 10 MB per image (and at most about 67 megapixels, 8192 × 8192 pixels in total), 1 GB per video, 100 MB per PDF. Each network enforces its own, usually smaller, caps when the post is scheduled or published (`GET /content-types`). A type the post's channel does not accept fails with `422`: `This file type is not supported by the post's channel.` Posts that are publishing, published, partially published or failed cannot take media: `422` on `post` with `This post has already been processed and cannot be re-published. Duplicate it to try again.` On Pinterest and TikTok the content type is chosen again from the post's media, as on create.", + "description": "Uploads one file as `multipart/form-data` and appends it to the post's media. Accepted files: JPEG, PNG, GIF and WebP images (HEIC and HEIF too when the server can convert them, stored as JPEG), MP4 and MOV videos, and PDF documents. Size caps: 10 MB per image (and at most about 67 megapixels, 8192 × 8192 pixels in total), 1 GB per video, 100 MB per PDF. Each network enforces its own, usually smaller, caps when the post is scheduled or published (`GET /content-types`). A type the post's channel does not accept fails with `422`: `This file type is not supported by the post's channel.` Posts that are publishing, published or failed cannot take media: `422` on `post` with `This post has already been processed and cannot be re-published. Duplicate it to try again.` On Pinterest and TikTok the content type is chosen again from the post's media, as on create.", "requestBody": { "required": true, "content": { @@ -2460,42 +2339,35 @@ "approved_at": null, "recurrence": null, "origin": "trypost", - "platforms": [ - { - "id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", - "platform": "x", - "content_type": "x_post", - "meta": { - "thread_replies": [ - { - "text": "2/ Replies publish under the first post.", - "media": [] - } - ] - }, - "status": "pending", - "enabled": true, - "platform_url": null, - "published_at": null, - "error_message": null, - "display_name": "TryPost", - "display_username": "trypostit", - "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", - "social_account": { - "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", - "platform": "x", - "display_name": "TryPost", - "username": "trypostit", - "status": "connected", - "has_posting_schedule": true, - "timezone": "America/Sao_Paulo", - "posting_goal": 5, - "max_content_length": 280, - "long_posts": false, - "verified_badge": null + "publish_status": "pending", + "platform": "x", + "content_type": "x_post", + "meta": { + "thread_replies": [ + { + "text": "2/ Replies publish under the first post.", + "media": [] } - } - ], + ] + }, + "platform_url": null, + "error_message": null, + "display_name": "TryPost", + "display_username": "trypostit", + "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", + "social_account": { + "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", + "platform": "x", + "display_name": "TryPost", + "username": "trypostit", + "status": "connected", + "has_posting_schedule": true, + "timezone": "America/Sao_Paulo", + "posting_goal": 5, + "max_content_length": 280, + "long_posts": false, + "verified_badge": null + }, "labels": [ { "id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f", @@ -2547,7 +2419,7 @@ ], "summary": "Attach media from upload", "operationId": "attachMediaFromUpload", - "description": "Appends a file uploaded earlier (`POST /uploads` or a signed upload URL) to the post's media. The token is spent by this post. An unknown or expired token fails with `422` on `upload_token`: `This upload expired. Add the file again.` A type the post's channel does not accept fails with `422`. Posts that are publishing, published, partially published or failed cannot take media: `422` on `post` with `This post has already been processed and cannot be re-published. Duplicate it to try again.` On Pinterest and TikTok the content type is chosen again from the post's media, as on create.", + "description": "Appends a file uploaded earlier (`POST /uploads` or a signed upload URL) to the post's media. The token is spent by this post. An unknown or expired token fails with `422` on `upload_token`: `This upload expired. Add the file again.` A type the post's channel does not accept fails with `422`. Posts that are publishing, published or failed cannot take media: `422` on `post` with `This post has already been processed and cannot be re-published. Duplicate it to try again.` On Pinterest and TikTok the content type is chosen again from the post's media, as on create.", "requestBody": { "required": true, "content": { @@ -2621,42 +2493,35 @@ "approved_at": null, "recurrence": null, "origin": "trypost", - "platforms": [ - { - "id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", - "platform": "x", - "content_type": "x_post", - "meta": { - "thread_replies": [ - { - "text": "2/ Replies publish under the first post.", - "media": [] - } - ] - }, - "status": "pending", - "enabled": true, - "platform_url": null, - "published_at": null, - "error_message": null, - "display_name": "TryPost", - "display_username": "trypostit", - "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", - "social_account": { - "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", - "platform": "x", - "display_name": "TryPost", - "username": "trypostit", - "status": "connected", - "has_posting_schedule": true, - "timezone": "America/Sao_Paulo", - "posting_goal": 5, - "max_content_length": 280, - "long_posts": false, - "verified_badge": null + "publish_status": "pending", + "platform": "x", + "content_type": "x_post", + "meta": { + "thread_replies": [ + { + "text": "2/ Replies publish under the first post.", + "media": [] } - } - ], + ] + }, + "platform_url": null, + "error_message": null, + "display_name": "TryPost", + "display_username": "trypostit", + "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", + "social_account": { + "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", + "platform": "x", + "display_name": "TryPost", + "username": "trypostit", + "status": "connected", + "has_posting_schedule": true, + "timezone": "America/Sao_Paulo", + "posting_goal": 5, + "max_content_length": 280, + "long_posts": false, + "verified_badge": null + }, "labels": [ { "id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f", @@ -2708,7 +2573,7 @@ ], "summary": "Attach media from URL", "operationId": "attachMediaFromUrl", - "description": "Downloads up to 10 public files and appends them to the post's media. Each URL must serve the file itself: redirects are not followed, and web pages, private-network hosts and files over the type's size cap are refused. Each download must finish within 20 seconds. Only the types the post's channel accepts are kept. URLs that could not be attached are listed in `failed_urls`, and `failures` gives the reason of each. Posts that are publishing, published, partially published or failed cannot take media: when a file was downloaded, the request fails with `422` on `post` with `This post has already been processed and cannot be re-published. Duplicate it to try again.` Accepted files: JPEG, PNG, GIF and WebP images (HEIC and HEIF too when the server can convert them, stored as JPEG), MP4 and MOV videos, and PDF documents. Size caps: 10 MB per image (and at most about 67 megapixels, 8192 × 8192 pixels in total), 1 GB per video, 100 MB per PDF. Each network enforces its own, usually smaller, caps when the post is scheduled or published (`GET /content-types`). On Pinterest and TikTok the content type is chosen again from the post's media, as on create.", + "description": "Downloads up to 10 public files and appends them to the post's media. Each URL must serve the file itself: redirects are not followed, and web pages, private-network hosts and files over the type's size cap are refused. Each download must finish within 20 seconds. Only the types the post's channel accepts are kept. URLs that could not be attached are listed in `failed_urls`, and `failures` gives the reason of each. Posts that are publishing, published or failed cannot take media: when a file was downloaded, the request fails with `422` on `post` with `This post has already been processed and cannot be re-published. Duplicate it to try again.` Accepted files: JPEG, PNG, GIF and WebP images (HEIC and HEIF too when the server can convert them, stored as JPEG), MP4 and MOV videos, and PDF documents. Size caps: 10 MB per image (and at most about 67 megapixels, 8192 × 8192 pixels in total), 1 GB per video, 100 MB per PDF. Each network enforces its own, usually smaller, caps when the post is scheduled or published (`GET /content-types`). On Pinterest and TikTok the content type is chosen again from the post's media, as on create.", "requestBody": { "required": true, "content": { @@ -2841,42 +2706,35 @@ "approved_at": null, "recurrence": null, "origin": "trypost", - "platforms": [ - { - "id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", - "platform": "x", - "content_type": "x_post", - "meta": { - "thread_replies": [ - { - "text": "2/ Replies publish under the first post.", - "media": [] - } - ] - }, - "status": "pending", - "enabled": true, - "platform_url": null, - "published_at": null, - "error_message": null, - "display_name": "TryPost", - "display_username": "trypostit", - "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", - "social_account": { - "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", - "platform": "x", - "display_name": "TryPost", - "username": "trypostit", - "status": "connected", - "has_posting_schedule": true, - "timezone": "America/Sao_Paulo", - "posting_goal": 5, - "max_content_length": 280, - "long_posts": false, - "verified_badge": null + "publish_status": "pending", + "platform": "x", + "content_type": "x_post", + "meta": { + "thread_replies": [ + { + "text": "2/ Replies publish under the first post.", + "media": [] } - } - ], + ] + }, + "platform_url": null, + "error_message": null, + "display_name": "TryPost", + "display_username": "trypostit", + "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", + "social_account": { + "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", + "platform": "x", + "display_name": "TryPost", + "username": "trypostit", + "status": "connected", + "has_posting_schedule": true, + "timezone": "America/Sao_Paulo", + "posting_goal": 5, + "max_content_length": 280, + "long_posts": false, + "verified_badge": null + }, "labels": [ { "id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f", @@ -4451,68 +4309,61 @@ "name": "Ana Souza" }, "content": "We just shipped scheduled threads. Here is how they work.", - "media": [ - { - "id": "9d3c1f30-8a9b-4c0d-9e1f-2a3b4c5d6e7f", - "path": "medias/9d3c1f30-8a9b-4c0d-9e1f-2a3b4c5d6e7f.jpg", - "url": "https://media.trypost.it/medias/9d3c1f30-8a9b-4c0d-9e1f-2a3b4c5d6e7f.jpg", - "type": "image", - "mime_type": "image/jpeg", - "original_filename": "launch.jpg", - "size": 482113, - "meta": { - "width": 1080, - "height": 1350, - "alt_text": "Product launch banner" - } - } - ], - "status": "scheduled", - "schedule_mode": "queue", - "scheduled_at": "2026-10-13 12:00:00", - "published_at": null, - "approval_requested_by": null, - "approval_requested_at": null, - "approved_by": null, - "approved_at": null, - "recurrence": null, - "origin": "trypost", - "platforms": [ - { - "id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", - "platform": "x", - "content_type": "x_post", - "meta": { - "thread_replies": [ - { - "text": "2/ Replies publish under the first post.", - "media": [] - } - ] - }, - "status": "pending", - "enabled": true, - "platform_url": null, - "published_at": null, - "error_message": null, - "display_name": "TryPost", - "display_username": "trypostit", - "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", - "social_account": { - "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", - "platform": "x", - "display_name": "TryPost", - "username": "trypostit", - "status": "connected", - "has_posting_schedule": true, - "timezone": "America/Sao_Paulo", - "posting_goal": 5, - "max_content_length": 280, - "long_posts": false, - "verified_badge": null + "media": [ + { + "id": "9d3c1f30-8a9b-4c0d-9e1f-2a3b4c5d6e7f", + "path": "medias/9d3c1f30-8a9b-4c0d-9e1f-2a3b4c5d6e7f.jpg", + "url": "https://media.trypost.it/medias/9d3c1f30-8a9b-4c0d-9e1f-2a3b4c5d6e7f.jpg", + "type": "image", + "mime_type": "image/jpeg", + "original_filename": "launch.jpg", + "size": 482113, + "meta": { + "width": 1080, + "height": 1350, + "alt_text": "Product launch banner" } } ], + "status": "scheduled", + "schedule_mode": "queue", + "scheduled_at": "2026-10-13 12:00:00", + "published_at": null, + "approval_requested_by": null, + "approval_requested_at": null, + "approved_by": null, + "approved_at": null, + "recurrence": null, + "origin": "trypost", + "publish_status": "pending", + "platform": "x", + "content_type": "x_post", + "meta": { + "thread_replies": [ + { + "text": "2/ Replies publish under the first post.", + "media": [] + } + ] + }, + "platform_url": null, + "error_message": null, + "display_name": "TryPost", + "display_username": "trypostit", + "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", + "social_account": { + "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", + "platform": "x", + "display_name": "TryPost", + "username": "trypostit", + "status": "connected", + "has_posting_schedule": true, + "timezone": "America/Sao_Paulo", + "posting_goal": 5, + "max_content_length": 280, + "long_posts": false, + "verified_badge": null + }, "labels": [ { "id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f", @@ -5886,7 +5737,6 @@ "reactions": [ { "id": "9d4a0b1c-2d3e-4f5a-8b6c-7d8e9f0a1b2c", - "post_platform_id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", "post_id": "9d3c1f2a-6b4e-4c8a-9f1e-2a7b5c8d0e41", "social_account_key": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", "platform": "x", @@ -5908,7 +5758,6 @@ "comments": [ { "id": "9d4a0b1c-2d3e-4f5a-8b6c-7d8e9f0a1b2c", - "post_platform_id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", "post_id": "9d3c1f2a-6b4e-4c8a-9f1e-2a7b5c8d0e41", "social_account_key": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", "platform": "x", @@ -6068,7 +5917,7 @@ "reason": null, "publication": { "id": "9d4a0b1c-2d3e-4f5a-8b6c-7d8e9f0a1b2c", - "post_platform_id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", + "post_id": "9d3c1f2a-6b4e-4c8a-9f1e-2a7b5c8d0e41", "social_account_key": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", "platform": "x", "origin": "trypost", @@ -6452,7 +6301,6 @@ "reactions": [ { "id": "9d4a0b1c-2d3e-4f5a-8b6c-7d8e9f0a1b2c", - "post_platform_id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", "post_id": "9d3c1f2a-6b4e-4c8a-9f1e-2a7b5c8d0e41", "social_account_key": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", "platform": "x", @@ -6474,7 +6322,6 @@ "comments": [ { "id": "9d4a0b1c-2d3e-4f5a-8b6c-7d8e9f0a1b2c", - "post_platform_id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", "post_id": "9d3c1f2a-6b4e-4c8a-9f1e-2a7b5c8d0e41", "social_account_key": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", "platform": "x", @@ -8083,7 +7930,6 @@ "post.scheduled", "post.unscheduled", "post.published", - "post.partially_published", "post.failed", "post.deleted" ], @@ -8108,7 +7954,6 @@ "post.scheduled", "post.unscheduled", "post.published", - "post.partially_published", "post.failed", "post.deleted" ], @@ -8253,32 +8098,24 @@ } ], "media": [], - "platforms": [ - { - "id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", - "social_account_id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", - "platform": "x", - "content_type": "x_post", - "enabled": true, - "status": "published", - "platform_post_id": "1843456789012345678", - "platform_url": "https://x.com/trypostit/status/1843456789012345678", - "published_at": "2026-10-08T14:00:02+00:00", - "error_message": null, - "error_context": null, - "display_name": "TryPost", - "display_username": "trypostit", - "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", - "meta": [], - "social_account": { - "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", - "platform": "x", - "display_name": "TryPost", - "username": "trypostit", - "status": "connected" - } - } - ] + "publish_status": "published", + "social_account_id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", + "platform": "x", + "content_type": "x_post", + "platform_post_id": "1843456789012345678", + "platform_url": "https://x.com/trypostit/status/1843456789012345678", + "error_message": null, + "display_name": "TryPost", + "display_username": "trypostit", + "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", + "meta": [], + "social_account": { + "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", + "platform": "x", + "display_name": "TryPost", + "username": "trypostit", + "status": "connected" + } }, "created_at": "2026-10-08T14:00:02+00:00" }, @@ -9402,12 +9239,8 @@ "posts": [ { "id": "9d3c1f40-7c8d-4e9f-8a0b-1c2d3e4f5a6b", - "platforms": [ - { - "platform": "tiktok", - "status": "published" - } - ] + "platform": "tiktok", + "publish_status": "published" } ], "created_at": "2026-10-07T21:15:00+00:00", @@ -9478,7 +9311,7 @@ ], "summary": "List content types", "operationId": "listContentTypes", - "description": "Lists every network with its text limit, hashtag limit, alt text limit, required settings (`required_meta`), default content type, and the media rules of each content type: media counts and sizes, formats, aspect ratios, whether text is sent (`captionless`) and whether thread replies are accepted. Use it to build `platforms[].content_type` and `meta` for posts. Not paginated.", + "description": "Lists every network with its text limit, hashtag limit, alt text limit, required settings (`required_meta`), default content type, and the media rules of each content type: media counts and sizes, formats, aspect ratios, whether text is sent (`captionless`) and whether thread replies are accepted. Use it to build a post's `content_type` and `meta`. Not paginated.", "responses": { "200": { "description": "Every network.", @@ -10683,126 +10516,105 @@ } ] }, - "PostPlatform": { + "Post": { "type": "object", - "description": "The post's destination: one social account and its format and settings.", + "description": "A post on one channel. Posts created together from several channels are independent posts that share a `post_group_id`.", "properties": { "id": { "type": "string", - "format": "uuid", - "description": "Destination id. Use it as `platforms[].id` on `PUT /posts/{post}`." + "format": "uuid" }, - "platform": { - "$ref": "#/components/schemas/Platform" + "post_group_id": { + "type": [ + "string", + "null" + ], + "format": "uuid", + "description": "Shared by the posts created in one batch." }, - "content_type": { + "author": { "oneOf": [ { - "$ref": "#/components/schemas/ContentType" + "$ref": "#/components/schemas/Person" }, { "type": "null" } ] }, - "meta": { + "content": { "type": [ - "object", - "array", + "string", "null" ], - "maxItems": 0, - "description": "Stored per-network settings (see `PlatformMeta`). Empty settings may serialize as an empty array." + "description": "Post text, stored as sent (posts written in the app hold the editor's HTML)." }, - "status": { + "media": { "type": [ - "string", + "array", "null" ], + "items": { + "$ref": "#/components/schemas/MediaItem" + } + }, + "status": { + "type": "string", "enum": [ - "pending", + "draft", + "pending_approval", + "scheduled", "publishing", - "retrying", - "pending_review", "published", - "failed", - "rejected", - null + "failed" ], - "description": "Publishing state on this network. `retrying` waits for a retry after a network limit; `pending_review` and `rejected` come from networks that review posts (Google Business)." + "description": "Lifecycle of the post in TryPost. `pending_approval` is a post a member who needs approval asked to schedule or publish. The outcome on the network is `publish_status`." }, - "enabled": { - "type": "boolean" - }, - "platform_url": { + "schedule_mode": { "type": [ "string", "null" ], - "description": "Link to the published post on the network." + "enum": [ + "queue", + "custom", + null + ], + "description": "`queue`: the post holds a slot of the channel's posting schedule. `custom`: it is scheduled at its own time." }, - "published_at": { + "scheduled_at": { "type": [ "string", "null" ], "description": "UTC, format `Y-m-d H:i:s`." }, - "error_message": { - "type": [ - "string", - "null" - ] - }, - "display_name": { + "published_at": { "type": [ "string", "null" ], - "description": "Account name, kept even after the account is disconnected." - }, - "display_username": { - "type": [ - "string", - "null" - ] - }, - "display_avatar": { - "type": [ - "string", - "null" - ] + "description": "UTC, format `Y-m-d H:i:s`." }, - "social_account": { + "approval_requested_by": { "oneOf": [ { - "$ref": "#/components/schemas/SocialAccount" + "$ref": "#/components/schemas/Person" }, { "type": "null" } ], - "description": "The account, or `null` once it was removed (the `display_*` fields keep its name)." - } - } - }, - "Post": { - "type": "object", - "description": "A post on one channel. Posts created together from several channels are independent posts that share a `post_group_id`.", - "properties": { - "id": { - "type": "string", - "format": "uuid" + "description": "Who asked for approval (not always the author)." }, - "post_group_id": { + "approval_requested_at": { "type": [ "string", "null" ], - "format": "uuid", - "description": "Shared by the posts created in one batch." + "description": "UTC, format `Y-m-d H:i:s`." }, - "author": { + "approved_by": { "oneOf": [ { "$ref": "#/components/schemas/Person" @@ -10812,137 +10624,135 @@ } ] }, - "content": { + "approved_at": { "type": [ "string", "null" ], - "description": "Post text, stored as sent (posts written in the app hold the editor's HTML)." + "description": "UTC, format `Y-m-d H:i:s`." }, - "media": { + "recurrence": { "type": [ - "array", + "object", "null" ], - "items": { - "$ref": "#/components/schemas/MediaItem" + "description": "Repeat rule, or `null`.", + "properties": { + "interval": { + "type": "integer" + }, + "frequency": { + "type": "string", + "enum": [ + "day", + "week", + "month", + "year" + ] + }, + "remaining": { + "type": [ + "integer", + "null" + ], + "description": "Repeats left after the next one." + } } }, - "status": { + "origin": { "type": "string", "enum": [ - "draft", - "pending_approval", - "scheduled", + "trypost", + "network" + ], + "description": "`network` for posts imported from the network (published outside TryPost)." + }, + "publish_status": { + "type": "string", + "enum": [ + "pending", "publishing", + "retrying", + "pending_review", "published", - "partially_published", - "failed" + "failed", + "rejected" ], - "description": "`pending_approval` is a post a member who needs approval asked to schedule or publish." + "description": "Publishing state on the network. `pending` until the post is sent; `retrying` waits for a retry after a network limit; `pending_review` and `rejected` come from networks that review posts (Google Business)." }, - "schedule_mode": { + "platform": { + "oneOf": [ + { + "$ref": "#/components/schemas/Platform" + }, + { + "type": "null" + } + ], + "description": "Network of the post's channel." + }, + "content_type": { + "oneOf": [ + { + "$ref": "#/components/schemas/ContentType" + }, + { + "type": "null" + } + ] + }, + "meta": { "type": [ - "string", + "object", + "array", "null" ], - "enum": [ - "queue", - "custom", - null - ], - "description": "`queue`: the post holds a slot of the channel's posting schedule. `custom`: it is scheduled at its own time." + "maxItems": 0, + "description": "Stored per-network settings (see `PlatformMeta`). Empty settings may serialize as an empty array." }, - "scheduled_at": { + "platform_url": { "type": [ "string", "null" ], - "description": "UTC, format `Y-m-d H:i:s`." + "description": "Link to the published post on the network." }, - "published_at": { + "error_message": { "type": [ "string", "null" ], - "description": "UTC, format `Y-m-d H:i:s`." - }, - "approval_requested_by": { - "oneOf": [ - { - "$ref": "#/components/schemas/Person" - }, - { - "type": "null" - } - ], - "description": "Who asked for approval (not always the author)." + "description": "Why publishing failed, when it did." }, - "approval_requested_at": { + "display_name": { "type": [ "string", "null" ], - "description": "UTC, format `Y-m-d H:i:s`." - }, - "approved_by": { - "oneOf": [ - { - "$ref": "#/components/schemas/Person" - }, - { - "type": "null" - } - ] + "description": "Account name, kept even after the account is disconnected." }, - "approved_at": { + "display_username": { "type": [ "string", "null" - ], - "description": "UTC, format `Y-m-d H:i:s`." + ] }, - "recurrence": { + "display_avatar": { "type": [ - "object", + "string", "null" - ], - "description": "Repeat rule, or `null`.", - "properties": { - "interval": { - "type": "integer" - }, - "frequency": { - "type": "string", - "enum": [ - "day", - "week", - "month", - "year" - ] + ] + }, + "social_account": { + "oneOf": [ + { + "$ref": "#/components/schemas/SocialAccount" }, - "remaining": { - "type": [ - "integer", - "null" - ], - "description": "Repeats left after the next one." + { + "type": "null" } - } - }, - "origin": { - "type": "string", - "enum": [ - "trypost", - "network" ], - "description": "`network` for posts imported from the network (published outside TryPost)." - }, - "platforms": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PostPlatform" - } + "description": "The account, or `null` once it was removed (the `display_*` fields keep its name)." }, "labels": { "type": "array", @@ -10994,42 +10804,35 @@ "approved_at": null, "recurrence": null, "origin": "trypost", - "platforms": [ - { - "id": "9d3c1f2b-1a2b-4c3d-8e4f-5a6b7c8d9e10", - "platform": "x", - "content_type": "x_post", - "meta": { - "thread_replies": [ - { - "text": "2/ Replies publish under the first post.", - "media": [] - } - ] - }, - "status": "pending", - "enabled": true, - "platform_url": null, - "published_at": null, - "error_message": null, - "display_name": "TryPost", - "display_username": "trypostit", - "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", - "social_account": { - "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", - "platform": "x", - "display_name": "TryPost", - "username": "trypostit", - "status": "connected", - "has_posting_schedule": true, - "timezone": "America/Sao_Paulo", - "posting_goal": 5, - "max_content_length": 280, - "long_posts": false, - "verified_badge": null + "publish_status": "pending", + "platform": "x", + "content_type": "x_post", + "meta": { + "thread_replies": [ + { + "text": "2/ Replies publish under the first post.", + "media": [] } - } - ], + ] + }, + "platform_url": null, + "error_message": null, + "display_name": "TryPost", + "display_username": "trypostit", + "display_avatar": "https://pbs.twimg.com/profile_images/1790000000000000000/avatar_400x400.jpg", + "social_account": { + "id": "9c8b7a65-4d3e-4f2a-8b1c-0d9e8f7a6b54", + "platform": "x", + "display_name": "TryPost", + "username": "trypostit", + "status": "connected", + "has_posting_schedule": true, + "timezone": "America/Sao_Paulo", + "posting_goal": 5, + "max_content_length": 280, + "long_posts": false, + "verified_badge": null + }, "labels": [ { "id": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f", @@ -11115,12 +10918,13 @@ "type": "string", "format": "uuid" }, - "post_platform_id": { + "post_id": { "type": [ "string", "null" ], - "format": "uuid" + "format": "uuid", + "description": "TryPost post, or `null` for a post made outside TryPost." }, "social_account_key": { "type": [ @@ -11349,66 +11153,73 @@ "type": "string", "format": "uuid" }, - "platforms": { - "type": "array", - "items": { - "type": "object", - "properties": { - "post_platform_id": { - "type": "string", - "format": "uuid" - }, - "platform": { - "$ref": "#/components/schemas/Platform" - }, - "status": { - "type": "string" - }, - "platform_post_id": { - "type": [ - "string", - "null" - ] - }, - "platform_url": { - "type": [ - "string", - "null" - ] - }, - "metrics": { - "description": "The saved analytics, or `{unsupported: true, reason}` when there are none: `not_published`, `platform_not_supported` (LinkedIn, LinkedIn Page, Telegram, Discord and Google Business are not in analytics) or `not_collected` (not collected yet).", - "oneOf": [ - { - "$ref": "#/components/schemas/PublicationAnalytics" - }, - { - "type": "object", - "title": "Unavailable", - "properties": { - "unsupported": { - "type": "boolean", - "const": true - }, - "reason": { - "type": "string", - "enum": [ - "not_published", - "platform_not_supported", - "not_collected" - ] - } - } - } - ] + "platform": { + "oneOf": [ + { + "$ref": "#/components/schemas/Platform" + }, + { + "type": "null" + } + ] + }, + "publish_status": { + "type": "string", + "enum": [ + "pending", + "publishing", + "retrying", + "pending_review", + "published", + "failed", + "rejected" + ], + "description": "Publishing state on the network (see `Post.publish_status`)." + }, + "platform_post_id": { + "type": [ + "string", + "null" + ], + "description": "The post's id on the network." + }, + "platform_url": { + "type": [ + "string", + "null" + ] + }, + "metrics": { + "description": "The saved analytics, or `{unsupported: true, reason}` when there are none: `not_published`, `platform_not_supported` (LinkedIn, LinkedIn Page, Telegram, Discord and Google Business are not in analytics) or `not_collected` (not collected yet).", + "oneOf": [ + { + "$ref": "#/components/schemas/PublicationAnalytics" + }, + { + "type": "object", + "title": "Unavailable", + "properties": { + "unsupported": { + "type": "boolean", + "const": true + }, + "reason": { + "type": "string", + "enum": [ + "not_published", + "platform_not_supported", + "not_collected" + ] + } } } - } + ] } } }, "PostPreview": { "type": "object", + "description": "Without a channel only `post_id`, `original_content` and `original_length` are returned.", "properties": { "post_id": { "type": "string", @@ -11421,68 +11232,60 @@ "type": "integer", "description": "Characters (code points) of the stored content." }, - "platforms": { + "platform": { + "allOf": [ + { + "$ref": "#/components/schemas/Platform" + } + ], + "description": "Present only when the post has a channel, like the fields below." + }, + "content_type": { + "type": [ + "string", + "null" + ] + }, + "sanitized_content": { + "type": "string", + "description": "The text the publisher sends to the network." + }, + "sanitized_length": { + "type": "integer" + }, + "max_content_length": { + "type": "integer", + "description": "Text limit of the account (an X account with long posts gets 25000)." + }, + "truncated": { + "type": "boolean", + "description": "Whether the sent text is shorter than the stored text." + }, + "title": { + "type": "string", + "description": "YouTube only: the video title." + }, + "description": { + "type": "string", + "description": "YouTube only: the video description." + }, + "description_length_bytes": { + "type": "integer", + "description": "YouTube only. The limit is 5000 bytes." + }, + "thread_replies": { "type": "array", - "description": "One entry per enabled destination.", + "description": "X, Bluesky and Mastodon, when the post has replies.", "items": { "type": "object", "properties": { - "post_platform_id": { - "type": "string", - "format": "uuid" - }, - "platform": { - "$ref": "#/components/schemas/Platform" - }, - "content_type": { - "type": [ - "string", - "null" - ] - }, - "sanitized_content": { - "type": "string", - "description": "The text the publisher sends to the network." - }, - "sanitized_length": { - "type": "integer" - }, - "max_content_length": { - "type": "integer", - "description": "Text limit of the account (an X account with long posts gets 25000)." - }, - "truncated": { - "type": "boolean", - "description": "Whether the sent text is shorter than the stored text." - }, - "title": { - "type": "string", - "description": "YouTube only: the video title." - }, - "description": { - "type": "string", - "description": "YouTube only: the video description." - }, - "description_length_bytes": { - "type": "integer", - "description": "YouTube only. The limit is 5000 bytes." + "text": { + "type": "string" }, - "thread_replies": { + "media": { "type": "array", - "description": "X, Bluesky and Mastodon, when the post has replies.", "items": { - "type": "object", - "properties": { - "text": { - "type": "string" - }, - "media": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MediaItem" - } - } - } + "$ref": "#/components/schemas/MediaItem" } } } @@ -11899,13 +11702,6 @@ "format": "uuid", "description": "Analytics publication id. Read its detail with `GET /analytics/publications/{publication}`." }, - "post_platform_id": { - "type": [ - "string", - "null" - ], - "format": "uuid" - }, "post_id": { "type": [ "string", @@ -12877,11 +12673,10 @@ "post.scheduled", "post.unscheduled", "post.published", - "post.partially_published", "post.failed", "post.deleted" ], - "description": "- `post.created`: a post was created. Its `data.status` is the status it was created with (`draft`, `scheduled`, `publishing` or `pending_approval`).\n- `post.scheduled`: an existing post was scheduled (for example a draft scheduled, or a request approved). A post created already scheduled sends only `post.created`.\n- `post.unscheduled`: a scheduled post went back to draft or to pending approval.\n- `post.published`: every destination was published.\n- `post.partially_published`: some destinations were published and others failed.\n- `post.failed`: publishing failed.\n- `post.deleted`: a post was deleted. Not sent for posts imported from the network or removed by disconnecting their channel. Its `data` is only `{id, workspace_id}`." + "description": "- `post.created`: a post was created. Its `data.status` is the status it was created with (`draft`, `scheduled`, `publishing` or `pending_approval`).\n- `post.scheduled`: an existing post was scheduled (for example a draft scheduled, or a request approved). A post created already scheduled sends only `post.created`.\n- `post.unscheduled`: a scheduled post went back to draft or to pending approval.\n- `post.published`: the post was published.\n- `post.failed`: publishing failed.\n- `post.deleted`: a post was deleted. Not sent for posts imported from the network or removed by disconnecting their channel. Its `data` is only `{id, workspace_id}`." }, "Webhook": { "type": "object", @@ -13040,7 +12835,7 @@ "$ref": "#/components/schemas/PlatformMeta" } ], - "description": "Per-network settings, with the same rules as a post's `platforms[].meta` (thread reply media by `url` is refused). On an active repurpose, the settings a network needs to publish are required, as on a scheduled post (for example TikTok `privacy_level`, Pinterest `board_id`, Discord `channel_id`, and a valid YouTube `description`)." + "description": "Per-network settings, with the same rules as a post's `meta` (thread reply media by `url` is refused). On an active repurpose, the settings a network needs to publish are required, as on a scheduled post (for example TikTok `privacy_level`, Pinterest `board_id`, Discord `channel_id`, and a valid YouTube `description`)." } } }, @@ -13241,7 +13036,7 @@ }, "posts": { "type": "array", - "description": "Posts created for this video, with the status of each destination.", + "description": "Posts created for this video, one per destination, with the network and its publishing state.", "items": { "type": "object", "properties": { @@ -13249,22 +13044,27 @@ "type": "string", "format": "uuid" }, - "platforms": { - "type": "array", - "items": { - "type": "object", - "properties": { - "platform": { - "$ref": "#/components/schemas/Platform" - }, - "status": { - "type": [ - "string", - "null" - ] - } + "platform": { + "oneOf": [ + { + "$ref": "#/components/schemas/Platform" + }, + { + "type": "null" } - } + ] + }, + "publish_status": { + "type": "string", + "enum": [ + "pending", + "publishing", + "retrying", + "pending_review", + "published", + "failed", + "rejected" + ] } } } diff --git a/platforms/bluesky.mdx b/platforms/bluesky.mdx index 31a1b72..43dd630 100644 --- a/platforms/bluesky.mdx +++ b/platforms/bluesky.mdx @@ -39,7 +39,7 @@ The video size is the lexicon's decimal value (300 000 000 bytes). Bluesky's ima - **300 characters** per post. TryPost counts code points, so an emoji counts as one. - **Threads** — click **Start thread** to add replies under the post, up to **24**. Each reply has its own text and up to 4 media items, following the same rules as a post. -- **Link preview** — the first link gets a preview card. Remove it with the **×** on the card (`platforms[].meta.link_preview: false` on the API). +- **Link preview** — the first link gets a preview card. Remove it with the **×** on the card (`meta.link_preview: false` on the API). - **Alt text** — up to 2,000 characters per image. ## Insights diff --git a/platforms/discord.mdx b/platforms/discord.mdx index e5795fa..6f8ffdd 100644 --- a/platforms/discord.mdx +++ b/platforms/discord.mdx @@ -25,7 +25,7 @@ Connect as many servers as you like — each one becomes its own channel in TryP ## Choosing a Discord channel -A Discord connection is the whole server, so every Discord post must target a **channel**. In the post editor's Discord settings, pick the channel from the searchable list. A post without a channel cannot be scheduled or published (`platforms[].meta.channel_id` on the API; list channels with [List Discord channels](/api-reference/social-accounts/list-discord-channels), `GET /social-accounts/{account}/channels`). +A Discord connection is the whole server, so every Discord post must target a **channel**. In the post editor's Discord settings, pick the channel from the searchable list. A post without a channel cannot be scheduled or published (`meta.channel_id` on the API; list channels with [List Discord channels](/api-reference/social-accounts/list-discord-channels), `GET /social-accounts/{account}/channels`). Only channels the bot can actually post in are shown — **text** and **announcement** channels where the bot has *View Channel* + *Send Messages*. Forum, voice, stage, and category channels are excluded (they don't accept a direct message), as are channels the bot has no access to. diff --git a/platforms/facebook.mdx b/platforms/facebook.mdx index f7827b4..ff53a94 100644 --- a/platforms/facebook.mdx +++ b/platforms/facebook.mdx @@ -42,7 +42,7 @@ Images are published at their own aspect ratio; TryPost does not crop them on th ## Text and per-post options - **10,000 characters** per post. -- **Link preview** — the first link in a post gets a preview card. Remove it with the **×** on the card (`platforms[].meta.link_preview: false` on the API), or use **Replace link preview with media**. +- **Link preview** — the first link in a post gets a preview card. Remove it with the **×** on the card (`meta.link_preview: false` on the API), or use **Replace link preview with media**. - **Alt text** — up to 1,000 characters per image. ## Insights diff --git a/platforms/google-business.mdx b/platforms/google-business.mdx index 323c707..95574a8 100644 --- a/platforms/google-business.mdx +++ b/platforms/google-business.mdx @@ -39,7 +39,7 @@ TryPost converts the image to JPEG (up to 2048 px wide, under 5 MB) before sendi **Text.** Up to **1,500 characters**, sent as plain text. The text and the image are each optional. -**Post type** (`platforms[].meta.topic_type`): +**Post type** (`meta.topic_type`): | Post type | `topic_type` | What it needs | |-----------|--------------|---------------| diff --git a/platforms/instagram.mdx b/platforms/instagram.mdx index e84390c..68ba55b 100644 --- a/platforms/instagram.mdx +++ b/platforms/instagram.mdx @@ -66,8 +66,8 @@ A feed image outside 3:4 to 1.91:1 blocks scheduling and publishing; use the cro - **2,200 characters** per caption. - **5 hashtags at most.** A post with more cannot be scheduled or published (web, API and MCP); a draft can still be saved. Stories are exempt because they send no caption. -- **Share to feed** — a Reel also appears on your profile grid by default; turn it off with **Share to feed** (`platforms[].meta.share_to_feed`). -- **AI-generated** — label the post as made with AI (`platforms[].meta.is_ai_generated`). +- **Share to feed** — a Reel also appears on your profile grid by default; turn it off with **Share to feed** (`meta.share_to_feed`). +- **AI-generated** — label the post as made with AI (`meta.is_ai_generated`). - **Tag people** — tag accounts on feed images in **Edit Media** > **Tag People**. - **Video cover** — pick the frame shown as the cover of a feed or reel video in **Edit Media** > **Thumbnail**. - **Alt text** — up to 1,000 characters per image. diff --git a/platforms/linkedin.mdx b/platforms/linkedin.mdx index d220a46..3f448a8 100644 --- a/platforms/linkedin.mdx +++ b/platforms/linkedin.mdx @@ -43,8 +43,8 @@ Images, a video and a PDF never mix in one post, and GIFs are not accepted. Offi - **3,000 characters** per post. Characters are counted as code points, so an emoji counts as one. - Bold text from the editor is converted to LinkedIn's bold characters; other formatting is sent as plain text. -- **Link preview** — the first link in the text gets a preview card. Remove it with the **×** on the card (`platforms[].meta.link_preview: false` on the API). -- **Document title** — for PDF posts, set the title shown on the document (`platforms[].meta.document_title`, up to 300 characters). Defaults to the file name. +- **Link preview** — the first link in the text gets a preview card. Remove it with the **×** on the card (`meta.link_preview: false` on the API). +- **Document title** — for PDF posts, set the title shown on the document (`meta.document_title`, up to 300 characters). Defaults to the file name. - **Alt text** — up to 4,086 characters per image. ## Insights diff --git a/platforms/mastodon.mdx b/platforms/mastodon.mdx index 4978ea1..5feee99 100644 --- a/platforms/mastodon.mdx +++ b/platforms/mastodon.mdx @@ -35,7 +35,7 @@ Official specs: [Attachments](https://docs.joinmastodon.org/user/posting/#attach ## Text and per-post options - **500 characters** per post, **including the content warning**. TryPost uses the default Mastodon limit even if your instance allows more. -- **Content warning** — hide the post behind a warning (`platforms[].meta.spoiler_text`, up to 500 characters). In a thread, the warning is repeated on every reply. +- **Content warning** — hide the post behind a warning (`meta.spoiler_text`, up to 500 characters). In a thread, the warning is repeated on every reply. - **Threads** — click **Start thread** to add replies under the post, up to **24**. Each reply has its own text and up to 4 media items. - **Alt text** — up to 1,500 characters per image. diff --git a/platforms/pinterest.mdx b/platforms/pinterest.mdx index 2f8e960..028907a 100644 --- a/platforms/pinterest.mdx +++ b/platforms/pinterest.mdx @@ -21,7 +21,7 @@ TryPost opens the new channel's **Publish** page and asks how many times a week Pinterest requires every pin to belong to a **board**. When you post to a Pinterest channel, its settings in the editor show **Pinning to** with a searchable board list. Pick a board, or type a name and click **Create** to make a new one. -A post without a board cannot be scheduled or published — the editor, the [REST API](/api-reference/introduction) and MCP all reject it with *Select a Pinterest board to publish this post.* On the API, send the board as `platforms[].meta.board_id`; list boards with [List Pinterest boards](/api-reference/social-accounts/list-pinterest-boards) (`GET /social-accounts/{account}/boards`) and create one with [Create Pinterest board](/api-reference/social-accounts/create-pinterest-board) (`POST /social-accounts/{account}/boards`). +A post without a board cannot be scheduled or published — the editor, the [REST API](/api-reference/introduction) and MCP all reject it with *Select a Pinterest board to publish this post.* On the API, send the board as `meta.board_id`; list boards with [List Pinterest boards](/api-reference/social-accounts/list-pinterest-boards) (`GET /social-accounts/{account}/boards`) and create one with [Create Pinterest board](/api-reference/social-accounts/create-pinterest-board) (`POST /social-accounts/{account}/boards`). ## Supported content types @@ -46,8 +46,8 @@ Official specs: [Creating boards and pins](https://developers.pinterest.com/docs ## Text and per-post options - **Description** — the post text, up to **800 characters**. -- **Title** — optional pin title (`platforms[].meta.title`, up to 100 characters). -- **Destination link** — where the pin leads (`platforms[].meta.link`, http or https, up to 2,048 characters). +- **Title** — optional pin title (`meta.title`, up to 100 characters). +- **Destination link** — where the pin leads (`meta.link`, http or https, up to 2,048 characters). - **Video cover** — pick the cover frame of a video pin in **Edit Media** > **Thumbnail**. - **Alt text** — up to 500 characters per image. diff --git a/platforms/threads.mdx b/platforms/threads.mdx index b4600ed..3aa6fcf 100644 --- a/platforms/threads.mdx +++ b/platforms/threads.mdx @@ -35,7 +35,7 @@ TryPost opens the new channel's **Publish** page and asks how many times a week ## Text and per-post options - **500 characters** per post. -- **Topic** — tag the post with a topic (`platforms[].meta.topic_tag`): 1 to 50 characters after a leading `#` is dropped, without `.` or `&`. +- **Topic** — tag the post with a topic (`meta.topic_tag`): 1 to 50 characters after a leading `#` is dropped, without `.` or `&`. - **Link preview** — Threads always shows a preview card for the first link; it cannot be removed. - **Alt text** — up to 1,000 characters per image. diff --git a/platforms/tiktok.mdx b/platforms/tiktok.mdx index cbe12bc..0c58141 100644 --- a/platforms/tiktok.mdx +++ b/platforms/tiktok.mdx @@ -39,7 +39,7 @@ Official specs: [Media Transfer Guide](https://developers.tiktok.com/doc/content ## Text and per-post options -TikTok allows **2,200 characters** of caption. The editor's TikTok settings (or `platforms[].meta` on the API) hold the options TikTok requires you to show: +TikTok allows **2,200 characters** of caption. The editor's TikTok settings (or `meta` on the API) hold the options TikTok requires you to show: | Setting | `meta` key | Values | Applies to | |---------|-----------|--------|------------| diff --git a/platforms/x-twitter.mdx b/platforms/x-twitter.mdx index 6210fa7..844b479 100644 --- a/platforms/x-twitter.mdx +++ b/platforms/x-twitter.mdx @@ -39,7 +39,7 @@ These are Post (`tweet_video`) limits — 8 GB / 20 minutes by default, 16 GB / - **280 characters** per post. Accounts with long posts — an X Basic, Premium or Premium+ subscription, or a verified business — get **25,000**. TryPost reads the subscription when you connect and re-checks it daily. - **Threads** — click **Start thread** to add replies under the post, up to **24**. Each reply has its own text and up to 4 media items, and is measured against the same limit as the post. X bills each reply as a post, and only allows replies through the API to your own post. -- **AI-generated** — mark the post as made with AI (`platforms[].meta.is_ai_generated`). +- **AI-generated** — mark the post as made with AI (`meta.is_ai_generated`). - **Alt text** — up to 1,000 characters per image. ## Links in posts diff --git a/platforms/youtube.mdx b/platforms/youtube.mdx index 708a824..0c16cc8 100644 --- a/platforms/youtube.mdx +++ b/platforms/youtube.mdx @@ -33,7 +33,7 @@ Official specs: [`videos.insert`](https://developers.google.com/youtube/v3/docs/ ## Title, description and settings -**Title.** Set it in the YouTube settings (**Title**, `platforms[].meta.title`, up to 100 characters, no `<` or `>` — rejected even on drafts). Leave it empty and TryPost uses the first non-empty line of your post text, with `<` and `>` removed and cut to 100 characters. YouTube rejects an upload without a title, so a post with neither a title nor any text cannot be scheduled or published. +**Title.** Set it in the YouTube settings (**Title**, `meta.title`, up to 100 characters, no `<` or `>` — rejected even on drafts). Leave it empty and TryPost uses the first non-empty line of your post text, with `<` and `>` removed and cut to 100 characters. YouTube rejects an upload without a title, so a post with neither a title nor any text cannot be scheduled or published. **Description.** The post text, unless you set **Description** (`meta.description`, plain text, at most 5,000 bytes). diff --git a/self-hosting/upgrading.mdx b/self-hosting/upgrading.mdx index 85d81af..fee9bfe 100644 --- a/self-hosting/upgrading.mdx +++ b/self-hosting/upgrading.mdx @@ -9,7 +9,7 @@ Pull the new version, then run the same steps you already run on a deploy: - Rebuild dependencies and assets, and re-run the [optimization commands](/self-hosting/production#optimizations) - Apply migrations with `php artisan migrate --force` -- When you cross into TryPost 2.0, run `php artisan release:trypost-2` once, right after the migrations — see [below](#upgrading-to-trypost-20) +- When you cross into TryPost 2.0, run `php artisan release:trypost-2` once, right after the migrations — see [below](#upgrading-to-trypost-20). Still on v1.1.0? Stop at v2.0.0 first — see [Upgrading past TryPost 2.0](#upgrading-past-trypost-20) - Restart the long-running processes so they pick up the new code — `php artisan horizon:terminate` and `php artisan reverb:restart` let Supervisor bring them back On Docker, back the database up, then `docker compose -f compose.prod.yaml pull app` and `docker compose -f compose.prod.yaml up -d` — see [Update to a new version](/self-hosting/docker#update-to-a-new-version). The container runs `migrate --force` itself on every boot, so the backup has to come first. @@ -18,8 +18,31 @@ On Docker, back the database up, then `docker compose -f compose.prod.yaml pull **Back the database up before `migrate --force`.** Migrations run unattended and some of them rewrite data, not just schema. A dump you can restore is the only reliable way back. +## Upgrading past TryPost 2.0 + +The release after 2.0 stores each post's channel on the post itself and removes the old per-post destinations table. It also removes `release:trypost-2` and the other 2.0 upgrade commands. + +**On v1.1.0? Upgrade in two steps.** First upgrade to **v2.0.0** and run its release command, as described in [Upgrading to TryPost 2.0](#upgrading-to-trypost-20): + +```bash +php artisan migrate --force +php artisan release:trypost-2 --force --include-unsubscribed +``` + +Only then upgrade to the newer version. Skipping v2.0.0 does not work: the first new migration stops and tells you how many posts still need the 2.0 release command (posts with more than one channel, for example). Nothing is changed when it stops; upgrade to v2.0.0, run the command, then migrate again. + +Already on 2.0 with the release command done? The routine above is enough. If the migration still stops, it names what is left; most of it settles once the posts in flight finish publishing. + + + This release also changes the REST API, MCP and webhook payloads: a post has `social_account_id`, `content_type` and `meta` at the top level instead of a `platforms` list, and the `post.partially_published` event is gone. See [Posting](/api-reference/guides/posting) and [Webhooks](/knowledge-base/settings/webhooks). + + ## Upgrading to TryPost 2.0 + + This section is for installs moving from 1.x to **v2.0.0**. The `release:trypost-2` command exists only in 2.0; versions after it no longer include it. + + TryPost 2.0 changes how posts, media and team permissions are stored. The schema changes run with the usual migrations; the data changes run in a separate one-off command, `release:trypost-2`.