Author API — HTTP-интерфейс «Дизайн-кабака», через который вы создаёте, обновляете и публикуете свои статьи из скрипта, n8n, собственного продукта или AI-агента. Доступ ограничен вашими статьями: публичную ленту через этот API читать нельзя.
Вызывайте API с сервера: из backend-приложения, worker, cron-задачи или локального скрипта. Запрос из браузера на стороннем сайте — не поддерживаемый способ интеграции. Не передавайте секрет dp_live_… в браузерный JavaScript и не встраивайте его в мобильное приложение.
Содержание
- Быстрый старт
- Авторизация и токены
- Общие соглашения
- Лимиты
- Жизненный цикл публикации
- Медиа
- Тело статьи
- Справочник маршрутов
- Полный сценарий
- Ограничения и частые вопросы
Быстрый старт
- Откройте настройки аккаунта и создайте токен с понятным именем, например
n8n publishing. - Скопируйте секрет сразу: он показывается один раз.
- Сохраните секрет в переменной окружения или в хранилище секретов.
- Сделайте первый запрос
GET /me. Он проверит авторизацию и вернёт ваш handle и действующие лимиты. - Создайте статью через
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"const baseUrl = 'https://designpub.ru/api/v1/author';
const token = 'dp_live_YOUR_TOKEN';
const response = await fetch(`${baseUrl}/me`, {
headers: { Authorization: `Bearer ${token}` },
});
const body = await response.json();
if (!response.ok) throw new Error(`${response.status}: ${JSON.stringify(body)}`);
console.log(body);import requests
BASE_URL = 'https://designpub.ru/api/v1/author'
TOKEN = 'dp_live_YOUR_TOKEN'
response = requests.get(
f'{BASE_URL}/me',
headers={'Authorization': f'Bearer {TOKEN}'},
)
response.raise_for_status()
print(response.json())Первый черновик и публикация
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"const baseUrl = 'https://designpub.ru/api/v1/author';
const token = 'dp_live_YOUR_TOKEN';
const headers = {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
};
const createResponse = await fetch(`${baseUrl}/posts`, {
method: 'POST',
headers,
body: JSON.stringify({
title: 'Первая статья через API',
markdown: 'Текст статьи из интеграции.\n',
}),
});
const draft = await createResponse.json();
if (!createResponse.ok) {
throw new Error(`${createResponse.status}: ${JSON.stringify(draft)}`);
}
const publishResponse = await fetch(`${baseUrl}/posts/${draft.id}/publish`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
});
const published = await publishResponse.json();
if (!publishResponse.ok) {
throw new Error(`${publishResponse.status}: ${JSON.stringify(published)}`);
}
console.log(published);import requests
BASE_URL = 'https://designpub.ru/api/v1/author'
TOKEN = 'dp_live_YOUR_TOKEN'
auth = {'Authorization': f'Bearer {TOKEN}'}
create_response = requests.post(
f'{BASE_URL}/posts',
headers={**auth, 'Content-Type': 'application/json'},
json={
'title': 'Первая статья через API',
'markdown': 'Текст статьи из интеграции.\n',
},
)
create_response.raise_for_status()
draft = create_response.json()
publish_response = requests.post(
f'{BASE_URL}/posts/{draft["id"]}/publish',
headers=auth,
)
publish_response.raise_for_status()
print(publish_response.json())Авторизация и токены
Каждый маршрут под 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:

При 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:

