<!-- Documentation Index: https://docs.publora.com/llms.txt -->
<!-- Canonical: https://docs.publora.com/changelog -->
# API Changelog and Deprecation Ledger

This page records externally relevant REST and MCP contract changes. Dates are deployment or documentation publication dates, not announcement estimates.

## Upcoming

### 2026-08-25 — scheduledTime strict-mode ramp

- **Affected surface:** REST `create-post` and `update-post`, plus MCP tools that schedule through them.
- **Tag:** **Potentially breaking**, configuration-dependent.
- **Change:** A `scheduledTime` at least five minutes in the past is scheduled to return `400 SCHEDULED_TIME_IN_PAST` starting on 2026-08-25. This calendar behavior applies only when production configuration does not explicitly override it with `SCHEDULED_TIME_STRICT`; an explicit flag wins in either direction.
- **Migration action:** Always send a future ISO 8601 UTC time. During the warn-first period, inspect `warnings[].code === "SCHEDULED_TIME_COERCED"` and the returned `scheduledTime` to find callers that need correction.

## 2026-10-15

### Post-log retention — publora.com #663

- **Affected surface:** `GET /api/v1/post-logs/:postGroupId`.
- **Tag:** **Potentially breaking** retention change.
- **Change:** Responses include events from the last 90 days by event `createdAt`,
  oldest first, limited to 100 entries. Old diagnostic events expire
  asynchronously; post groups and published post history remain available.
- **Migration action:** Handle HTTP 200 with `success: true` and `logs: []` for
  an owned group without recent events. Events older than 90 days are no longer
  returned; store diagnostic events on your side if your integration requires a
  longer history. Ownership and 404 behavior are unchanged. See
  [Post logs](endpoints/post-logs.md).

## 2026-10-02

### Publish Now — publora.com #608, #610

