API автора

Author API — HTTP-интерфейс «Дизайн-кабака», через который вы создаёте, обновляете и публикуете свои статьи из скрипта, n8n, собственного продукта или AI-агента. Доступ ограничен вашими статьями: публичную ленту через этот API читать нельзя.

Вызывайте API с сервера: из backend-приложения, worker, cron-задачи или локального скрипта. Запрос из браузера на стороннем сайте — не поддерживаемый способ интеграции. Не передавайте секрет dp_live_… в браузерный JavaScript и не встраивайте его в мобильное приложение.

Содержание

Быстрый старт

  1. Откройте настройки аккаунта и создайте токен с понятным именем, например n8n publishing.
  2. Скопируйте секрет сразу: он показывается один раз.
  3. Сохраните секрет в переменной окружения или в хранилище секретов.
  4. Сделайте первый запрос GET /me. Он проверит авторизацию и вернёт ваш handle и действующие лимиты.
  5. Создайте статью через POST /posts, сохраните поле id, затем вызовите POST /posts/{id}/publish.

Примеры на Python используют пакет requests: установите его командой python3 -m pip install requests. Для JavaScript нужен Node.js 18+ с глобальным fetch.

Первый запрос: GET /me

TOKEN=dp_live_YOUR_TOKEN
BASE=https://designpub.ru/api/v1/author
curl -sS "$BASE/me" \
  -H "Authorization: Bearer $TOKEN"

Первый черновик и публикация

POST /posts всегда создаёт draft. После publish статья получает статус published или pending_moderation.

TOKEN=dp_live_YOUR_TOKEN
BASE=https://designpub.ru/api/v1/author

DRAFT_JSON=$(
  curl -sS -X POST "$BASE/posts" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"title":"Первая статья через API","markdown":"Текст статьи из интеграции.\n"}'
)
POST_ID=$(
  printf '%s' "$DRAFT_JSON" |
    python3 -c 'import json, sys; print(json.load(sys.stdin)["id"])'
)
curl -sS -X POST "$BASE/posts/$POST_ID/publish" \
  -H "Authorization: Bearer $TOKEN"

Авторизация и токены

Каждый маршрут под https://designpub.ru/api/v1/author требует заголовок:

Authorization: Bearer dp_live_YOUR_TOKEN

Принимается только токен с префиксом dp_live_. Сессионная кука сайта и её JWT — не токен Author API: с ними сервер отвечает 401 и code: "unauthorized".

Где создать токен

Токены создаются, перевыпускаются и отзываются в настройках аккаунта.

  • Секрет показывается один раз. Потом в интерфейсе видны только dp_live_ и последние четыре символа.
  • На аккаунт можно иметь максимум пять активных токенов.
  • У токена нет срока действия: он работает, пока вы его не отзовёте или не перевыпустите.
  • Отзыв мгновенно ломает старый секрет.
  • Перевыпуск выдаёт новый секрет для той же записи: id и name токена не меняются.
  • Сброс пароля токены не отзывает. Удаление аккаунта отзывает все его токены.
  • Лимиты считаются на аккаунт, а не на каждый токен в отдельности.

Храните секрет как пароль: не записывайте его в git, логи, URL, issue и в данные, доступные браузеру. Если секрет утёк, перевыпустите токен в настройках.

Общие соглашения

Базовый URL и формат

Относительный путь каждого маршрута ниже добавляется к:

https://designpub.ru/api/v1/author

Например, GET /posts означает полный URL https://designpub.ru/api/v1/author/posts.

Запросы и ответы с JSON используют Content-Type: application/json. У GET тела нет. POST /media использует multipart/form-data, поэтому boundary выставляет HTTP-клиент. Ответы 204 No Content не содержат тела.

Даты — ISO-8601 с часовым поясом, обычно UTC:

2026-08-20T17:00:00Z

public_id и id — разные сущности. id у статей и медиафайлов — UUID в виде строки. public_id — отдельный короткий публичный идентификатор, который используется в canonical_path (добавляется в конце URL опубликованной страницы).

Ресурс AuthorPost

Статья, возвращённая API, имеет следующие поля:

Поле Тип Значение
id string (UUID) Идентификатор статьи в маршрутах API.
public_id string Публичный короткий идентификатор.
canonical_path string Канонический путь публичной статьи, например /my-article-a1b2c3d4e5f6.
title string Заголовок, отдельное JSON-поле.
subtitle string или null Подзаголовок.
slug string Часть канонической ссылки до добавления public_id.
status string draft, pending_moderation или published.
tags array Объекты { "slug": "...", "name": "..." }.
cover_asset_id string (UUID) или null Медиафайл обложки.
cover_url string или null Обычно /api/media/{uuid} для сохранённой обложки.
cover_width integer или null Ширина обложки в пикселях.
cover_height integer или null Высота обложки в пикселях.
published_at string или null Время текущей публикации.
created_at string Время создания статьи.
updated_at string Время последнего изменения рабочей версии.
has_unpublished_changes boolean Рабочая версия новее опубликованной.
like_count integer Число лайков.
comment_count integer Число комментариев.
save_count integer Число закладок.
popularity integer Индекс популярности.
impression_count integer Показы в ленте.
open_count integer Открытия статьи.
read_count integer Дочитывания до порога чтения.
complete_count integer Полные дочитывания.
htmlтолько GET /posts/{id} string HTML рабочей версии статьи.
rehost_failedпосле записи, если есть ошибки array of strings Внешние URL изображений, которые не удалось сохранить. Поле опускается, если массив пуст.
warningsпосле записи, если есть предупреждения array of strings Например, остановка рехоста из-за лимита. Поле опускается, если массив пуст.

Пример полного ответа с деталями:

