> ## Documentation Index
> Fetch the complete documentation index at: https://apidocs.scripe.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Conflict

# `conflict`

| HTTP | When                                                                                     |
| ---- | ---------------------------------------------------------------------------------------- |
| 409  | The request is valid and authorized, but the resource's current state does not allow it. |

In v1 this is most often the **scheduling pre-flight**: `POST /v1/posts`
with `scheduledFor`, `PATCH /v1/posts/{postId}` with `scheduledFor`, and
the confirmed second call of the MCP `schedule_post` all verify that the
project's LinkedIn connection can still publish before they write
anything. If it cannot, nothing is created, nothing is changed, and you
get this.

The other cases share the shape but not the repair:

* **A publish is already in flight** for the post, or a publish
  **failed within the last 10 minutes** and it is not yet known whether
  LinkedIn accepted it (MCP `publish_post`) — check the post on
  LinkedIn instead of retrying.
* **The workspace requires review and the post has no approved
  review** (MCP `publish_post`, `schedule_post`, and `create_post_draft`
  / `generate_post` with `scheduledFor`; never the REST write paths) —
  `details.reason` is `review_not_requested` / `review_pending` /
  `review_rejected` and `details.reviewerUserId` names the current
  reviewer or is `null`. This one needs the workspace's plan to include
  content approvals as well as the flag: below that plan nothing here
  refuses, so `review.requiredByWorkspace: true` on a post is not on its
  own a prediction that you will see this. Approval happens in the
  dashboard; the states and the repair are in
  [MCP tools §1.3](../mcp-tools.md#1-read-tools).
* **A knowledge document that is one page of a dashboard sync refused
  deletion** (MCP `delete_knowledge_doc`) — deleting one page of a website,
  Notion or [synced source](../synced-sources.md) sync is pointless because
  the next sync run restores it; manage the sync in the dashboard instead.
* **A deleted media asset could not be restored** (`POST /v1/media`
  with the bytes of an asset removed by MCP `delete_media_asset`) — the
  row's record of superseded storage keys is what the row-driven purge
  follows, so a restore that cannot record the key it replaces is
  refused rather than overwriting that record. Two states do that, and
  the message names which: the record is **full**
  (that many delete/restore cycles), in which case wait out the rest of
  the asset's [30-day retention window](../media.md#deleting) — the
  sweep erases those files and clears the record — then send the bytes
  again; or the record is **unreadable**, which the sweep also refuses
  to act on, so waiting does not clear it and the message asks you to
  report the asset id to support instead of retrying.

Two sibling 409s have their own codes and their own pages:
[`idempotency_key_conflict`](./idempotency_key_conflict.md) and
[`not_cancellable`](./not_cancellable.md). Unlike those, a `conflict` is
about the **resource's** state, not your request's replay semantics —
retrying without changing the underlying state returns the same error.

## Body (scheduling pre-flight)

```json theme={null}
{
  "error": {
    "code": "conflict",
    "message": "No LinkedIn account is connected to this project, so a scheduled post can never publish. Connect LinkedIn in Scripe, then schedule the post again.",
    "request_id": "req_…",
    "docs_url": "https://docs.scripe.io/api/v1/errors#conflict",
    "details": {
      "reason": "not_connected",
      "reconnectUrl": "https://app.scripe.io/oauth?projectId=…&reconnect=true",
      "retryable": false
    }
  }
}
```

Switch on `details.reason`, not on the message:

| `reason`                | What is true                                                                                                                             | `reconnectUrl`                                                                                                                      |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `not_connected`         | No LinkedIn access token is on file for this project — it was never connected, or the authorization was withdrawn and the token cleared. | The project's reconnect page.                                                                                                       |
| `token_invalid`         | A token exists, LinkedIn rejected it, and the refresh did not recover it.                                                                | The project's reconnect page.                                                                                                       |
| `no_company_page_admin` | This is a company page and no workspace member who administers it has a working LinkedIn connection.                                     | `null` — the repair is on a *different* project (an admin's own account), so a link to this one would send the user nowhere useful. |

`details.retryable` is always `false` here. Every one of these needs a
human to complete an OAuth flow in a browser; no amount of retrying,
backing off, or re-authorizing your API credentials changes the answer.
Of the non-pre-flight cases above, only the review-gate refusal carries
`details` (its own `reason` values, no `reconnectUrl`); the rest carry
none — read `message`.

## What to do

1. **Do not retry.** Stop the scheduling attempt.
2. **Tell the user what is wrong and hand them the link.** Only they can
   reconnect. `message` is written to be shown as-is;
   `details.reconnectUrl` is where they go.
3. **After they say they are done**, schedule again. Nothing was
   written, so there is no half-scheduled post to clean up.

## Seeing it coming

Two reads answer "will this work?" before you attempt the write:

* **`GET /v1/projects/{projectId}` → `linkedIn.canPublish`** (MCP:
  `get_project`) — the stored verdict, cheap, with the same `detail`
  and `reconnectUrl` fields.
* **The MCP `schedule_post` proposal → `linkedIn.canSchedule`** — the
  *live* check, run at phase one so a user is never asked to confirm a
  schedule that cannot happen. It reports rather than refuses: you
  still get a `confirmationToken`, because reconnecting and then
  confirming is exactly the flow this is meant to enable.

## Why an unconnected project is refused rather than queued

A post scheduled against a project with no LinkedIn token cannot ever
publish. The publish cron records the failure, retries for 24 hours and
gives up; the post keeps saying `scheduled` and nothing tells the user.
That state is visible after the fact as
[`delivery.state: "missed"`](../posts.md), which is a diagnosis, not a
repair. Refusing at the moment of scheduling is the only point at which
the person who can fix it is still in the room.
