{"openapi":"3.1.0","info":{"title":"SMM ERRA.CLUB — публичный API","version":"1.0.0","description":"API работает через **ваш собственный браузер**: запросы ставят задание, а\nвыполняет его расширение Chrome на вашем компьютере — в вашем уже\nзалогиненном Claude, Gemini, VC.ru и остальных площадках. Поэтому ответы\nприходят не мгновенно: `POST` возвращается сразу, а результат забирают\nполлингом или вебхуком.\n\n## Авторизация\n\nКлюча в заголовке нет — каждый запрос подписывается **HMAC-SHA256**.\nПубличный ключ живёт в самом адресе (`/ai/{apiKey}`), секрет — только в\nподписи и никогда не передаётся.\n\nПодписывается склейка `METHOD + PATH + TIMESTAMP + RAW_BODY`, где `PATH` —\nчасть адреса **после** `/ai/{apiKey}` (вместе со строкой запроса,\nнапример `/api/dialogs?page=1`), `TIMESTAMP` — Unix-время в секундах,\n`RAW_BODY` — тело запроса ровно тем текстом, что уходит в сеть (для GET —\nпустая строка).\n\nЗаголовки: `X-Signature` (hex) и `X-Timestamp`. Расхождение времени больше\n**5 минут** — запрос отклоняется с 401, так что сверьте часы на сервере.\n\n```php\n$timestamp = (string) time();\n$body      = json_encode(['text' => 'Привет!'], JSON_UNESCAPED_UNICODE);\n$signature = hash_hmac('sha256', 'POST'.'/api/dialogs'.$timestamp.$body, $secret);\n```\n\n```js\nconst enc = new TextEncoder();\nconst key = await crypto.subtle.importKey('raw', enc.encode(secret),\n  { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);\nconst raw = await crypto.subtle.sign('HMAC', key, enc.encode(method + path + timestamp + body));\nconst signature = [...new Uint8Array(raw)].map(b => b.toString(16).padStart(2, '0')).join('');\n```\n\n## Ошибки\n\n`401` — подпись неверна или просрочена, `404` — ключ не найден либо объект\nпринадлежит другому аккаунту, `409` — с объектом уже нельзя работать\n(например, чат удалён по истечении срока жизни), `422` — не прошла\nвалидация, `503` — ни одного устройства аккаунта нет в сети.","contact":{"email":"info@it-healer.com"}},"servers":[{"url":"https://smm.erra.club/ai/{apiKey}","description":"Ваш личный base URL. {apiKey} — публичный ключ аккаунта со страницы «Панель управления».","variables":{"apiKey":{"default":"YOUR_API_KEY","description":"Публичный ключ аккаунта (UUID)."}}}],"tags":[{"name":"AI-диалоги","description":"Диалоги с Claude и Gemini через ваш собственный браузер."},{"name":"Публикации","description":"Публикация одного поста сразу на несколько площадок."},{"name":"Служебное","description":"Проверка доступности и подписи."}],"components":{"securitySchemes":{"hmacSignature":{"type":"apiKey","in":"header","name":"X-Signature","description":"HMAC-SHA256 от METHOD + PATH + TIMESTAMP + RAW_BODY, hex. Рядом обязателен заголовок X-Timestamp."}},"schemas":{"Dialog":{"type":"object","properties":{"id":{"type":"integer","examples":[42]},"initial_text":{"type":["string","null"],"description":"Первое отправленное сообщение."},"claude_url":{"type":["string","null"],"description":"Адрес реального чата у ИИ. Появляется, когда расширение его откроет; обнуляется, когда чат удалён по истечении срока жизни."},"webhook_url":{"type":["string","null"]},"ttl_minutes":{"type":["integer","null"],"description":"Срок жизни чата у ИИ: 0 — удалить сразу после первого ответа, N — через N минут, null — не удалять."},"expires_at":{"type":["string","null"],"format":"date-time"},"chat_deleted_at":{"type":["string","null"],"format":"date-time"},"ai":{"type":"string","enum":["claude","gemini"]},"model":{"type":["string","null"],"enum":["opus","sonnet","haiku",null]},"status":{"type":"string","enum":["pending","processing","completed","error","deleted"],"description":"pending → processing → completed | error; deleted — чат стёрт у ИИ по истечении срока жизни."},"error_message":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"DialogListItem":{"type":"object","description":"Сокращённая форма для списка — не путать с Dialog, который отдают POST/GET одного диалога.","properties":{"id":{"type":"integer"},"ai":{"type":"string","enum":["claude","gemini"]},"model":{"type":["string","null"]},"claude_url":{"type":["string","null"]},"status":{"type":"string","enum":["pending","processing","completed","error","deleted"]},"ttl_minutes":{"type":["integer","null"]},"expires_at":{"type":["string","null"],"format":"date-time"},"chat_deleted_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"outgoing_count":{"type":"integer"},"incoming_count":{"type":"integer"}}},"Message":{"type":"object","properties":{"id":{"type":"integer"},"dialog_id":{"type":"integer"},"parent_id":{"type":["integer","null"],"description":"Для ответа ИИ — id сообщения, на которое он отвечает."},"direction":{"type":"string","enum":["outgoing","incoming"]},"text":{"type":"string"},"html":{"type":["string","null"],"description":"Заполняется только у ответов Gemini."},"webhook_url":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"files":{"type":"array","items":{"$ref":"#/components/schemas/File"}}}},"File":{"type":"object","properties":{"id":{"type":"integer"},"message_id":{"type":"integer"},"file_path":{"type":"string","description":"Готовый публичный адрес для скачивания."},"original_name":{"type":"string"}}},"FileInput":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"Адрес файла, который нужно приложить к сообщению. Локальные пути не принимаются."}}},"Publication":{"type":"object","properties":{"id":{"type":"integer"},"title":{"type":"string"},"body":{"type":"string"},"status":{"type":"string","enum":["pending","publishing","completed","partial","error"],"description":"Сводный статус по всем целям: pending — ещё ни одна не начата, publishing — хотя бы одна в работе, completed — все опубликованы, partial — часть опубликована, часть упала, error — упали все."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"images":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string"},"original_name":{"type":"string"}}}},"targets":{"type":"array","items":{"$ref":"#/components/schemas/PublicationTarget"}}}},"PublicationTarget":{"type":"object","description":"Одна пара «площадка × устройство»: у поста их столько, сколько площадок выбрано.","properties":{"id":{"type":"integer"},"platform":{"type":"string","enum":["threads","vc","livejournal","medium","reddit","linkedin","facebook","dzen","teletype","tumblr","vk","telegraph","woman"]},"community_url":{"type":["string","null"],"description":"Сообщество, куда публикуется эта цель (только у площадок, публикующих в сообщество)."},"instance_id":{"type":["string","null"],"format":"uuid","description":"Устройство, которое выполняет эту цель."},"status":{"type":"string","enum":["pending","processing","published","error"],"description":"Статус ОДНОЙ цели — набор значений отличается от сводного статуса публикации."},"post_url":{"type":["string","null"]},"error_message":{"type":["string","null"]},"published_at":{"type":["string","null"],"format":"date-time"}}},"Error":{"type":"object","properties":{"error":{"type":"string"}}}}},"security":[{"hmacSignature":[]}],"paths":{"/api/health":{"get":{"tags":["Служебное"],"summary":"Проверка доступности и подписи","description":"Самый дешёвый способ убедиться, что ключ и подпись собраны верно.","responses":{"200":{"description":"Готово","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","examples":["ok"]}}}}}}}}},"/api/dialogs":{"get":{"tags":["AI-диалоги"],"summary":"Список диалогов","parameters":[{"name":"page","in":"query","required":false,"description":"Страница, с 1.","schema":{"type":"integer"}},{"name":"per_page","in":"query","required":false,"description":"Размер страницы, максимум 100.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Страница списка","content":{"application/json":{"schema":{"type":"object","properties":{"dialogs":{"type":"array","items":{"$ref":"#/components/schemas/DialogListItem"}},"total":{"type":"integer"},"page":{"type":"integer"},"per_page":{"type":"integer"},"pages":{"type":"integer"}}}}}}}},"post":{"tags":["AI-диалоги"],"summary":"Создать диалог","description":"Возвращается сразу: открытие вкладки, отправка сообщения и ожидание ответа идут в фоне.\n\nОтвет ИИ забирайте через `GET /api/dialogs/{id}/messages` или вебхуком.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","description":"Сообщение для ИИ. Вместо него можно прислать text_base64.","examples":["Привет! Расскажи о себе в двух предложениях."]},"text_base64":{"type":"string","description":"То же сообщение в base64 — на случай, если текст плохо переживает ваш транспорт."},"ai":{"type":"string","enum":["claude","gemini"],"default":"claude"},"model":{"type":["string","null"],"enum":["opus","sonnet","haiku",null],"description":"Только для claude — у Gemini выбора модели нет. Не передан — используется sonnet."},"ttl_minutes":{"type":["integer","null"],"default":0,"minimum":0,"maximum":43200,"description":"Срок жизни чата у ИИ. 0 (по умолчанию) — расширение удалит чат сразу после первого ответа; N — через N минут после создания; null — не удалять. Переписка у нас сохраняется в любом случае."},"files":{"type":"array","items":{"$ref":"#/components/schemas/FileInput"}},"webhook_url":{"type":["string","null"],"format":"uri","description":"Вызывается на КАЖДЫЙ ответ ИИ в этом диалоге."},"instance_id":{"type":["string","null"],"format":"uuid","description":"Конкретное устройство аккаунта. Не передано — сервер возьмёт наименее загруженное из тех, что в сети."}}}}}},"responses":{"201":{"description":"Диалог создан","content":{"application/json":{"schema":{"type":"object","properties":{"dialog":{"$ref":"#/components/schemas/Dialog"},"outgoing_message":{"$ref":"#/components/schemas/Message"}}}}}},"422":{"description":"Не прошла валидация","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Ни одного устройства нет в сети","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/dialogs/{id}":{"get":{"tags":["AI-диалоги"],"summary":"Один диалог","parameters":[{"name":"id","in":"path","required":true,"description":"Идентификатор диалога.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Диалог","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Dialog"}}}},"404":{"description":"Не найден","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["AI-диалоги"],"summary":"Удалить диалог","description":"Удаляет и переписку у нас, и чат у ИИ. Если нужно стереть только чат у ИИ, сохранив переписку, — задайте при создании `ttl_minutes`.","parameters":[{"name":"id","in":"path","required":true,"description":"Идентификатор диалога.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Удалён","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}}}}},"/api/dialogs/{id}/messages":{"get":{"tags":["AI-диалоги"],"summary":"Сообщения диалога","description":"Обычный способ дождаться ответа без вебхука: опрашивайте с `after_id` последнего известного сообщения.","parameters":[{"name":"id","in":"path","required":true,"description":"Идентификатор диалога.","schema":{"type":"integer"}},{"name":"after_id","in":"query","required":false,"description":"Только сообщения новее указанного id.","schema":{"type":"integer"}},{"name":"parent_id","in":"query","required":false,"description":"Только ответы на конкретное сообщение.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Сообщения, по возрастанию id. У каждого дополнительно раскрыт parent — сообщение, на которое отвечали (или null).","content":{"application/json":{"schema":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Message"},{"type":"object","properties":{"parent":{"anyOf":[{"$ref":"#/components/schemas/Message"},{"type":"null"}]}}}]}}}}}}},"post":{"tags":["AI-диалоги"],"summary":"Дописать в диалог","description":"Продолжает уже существующий чат. `ai` и `model` менять нельзя — они зафиксированы при создании.","parameters":[{"name":"id","in":"path","required":true,"description":"Идентификатор диалога.","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","examples":["А теперь короче, одним предложением."]},"files":{"type":"array","items":{"$ref":"#/components/schemas/FileInput"}},"webhook_url":{"type":["string","null"],"format":"uri","description":"В отличие от диалогового, сработает только на ответ именно на это сообщение."}}}}}},"responses":{"201":{"description":"Отправлено","content":{"application/json":{"schema":{"type":"object","properties":{"outgoing_message":{"$ref":"#/components/schemas/Message"}}}}}},"409":{"description":"Чат удалён у ИИ по истечении срока жизни — продолжать его негде","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Ни одного устройства нет в сети","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/publications":{"get":{"tags":["Публикации"],"summary":"Список публикаций","parameters":[{"name":"page","in":"query","required":false,"description":"Страница, с 1.","schema":{"type":"integer"}},{"name":"per_page","in":"query","required":false,"description":"Размер страницы, максимум 100.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Страница списка","content":{"application/json":{"schema":{"type":"object","properties":{"publications":{"type":"array","items":{"$ref":"#/components/schemas/Publication"}},"total":{"type":"integer"},"page":{"type":"integer"},"per_page":{"type":"integer"},"pages":{"type":"integer"}}}}}}}},"post":{"tags":["Публикации"],"summary":"Опубликовать пост","description":"Один текст уходит на все перечисленные площадки. Публикация идёт в фоне — следите за `targets[].status` через `GET /api/publications/{id}` или вебхуком.\n\nДля площадок reddit, vk обязателен адрес сообщества: `community_urls` (свой на площадку) либо `community_url` (один на все).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","body","platforms"],"properties":{"title":{"type":"string","maxLength":255,"examples":["Как мы автоматизировали SMM"]},"body":{"type":"string","examples":["Полный текст поста…"]},"platforms":{"type":"array","minItems":1,"items":{"type":"string","enum":["threads","vc","livejournal","medium","reddit","linkedin","facebook","dzen","teletype","tumblr","vk","telegraph","woman"]},"examples":[["vc","livejournal"]]},"community_urls":{"type":"object","description":"Адрес сообщества на площадку: {\"reddit\": \"https://www.reddit.com/r/…\"}. Обязателен для reddit, vk.","additionalProperties":{"type":"string","format":"uri"}},"community_url":{"type":"string","format":"uri","description":"Один адрес сразу на все площадки, которым он нужен. Оставлено для совместимости со старыми клиентами."},"instance_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Устройства аккаунта. Не переданы — сервер подберёт доступное сам."},"images":{"type":"array","items":{"$ref":"#/components/schemas/FileInput"}},"webhook_url":{"type":["string","null"],"format":"uri","description":"Вызывается на каждое изменение статуса цели."}}}}}},"responses":{"201":{"description":"Публикация создана","content":{"application/json":{"schema":{"type":"object","properties":{"publication":{"$ref":"#/components/schemas/Publication"}}}}}},"422":{"description":"Не прошла валидация","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Ни одного устройства нет в сети","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/publications/{id}":{"get":{"tags":["Публикации"],"summary":"Одна публикация","description":"Здесь и смотрят результат: у каждой цели свой статус, адрес опубликованного поста и текст ошибки.","parameters":[{"name":"id","in":"path","required":true,"description":"Идентификатор публикации.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Публикация","content":{"application/json":{"schema":{"type":"object","properties":{"publication":{"$ref":"#/components/schemas/Publication"}}}}}},"404":{"description":"Не найдена","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Публикации"],"summary":"Удалить публикацию","description":"Убирает запись у нас. Уже опубликованные посты на самих площадках остаются.","parameters":[{"name":"id","in":"path","required":true,"description":"Идентификатор публикации.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Удалена","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}}}}},"/api/publications/{id}/targets":{"post":{"tags":["Публикации"],"summary":"Дослать на новые площадки","description":"Тот же пост уходит ещё куда-то, без создания копии записи.","parameters":[{"name":"id","in":"path","required":true,"description":"Идентификатор публикации.","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["platforms"],"properties":{"platforms":{"type":"array","minItems":1,"items":{"type":"string","enum":["threads","vc","livejournal","medium","reddit","linkedin","facebook","dzen","teletype","tumblr","vk","telegraph","woman"]}},"instance_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"force":{"type":"boolean","description":"Опубликовать заново даже туда, где пост уже есть — нужно, если запись на площадке удалили вручную."},"community_urls":{"type":"object","additionalProperties":{"type":"string","format":"uri"}}}}}}},"responses":{"200":{"description":"Цели добавлены","content":{"application/json":{"schema":{"type":"object","properties":{"publication":{"$ref":"#/components/schemas/Publication"},"added":{"type":"integer","description":"Сколько целей создано впервые."},"retried":{"type":"integer","description":"Сколько уже существовавших целей отправлено заново."},"skipped":{"type":"integer","description":"Сколько пропущено — пост там уже опубликован, а force не передан."}}}}}},"503":{"description":"Ни одного устройства нет в сети","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/publications/{id}/retry":{"post":{"tags":["Публикации"],"summary":"Повторить неудавшиеся","description":"Ставит задание заново по целям со статусом error. Цели, чьё устройство сейчас не в сети, пропускаются молча — повторите запрос, когда оно вернётся.","parameters":[{"name":"id","in":"path","required":true,"description":"Идентификатор публикации.","schema":{"type":"integer"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"all":{"type":"boolean","description":"true — перепубликовать вообще все цели, а не только упавшие с ошибкой."}}}}}},"responses":{"200":{"description":"Задания поставлены заново","content":{"application/json":{"schema":{"type":"object","properties":{"publication":{"$ref":"#/components/schemas/Publication"}}}}}}}}}},"webhooks":{"aiDialogReply":{"post":{"tags":["AI-диалоги"],"summary":"Ответ ИИ получен","description":"Приходит на каждый ответ ИИ: на `webhook_url` диалога — всегда, на `webhook_url` конкретного сообщения — только когда отвечали именно на него.\n\nМетод всегда `POST`, заголовки — те же `X-Signature` и `X-Timestamp`.\nПроверяйте подпись своим секретом: подписывается\n`'POST' + PATH + TIMESTAMP + RAW_BODY`, где `PATH` — путь и строка запроса\nвашего собственного обработчика.\n\nОтправка — одна попытка, без повторов: доставка не часть контракта. Если\nвебхук не дошёл, состояние всегда можно забрать обычным `GET`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"dialog":{"$ref":"#/components/schemas/Dialog"},"outgoing_message":{"anyOf":[{"$ref":"#/components/schemas/Message"},{"type":"null"}],"description":"Сообщение, на которое ответили."},"incoming_message":{"$ref":"#/components/schemas/Message"}}}}}},"responses":{"200":{"description":"Ответьте любым 2xx."}}}},"publicationTargetFinished":{"post":{"tags":["Публикации"],"summary":"Площадка отработала","description":"Приходит только на терминальных статусах цели (`published` или `error`) — промежуточный `processing` вебхуком не шумит.\n\nМетод всегда `POST`, заголовки — те же `X-Signature` и `X-Timestamp`.\nПроверяйте подпись своим секретом: подписывается\n`'POST' + PATH + TIMESTAMP + RAW_BODY`, где `PATH` — путь и строка запроса\nвашего собственного обработчика.\n\nОтправка — одна попытка, без повторов: доставка не часть контракта. Если\nвебхук не дошёл, состояние всегда можно забрать обычным `GET`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"publication":{"$ref":"#/components/schemas/Publication"},"target":{"$ref":"#/components/schemas/PublicationTarget"}}}}}},"responses":{"200":{"description":"Ответьте любым 2xx."}}}}}}