{
  "id": "2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f",
  "public_id": "a1b2c3d4e5f6",
  "canonical_path": "/kak-peredat-statyu-a1b2c3d4e5f6",
  "title": "Как передать статью через API",
  "subtitle": "Рабочий пример",
  "slug": "kak-peredat-statyu",
  "status": "draft",
  "tags": [
    { "slug": "api", "name": "API" },
    { "slug": "automation", "name": "Автоматизация" }
  ],
  "cover_asset_id": null,
  "cover_url": null,
  "cover_width": null,
  "cover_height": null,
  "published_at": null,
  "created_at": "2026-08-20T17:00:00Z",
  "updated_at": "2026-08-20T17:00:00Z",
  "has_unpublished_changes": false,
  "like_count": 0,
  "comment_count": 0,
  "save_count": 0,
  "popularity": 0,
  "impression_count": 0,
  "open_count": 0,
  "read_count": 0,
  "complete_count": 0,
  "html": "<h1>Как передать статью через API</h1><p>Текст статьи.</p>",
  "rehost_failed": [
    "https://cdn.example.com/image-that-is-temporarily-unavailable.png"
  ],
  "warnings": [
    "Рехост остановился: бюджет 50 МиБ на запись"
  ]
}

В списке html никогда не возвращается. Пустые rehost_failed и warnings не приходят как пустые массивы: поля просто отсутствуют.

Ошибки

Тело ошибки имеет форму { "error": "...", "code": "..." }. Поле code необязательное: если для ошибки нет стабильного машинного кода, сервер его не отправляет.

HTTP code Когда
401 Unauthorized unauthorized Заголовок отсутствует, токен неверен или отозван, аккаунт удалён, либо вместо токена передан JWT сайта.
400 Bad Request отсутствует Невалидный JSON, обязательное поле не передано, пустой документ, неверный query-параметр или другая ошибка валидации.
404 Not Found not_found Статья или медиа не найдены. Чужая статья тоже выглядит как not_found.
409 Conflict отсутствует Запрошенный slug уже занят у вас.
422 Unprocessable Entity зависит от маршрута Для некоторых проверок медиа сервер различает тип содержимого, например animated_image.
429 Too Many Requests rate_limited Исчерпан минутный лимит или суточная квота медиа.
503 Service Unavailable service_unavailable Временно недоступна внутренняя зависимость API.

Примеры:

{
  "error": "Нужен токен API",
  "code": "unauthorized"
}
{
  "error": "Нужен текст статьи"
}
{
  "error": "У вас уже есть пост с такой ссылкой"
}
{
  "error": "Слишком много запросов. Подождите немного",
  "code": "rate_limited"
}

На ответе 429 смотрите заголовки:

Заголовок Значение
Retry-After Сколько секунд подождать перед новой попыткой.
RateLimit-Limit Размер лимита в текущем окне.
RateLimit-Remaining Сколько запросов осталось в текущем окне.
RateLimit-Reset Unix-время сброса окна.

Минутные лимиты работают фиксированным окном в 60 секунд: счётчик обнуляется целиком в начале следующего окна, а не пополняется постепенно.

После сетевого таймаута не повторяйте изменяющий запрос вслепую: сначала проверьте статью через GET /posts/{id}. Заголовок Idempotency-Key не поддерживается.

Лимиты

Лимиты считаются на аккаунт, не на каждый токен. Текущие значения и расход суточной квоты возвращает GET /me.

Действия Ограничение
ЧтениеGET /me,
GET /posts,
GET /posts/{id}
60/мин
ЗаписьPOST /posts,
PATCH /posts/{id},
POST /posts/{id}/unpublish,
DELETE /posts/{id}
30/мин
ПубликацияPOST /posts/{id}/publish 1/мин
МедиаPOST /media,
POST /media/from-url
10/мин
200 МиБ в UTC-день

POST /posts и PATCH /posts/{id} расходуют лимит записи, а каждое загруженное внешнее изображение или обложка — дополнительно лимит медиа и суточную квоту. На одну запись действуют ещё два ограничения:

  • не больше 100 внешних URL изображений;
  • не больше 50 МиБ.

Если минутный лимит исчерпан, запрос получает 429 с code: "rate_limited" и заголовками сброса. Суточная квота медиа обнуляется в полночь UTC.

При рехосте во время записи API останавливает только рехост: черновик всё равно сохраняется, необработанный URL остаётся внешним, а причина попадает в rehost_failed или warnings.

Жизненный цикл публикации

Статья проходит такие состояния:

draft
  └─ POST /posts/{id}/publish
       ├─ published
       └─ pending_moderation
  • POST /posts всегда создаёт draft.
  • После POST /posts/{id}/publish статья получает published или pending_moderation. Второй статус означает, что статья ушла на модерацию; следите за результатом через GET /posts/{id}.
  • PATCH /posts/{id} никогда не публикует статью. Для опубликованной статьи он меняет рабочую версию.
  • has_unpublished_changes: true означает, что рабочая версия новее опубликованной. После успешного publish поле станет false.
  • POST /posts/{id}/unpublish снимает публикацию и оставляет рабочий текст как черновик; ответ — 204.
  • DELETE /posts/{id} мягко удаляет статью. Восстановление через Author API не предусмотрено.

В каждом AuthorPost есть статистика, видимая только вам: impression_count, open_count, read_count и complete_count. Отдельного маршрута статистики нет, чужие статьи через этот API не видны.

Медиа

Есть три способа добавить изображение в статью. Во всех случаях сохранённый у «Дизайн-кабака» файл доступен по пути /api/media/{uuid}.

Рехост — это когда API сам скачивает внешнюю картинку и сохраняет её копию у себя.

