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

# Publishing

> The requests your Custom API receives, the receipt it must return, and how every outcome is classified.

Once an administrator has saved the mapping, Weelkit dispatches approved article versions to your declared publish endpoint. Creates arrive as `POST` and updates as `PUT`, both to the `endpoints.publish` path from your [manifest](https://docs.weelkit.com/integrations/custom-api/manifest.md). Every request carries your [API key](https://docs.weelkit.com/integrations/custom-api/authentication.md), `Accept: application/json` and a `User-Agent` that starts with `Weelkit-Custom-Publishing/`.

## Publish request

```json theme={null}
{
  "protocolVersion": "weelkit-custom-publishing.v2",
  "operation": "create",
  "contentTypeId": "article",
  "status": "draft",
  "schemaFingerprint": "custom-api.v2.0123456789abcdef0123456789abcdef",
  "operationId": "3f8c2d1e-9a7b-4c6d-8e2f-0b4a6c8d1e3f",
  "article": { "title": "...", "contentHtml": "<p>...</p>" },
  "fields": { "title": "...", "body": "<h1>...</h1>", "topics": ["..."] }
}
```

**protocolVersion** `string`

Always `weelkit-custom-publishing.v2`.

**operation** `string`

`create` for a `POST`, `update` for a `PUT`.

**contentTypeId** `string`

The content type id from your manifest that the administrator selected.

**status** `string`

Your status value from the manifest's `statuses` map — for example `draft` or `live`.

**schemaFingerprint** `string`

The fingerprint of the schema this dispatch was mapped against. v2 fingerprints begin with `custom-api.v2.`. If it does not match your current schema, respond with an error rather than guessing — see [Schema and limits](https://docs.weelkit.com/integrations/custom-api/schema-and-limits.md#schema-fingerprint-and-drift).

**operationId** `string (UUID)`

Stable operation identity. Retries of the same dispatch reuse it.

**article** `object`

The canonical Weelkit article rendering — `title` and sanitized semantic `contentHtml` — for destinations that want the source form regardless of mapping.

**fields** `object`

The mapped values keyed by your field paths, exactly as the administrator's saved mapping produces them. This is the object shaped like your manifest schema. A dotted path is a nested object: `seo.metaTitle` arrives as `{ "seo": { "metaTitle": "..." } }`, and fields that share a parent share its object.

**remoteId** `string`

Present on `update` only: the id your receipt returned when the content was created.

## Updates are partial

`fields` contains **only the fields the administrator mapped**. Treat an update as a merge patch:

* A field that is **absent** is left exactly as it is in your system. Fields you declare but the administrator did not map, and anything your own editors changed, are never touched.
* A field sent as **`null`** is cleared. Weelkit sends `null` only for an optional mapped field that the article no longer has a value for, and never for a required field.
* Every other value replaces the stored one.

`article.title` and `article.contentHtml` are always present for reference; apply them only if your own mapping says so.

## Images

Weelkit does not upload files to your API. Images travel as **public URLs**, and your system fetches or links them itself:

* **Cover image.** Map the cover image to a `url` (or `string`) field. The value is an absolute `https` URL.
* **Cover image alt text.** Map the cover image alt text to a `string` field next to it. It arrives in that field as plain text. Declare it with a `maxLength` so an over-long alt text is caught before the dispatch.
* **Images in the body.** They are `<img src="https://…" alt="…">` elements in the HTML field, inside `<figure>` with an optional `<figcaption>`, with their `width` and `height` when known. Weelkit's internal asset ids are never sent.

Keep the URLs stable and publicly reachable. If your system copies an image into its own storage, it should do so when it receives the dispatch.

## References

A reference field is declared with `reference.targetContentTypeId` in the manifest. Weelkit asks your [list endpoint](#list-endpoint) for that content type's items, lets the administrator pick, and sends the chosen items' `remoteId` values in the field: a string for a `single` reference, an array of strings for a `multiple` one, in the order chosen. Reject an id you did not list.

## Idempotency and retries

When your manifest declares `idempotency.supported: true`, every `create` and `update` carries an `Idempotency-Key` header whose value is the `operationId`. Deduplicate by it: if you have already processed that key, return the original receipt instead of creating a duplicate. When your manifest does not declare support, the header is not sent.

Weelkit retries a failed request up to three times with exponential backoff and jitter. A `Retry-After` of up to 30 seconds is honored; a longer one is not waited for, and the attempt ends with the answer it got.

| Response | Read (`GET`) | Create or update with `Idempotency-Key` | Create or update without it |
| - | - | - | - |
| 408, 429, 5xx (not 501), timeout, connection loss | Retried | Retried with the same key and body | Not retried |
| Any other 4xx | Not retried | Not retried | Not retried |
| 3xx | Failed, never followed | Failed, never followed | Failed, never followed |

Every attempt waits up to 60 seconds for your answer.

## Required receipt

Respond to a successful mutation with a 2xx status and a JSON receipt:

```json theme={null}
{
  "status": "draft",
  "remoteId": "art-9",
  "remoteUrl": "https://acme.example.com/articles/art-9",
  "remoteVersion": "3",
  "message": "Stored as draft pending editorial review."
}
```

**status** `string` *required*

One of `accepted`, `draft`, `published`, `rejected`.

**remoteId** `string`

Your identifier for the content, up to 256 characters from letters, digits, `-`, `_`, `.` and `:`. **Required on `create`** — without it Weelkit cannot address the content for updates, readback or status. On `update` it may be omitted; if present it must equal the `remoteId` of the request.

**remoteUrl** `string`

Absolute `https` (or `http`) URL of the content.

**remoteVersion** `string`

Your version identifier for the content.

**message** `string`

Optional human-readable note, up to 500 characters.

Receipt semantics are strict:

* A 2xx **without a valid receipt** — no `status` from the list, a `remoteId` outside the allowed alphabet, no `remoteId` on a create, or a different `remoteId` on an update — is recorded as **outcome-unknown**, not success. Weelkit knows your server answered but cannot prove what happened to the content.
* `"status": "rejected"` is a **confirmed non-application**: your API deliberately declined the content. Weelkit records it as rejected and does not retry.

## Status and readback

When your manifest declares `capabilities.readback: true`, Weelkit reads items back with a `GET` to your readback path, substituting the `{remoteId}` placeholder. It does this to verify the test draft during setup, and afterwards to follow each item's status and public URL, on the same schedule as every CMS (see [After you send](https://docs.weelkit.com/integrations/overview.md#after-you-send)). Respond with:

```json theme={null}
{
  "remoteId": "art-9",
  "contentTypeId": "article",
  "title": "...",
  "status": "draft",
  "slug": "...",
  "remoteUrl": "https://acme.example.com/articles/art-9",
  "fields": {
    "title": "...",
    "body": "<h1>...</h1>",
    "topics": ["topic-1", "topic-3"],
    "summary": null
  }
}
```

* `remoteId` must equal the requested id exactly, or the read counts as failed.
* `fields` must return the **current stored value of every field Weelkit mapped**, in the same shape as in the publish request (dotted paths nested). During **Verify test draft**, Weelkit compares each mapped field with what it sent and shows the result per field: a field missing from `fields` is reported as not read back, and a different value as a mismatch. A cleared field must come back as `null`. Fields Weelkit did not map may be returned or left out. (A mapped value may also be returned at the top level of the document instead of inside `fields`; `fields` is the documented place.)
* `status` is matched against your manifest's `statuses` values, without regard to case. Weelkit also understands `published`, `draft`, `scheduled`, `pending`, `unpublished` and `archived`. A published item makes the article **Published**; the others make it **Draft**. Any other value changes nothing; Weelkit never guesses.
* `remoteUrl` (or `url`) becomes the article's public address while the item is published. It is ignored while the item is a draft. When you don't return one, Weelkit matches `slug` against the organization's sitemap.
* A `404` means the item is gone: after two such reads, the article becomes **Draft**.
* A `401`, `403` or `429` stops that round of checks; Weelkit tries again later.

The `remoteUrl` in a publish receipt is a link to the item, not its confirmed public address.

### How status reaches Weelkit

Weelkit **reads** status; your API never calls Weelkit, so there is no callback endpoint and nothing to authenticate on Weelkit's side. When you publish or unpublish an item in your own system, the next scheduled readback shows it, and the article follows. Declare `capabilities.readback` or articles sent to your API never change status in Weelkit.

## List endpoint

When your manifest declares `capabilities.list`, Weelkit calls the `endpoints.list` path with `{contentTypeId}` replaced by the reference target:

```http theme={null}
GET /weelkit/content/topic?offset=0&limit=25&search=remote HTTP/1.1
```

| Query | Meaning |
| - | - |
| `offset` | Number of items to skip. |
| `limit` | Page size, 1 to 100. |
| `search` | Optional text to narrow the items, matched however you like. |

```json theme={null}
{
  "items": [
    { "remoteId": "topic-1", "title": "Remote work", "slug": "remote-work", "excerpt": "...", "status": "published", "remoteUrl": "https://acme.example.com/topics/remote-work" }
  ]
}
```

Only `remoteId` is required in an item; `title` is what an administrator sees. Return at most `limit` items. A page with `limit` items means another page may follow, and Weelkit asks for the next `offset`; a shorter page ends the list.

## Delete

If you declare `capabilities.delete`, Weelkit can remove the temporary test draft that **Verify test draft** created. It sends a `POST` to the publish endpoint (not a `DELETE`), so route on `operation` before treating a `POST` as a create:

```json theme={null}
{
  "protocolVersion": "weelkit-custom-publishing.v2",
  "operation": "delete",
  "remoteId": "art-9"
}
```

Answer 2xx when the item is gone, and `404` when it was already absent; Weelkit treats both as removed. Weelkit uses delete for the test draft only, never for an article you sent.

A verified test draft stays in your system for you to inspect. If the check fails, **Delete draft** sends this request. If you do not declare `delete`, Weelkit cannot remove the draft: delete it in your own system, then choose **I removed the test draft** in Weelkit.

## Error categories

Every failed interaction is classified into one category, which determines what Weelkit does next:

| Category | Trigger | Behavior |
| - | - | - |
| `reauthorization_required` | 401 or 403 | Publishing pauses; the connection shows **Connection Error** and **Reconnect** asks for a new key. Your Weelkit session is unaffected. |
| `provider_rate_limited` | 429 | Retried; a `Retry-After` header is honored. A create or update is known not applied. |
| `provider_timeout` | No response within the timeout, or 408 | Mutations become outcome-unknown; safe reads are retried. |
| `provider_unavailable` | 5xx | Retried. A mutation that still fails becomes outcome-unknown. |
| `provider_rejected` | Other 4xx, or any redirect | Failed immediately; redirects are never followed. A create or update is known not applied. |
| `provider_invalid_response` | Non-JSON or unparseable response where JSON is required | Failed; the raw body is not interpreted further. |
| `invalid_receipt` | 2xx whose body is not a valid receipt | Recorded as outcome-unknown, as above. |
| `provider_delete_unsupported` | **Delete draft** against a manifest without `capabilities.delete` | Nothing is sent; remove the draft in your system and confirm. |

Weelkit shows its own wording for each category and never displays the body of your error response, so put the reason in your logs.

## Outcome-unknown discipline

A timeout or transport loss **after a mutation was dispatched** is recorded as **outcome-unknown**: Weelkit cannot know whether your API applied the change. Weelkit never blindly redispatches under a new identity — any retry reuses the same `operationId`, so a destination that supports idempotency resolves the uncertainty safely, and one that does not is reconciled rather than double-published.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.