- **Affected surface:** personal REST `create-post` and `update-post`; MCP `create_post` and `update_post`; `GET /account-context` and MCP `account_context` queue counts.
- **Tag:** Additive.
- **Change:** `publishNow: true` publishes immediately at server time. It uses the monthly post allowance (`POST_LIMIT_REACHED` still applies) but takes no slot in the scheduled-post queue and ignores the schedule horizon. Combining it with a future `scheduledTime` or `status: "draft"` returns `400 PUBLISH_NOW_CONFLICT`; a non-boolean value returns `400 INVALID_PUBLISH_NOW`. Moving a scheduled post to Publish Now frees its queue slot and keeps its monthly reservation. A `scheduledTime` within a few seconds of server time is still treated as immediate for existing clients.
- **Docs:** `GET /account-context`, released with publora.com #589 on 2026-10-01, now has its own [REST reference page](endpoints/account-context.md) and OpenAPI entry.
- **Migration action:** None. New integrations should send `publishNow: true` instead of a current `scheduledTime`. See [Publish now](endpoints/create-post.md#publish-now).

## 2026-10-01

### YouTube granted scopes — publora.com #490

- **Affected surface:** REST `POST /test-connection/{platformId}`, YouTube only. Released in [backend PR #590](https://github.com/publora/publora.com/pull/590) on 2026-10-01.
- **Tag:** **Potentially breaking** permission values, with an additive `permissionsKnown` field.
- **Change:** `permissions` returns Google's recorded full scope URIs, such as `https://www.googleapis.com/auth/youtube`, instead of the fixed short `youtube.upload` and `youtube.readonly` list. A broad grant is not expanded into narrower grants. Connections without recorded scopes return `permissions: []` with `permissionsKnown: false`.
- **Migration action:** Handle `permissionsKnown: false` as unknown; recognize broad `youtube` access when checking capabilities. Do not require the old short scope labels or treat an unknown empty list as denied access. See [YouTube connection diagnostics](endpoints/test-connection.md#youtube-permissions--490-update).

### `account_context` MCP tool — publora.com #589

- **Affected surface:** New MCP tool `account_context`, which brings the personal MCP toolset from 19 to 20 tools. It reads the new REST endpoint `GET /api/v1/account-context` with the same API key.
- **Tag:** Additive.
- **Changes:**
  - `account_context` is read-only. It returns the plan, available features, publishing quotas (monthly platform posts, scheduled posts, connected channels), the schedule horizon with `maxScheduledDate`, and `allowedPlatforms` for the personal account behind the key. Quotas count platform posts, `limit: null` means unlimited, and a per-connection monthly limit (`scope: "connection"`) is broken down in `quotas.monthlyPosts.connections[]`.
  - The optional `scheduledTime` (ISO 8601 with `Z` or an offset) selects the monthly quota period of a planned batch. A past time resolves to the current period, and `effectiveQuotaTime` shows the date used. A malformed value returns `400 INVALID_SCHEDULED_TIME` from the REST endpoint; the MCP tool rejects it before calling the API.
  - The response is an advisory snapshot (`advisory: true`): it reserves nothing, and every write is still checked against the limits.
- **Migration action:** None. Optionally, call `account_context` before a batch of posts and after a quota or feature error. See [account_context](mcp/tools-reference.md#account_context).

## 2026-09-30

### `connectionStatus` on connections — publora.com #526

- **Affected surface:** REST `GET /platform-connections`; MCP `list_connections`.
- **Tag:** Additive. The field has been returned since publora.com #526 was merged on 2026-09-25, and is documented from 2026-09-30.
- **Changes:**
  - Each connection carries `connectionStatus`: `active` or `reconnect_required`, Publora's verdict on whether it can publish with the connection now. It is computed from stored data (reconnect flags, stored credentials, access-token expiry) without calling the platform. See [`connectionStatus` and `tokenStatus`](endpoints/platform-connections.md#connectionstatus-and-tokenstatus).
  - It differs from `tokenStatus` in two systematic cases. Telegram connections store no expiry date and are never flagged, so `tokenStatus` is always `unknown` while `connectionStatus` is `active`. An X or YouTube account reported suspended is `reconnect_required` while its `tokenStatus` stays `valid`.
  - Documentation corrections, no behavior change: `displayName` and `profileImageUrl` can be `null`, and `subscriptionType` (X only: `None`, `Basic`, `Premium`, `PremiumPlus`, re-checked when publishing to X at most once a day) is now in the OpenAPI schema.
- **Migration action:** Prompt a reconnect on `connectionStatus: "reconnect_required"` instead of `tokenStatus: "expired"`, and keep using `tokenStatus: "expiring_soon"` for advance warnings. Clients generated from the OpenAPI spec should treat `displayName` and `profileImageUrl` as nullable.

## 2026-09-29

### Ordered first comments and Agency first comment — publora.com #537, #538, #552

- **Affected surface:** personal REST `create-post`, `update-post`, `get-post`; MCP `create_post`, `update_post`, `get_post`; Agency Company post save and detail routes and MCP tools.
- **Tag:** Additive.
- **Change:** Personal posts accept `firstComments[]` with up to three ordered `{text, platforms?, delaySeconds?}` items. Delay is 0–86,400 seconds and delivery runs on scheduler ticks. A later comment waits for the previous one to post; failure stops the sequence. `firstComments: null` or `[]` clears it on update. Supplying both `firstComment` and `firstComments` returns `400 FIRST_COMMENT_INVALID`.
- **Read contract:** `get-post` / `get_post` return group `firstComments[]` and per-target `firstCommentResults[]`. Existing `firstComment` / `firstCommentResult` fields remain aliases for item 0; old documents read as a one-element array.
- **Agency:** Company draft save accepts one `firstComment`; Company post detail returns its per-target result. Multiple comments and delays remain personal-only. See the [Agency first comment guide](guides/agency-first-comments.md).
- **Migration action:** none for clients using the single-comment fields.

### Telegram: private channels over a bot connection — publora.com #570

- **Affected surface:** Telegram bot connections (dashboard and client connection pages); REST `GET /platform-connections` and `POST /test-connection/{platformId}`; MCP `list_connections`.
- **Tag:** Additive; **potentially breaking** for clients that assume `username` is always a string. Behavioral change for `test-connection`: a Telegram bot connection whose token still works can now return `error` where it returned `ok`, and its message strings changed.
- **Changes:**
  - The channel field accepts a public channel's `@username` or `t.me/username`, and for a private channel its `-100…` chat ID or a `t.me/c/…` link to any of its messages. Invite links (`t.me/+…`, `t.me/joinchat/…`) are rejected with guidance, because the Bot API cannot identify a channel from them. See [Telegram](platforms/telegram.md#bot-connection-default).
  - A channel without a public username is saved with `username: null`, and `displayName` holds the channel title. `GET /platform-connections`, `test-connection` and `list_connections` return that `null`. The platform ID comes from the chat ID, as for any bot connection, for example `telegram--1001234567890`.
  - `test-connection` on a Telegram bot connection now also checks that the bot can read the channel and is an administrator allowed to post, not only that the token works. A passing check returns `Bot can post to <channel title>` instead of `Connected as @<bot username>`; the new failure messages include `Channel is unavailable` and `Bot needs channel administrator posting rights`, and Telegram API errors from the new channel checks (for example, a bot removed from a private channel) return `Request failed with status code <code>`. MTProto connections now fail with `Invalid bot connection` instead of `Invalid bot token`. See [Test Connection](endpoints/test-connection.md#telegram-connections).
- **Migration action:** Treat `username` as nullable and label connections by `displayName` when it is `null`. Keep targeting posts by `platformId`. If you gate scheduling on `test-connection`, a Telegram connection that now returns `error` needs its bot's administrator posting rights restored in Telegram.

### Media URL errors: hosts that resolve to private addresses report `MEDIA_URL_BLOCKED_HOST` — publora.com #565

- **Affected surface:** REST `create-post` and `update-post` (`mediaUrls`) and `POST /attach-media/{postGroupId}`; MCP `create_post`, `update_post` and `attach_media`.
- **Tag:** Fix.
- **Change:** When a media URL's host, or a redirect target, resolves to a private or reserved address, `mediaResults[].code` is now `MEDIA_URL_BLOCKED_HOST`. It was reported as `MEDIA_URL_FETCH_FAILED` before. The download was refused then and still is; only the code and message change. A DNS lookup that fails is still reported as `MEDIA_URL_FETCH_FAILED`, whether the domain does not exist or the host has no A or AAAA records. See [`mediaUrls` per-URL codes](guides/error-codes.md#mediaurls-per-url-codes).
- **Migration action:** Clients that treated `MEDIA_URL_FETCH_FAILED` as retryable for such hosts should not retry `MEDIA_URL_BLOCKED_HOST`; it needs a publicly reachable URL.

## 2026-09-28

### ChatGPT file attachments — publora.com #539

- **Affected surface:** New REST endpoint `POST /attach-media/{postGroupId}` and new MCP tool `attach_media`, which brings the personal MCP toolset from 18 to 19 tools. The existing `mediaUrls` and presigned-upload flows are unchanged.
- **Tag:** Additive.
- **Changes:**
  - `attach_media` declares `_meta["openai/fileParams"]: ["file"]`, so ChatGPT passes a file from the conversation as `{ download_url, file_id, mime_type?, file_name? }`. Images ChatGPT generates can be attached when the client exposes them as file references. The `openai/fileParams` declaration is included in authenticated `tools/list`, in unauthenticated discovery and in the server card.
  - The endpoint accepts only `{ file, fileName? }`. It downloads the file right away through the `mediaUrls` pipeline (images up to 25 MB, videos up to 150 MB, type detected from the bytes, then probed) and attaches it as `ready`. It always leaves the post in `draft`, so a scheduled post is demoted (without a `post.demoted` webhook), and it never publishes. The optional `fileName` (1–255 characters, cleaned to a safe name) becomes `media[].sourceFileName`. Without it, the name comes from `file.file_name`, or is `chatgpt-media` when that is absent.
  - `Idempotency-Key` matches on the post ID, `file_id` and file name, not the expiring `download_url`, so a retry with a refreshed URL replays the first result instead of attaching the file twice. The MCP tool sends a stable key by default. A failed download frees the key.
  - New top-level 400 code `INVALID_MEDIA_FILE` for any other body field, a malformed file object, a `download_url` that fails the static URL checks, or an invalid file name.
- **Migration action:** None for existing callers. ChatGPT users who already added the Publora connector should refresh its tool list so `attach_media` appears.

### First comment for LinkedIn company Pages — publora.com #533, #534

- **Affected surface:** REST `create-post`, `update-post`, `get-post`; MCP `create_post`, `update_post`, `get_post` — LinkedIn targets only.
- **Tag:** Additive.
- **Change:** A LinkedIn company Page connection now gets the first comment too, posted as the organization (`urn:li:organization:<id>`), the way its posts are published. Before this change, Page targets were recorded as `skipped` with `skipReason: "unsupported_account_type"`; that reason now only remains on results recorded before the change.
- **Migration action:** none.

### First comment (wave 1) — publora.com #529, #530

- **Affected surface:** REST `create-post`, `update-post` and `get-post`; MCP `create_post`, `update_post` and `get_post`.
- **Tag:** Additive.
- **Changes:**
  - New optional top-level `firstComment { text, platforms? }` on create and update: a text comment posted under the account's own post right after it publishes. `platforms` are platform types (never connection IDs); omitted = every supported target. Wave 1 platforms: LinkedIn member profiles (company Pages followed in publora.com #534), X, Threads, Bluesky, Mastodon.
  - Length is validated per applicable platform (X 280 / 25,000 Premium-only, LinkedIn 1,250, Threads 500 code points, Bluesky 300 graphemes and 3,000 bytes, Mastodon 500 with links = 23) with an absolute cap of 25,000. New 400 codes `FIRST_COMMENT_INVALID`, `FIRST_COMMENT_PLATFORM_UNKNOWN`, `FIRST_COMMENT_TOO_LONG`; new warnings `FIRST_COMMENT_PLATFORM_NOT_TARGETED`, `FIRST_COMMENT_UNSUPPORTED_PLATFORMS`.
  - `get-post` / `get_post` return the stored `firstComment` (or `null`) and a per-target `posts[].firstCommentResult` (`pending | posted | failed | skipped`, `commentId`, `skipReason`, `attemptedAt`, `postedAt`, `error` with `outcomeUnknown`). The comment is attempted once, right after publish, and never retried; a failed comment never changes the published post.
  - On `update-post`, `firstComment: null` removes the comment; changing only the time leaves it untouched; the stored comment is re-validated when `platforms` change.
- **Migration action:** none. Clients that mirror `get-post` responses should tolerate the two new keys.

## 2026-09-24

### LinkedIn image-only comments

- **Affected surface:** REST `POST /linkedin-comments` and MCP `linkedin_create_comment`.
- **Tag:** Additive validation change.
- **Change:** `message` remains a required string, but `message: ""` with a valid `imageUrl` is accepted for personal and organization comments. Text and image URLs are trimmed. Blank text without an image still returns HTTP 400, now with `message cannot be empty without imageUrl`. No placeholder text is inserted; failed image validation or upload prevents publication.
- **Migration action:** Callers can send an empty string with `imageUrl` instead of padding image-only comments with text. Existing text comments are unchanged.

## 2026-09-14

### Threads multi-part chains — publora.com #472, #474

- **Affected surface:** REST `create-post`, `update-post` and `get-post`; MCP `create_post`, `update_post` and `get_post`; `GET /platform-limits`.
- **Tag:** Additive, with one behaviour change for over-limit Threads content.
- **Changes:**
  - Meta Threads now supports multi-part chains. `platforms.threads.requirements.supportsThreading` in `GET /platform-limits` is `true`; it was `false`.
  - Threads content over 500 characters is **no longer rejected**. It is accepted and split into a chain instead of being rejected with `CONTENT_TOO_LONG` in `validation.errors[]`. A successful request carries no `validation` object, so there is nothing new to read on success — the split is simply performed. A standalone `---` line, or a complete `[n/m]` marker set, splits the content explicitly; automatically derived parts get a ` (1/N)` suffix, explicit ones are published verbatim. Media attach to the first part only.
  - `get-post` `posts[]` entries gained **`isThread`** (boolean) and **`threadParts`** (array). Both are always present — `false` and `[]` for a non-chain target. Each part carries `index` (zero-based), `content`, `status` (`pending`, `published`, `failed`) and `publishedId` (`null` until confirmed). `postedId` is the first part's ID. `list-posts` does not include these fields. The same fields reach MCP through `get_post`, which passes the REST body through unchanged. They are populated for X/Twitter chains as well as Threads.
  - Scheduling a Threads chain requires the connection to have granted `threads_manage_replies`. The grant is checked at scheduling (from a per-connection cache up to five minutes old) and re-checked against Meta immediately before publishing: a missing grant returns `400` with `THREADS_PERMISSION_REQUIRED` in `validation.errors[]` and nothing is published. A failure of the permission lookup itself returns `503` with `{ "code": "THREADS_PERMISSIONS_UNAVAILABLE", "error": … }` and no `validation` object — that one never means reconnect. `GET /platform-connections` does not report granted scopes, so the grant cannot be pre-checked.
  - New publish-time codes in `posts[].error.code`: `THREADS_INVALID_THREAD_PART`, and `THREAD_PARTIALLY_PUBLISHED` / `PUBLISH_OUTCOME_UNKNOWN` now reachable for Threads. A chain with published parts is never republished from the beginning.
  - The error message `"Multi-part threads (nested replies) are temporarily blocked while we wait for Meta to approve additional permissions for our app."` no longer occurs.
- **Migration action:** If your client special-cased Threads by truncating text at 500 characters or by treating over-limit content as a hard error, that workaround is now unnecessary. After a partial or unknown publication, read `threadParts` and publish only the missing parts — do not resubmit the post.

## 2026-09-08

### Mastodon and Bluesky statistics — publora.com #460

- **Affected surface:** New REST endpoints `POST /post-statistics` and `POST /profile-statistics`, and new MCP tools `post_stats` and `profile_stats`. Nothing existing changes shape.
- **Tag:** Additive.
- **Changes:**
  - `POST /post-statistics` takes `{ posts: [{ platform, platformId, postedId }] }` (1–50 entries, `mastodon` and `bluesky`, both platforms may be mixed in one request) and returns `stats` keyed by `postedId`. Each value is a metric object — `reactions`, `comments`, `reposts`, `quotes`, `saves`, `impressions`, `reach`, `clicks` — or `null`. Every key is always present; a metric the platform does not expose is `null`, never `0`.
  - `POST /profile-statistics` takes `{ platform, platformId }` and returns `profile` (`followers`, `following`, `posts`) with `cached` and `fetchedAt`, or `404` when the connection is not the caller's.
  - `platformId` is accepted with or without the `<platform>-` prefix returned by `GET /platform-connections`.
  - Per-connection problems do not fail the request: `post-statistics` reports them in an `issues` object keyed by connection ID (`CONNECTION_NOT_FOUND`, `AUTH_REVOKED`, `FORBIDDEN`, `RATE_LIMITED`, `FETCH_FAILED`), and `profile-statistics` reports them in `unavailable` and `rateLimited`.
  - Both endpoints, and both MCP tools, are gated by the analytics plan feature that already gated the LinkedIn analytics endpoints; a plan without it receives `403 ANALYTICS_PLAN_REQUIRED`. A request that exceeds its 90-second budget returns `504 ANALYTICS_REQUEST_TIMEOUT`.
  - Values are fetched from the platform on request and cached for about 2 hours. Nothing is collected in the background, so there is no history — only the counters as they stand when you ask.
- **Known limitation:** only posts Publora published for you **and stored a platform post ID for** can be queried. Publora began storing `postedId` for Mastodon and Bluesky on 2026-09-07; posts published before that have no `postedId` and are answered `null`. For a thread, only the root part is addressable.
- **Migration action:** None. New capability.

## 2026-08-27

### Native Zapier app (beta)

- **Affected surface:** No REST or MCP contract change. Publora now has a native app on Zapier (beta, v1.2.0), built on the public API and [webhooks](endpoints/webhooks.md): instant triggers **New Published Post** and **New Scheduled Post**, actions **Create Post**, **Update Post** and **Delete Post**, and searches **Find Connected Account** and **Find Posts**.
- **Tag:** Additive.
- **Change:** The [Zapier guide](examples/no-code/zapier-integration.md) was rewritten for the native app. It previously documented a workaround through Webhooks by Zapier; that approach still works and stays documented on the same page for endpoints the app does not expose.
- **Migration action:** None. Existing Webhooks-by-Zapier Zaps keep working; the native app is the recommended path for new Zaps.

## 2026-08-24

### Connection health reporting corrected — publora.com #407

- **Affected surface:** REST `GET /platform-connections` and the MCP `list_connections` tool, which passes that response through unchanged. `test-connection` reports the same corrected expiry.
- **Tag:** **Behavioral correction.** No request shape changes, no field is added or removed; two response fields now report different — and correct — values for some connections.
- **Changes:**
  - `accessTokenExpiresAt` now carries the **effective** credential expiry, the same value `tokenStatus` and `tokenExpiresIn` are derived from, instead of the raw stored access-token timestamp. For YouTube it is now always `null`: the access token is refreshed on demand before each publish, and Google publishes no refresh-token lifetime, so no authoritative date exists. TikTok continues to report its real refresh-token expiry; every other platform is unchanged.
  - Previously, healthy YouTube and TikTok connections returned a timestamp already in the past alongside `tokenStatus: "valid"`. Clients that compared that date against the current time concluded the connection was dead and prompted users to reconnect working channels.
  - `tokenStatus` now returns `expired` for any connection Publora has flagged for reconnection after the platform rejected its credential — including on platforms that never expire on a schedule, where such a connection previously reported `valid`. In that case `accessTokenExpiresAt` may be `null` or still in the future.
- **Migration action:** Decide about reconnecting from **`tokenStatus`**, not by comparing `accessTokenExpiresAt` against the clock. Treat `expired` as "prompt the user to reconnect" and `expiring_soon` as "warn". Treat a `null` expiry as "no scheduled expiry", never as a problem. The examples on the endpoint, guide and MCP pages were rewritten accordingly; code copied from earlier versions of those examples should be updated.

### X replies and quote posts — publora.com #405

- **Affected surface:** REST `create-post` and `update-post`, the MCP `create_post` and `update_post` tools, and the `posts[].error.code` reported by `GET /get-post` and the `post.failed` webhook.
- **Tag:** Additive. No existing request shape changes behavior.
- **Changes:**
  - `platformSettings` accepts a new top-level `twitter` object with two string keys, `replyTo` and `quoteTweet`. Both take a full `x.com`/`twitter.com` status URL or a bare 1–19 digit post ID, and both are normalized to the numeric ID before storage, so `GET /get-post` echoes the ID rather than the URL you sent.
  - `replyTo` publishes the post — or the head part of a thread — as a reply to the target; the remaining thread parts chain under it as before. `quoteTweet` applies to the single post or the thread head only. The two fields combine with each other and with media. An empty string clears either one.
  - A malformed reference is rejected at intake with `400` and a plain `error` message (`platformSettings.twitter.replyTo must be a tweet URL (https://x.com/user/status/123...) or a numeric tweet ID`); no `code` field accompanies it. An unknown key under `twitter` is still `400 PLATFORM_SETTING_UNKNOWN`, which is evaluated first.
  - Two publish-time codes were added: `X_REPLY_NOT_AUTHORIZED` when X refuses the reply or quote relationship, and `X_TARGET_REJECTED` when the target is deleted, protected, or its author blocked the account. Both are permanent — `retryable: false`.
  - `twitter` is no longer an example of a rejected `platformSettings` platform; the allowlist now has seven platforms.
- **Restriction to know before integrating:** X allows a programmatic reply or quote on self-serve API tiers only when the target post's author mentioned the connected account **in that same post**, quoted one of the account's posts, or the connected account authored the target. Enterprise apps are exempt. Publora cannot check that relationship at intake, so an unrelated target is accepted by `create-post`/`update-post` and fails later at publish time.
- **Migration action:** None for existing callers. New integrations should treat these fields as inbound-engagement and own-post tools, match on `error.code` rather than message text, and not retry `X_REPLY_NOT_AUTHORIZED` or `X_TARGET_REJECTED` with the same target.

## 2026-08-03

### MCP OAuth consent no longer asks for an API key

- **Affected surface:** the OAuth 2.1 flow on `mcp.publora.com` (Dynamic Client Registration + PKCE), used by claude.ai's custom connector, Claude Code, Codex/ChatGPT, Cursor, VS Code, Manus and other browser-capable clients.
- **Tag:** Behavioral, non-breaking for existing credentials.
- **Change:** The consent step is a sign-in-and-approve page — *"An application is requesting access to your Publora account"*, naming the account being authorized, with **Approve** and **Cancel**. It no longer asks you to paste an `sk_...` key; that step was replaced when SSO login shipped on 2026-07-27, and the wording was finalized on 2026-08-03. On approval Publora mints a dedicated API key for that client registration, named `MCP (<client> #<id>)`, and returns it as the access token. Static API-key headers (`Authorization: Bearer sk_...` / `x-publora-key`) are unaffected and remain the option for headless clients.
- **Migration action:** None for connectors that already work. Re-authorizing a client mints a fresh key; per-client keys are listed and revocable on the **API** page in the dashboard. If you built on the old instructions and expected a key-paste page, drop that step.

## 2026-07-21

### Editable draft and scheduled posts — publora.com #231

- **Affected surface:** REST `PUT /update-post/:postGroupId` and the MCP `update_post` tool.
- **Tag:** Additive, with new stable conflict codes.
- **Changes:**
  - `update-post` accepts two new optional patch fields: `content` (replacement base text) and `platforms` (replacement target set). Omitting a field leaves the stored value unchanged.
  - A `content` edit rewrites every platform post to its effective text while preserving explicit per-account overrides; editing the text of a Twitter or Threads target clears its derived thread split.
  - A `platforms` edit replaces the whole target set: dropped IDs have their platform posts deleted, added IDs are validated for ownership and plan entitlement, and adding a target to a scheduled post re-runs scheduling limits plus full content/media validation. `[]` is accepted only while the post remains a draft.
  - Added stable codes `POST_NOT_EDITABLE` (400), `POST_PUBLISH_IN_PROGRESS` (409), and `POST_GROUP_VERSION_CONFLICT` (409), plus `INVALID_CONTENT`, `INVALID_PLATFORMS`, `INVALID_PLATFORM_CONNECTION`, and `INVALID_PLATFORM_ID` (400). The pre-existing `"Cannot update post: post is currently in {status} status"` 400 now also carries `code: "POST_NOT_EDITABLE"`.
  - The success snapshot's `postGroup` now always includes the effective `content` and `platforms`.
  - The "at least one field" error text changed to `"At least one of status, scheduledTime, content, platforms, platformSettings, or mediaUrls must be provided"`.
  - `platforms` arrays on both `create-post` and `update-post` now reject duplicate connection IDs with `400 "Platforms must not contain duplicates"`.
  - MCP `update_post` exposes `content` and `platforms`, and instructs clients to call `list_connections` before changing targets.
- **Migration action:** No change is required for existing callers. Integrations that previously deleted and recreated a post to fix its text or targets should switch to `update-post`. Match on the new `code` values rather than message text, send an `Idempotency-Key` with content/platform edits, and re-read the post with `GET /get-post` on a 409 instead of blind-retrying. If you relied on a repeated connection ID in `platforms` being tolerated, de-duplicate the array.

## 2026-07-17

### LinkedIn mentionable-people directory — publora.com #218

- **Affected surface:** REST and MCP.
- **Tag:** Additive.
- **Changes:** Added `GET /linkedin-mentionables` and the `linkedin_list_mentionables` MCP tool (the 15th active tool). Both list the per-user directory of native LinkedIn member ids captured automatically from engagement on connected company pages, with `q`/`limit` filtering and a ready-to-paste `mention` token per person. Paid plans only — free plans receive `403 UPGRADE_REQUIRED`.
- **Migration action:** None. New integrations that build person mentions should prefer this directory over harvesting actor URNs manually.

## 2026-07-15

### API/MCP correctness release — publora.com #198

- **Affected surface:** REST create/update/get-post, webhooks, MCP sessions and tool responses.
- **Tags:** **Breaking** for unknown `platformSettings` paths; additive/behavioral for the remaining items.
- **Changes:**
  - Added Mongo-backed `Idempotency-Key` handling to create/update.
  - Added the warn-first `scheduledTime` ramp and structured coercion/rejection codes.
  - Unknown `platformSettings` paths now return `400 PLATFORM_SETTING_UNKNOWN`; they are no longer silently discarded.
  - Added MCP session admission checks and more robust MCP response parsing.
  - Publication identity now flows through get-post and `post.published`, including `platformId`, `postedId`, and nullable `permalink`.
- **Migration action:** Remove unknown settings before retrying; add idempotency keys to retryable create/update workflows; read publication identity from documented fields; audit past-time warnings before strict mode.

### LinkedIn repost/reshare — publora.com #194

- **Affected surface:** REST, MCP, and scheduled LinkedIn publishing.
- **Tag:** Additive.
- **Changes:** Added `POST /linkedin-reshare`, the `linkedin_create_reshare` MCP tool (the 14th active tool), and `platformSettings.linkedin` with `repostEnabled`, `repostParentUrn`, and `repostVisibility` as the sixth accepted settings branch.
- **Migration action:** No change is required for existing callers. New integrations should use a `urn:li:share:*` or `urn:li:ugcPost:*` parent. `CONNECTIONS` visibility is personal-profile-only; use `PUBLIC` for company-page reposts.

### Correctness-release documentation — publora-api-docs #21

- **Affected surface:** Public API, MCP, webhook, and OpenAPI documentation.
- **Tag:** Documentation.
- **Change:** Published the idempotency, scheduled-time, strict-settings, MCP-draft, and publication-identity contract introduced by the correctness release.
- **Migration action:** Compare existing integrations with the updated create/update, get-post, webhook, and MCP reference pages; no separate runtime change was introduced by the docs release.

## Deprecation policy

No additional public API deprecations are currently scheduled. Future entries will identify the affected surface, breaking behavior, effective date, and migration action.