В cover_asset_id передаётся именно id, а не полный url:
{
"title": "Статья с локальной обложкой",
"markdown": "\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, либо напишите один # сами.
Абзац
Один абзац текста с переносом строки.
Это уже второй абзац.<p>Один абзац текста с переносом строки.</p>
<p>Это уже второй абзац.</p>Пустая строка отделяет абзацы. Одинарный перенос внутри абзаца может быть нормализован.
Заголовки ##–######
## Раздел
### Подраздел
###### Самый глубокий уровень<h2>Раздел</h2>
<h3>Подраздел</h3>
<h6>Самый глубокий уровень</h6>В статье один H1. Если в Markdown нет #, API ставит H1 из title в начало. Если # один — его не трогает, где бы он ни стоял. Если # несколько, первый остаётся H1, остальные становятся H2. Для структуры разделов используйте ##–######.
Жирный, курсив, зачёркнутый текст и inline code
**жирный текст**, *курсив*, ~~зачёркнутый текст~~ и `inline code`.<p><strong>жирный текст</strong>, <em>курсив</em>, <s>зачёркнутый текст</s> и <code>inline code</code>.</p>Начертания можно комбинировать и вкладывать друг в друга.
Ссылки
Откройте [документацию API](https://designpub.ru/handbook/api).<p>Откройте <a href="https://designpub.ru/handbook/api">документацию API</a>.</p>Используйте абсолютные http:// или https:// URL. Ссылки с опасными схемами и неподдерживаемыми атрибутами проходят санитизацию.
Списки
- Первый пункт
- Второй пункт
- Вложенный пункт
1. Шаг один
2. Шаг два<ul>
<li>Первый пункт</li>
<li>Второй пункт
<ul><li>Вложенный пункт</li></ul>
</li>
</ul>
<ol>
<li>Шаг один</li>
<li>Шаг два</li>
</ol>Нумерация в исходном Markdown может начинаться с любого числа, но в HTML приходит обычный <ol>.
Цитата
> Цитата автора.
>
> Второй абзац внутри цитаты.<blockquote>
<p>Цитата автора.</p>
<p>Второй абзац внутри цитаты.</p>
</blockquote>Блок кода
~~~javascript
const answer = 42;
console.log(answer);
~~~<pre><code class="language-javascript">const answer = 42;
console.log(answer);
</code></pre>Язык после открывающего fence сохраняется как подсказка для подсветки.
Горизонтальная линия
Текст до линии.
---
Текст после линии.<p>Текст до линии.</p>
<hr>
<p>Текст после линии.</p>Изображение

<img src="/api/media/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" alt="Схема процесса">
<img src="/api/media/2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f" alt="Уже сохранённое изображение">Внешний https:// URL API рехостит при записи. Путь /api/media/{uuid} уже локальный и повторно не загружается. alt сохраняется и нужен для доступности.
Изображение с подписью
Markdown передаёт только alt. Для подписи используйте HTML-остров:
<figure class="dp-figure">
<img src="/api/media/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" alt="График">
<figcaption class="dp-figure__caption">Подпись под графиком</figcaption>
</figure>Классы dp-figure и dp-figure__caption обязательны. Внутри figure должен быть один основной img и, при необходимости, один figcaption.
Layout: full и mid
Ширину одиночной картинки задаёт атрибут data-layout на figure. Разрешены только значения full и mid:
<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 сразу после контейнера с изображениями, не внутри каждого изображения.
<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 | Автоматизация |<table>
<tbody>
<tr><th>Инструмент</th><th>Назначение</th></tr>
<tr><td>API</td><td>Создание статьи</td></tr>
<tr><td>n8n</td><td>Автоматизация</td></tr>
</tbody>
</table>Строка заголовка приходит в HTML первой строкой внутри <tbody>; отдельного <thead> не будет.
Видео-встраивание
Видео передаётся только HTML-островом. Пример для YouTube:
<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:
<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 сам, но контейнер лучше задавать явно.
Link card
Карточка ссылки — HTML-only узел. Адрес храните в href, видимый текст — в этих классах:
<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><details>
<summary>Показать ответ</summary>
<p>Скрытый текст появится после раскрытия.</p>
</details>Внутри спойлера используйте обычные разрешённые узлы. JavaScript-обработчики удаляются санитизацией.
GIF
GIF хранится как сохранённый медиафайл и доставляется через <video>:
<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 удаляются санитайзером:
<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"const response = await fetch('https://designpub.ru/api/v1/author/me', {
headers: { Authorization: 'Bearer dp_live_YOUR_TOKEN' },
});
const body = await response.json();
if (!response.ok) throw new Error(`${response.status}: ${JSON.stringify(body)}`);
console.log(body);import requests
response = requests.get(
'https://designpub.ru/api/v1/author/me',
headers={'Authorization': 'Bearer dp_live_YOUR_TOKEN'},
)
response.raise_for_status()
print(response.json())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"const url = new URL('https://designpub.ru/api/v1/author/posts');
url.searchParams.set('status', 'published');
url.searchParams.set('limit', '20');
const response = await fetch(url, {
headers: { Authorization: 'Bearer dp_live_YOUR_TOKEN' },
});
const body = await response.json();
if (!response.ok) throw new Error(`${response.status}: ${JSON.stringify(body)}`);
console.log(body);import requests
response = requests.get(
'https://designpub.ru/api/v1/author/posts',
params={'status': 'published', 'limit': 20},
headers={'Authorization': 'Bearer dp_live_YOUR_TOKEN'},
)
response.raise_for_status()
print(response.json())Чтобы получить следующую страницу, передайте 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"const postId = '2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f';
const response = await fetch(
`https://designpub.ru/api/v1/author/posts/${postId}`,
{ headers: { Authorization: 'Bearer dp_live_YOUR_TOKEN' } },
);
const body = await response.json();
if (!response.ok) throw new Error(`${response.status}: ${JSON.stringify(body)}`);
console.log(body.html);import requests
post_id = '2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f'
response = requests.get(
f'https://designpub.ru/api/v1/author/posts/{post_id}',
headers={'Authorization': 'Bearer dp_live_YOUR_TOKEN'},
)
response.raise_for_status()
print(response.json()['html'])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\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\n",
"subtitle": "Рабочий пример",
"tags": ["api", "automation"],
"slug": "kak-peredat-statyu"
}'const response = await fetch('https://designpub.ru/api/v1/author/posts', {
method: 'POST',
headers: {
Authorization: 'Bearer dp_live_YOUR_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Как передать статью через API',
markdown: '## Введение\n\nТекст статьи.\n\n\n',
subtitle: 'Рабочий пример',
tags: ['api', 'automation'],
slug: 'kak-peredat-statyu',
}),
});
const body = await response.json();
if (!response.ok) throw new Error(`${response.status}: ${JSON.stringify(body)}`);
console.log(body.id);import requests
response = requests.post(
'https://designpub.ru/api/v1/author/posts',
headers={
'Authorization': 'Bearer dp_live_YOUR_TOKEN',
'Content-Type': 'application/json',
},
json={
'title': 'Как передать статью через API',
'markdown': '## Введение\n\nТекст статьи.\n\n\n',
'subtitle': 'Рабочий пример',
'tags': ['api', 'automation'],
'slug': 'kak-peredat-statyu',
},
)
response.raise_for_status()
print(response.json()['id'])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
}'const postId = '2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f';
const response = await fetch(
`https://designpub.ru/api/v1/author/posts/${postId}`,
{
method: 'PATCH',
headers: {
Authorization: 'Bearer dp_live_YOUR_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Обновлённый заголовок',
markdown: 'Новый текст.\n',
subtitle: null,
tags: ['api', 'markdown'],
cover_asset_id: null,
}),
},
);
const body = await response.json();
if (!response.ok) throw new Error(`${response.status}: ${JSON.stringify(body)}`);
console.log(body);import requests
post_id = '2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f'
response = requests.patch(
f'https://designpub.ru/api/v1/author/posts/{post_id}',
headers={
'Authorization': 'Bearer dp_live_YOUR_TOKEN',
'Content-Type': 'application/json',
},
json={
'title': 'Обновлённый заголовок',
'markdown': 'Новый текст.\n',
'subtitle': None,
'tags': ['api', 'markdown'],
'cover_asset_id': None,
},
)
response.raise_for_status()
print(response.json())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"const postId = '2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f';
const response = await fetch(
`https://designpub.ru/api/v1/author/posts/${postId}/publish`,
{
method: 'POST',
headers: { Authorization: 'Bearer dp_live_YOUR_TOKEN' },
},
);
const body = await response.json();
if (!response.ok) throw new Error(`${response.status}: ${JSON.stringify(body)}`);
console.log(body.status);import requests
post_id = '2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f'
response = requests.post(
f'https://designpub.ru/api/v1/author/posts/{post_id}/publish',
headers={'Authorization': 'Bearer dp_live_YOUR_TOKEN'},
)
response.raise_for_status()
print(response.json()['status'])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"const postId = '2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f';
const response = await fetch(
`https://designpub.ru/api/v1/author/posts/${postId}/unpublish`,
{
method: 'POST',
headers: { Authorization: 'Bearer dp_live_YOUR_TOKEN' },
},
);
if (!response.ok) {
const body = await response.json();
throw new Error(`${response.status}: ${JSON.stringify(body)}`);
}
console.log(response.status);import requests
post_id = '2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f'
response = requests.post(
f'https://designpub.ru/api/v1/author/posts/{post_id}/unpublish',
headers={'Authorization': 'Bearer dp_live_YOUR_TOKEN'},
)
response.raise_for_status()
print(response.status_code)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"const postId = '2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f';
const response = await fetch(
`https://designpub.ru/api/v1/author/posts/${postId}`,
{
method: 'DELETE',
headers: { Authorization: 'Bearer dp_live_YOUR_TOKEN' },
},
);
if (!response.ok) {
const body = await response.json();
throw new Error(`${response.status}: ${JSON.stringify(body)}`);
}
console.log(response.status);import requests
post_id = '2f1c9a0e-4b3d-4e8a-9c11-0a7b6d5e4c3f'
response = requests.delete(
f'https://designpub.ru/api/v1/author/posts/{post_id}',
headers={'Authorization': 'Bearer dp_live_YOUR_TOKEN'},
)
response.raise_for_status()
print(response.status_code)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, status — ready, 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"import { readFile } from 'node:fs/promises';
const bytes = await readFile('./cover.jpg');
const form = new FormData();
form.append('file', new Blob([bytes], { type: 'image/jpeg' }), 'cover.jpg');
const response = await fetch('https://designpub.ru/api/v1/author/media', {
method: 'POST',
headers: { Authorization: 'Bearer dp_live_YOUR_TOKEN' },
body: form,
});
const body = await response.json();
if (!response.ok) throw new Error(`${response.status}: ${JSON.stringify(body)}`);
console.log(body);import requests
with open('./cover.jpg', 'rb') as image_file:
response = requests.post(
'https://designpub.ru/api/v1/author/media',
headers={'Authorization': 'Bearer dp_live_YOUR_TOKEN'},
files={'file': ('cover.jpg', image_file, 'image/jpeg')},
)
response.raise_for_status()
print(response.json())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безcode—urlотсутствует или пуст, схема не 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"}'const response = await fetch(
'https://designpub.ru/api/v1/author/media/from-url',
{
method: 'POST',
headers: {
Authorization: 'Bearer dp_live_YOUR_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({ url: 'https://cdn.example.com/diagram.png' }),
},
);
const body = await response.json();
if (!response.ok) throw new Error(`${response.status}: ${JSON.stringify(body)}`);
console.log(body.url);import requests
response = requests.post(
'https://designpub.ru/api/v1/author/media/from-url',
headers={
'Authorization': 'Bearer dp_live_YOUR_TOKEN',
'Content-Type': 'application/json',
},
json={'url': 'https://cdn.example.com/diagram.png'},
)
response.raise_for_status()
print(response.json()['url'])Полный сценарий: черновик с изображением и публикация
Надёжная последовательность из четырёх шагов:
- Загрузите локальное изображение через
POST /mediaили сохраните внешнее черезPOST /media/from-url. - Возьмите из ответа
url, например/api/media/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa. - Вставьте путь в
markdownи создайте статью черезPOST /posts. - Проверьте
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\\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"import { readFile } from 'node:fs/promises';
const baseUrl = 'https://designpub.ru/api/v1/author';
const token = 'dp_live_YOUR_TOKEN';
const auth = { Authorization: `Bearer ${token}` };
const bytes = await readFile('./cover.jpg');
const form = new FormData();
form.append('file', new Blob([bytes], { type: 'image/jpeg' }), 'cover.jpg');
const mediaResponse = await fetch(`${baseUrl}/media`, {
method: 'POST',
headers: auth,
body: form,
});
const media = await mediaResponse.json();
if (!mediaResponse.ok) {
throw new Error(`${mediaResponse.status}: ${JSON.stringify(media)}`);
}
const postResponse = await fetch(`${baseUrl}/posts`, {
method: 'POST',
headers: { ...auth, 'Content-Type': 'application/json' },
body: JSON.stringify({
title: 'Статья с локальным изображением',
markdown: `## Изображение\n\n\n\nТекст статьи.\n`,
cover_asset_id: media.id,
tags: ['api'],
}),
});
const draft = await postResponse.json();
if (!postResponse.ok) {
throw new Error(`${postResponse.status}: ${JSON.stringify(draft)}`);
}
const detailResponse = await fetch(`${baseUrl}/posts/${draft.id}`, {
headers: auth,
});
const detail = await detailResponse.json();
if (!detailResponse.ok) {
throw new Error(`${detailResponse.status}: ${JSON.stringify(detail)}`);
}
console.log(detail.html);
const publishResponse = await fetch(`${baseUrl}/posts/${draft.id}/publish`, {
method: 'POST',
headers: auth,
});
const published = await publishResponse.json();
if (!publishResponse.ok) {
throw new Error(`${publishResponse.status}: ${JSON.stringify(published)}`);
}
console.log(published.status);import requests
BASE_URL = 'https://designpub.ru/api/v1/author'
TOKEN = 'dp_live_YOUR_TOKEN'
auth = {'Authorization': f'Bearer {TOKEN}'}
with open('./cover.jpg', 'rb') as image_file:
media_response = requests.post(
f'{BASE_URL}/media',
headers=auth,
files={'file': ('cover.jpg', image_file, 'image/jpeg')},
)
media_response.raise_for_status()
media = media_response.json()
post_response = requests.post(
f'{BASE_URL}/posts',
headers={**auth, 'Content-Type': 'application/json'},
json={
'title': 'Статья с локальным изображением',
'markdown': f'## Изображение\n\n\n\nТекст статьи.\n',
'cover_asset_id': media['id'],
'tags': ['api'],
},
)
post_response.raise_for_status()
draft = post_response.json()
detail_response = requests.get(
f'{BASE_URL}/posts/{draft["id"]}',
headers=auth,
)
detail_response.raise_for_status()
print(detail_response.json()['html'])
publish_response = requests.post(
f'{BASE_URL}/posts/{draft["id"]}/publish',
headers=auth,
)
publish_response.raise_for_status()
print(publish_response.json()['status'])Если последний ответ вернул 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}.