---
title: "Send, test, withdraw or split articles"
method: POST
path: "/publishers-content"
tags: ["Publish content"]
---

# Send, test, withdraw or split articles

`POST /publishers-content`

## Request

- Base URL: `https://api.opedd.com`
- URL: `https://api.opedd.com/publishers-content`
- Auth: HTTP bearer

Send up to 100 articles per request; sending the same `url` again updates it. Required per article: `title`, `url`, `html_body` (up to 200,000 characters). Every article gets a result: `created`, `updated`, `unchanged` or `failed`. Add `?validate=true` to test without saving. Other actions in the same endpoint: `{"action": "revoke", "reason": "voluntary" | "rights", "urls": […]}` withdraws articles: `voluntary` stops selling them (buyers who already paid keep access until their licence ends); `rights` is for a rights problem (every licensed buyer fetching them gets `410 CONTENT_REVOKED` and must delete copies within 30 days). `reason` is required and `{"action": "chunk", "urls": […]}` splits them into sections. Send an `Idempotency-Key` header to retry safely. 60 writes and 300 test runs per hour per key. See the [publisher guide](/publishers).

## Query parameters

- `validate` boolean

## Headers

- `Idempotency-Key` string
- `X-Request-Id` string

## Request body

- object: Send `articles`, or `action: "revoke"` with `reason` and `urls`, or `action: "chunk"` with `urls`.
  - `articles` PublisherArticleInput[]
    - `title` string, required
    - `url` string, uri, required: The article's web address. Sending the same address again updates the article.
    - `published_at` string, date-time: When the article was published. Optional; defaults to when Opedd receives it. Send the real date: buyers filter on it.
    - `html_body` string, required: The full text as HTML, up to 200,000 characters. Opedd cleans it; send it as it is.
    - `description` string: Optional summary.
    - `is_paid` boolean: Whether the article is for paying readers. `audience` says more and is preferred.
    - `audience` 'everyone' | 'only_free' | 'only_paid': `everyone`, `only_free` or `only_paid`. Preferred over `is_paid`.
    - `category` string
    - `metadata` object: Optional extra key-value data.
    - `author` string
    - `language` string: A language code such as `en`.
    - `image_urls` string[]
    - `thumbnail_url` string, uri: Cover image; `https://` only.
    - `canonical_url` string, uri: If different from `url`.
    - `tags` string[]
    - `sections` object[]: Optional: the article split into parts, so buyers can fetch one part. Without it the article is one piece.
      - `heading` string, nullable: Optional heading for the section.
      - `content` string, required: The section text, up to 50,000 characters.
  - `dry_run` boolean: Validate and preview without writing (same as `?validate=true`).
  - `action` 'upsert' | 'revoke' | 'chunk': Leave out to send articles. `revoke` withdraws the articles in `urls` (with `reason`); `chunk` splits them into sections.
  - `urls` string[]: For `revoke` and `chunk`: the article URLs to act on (yours only).
  - `reason` 'voluntary' | 'rights': Required with `revoke`. `voluntary`: stop selling; paid buyers keep access until their licence ends. `rights`: withdraw because of a rights problem; every buyer gets `410` and a 30-day deletion deadline.

## Response `200`

Articles sent: each has its own result; failures are listed in `data.errors` without failing the rest. For `revoke`, see the examples.

- object: Every successful response. The endpoint's data is in `data`.
  - `success` true, required
  - `data` union, required
    - object: Response payload for `POST /publishers-content`.
      - `inserted_count` integer, required: Number of articles successfully inserted/updated.
      - `total_submitted` integer, required
      - `errors` object[], required: Per-article errors, if any. Empty array on full success.
        - `source_url` string, required
        - `error` string, required
      - `articles` object[]: One result per article sent.
        - `url` string
        - `status` 'created' | 'updated' | 'unchanged' | 'failed'
        - `id` string, uuid, nullable
        - `word_count` integer, nullable
        - `content_hash` string, nullable
        - `sections` integer
        - `error` string, nullable
      - `dry_run` boolean: Present and true on a dry run; nothing was written.
      - `valid` boolean: Dry run only.
    - object: Answer to `revoke` with `reason: "voluntary"`.
      - `reason` 'voluntary'
      - `stopped_count` integer
      - `stopped` string[]
      - `already_stopped_or_withdrawn` string[]
      - `not_found` string[]
    - object: Answer to `revoke` with `reason: "rights"`.
      - `reason` 'rights'
      - `revoked_count` integer
      - `revoked` string[]
      - `already_revoked` string[]
      - `not_found` string[]
      - `deletion_deadline` string, date-time
  - `request_id` string: Echoed from request `X-Request-Id` header or server-generated.

## Other responses

- `400`: The body failed validation (including `revoke` without `reason`). `error.details.issues` lists each problem with the field and the article, for example `articles.3.html_body`.
- `401`: Missing or invalid credential
- `422`: Idempotency-Key body mismatch. Same key was used previously with a different request body.
- `429`: Too many requests. Wait for the number of seconds in `Retry-After`, then retry.

---

[API](https://skmtc.dev/opedd/apis/opedd-api.md) · [All operations](https://skmtc.dev/opedd/apis/opedd-api/llms.txt) · [OpenAPI document](https://skmtc.dev/opedd/apis/opedd-api/revisions/a33336810cf0?raw)