Вариант A: внешние URL в тексте

Передайте обычный https:// URL в Markdown или HTML:

![Схема процесса](https://cdn.example.com/diagram.png)

При POST /posts или PATCH /posts/{id} API рехостит такое изображение и заменяет src на /api/media/{uuid}. Это расходует лимит медиа и суточную квоту. В одной записи разрешено максимум 100 внешних URL и 50 МиБ. Повторяющийся URL достаточно указать в тексте как есть — второй раз он не загружается.

Если источник не отвечает, файл слишком большой или загрузка не удалась, статья не теряется. Для изображения в тексте внешний src остаётся в сохранённом документе, а URL добавляется в rehost_failed. Если рехост остановился из-за минутного лимита или суточной квоты, причина дополнительно попадает в warnings. Для cover_url при неудаче обложка просто не создаётся.

Та же обработка применяется к cover_url:

{
  "title": "Статья с внешней обложкой",
  "markdown": "Текст статьи.",
  "cover_url": "https://cdn.example.com/cover.jpg"
}

Вариант B: загрузить локальный файл

Вызовите POST /media с multipart-полем file. Маршрут принимает изображения размером не больше 50 МиБ. В ответе вы получите id и url; вставьте url в Markdown или HTML:

![Схема](/api/media/2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f)

В cover_asset_id передаётся именно id, а не полный url:

{
  "title": "Статья с локальной обложкой",
  "markdown": "![Иллюстрация](/api/media/2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f)\n",
  "cover_asset_id": "2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f"
}

Ширину и высоту в запросе указывать не нужно: API берёт их из самого медиафайла, заполняет cover_width и cover_height и отклоняет неподходящий или чужой файл.

Вариант C: заранее сохранить один URL

Когда нужно получить сохранённый медиафайл до сборки текста, вызовите POST /media/from-url с одним URL. Так интеграция сначала сохраняет картинки, а потом формирует Markdown.

Обложка

Выберите один из вариантов:

  • cover_asset_id — UUID уже загруженного изображения из POST /media;
  • cover_url — внешний https:// URL, который API рехостит при записи.

cover_asset_id: null в PATCH /posts/{id} очищает обложку. Ширина и высота отдельно не передаются.

Что не поддерживается

  • Локальный видеофайл нельзя загрузить через Author API.
  • Прямой mp4 или другой video src из внешнего URL не становится медиафайлом статьи: санитайзер удаляет такой src.
  • Разрешены встраивания YouTube, Vimeo, VK, Rutube и других хостов из allowlist; это iframe, а не загрузка видео.
  • Для GIF используйте сохранённый медиафайл /api/media/{uuid} и HTML-блок data-gif, описанный ниже.

Тело статьи

Запись и чтение используют разные форматы

На запись POST /posts и PATCH /posts/{id} принимают строковое поле markdown. Это GitHub Flavored Markdown (GFM) с HTML-островами для узлов, которых нет в обычном Markdown.

На чтение GET /posts/{id} возвращает строковое поле html.

html всегда отражает рабочую версию. Если после публикации вы сделали PATCH, ответ будет отличаться от того, что видит читатель, пока вы не вызвали publish.

HTML из GET /posts/{id} можно отправить обратно как значение markdown: конвертер сохраняет raw HTML перед санитизацией. Это удобно для round-trip, но не делает разрешённым произвольный HTML — неизвестные теги и атрибуты всё равно удаляются.

title — отдельное JSON-поле запроса, а не первый заголовок Markdown. Не дублируйте title первым # в теле: либо оставьте H1 API, либо напишите один # сами.

Абзац

Один абзац текста с переносом строки.

Это уже второй абзац.

Пустая строка отделяет абзацы. Одинарный перенос внутри абзаца может быть нормализован.

Заголовки ########

## Раздел
### Подраздел
###### Самый глубокий уровень

В статье один H1. Если в Markdown нет #, API ставит H1 из title в начало. Если # один — его не трогает, где бы он ни стоял. Если # несколько, первый остаётся H1, остальные становятся H2. Для структуры разделов используйте ########.

Жирный, курсив, зачёркнутый текст и inline code

**жирный текст**, *курсив*, ~~зачёркнутый текст~~ и `inline code`.

Начертания можно комбинировать и вкладывать друг в друга.

Ссылки

Откройте [документацию API](https://designpub.ru/handbook/api).

Используйте абсолютные http:// или https:// URL. Ссылки с опасными схемами и неподдерживаемыми атрибутами проходят санитизацию.

Списки

- Первый пункт
- Второй пункт
  - Вложенный пункт

1. Шаг один
2. Шаг два

Нумерация в исходном Markdown может начинаться с любого числа, но в HTML приходит обычный <ol>.

Цитата

> Цитата автора.
>
> Второй абзац внутри цитаты.

Блок кода

~~~javascript
const answer = 42;
console.log(answer);
~~~

Язык после открывающего fence сохраняется как подсказка для подсветки.

Горизонтальная линия

Текст до линии.

---

Текст после линии.

Изображение

![Схема процесса](https://cdn.example.com/diagram.png)
![Уже сохранённое изображение](/api/media/2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f)

Внешний https:// URL API рехостит при записи. Путь /api/media/{uuid} уже локальный и повторно не загружается. alt сохраняется и нужен для доступности.

Изображение с подписью

Markdown передаёт только alt. Для подписи используйте HTML-остров:

![График](https://cdn.example.com/chart.png)

Классы dp-figure и dp-figure__caption обязательны. Внутри figure должен быть один основной img и, при необходимости, один figcaption.

Layout: full и mid

Ширину одиночной картинки задаёт атрибут data-layout на figure. Разрешены только значения full и mid:

HTML
<figure class="dp-figure" data-layout="full">
  <img src="/api/media/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" alt="Широкая схема">
</figure>
<figure class="dp-figure" data-layout="mid">
  <img src="/api/media/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" alt="Схема средней ширины">
</figure>

Другие значения data-layout будут удалены.

Галерея

Галерея — это figure.dp-gallery с контейнером div.dp-gallery__items и вложенными figure.dp-figure. Изображений может быть сколько угодно в пределах лимитов записи: 100 внешних URL и 50 МиБ на запрос. Подпись опциональна: без неё галерея тоже валидна. Если подпись нужна, она одна на всю галерею — figcaption сразу после контейнера с изображениями, не внутри каждого изображения.

HTML
<figure class="dp-gallery" data-layout="full">
  <div class="dp-gallery__items">
    <figure class="dp-figure">
      <img src="/api/media/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" alt="Первое изображение">
    </figure>
    <figure class="dp-figure">
      <img src="/api/media/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" alt="Второе изображение">
    </figure>
  </div>
  <figcaption class="dp-figure__caption">Подпись ко всей галерее</figcaption>
</figure>

data-layout="mid" можно поставить на корневой figure.dp-gallery. Не вставляйте img напрямую рядом с div.dp-gallery__items. figcaption можно опустить. Если ставите — класс dp-figure__caption тот же, что у одиночной картинки; без него подпись не распознается. figcaption внутри изображения в галерее не становится подписью галереи.

GFM-таблица

| Инструмент | Назначение |
| --- | --- |
| API | Создание статьи |
| n8n | Автоматизация |

Строка заголовка приходит в HTML первой строкой внутри <tbody>; отдельного <thead> не будет.

Видео-встраивание

Видео передаётся только HTML-островом. Пример для YouTube:

HTML
<div data-video data-provider="youtube" data-source="embed">
  <iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ"></iframe>
</div>

Так же оформляются Vimeo, VK и Rutube. Не меняйте data-source="embed" на путь к файлу и не подставляйте прямой mp4 в iframe или <video>.

Универсальный iframe и dp-embed

Для Figma и других разрешённых встраиваний используйте контейнер dp-embed:

HTML
<div class="dp-embed" data-dp-embed>
  <iframe
    src="https://www.figma.com/embed?embed_host=share&url=https%3A%2F%2Fwww.figma.com%2Fdesign%2Fexample"
    loading="lazy"
    allowfullscreen
    title="Figma design">
  </iframe>
</div>

Сервер разрешает только хосты из allowlist: Figma, YouTube, Vimeo, VK, Rutube и несколько специализированных embed-хостов. Iframe с неизвестного домена будет удалён. Одиночный <iframe> с разрешённым хостом сервер может обернуть в dp-embed сам, но контейнер лучше задавать явно.

Карточка ссылки — HTML-only узел. Адрес храните в href, видимый текст — в этих классах:

HTML
<a class="dp-link-card" href="https://example.com/article" target="_blank" rel="noopener noreferrer" data-dp-link-card>
  <div class="dp-link-card__body">
    <div class="dp-link-card__title">Title</div>
    <div class="dp-link-card__desc">Description</div>
    <div class="dp-link-card__domain">
      <img class="dp-link-card__favicon" src="/api/media/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" alt="" width="16" height="16">
      <span class="dp-link-card__domain-label">example.com</span>
    </div>
  </div>
  <div class="dp-link-card__media" style="background-image:url(/api/media/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb)" role="presentation"></div>
</a>

img.dp-link-card__favicon и div.dp-link-card__media необязательны. Если сохранённого медиафайла нет, уберите элемент целиком, а не оставляйте внешний адрес. Атрибуты data-dp-link-card, target и rel сохраняют поведение карточки.

Спойлер

Используйте стандартные details и summary:

<details>
  <summary>Показать ответ</summary>
  <p>Скрытый текст появится после раскрытия.</p>
</details>

Внутри спойлера используйте обычные разрешённые узлы. JavaScript-обработчики удаляются санитизацией.

GIF

GIF хранится как сохранённый медиафайл и доставляется через <video>:

HTML
<div class="dp-gif" data-gif="2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f">
  <video
    src="/api/media/2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f"
    autoplay
    loop
    muted
    playsinline>
  </video>
</div>

src должен быть относительным /api/media/{uuid}. Внешний GIF сначала сохраните через POST /media/from-url или загрузите файл через POST /media.

Неизвестный HTML

Неизвестные теги, атрибуты, опасные схемы и неразрешённые iframe удаляются санитайзером:

HTML
<marquee onclick="alert('removed')">Этот HTML не является узлом статьи.</marquee>

Если после конвертации документ оказался пуст, API отвечает 400 Bad Request без code. Прямой <video src="https://cdn.example.com/movie.mp4"> тоже не поддерживается: его src будет снят. Используйте embed-провайдера или сохранённый GIF.

Справочник маршрутов

Все маршруты этого раздела требуют токен в заголовке Authorization. Если не указано иное, тело ошибки имеет формат { "error": "...", "code": "..." }, а ответ 429 содержит заголовки сброса лимита.

GET /me

Возвращает ваш handle и действующие лимиты. Вызывайте его в начале интеграции и перед большими медиа-операциями, чтобы знать расход daily_media_bytes_used.

Авторизация: Authorization: Bearer dp_live_YOUR_TOKEN.

Запрос: query-параметров и тела нет. Content-Type не требуется.

Успех: 200 OK. Ответ:

{
  "handle": "anton",
  "limits": {
    "read_per_min": 60,
    "write_per_min": 30,
    "publish_per_min": 1,
    "media_per_min": 10,
    "daily_media_bytes": 209715200,
    "daily_media_bytes_used": 0
  }
}

daily_media_bytes — суточная квота медиа в байтах, те же 200 МиБ. daily_media_bytes_used — израсходованные байты с начала текущего UTC-дня, а не процент.

Ошибки:

  • 401 Unauthorized, code: "unauthorized" — токен отсутствует, неверен, отозван или заменён перевыпуском.
  • 429 Too Many Requests, code: "rate_limited" — исчерпан лимит чтения.
  • 503 Service Unavailable, code: "service_unavailable" — временно недоступна зависимость API.
TOKEN=dp_live_YOUR_TOKEN
curl -sS "https://designpub.ru/api/v1/author/me" \
  -H "Authorization: Bearer $TOKEN"

GET /posts

Возвращает страницы ваших статей без поля html. Чужие и публичные статьи этим маршрутом не ищутся.

Авторизация: Authorization: Bearer dp_live_YOUR_TOKEN.

Запрос: query-параметры:

Параметр Значения По умолчанию
status draft, pending_moderation, published Все статусы
limit Целое число, максимум 50 20
cursor Непрозрачная строка из next_cursor Первая страница

Тела нет. Сортировка идёт по (created_at, id) в порядке DESC, поэтому после PATCH статья не поднимается наверх. Курсор не декодируйте и не собирайте сами.

Успех: 200 OK. У каждого элемента items тот же набор полей AuthorPost, кроме html; пустые rehost_failed и warnings опущены:

{
  "items": [
    {
      "id": "2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f",
      "public_id": "a1b2c3d4e5f6",
      "canonical_path": "/kak-peredat-statyu-a1b2c3d4e5f6",
      "title": "Как передать статью через API",
      "subtitle": null,
      "slug": "kak-peredat-statyu",
      "status": "draft",
      "tags": [{ "slug": "api", "name": "API" }],
      "cover_asset_id": null,
      "cover_url": null,
      "cover_width": null,
      "cover_height": null,
      "published_at": null,
      "created_at": "2026-08-20T17:00:00Z",
      "updated_at": "2026-08-20T17:00:00Z",
      "has_unpublished_changes": false,
      "like_count": 0,
      "comment_count": 0,
      "save_count": 0,
      "popularity": 0,
      "impression_count": 0,
      "open_count": 0,
      "read_count": 0,
      "complete_count": 0
    }
  ],
  "next_cursor": "opaque-cursor-from-server"
}

Когда следующей страницы нет, next_cursor равен null.

Ошибки:

  • 400 Bad Request без code — неизвестное значение status или испорченный cursor.
  • 401 Unauthorized, code: "unauthorized" — проблема с токеном.
  • 429 Too Many Requests, code: "rate_limited" — исчерпан лимит чтения.
  • 503 Service Unavailable, code: "service_unavailable" — временная ошибка сервиса.
TOKEN=dp_live_YOUR_TOKEN
curl -sS --get "https://designpub.ru/api/v1/author/posts" \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "status=published" \
  --data-urlencode "limit=20"

Чтобы получить следующую страницу, передайте next_cursor как есть:

curl -sS --get "https://designpub.ru/api/v1/author/posts" \
  -H "Authorization: Bearer dp_live_YOUR_TOKEN" \
  --data-urlencode "cursor=opaque-cursor-from-server"

GET /posts/{id}

Возвращает одну вашу статью вместе с рабочим HTML. Это основной маршрут, чтобы проверить результат записи, восстановить текст после сетевого таймаута и получить HTML для round-trip.

Авторизация: Authorization: Bearer dp_live_YOUR_TOKEN.

Запрос: в path передайте UUID статьи. Query-параметров и тела нет.

Успех: 200 OK. В отличие от списка, ответ содержит html:

{
  "id": "2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f",
  "public_id": "a1b2c3d4e5f6",
  "canonical_path": "/kak-peredat-statyu-a1b2c3d4e5f6",
  "title": "Как передать статью через API",
  "subtitle": "Рабочий пример",
  "slug": "kak-peredat-statyu",
  "status": "draft",
  "tags": [
    { "slug": "api", "name": "API" },
    { "slug": "automation", "name": "Автоматизация" }
  ],
  "cover_asset_id": null,
  "cover_url": null,
  "cover_width": null,
  "cover_height": null,
  "published_at": null,
  "created_at": "2026-08-20T17:00:00Z",
  "updated_at": "2026-08-20T17:00:00Z",
  "has_unpublished_changes": false,
  "like_count": 0,
  "comment_count": 0,
  "save_count": 0,
  "popularity": 0,
  "impression_count": 12,
  "open_count": 9,
  "read_count": 6,
  "complete_count": 4,
  "html": "<h1>Как передать статью через API</h1><p>Текст статьи.</p>"
}

html строится из рабочей версии: если статья опубликована, но после публикации был PATCH, вы увидите новый рабочий HTML.

Ошибки:

  • 400 Bad Request без code — path не является UUID.
  • 401 Unauthorized, code: "unauthorized" — проблема с токеном.
  • 404 Not Found, code: "not_found" — UUID не существует, статья удалена или принадлежит другому автору.
  • 429 Too Many Requests, code: "rate_limited" — исчерпан лимит чтения.
  • 503 Service Unavailable, code: "service_unavailable" — временно недоступна зависимость API.
TOKEN=dp_live_YOUR_TOKEN
POST_ID=2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f
curl -sS "https://designpub.ru/api/v1/author/posts/$POST_ID" \
  -H "Authorization: Bearer $TOKEN"

POST /posts

Создаёт новую статью как draft. Маршрут не публикует статью.

Авторизация: Authorization: Bearer dp_live_YOUR_TOKEN.

Запрос: Content-Type: application/json, query-параметров нет. Поля:

Поле Обязательность Тип Описание
title обязательно string Непустой заголовок.
markdown обязательно string Непустой документ: GFM и HTML-острова.
subtitle нет string или null Подзаголовок.
tags нет array of strings Названия тегов, например ["api", "automation"]. Сервер находит существующие или создаёт новые по имени.
slug нет string Явный slug. Если не передать, API выведет его из title.
cover_asset_id нет UUID или null Уже загруженное изображение. Размеры берутся из медиафайла.
cover_url нет string или null Внешний URL для рехоста обложки.

Тело запроса:

{
  "title": "Как передать статью через API",
  "markdown": "## Введение\n\nТекст статьи.\n\n![Схема](https://cdn.example.com/diagram.png)\n",
  "subtitle": "Рабочий пример",
  "tags": ["api", "automation"],
  "slug": "kak-peredat-statyu",
  "cover_asset_id": null,
  "cover_url": null
}

title и markdown не могут быть пустыми после trim. Занятый slug даёт 409. Не отправляйте cover_width и cover_height: их вычисляет сервер.

Успех: 201 Created, тело — AuthorPost со статусом draft. Поля html в ответе нет; если рехост прошёл частично, добавляются rehost_failed и warnings.

{
  "id": "2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f",
  "public_id": "a1b2c3d4e5f6",
  "canonical_path": "/kak-peredat-statyu-a1b2c3d4e5f6",
  "title": "Как передать статью через API",
  "subtitle": "Рабочий пример",
  "slug": "kak-peredat-statyu",
  "status": "draft",
  "tags": [
    { "slug": "api", "name": "API" },
    { "slug": "automation", "name": "Автоматизация" }
  ],
  "cover_asset_id": null,
  "cover_url": null,
  "cover_width": null,
  "cover_height": null,
  "published_at": null,
  "created_at": "2026-08-20T17:00:00Z",
  "updated_at": "2026-08-20T17:00:00Z",
  "has_unpublished_changes": false,
  "like_count": 0,
  "comment_count": 0,
  "save_count": 0,
  "popularity": 0,
  "impression_count": 0,
  "open_count": 0,
  "read_count": 0,
  "complete_count": 0
}

Ошибки:

  • 400 Bad Request без code — нет title или markdown, поле пустое, slug некорректен, обложка не найдена либо документ стал пустым после конвертации.
  • 401 Unauthorized, code: "unauthorized" — проблема с токеном.
  • 409 Conflict без code — slug уже занят у этого автора.
  • 429 Too Many Requests, code: "rate_limited" — исчерпан лимит записи. Если во время записи закончился лимит медиа или суточная квота, рехост останавливается, но статья сохраняется: причина будет в warnings, а URL — в rehost_failed.
  • 503 Service Unavailable, code: "service_unavailable" — временно недоступна зависимость API.
TOKEN=dp_live_YOUR_TOKEN
curl -sS -X POST "https://designpub.ru/api/v1/author/posts" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Как передать статью через API",
    "markdown": "## Введение\n\nТекст статьи.\n\n![Схема](https://cdn.example.com/diagram.png)\n",
    "subtitle": "Рабочий пример",
    "tags": ["api", "automation"],
    "slug": "kak-peredat-statyu"
  }'

PATCH /posts/{id}

Частично обновляет рабочую версию вашей статьи. Маршрут не публикует изменения.

Авторизация: Authorization: Bearer dp_live_YOUR_TOKEN.

Запрос: Content-Type: application/json, в path — UUID статьи. Query-параметров нет. Все поля тела необязательны, но передайте хотя бы одно:

{
  "title": "Обновлённый заголовок",
  "markdown": "Новый текст.\n",
  "subtitle": null,
  "tags": ["api", "markdown"],
  "slug": "obnovlennyi-zagolovok",
  "cover_asset_id": null
}

Семантика полей:

  • отсутствующее поле не меняется;
  • subtitle: null очищает подзаголовок;
  • tags заменяет текущий список тегов;
  • cover_asset_id: null очищает обложку;
  • новое значение cover_asset_id должно ссылаться на доступное вам изображение;
  • markdown конвертируется заново; пустая строка отклоняется;
  • cover_url запускает рехост внешнего URL.

Успех: 200 OK, тело — обновлённый AuthorPost без html. Поля rehost_failed и warnings добавляются только при необходимости.

{
  "id": "2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f",
  "public_id": "a1b2c3d4e5f6",
  "canonical_path": "/obnovlennyi-zagolovok-a1b2c3d4e5f6",
  "title": "Обновлённый заголовок",
  "subtitle": null,
  "slug": "obnovlennyi-zagolovok",
  "status": "draft",
  "tags": [
    { "slug": "api", "name": "API" },
    { "slug": "markdown", "name": "Markdown" }
  ],
  "cover_asset_id": null,
  "cover_url": null,
  "cover_width": null,
  "cover_height": null,
  "published_at": null,
  "created_at": "2026-08-20T17:00:00Z",
  "updated_at": "2026-08-20T17:10:00Z",
  "has_unpublished_changes": false,
  "like_count": 0,
  "comment_count": 0,
  "save_count": 0,
  "popularity": 0,
  "impression_count": 0,
  "open_count": 0,
  "read_count": 0,
  "complete_count": 0
}

После PATCH опубликованной статьи ждите has_unpublished_changes: true и вызывайте publish отдельно.

Ошибки:

  • 400 Bad Request без code — некорректный UUID, невалидное поле, пустой Markdown, отсутствующая обложка или пустой документ после конвертации.
  • 401 Unauthorized, code: "unauthorized" — проблема с токеном.
  • 404 Not Found, code: "not_found" — статья удалена, не существует или принадлежит другому автору.
  • 409 Conflict без code — новый slug уже занят.
  • 429 Too Many Requests, code: "rate_limited" — исчерпан лимит записи; рехост дополнительно расходует лимит медиа.
  • 503 Service Unavailable, code: "service_unavailable" — временно недоступна зависимость API.
TOKEN=dp_live_YOUR_TOKEN
POST_ID=2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f
curl -sS -X PATCH "https://designpub.ru/api/v1/author/posts/$POST_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Обновлённый заголовок",
    "markdown": "Новый текст.\n",
    "subtitle": null,
    "tags": ["api", "markdown"],
    "cover_asset_id": null
  }'

POST /posts/{id}/publish

Отправляет рабочую версию статьи в публикацию. Статья должна принадлежать владельцу токена.

Авторизация: Authorization: Bearer dp_live_YOUR_TOKEN.

Запрос: в path — UUID статьи. Query-параметров и тела нет. Content-Type не требуется.

Успех: 200 OK, тело — AuthorPost без html. В поле status придёт published с заполненным published_at либо pending_moderation:

{
  "id": "2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f",
  "public_id": "a1b2c3d4e5f6",
  "canonical_path": "/kak-peredat-statyu-a1b2c3d4e5f6",
  "title": "Как передать статью через API",
  "subtitle": "Рабочий пример",
  "slug": "kak-peredat-statyu",
  "status": "pending_moderation",
  "tags": [{ "slug": "api", "name": "API" }],
  "cover_asset_id": null,
  "cover_url": null,
  "cover_width": null,
  "cover_height": null,
  "published_at": null,
  "created_at": "2026-08-20T17:00:00Z",
  "updated_at": "2026-08-20T17:10:00Z",
  "has_unpublished_changes": false,
  "like_count": 0,
  "comment_count": 0,
  "save_count": 0,
  "popularity": 0,
  "impression_count": 0,
  "open_count": 0,
  "read_count": 0,
  "complete_count": 0
}

Ошибки:

  • 400 Bad Request без code — статья не может быть опубликована в текущем состоянии или её рабочий документ невалиден.
  • 401 Unauthorized, code: "unauthorized" — проблема с токеном.
  • 404 Not Found, code: "not_found" — статья отсутствует или принадлежит другому автору.
  • 429 Too Many Requests, code: "rate_limited" — исчерпан лимит публикации: 1/мин.
  • 503 Service Unavailable, code: "service_unavailable" — временно недоступна зависимость API.
TOKEN=dp_live_YOUR_TOKEN
POST_ID=2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f
curl -sS -X POST "https://designpub.ru/api/v1/author/posts/$POST_ID/publish" \
  -H "Authorization: Bearer $TOKEN"

POST /posts/{id}/unpublish

Снимает статью с публикации и оставляет её текст как черновик. Маршрут не удаляет статью.

Авторизация: Authorization: Bearer dp_live_YOUR_TOKEN.

Запрос: в path — UUID статьи. Query-параметров и тела нет.

Успех: 204 No Content. Тело пустое; не вызывайте response.json() без проверки статуса.

Ошибки:

  • 400 Bad Request без code — некорректный UUID или недопустимое состояние.
  • 401 Unauthorized, code: "unauthorized" — проблема с токеном.
  • 404 Not Found, code: "not_found" — статья отсутствует или принадлежит другому автору.
  • 429 Too Many Requests, code: "rate_limited" — исчерпан лимит записи.
  • 503 Service Unavailable, code: "service_unavailable" — временно недоступна зависимость API.
TOKEN=dp_live_YOUR_TOKEN
POST_ID=2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f
curl -i -sS -X POST "https://designpub.ru/api/v1/author/posts/$POST_ID/unpublish" \
  -H "Authorization: Bearer $TOKEN"

DELETE /posts/{id}

Мягко удаляет вашу статью: она перестаёт возвращаться из Author API и с публичных страниц. Восстановление этим API не предусмотрено.

Авторизация: Authorization: Bearer dp_live_YOUR_TOKEN.

Запрос: в path — UUID статьи. Query-параметров и тела нет.

Успех: 204 No Content, без тела.

Ошибки:

  • 400 Bad Request без code — некорректный UUID.
  • 401 Unauthorized, code: "unauthorized" — проблема с токеном.
  • 404 Not Found, code: "not_found" — статья отсутствует, уже удалена или принадлежит другому автору.
  • 429 Too Many Requests, code: "rate_limited" — исчерпан лимит записи.
  • 503 Service Unavailable, code: "service_unavailable" — временно недоступна зависимость API.
TOKEN=dp_live_YOUR_TOKEN
POST_ID=2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f
curl -i -sS -X DELETE "https://designpub.ru/api/v1/author/posts/$POST_ID" \
  -H "Authorization: Bearer $TOKEN"

POST /media

Загружает локальное изображение и создаёт принадлежащий вам медиафайл. Это единственный маршрут Author API для загрузки файла; видео он не принимает.

Авторизация: Authorization: Bearer dp_live_YOUR_TOKEN.

Запрос: multipart/form-data, query-параметров нет. Передайте поле file с непустым изображением не больше 50 МиБ. В curl используйте -F; в Node fetch не задавайте Content-Type вручную, иначе сломается multipart boundary.

Успех: 200 OK. url можно вставить в Markdown, а id — в cover_asset_id:

{
  "id": "2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f",
  "url": "/api/media/2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f",
  "kind": "image",
  "mime_type": "image/jpeg",
  "byte_size": 245760,
  "width": 1600,
  "height": 900,
  "status": "ready",
  "poster_url": null
}

Для изображения kind равен image, statusready, poster_url — всегда null.

Ошибки:

  • 400 Bad Request без code — поле file отсутствует, файл пуст, больше 50 МиБ, повреждён или имеет неподдерживаемый формат.
  • 401 Unauthorized, code: "unauthorized" — проблема с токеном.
  • 422 Unprocessable Entity, code: "animated_image" — файл распознан как анимированное изображение, которое этот маршрут не принимает.
  • 429 Too Many Requests, code: "rate_limited" — исчерпан лимит медиа или суточная квота медиа.
  • 503 Service Unavailable, code: "service_unavailable" — временно недоступно хранилище.
TOKEN=dp_live_YOUR_TOKEN
curl -sS -X POST "https://designpub.ru/api/v1/author/media" \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@./cover.jpg"

POST /media/from-url

Скачивает один внешний URL и создаёт принадлежащий вам медиафайл. Используйте маршрут, когда нужно получить UUID заранее и проверить результат до сборки статьи.

Авторизация: Authorization: Bearer dp_live_YOUR_TOKEN.

Запрос: Content-Type: application/json, query-параметров нет:

{
  "url": "https://cdn.example.com/diagram.png"
}

Принимается только HTTP(S)-URL; локальные и внутренние адреса запрещены. Уже сохранённый /api/media/{uuid} отправлять повторно не нужно. Прямой URL на видеофайл маршрут не принимает.

Успех: 200 OK:

{
  "id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
  "url": "/api/media/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
  "kind": "image",
  "mime_type": "image/png",
  "byte_size": 184320,
  "width": 1200,
  "height": 800,
  "status": "ready",
  "poster_url": null
}

Ошибки:

  • 400 Bad Request без codeurl отсутствует или пуст, схема не HTTP(S), адрес запрещён, это уже локальный путь медиа либо содержимое не является поддерживаемым изображением.
  • 401 Unauthorized, code: "unauthorized" — проблема с токеном.
  • 429 Too Many Requests, code: "rate_limited" — исчерпан лимит медиа или суточная квота медиа.
  • 503 Service Unavailable, code: "service_unavailable" — хранилище или загрузка временно недоступны.
TOKEN=dp_live_YOUR_TOKEN
curl -sS -X POST "https://designpub.ru/api/v1/author/media/from-url" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://cdn.example.com/diagram.png"}'

Полный сценарий: черновик с изображением и публикация

Надёжная последовательность из четырёх шагов:

  1. Загрузите локальное изображение через POST /media или сохраните внешнее через POST /media/from-url.
  2. Возьмите из ответа url, например /api/media/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa.
  3. Вставьте путь в markdown и создайте статью через POST /posts.
  4. Проверьте GET /posts/{id} и вызовите POST /posts/{id}/publish.

Ниже один сценарий на curl, Node.js 18+ и Python: загрузить cover.jpg, создать черновик и опубликовать. Ширину и высоту отправлять не нужно, а в cover_asset_id передаётся id из ответа медиа.

TOKEN=dp_live_YOUR_TOKEN
BASE=https://designpub.ru/api/v1/author

MEDIA_JSON=$(
  curl -sS -X POST "$BASE/media" \
    -H "Authorization: Bearer $TOKEN" \
    -F "file=@./cover.jpg"
)
MEDIA_ID=$(
  printf '%s' "$MEDIA_JSON" |
    python3 -c 'import json, sys; print(json.load(sys.stdin)["id"])'
)
MEDIA_URL=$(
  printf '%s' "$MEDIA_JSON" |
    python3 -c 'import json, sys; print(json.load(sys.stdin)["url"])'
)

DRAFT_JSON=$(
  curl -sS -X POST "$BASE/posts" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d "$(python3 -c "import json; print(json.dumps({
      'title': 'Статья с локальным изображением',
      'markdown': '## Изображение\\n\\n![Обложка]($MEDIA_URL)\\n\\nТекст статьи.\\n',
      'cover_asset_id': '$MEDIA_ID',
      'tags': ['api'],
    }))")"
)
POST_ID=$(
  printf '%s' "$DRAFT_JSON" |
    python3 -c 'import json, sys; print(json.load(sys.stdin)["id"])'
)

curl -sS "$BASE/posts/$POST_ID" -H "Authorization: Bearer $TOKEN"
curl -sS -X POST "$BASE/posts/$POST_ID/publish" \
  -H "Authorization: Bearer $TOKEN"

Если последний ответ вернул pending_moderation, это ожидаемый результат, а не ошибка запроса.

Важные ограничения и ответы на частые вопросы

Есть ли webhooks?

Нет. Чтобы узнать результат модерации или публикации, периодически вызывайте GET /posts или GET /posts/{id} в пределах лимита чтения.

Можно ли загрузить видеофайл?

Нет. YouTube, Vimeo, VK, Rutube и другие разрешённые сервисы подключаются HTML-встраиванием. GIF в статье — сохранённый медиафайл с блоком data-gif.

Есть ли OAuth-приложения?

Нет. Используйте токен dp_live_…, созданный в настройках. OAuth-клиентов и scope в Author API нет.

Почему после PATCH публичная статья ещё старая?

PATCH меняет рабочую версию, но не публикует её. Проверьте has_unpublished_changes и вызовите POST /posts/{id}/publish. Публикация может уйти в pending_moderation.

Почему внешний iframe или mp4 исчез?

Санитайзер оставляет только разрешённые HTML-узлы и хосты. Прямой <video src="https://...mp4"> и iframe с неизвестного домена удаляются. Разрешённое встраивание оформляйте как data-video или div.dp-embed, а картинку — через рехост внешнего URL или /api/media/{uuid}.

Индекс популярности

Как считается индекс