Vortholm — документация
Личный ключ

Готовые сценарии

Три готовых сценария с личным ключом: задача с файлом, чат с уточнением, обработка ошибок — и чек-лист вызовов для ИИ-агента.

Страница продолжает «Личный ключ». Здесь три готовых сценария, каждый с объяснением по шагам и двумя скриптами: на bash с curl и jq и на Python 3 с библиотекой requests. Для ИИ-агента — чек-лист тех же вызовов. Справочник запросов — в разделах «Задачи» и «Чаты».

Переменные скриптов

В скриптах две переменные окружения, общие для всех сценариев. Это переменные вашего скрипта, не настройки портала:

  • FORGE_URL — адрес портала, по которому вы открываете его в браузере. В примерах это https://portal.example.com.
  • FORGE_API_KEY — ваш личный ключ.

Скрипт сценария 2 дополнительно ждёт TASK_TYPE_ID — идентификатор типа задачи.

Ключ передаётся в заголовке Authorization: Bearer … каждого запроса.

Типичный сценарий

Порядок действий, который повторяют все сценарии:

  1. Создайте ключ в разделе «Настройки» → «API-ключи» портала — как это сделать.

    Кнопка «Создать ключ» в форме создания ключа
    Кнопка «Создать ключ» в форме создания ключа
  2. POST /api/tasks — запустите задачу с файлами. Ответ 201 содержит id задачи.

  3. Опрашивайте GET /api/tasks/:id раз в 5 секунд, пока status не станет done, partial или failed.

  4. GET /api/tasks/:id/files — список файлов, GET /api/tasks/:id/files/:fileId/download — скачивание каждого.

  5. Для чата: POST /api/chat-sessions создаёт сессию, POST /api/chat-sessions/:id/messages отправляет вопрос, GET /api/chat-sessions/:id — опрос до готовности ответа.

Сценарий 1. Задача с файлом на входе

Скрипт запускает задачу типа «Сводка по продажам» с файлом на входе, дожидается результата и скачивает файлы.

  1. Запрос POST /api/tasks отправляет slug типа, параметры в JSON-строке params и файл в поле files; полный список полей — в разделе «Задачи». Портал отвечает 201 и id задачи.
  2. Затем скрипт опрашивает GET /api/tasks/:id раз в 5 секунд. Ожидание останавливается на статусах done (выполнена), partial (часть шагов не дала результата) и failed (ошибка).
  3. GET /api/tasks/:id/files возвращает файлы задачи. Скрипт скачивает только файлы с direction: "output" — это результат, а не вход.
  4. Каждый файл скачивается запросом GET /api/tasks/:id/files/:fileId/download. Имя файла приходит в заголовке Content-Disposition, и curl -OJ сохраняет файл под этим именем.

Два ограничения на запуск. Повторный запуск того же типа раньше, чем через 60 секунд после предыдущего, портал отклоняет с кодом 429 и сообщением «Подождите N сек. перед повторным запуском этой задачи.». У учётной записи может быть лимит задач в час — он тоже отвечает 429, но с числом минут в сообщении. Оба случая разбирает сценарий 3.

bash

#!/usr/bin/env bash
set -euo pipefail

: "${FORGE_URL:?укажите адрес портала}"
: "${FORGE_API_KEY:?укажите личный ключ}"

AUTH="Authorization: Bearer ${FORGE_API_KEY}"

# 1. Запуск задачи с файлом на входе
TASK_ID=$(curl -sS -X POST "${FORGE_URL}/api/tasks" \
  -H "$AUTH" \
  -F "taskTypeSlug=sales-summary" \
  -F "params={\"period\":\"2026-33\"}" \
  -F "visibility=private" \
  -F "files=@продажи-33-неделя.xlsx" \
  | jq -r '.data.id')
echo "задача: ${TASK_ID}"

# 2. Ожидание завершения: опрос раз в 5 секунд
while :; do
  STATUS=$(curl -sS "${FORGE_URL}/api/tasks/${TASK_ID}" -H "$AUTH" | jq -r '.data.status')
  case "${STATUS}" in
    done|partial|failed) break ;;
  esac
  sleep 5
done
echo "статус: ${STATUS}"

# 3. Список файлов задачи
curl -sS "${FORGE_URL}/api/tasks/${TASK_ID}/files" -H "$AUTH" \
  | jq -r '.data[] | [.filename, .direction, (.isFinalResult|tostring)] | @tsv'

# 4. Скачивание выходных файлов
for FILE_ID in $(curl -sS "${FORGE_URL}/api/tasks/${TASK_ID}/files" -H "$AUTH" \
  | jq -r '.data[] | select(.direction == "output") | .id'); do
  curl -sS -OJ "${FORGE_URL}/api/tasks/${TASK_ID}/files/${FILE_ID}/download" -H "$AUTH"
done

Python

import json
import os
import time
from pathlib import Path

import requests

FORGE_URL = os.environ["FORGE_URL"].rstrip("/")
HEADERS = {"Authorization": f"Bearer {os.environ['FORGE_API_KEY']}"}
POLL_SECONDS = 5


def main() -> None:
    # 1. Запуск задачи с файлом на входе
    with open("продажи-33-неделя.xlsx", "rb") as fh:
        response = requests.post(
            f"{FORGE_URL}/api/tasks",
            headers=HEADERS,
            data={
                "taskTypeSlug": "sales-summary",
                "params": json.dumps({"period": "2026-33"}),
                "visibility": "private",
            },
            files={"files": ("продажи-33-неделя.xlsx", fh)},
            timeout=120,
        )
    response.raise_for_status()
    task_id = response.json()["data"]["id"]
    print("задача:", task_id)

    # 2. Ожидание завершения: опрос раз в 5 секунд
    while True:
        task = requests.get(
            f"{FORGE_URL}/api/tasks/{task_id}", headers=HEADERS, timeout=30
        ).json()["data"]
        if task["status"] in ("done", "partial", "failed"):
            break
        time.sleep(POLL_SECONDS)
    print("статус:", task["status"])

    # 3. Файлы задачи
    files = requests.get(
        f"{FORGE_URL}/api/tasks/{task_id}/files", headers=HEADERS, timeout=30
    ).json()["data"]

    # 4. Скачивание выходных файлов
    for item in files:
        if item["direction"] != "output":
            continue
        stream = requests.get(
            f"{FORGE_URL}/api/tasks/{task_id}/files/{item['id']}/download",
            headers=HEADERS,
            timeout=300,
            stream=True,
        )
        stream.raise_for_status()
        Path(item["filename"]).write_bytes(stream.content)
        print("сохранено:", item["filename"])


if __name__ == "__main__":
    main()

Сценарий 2. Чат: вопрос, ответ, уточнение

Скрипт открывает чат по типу задачи, задаёт вопрос, дожидается ответа, печатает его и задаёт уточнение.

  1. POST /api/chat-sessions создаёт сессию; поля запроса — в разделе «Чаты». Обязательное поле — taskTypeId, идентификатор типа задачи. Возьмите его из ответа GET /api/tasks/:id любой вашей задачи этого типа — это поле taskTypeId.
  2. POST /api/chat-sessions/:id/messages отправляет вопрос. Ответ на этот запрос — поток событий; он закрывается, когда ход завершён (событие message_complete), или когда чат был занят (событие queued). Ход выполняется, пока соединение открыто; если закрыть его раньше, ход остановится. Скрипт держит соединение, пока портал не завершит ход.
  3. Затем скрипт опрашивает GET /api/chat-sessions/:id раз в 3 секунды, пока последнее сообщение в списке не станет ответом ассистента с финальным статусом completed, stopped или failed. После message_complete первый же опрос увидит готовый ответ; опрос нужен, когда ход поставлен в очередь — поток закрылся событием queued, а ответ появится позже.
  4. Текст ответа — поле content последнего сообщения ассистента.
  5. Уточнение отправляется тем же запросом в ту же сессию. Контекст сохраняется, а clientMessageId генерируется заново для каждого сообщения.

bash

#!/usr/bin/env bash
set -euo pipefail

: "${FORGE_URL:?укажите адрес портала}"
: "${FORGE_API_KEY:?укажите личный ключ}"
: "${TASK_TYPE_ID:?укажите taskTypeId из ответа GET /api/tasks/:id}"

AUTH="Authorization: Bearer ${FORGE_API_KEY}"

new_uuid() { uuidgen | tr '[:upper:]' '[:lower:]'; }

# 1. Создание сессии
SESSION_ID=$(curl -sS -X POST "${FORGE_URL}/api/chat-sessions" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d "{\"taskTypeId\":\"${TASK_TYPE_ID}\"}" \
  | jq -r '.data.id')
echo "сессия: ${SESSION_ID}"

# 2. Отправка вопроса. Ответ — поток событий; соединение держится,
# пока портал не завершит ход.
send_message() {
  local text="$1"
  curl -sS -N --max-time 600 -X POST "${FORGE_URL}/api/chat-sessions/${SESSION_ID}/messages" \
    -H "$AUTH" -H "Content-Type: application/json" \
    -d "{\"text\":\"${text}\",\"attachedFileIds\":[],\"clientMessageId\":\"$(new_uuid)\"}" \
    > /dev/null
}

# 3. Ожидание ответа: опрос раз в 3 секунды, пока последнее сообщение
# не станет готовым ответом ассистента.
wait_for_answer() {
  while :; do
    RESULT=$(curl -sS "${FORGE_URL}/api/chat-sessions/${SESSION_ID}" -H "$AUTH" \
      | jq -r '.data.messages[-1] | select(. != null) | "\(.role) \(.status)"')
    case "${RESULT}" in
      "assistant completed"|"assistant stopped"|"assistant failed") break ;;
    esac
    sleep 3
  done
}

# 4. Печать последнего ответа ассистента
print_answer() {
  curl -sS "${FORGE_URL}/api/chat-sessions/${SESSION_ID}" -H "$AUTH" \
    | jq -r '[.data.messages[] | select(.role == "assistant")] | last | .content'
}

send_message "Сколько продаж закрыто на этой неделе?"
wait_for_answer
print_answer

# 5. Уточнение в ту же сессию
send_message "Из них сколько закрыто без замечаний?"
wait_for_answer
print_answer

Python

import os
import time
import uuid

import requests

FORGE_URL = os.environ["FORGE_URL"].rstrip("/")
HEADERS = {"Authorization": f"Bearer {os.environ['FORGE_API_KEY']}"}
TASK_TYPE_ID = os.environ["TASK_TYPE_ID"]  # поле taskTypeId из ответа GET /api/tasks/:id
POLL_SECONDS = 3
TERMINAL = ("completed", "stopped", "failed")


def create_session() -> str:
    response = requests.post(
        f"{FORGE_URL}/api/chat-sessions",
        headers=HEADERS,
        json={"taskTypeId": TASK_TYPE_ID},
        timeout=30,
    )
    response.raise_for_status()
    return response.json()["data"]["id"]


def send_message(session_id: str, text: str) -> None:
    """Отправляет сообщение и держит соединение, пока портал не завершит ход."""
    body = {
        "text": text,
        "attachedFileIds": [],
        "clientMessageId": str(uuid.uuid4()),
    }
    with requests.post(
        f"{FORGE_URL}/api/chat-sessions/{session_id}/messages",
        headers=HEADERS,
        json=body,
        stream=True,
        timeout=(10, 600),
    ) as stream:
        for _ in stream.iter_lines():
            pass


def wait_for_answer(session_id: str) -> str:
    """Опрашивает сессию, пока последнее сообщение не станет готовым ответом."""
    while True:
        data = requests.get(
            f"{FORGE_URL}/api/chat-sessions/{session_id}", headers=HEADERS, timeout=30
        ).json()["data"]
        messages = data["messages"]
        if messages and messages[-1]["role"] == "assistant" and messages[-1]["status"] in TERMINAL:
            return messages[-1]["content"]
        time.sleep(POLL_SECONDS)


def main() -> None:
    session_id = create_session()
    print("сессия:", session_id)

    send_message(session_id, "Сколько продаж закрыто на этой неделе?")
    print(wait_for_answer(session_id))

    # Уточнение в ту же сессию
    send_message(session_id, "Из них сколько закрыто без замечаний?")
    print(wait_for_answer(session_id))


if __name__ == "__main__":
    main()

Сценарий 3. Ошибки: 401, 403, 429, таймауты

Скрипт по личному ключу сталкивается с тремя кодами отказа. Разбирайте поле error.message — по нему видно причину.

401 — портал не узнал ключ:

error.messageПричина
Invalid API keyключ не передан, передан не в том виде или не найден
API key has been revokedключ отозван — выпустите новый
API key has expiredнаступил срок действия — выпустите новый
User is not activeучётная запись владельца ключа отключена

403 — ключ подлинный, но запрос отклонён:

error.messageПричина
This API key cannot act on behalf of a userэто интеграционный ключ, а не личный
API keys are not available for admin accountsключ выпущен администраторской учётной записью
API access is disabled for this accountадминистратор выключил «Доступ к API»

При 401 и 403 повтор того же запроса без смены ключа не поможет.

429 — превышен лимит. Заголовка Retry-After портал не шлёт, пауза стоит в тексте сообщения: «Слишком много попыток. Повторите через N минут.», «Подождите N сек. перед повторным запуском этой задачи.» или «Превышен лимит задач (N в час). Попробуйте через M мин.». Скрипт читает число из сообщения и ждёт; если число не распознано, ждёт 60 секунд.

Таймауты. Ставьте таймаут на каждый запрос. Если портал не ответил за отведённое время, запрос не завершён. Повторять безопасно запросы GET; повтор POST может создать задачу или сообщение дважды. Исключение — повтор вопроса в чат с тем же clientMessageId: портал распознаёт повтор и не создаёт дубля.

bash

#!/usr/bin/env bash
set -euo pipefail

: "${FORGE_URL:?укажите адрес портала}"
: "${FORGE_API_KEY:?укажите личный ключ}"

AUTH="Authorization: Bearer ${FORGE_API_KEY}"

# Пауза перед повтором: число читается из текста сообщения об ошибке.
wait_seconds() {
  local msg="$1" n
  if n=$(printf '%s' "$msg" | grep -oE 'через [0-9]+ минут' | grep -oE '[0-9]+' | head -1); then
    echo $((n * 60))
  elif n=$(printf '%s' "$msg" | grep -oE 'через [0-9]+ мин' | grep -oE '[0-9]+' | head -1); then
    echo $((n * 60))
  elif n=$(printf '%s' "$msg" | grep -oE '[0-9]+ сек' | grep -oE '[0-9]+' | head -1); then
    echo "$n"
  else
    echo 60
  fi
}

# Запрос с повтором при 429 и таймаутом 60 секунд.
request() {
  local method="$1" url="$2" body="${3:-}"
  local attempt=0 response code body_text msg pause
  while :; do
    attempt=$((attempt + 1))
    if [ -n "$body" ]; then
      response=$(curl -sS --max-time 60 -w $'\n%{http_code}' -X "$method" \
        "$url" -H "$AUTH" -H "Content-Type: application/json" -d "$body")
    else
      response=$(curl -sS --max-time 60 -w $'\n%{http_code}' -X "$method" \
        "$url" -H "$AUTH")
    fi
    code="${response##*$'\n'}"
    body_text="${response%$'\n'*}"
    case "$code" in
      2*) printf '%s\n' "$body_text"; return 0 ;;
      429)
        msg=$(printf '%s' "$body_text" | jq -r '.error.message // empty')
        pause=$(wait_seconds "$msg")
        echo "429, повтор через ${pause} секунд (попытка ${attempt})" >&2
        sleep "$pause"
        ;;
      401|403)
        echo "код ${code}: $(printf '%s' "$body_text" | jq -r '.error.message')" >&2
        return 1
        ;;
      *)
        echo "код ${code}: ${body_text}" >&2
        return 1
        ;;
    esac
  done
}

# Пример: статус задачи с повтором при 429
request GET "${FORGE_URL}/api/tasks/7d4f2a91-3c58-4e6b-9a02-1f8e6c4d2b37" \
  | jq -r '.data.status'

Python

import os
import re
import sys
import time

import requests

FORGE_URL = os.environ["FORGE_URL"].rstrip("/")
HEADERS = {"Authorization": f"Bearer {os.environ['FORGE_API_KEY']}"}

# Портал не шлёт заголовок Retry-After: пауза стоит в тексте сообщения.
MINUTES_RE = re.compile(r"через (\d+) мин(ут)?")
SECONDS_RE = re.compile(r"(\d+) сек")


def wait_seconds(message: str) -> int:
    match = MINUTES_RE.search(message)
    if match:
        return int(match.group(1)) * 60
    match = SECONDS_RE.search(message)
    if match:
        return int(match.group(1))
    return 60


def request(method: str, url: str, **kwargs) -> requests.Response:
    """Выполняет запрос с таймаутом и повторяет его при 429."""
    while True:
        response = requests.request(method, url, headers=HEADERS, timeout=60, **kwargs)
        if response.status_code != 429:
            return response
        message = ""
        try:
            message = response.json()["error"]["message"]
        except (ValueError, KeyError):
            pass
        pause = wait_seconds(message)
        print(f"429, повтор через {pause} секунд", file=sys.stderr)
        time.sleep(pause)


# Пример: статус задачи с повтором при 429
task = request("GET", f"{FORGE_URL}/api/tasks/7d4f2a91-3c58-4e6b-9a02-1f8e6c4d2b37").json()["data"]
print(task["status"])

Если документацию читает ИИ-агент

Чек-лист вызовов в порядке, в котором их делает скрипт. Поля запросов — в разделах «Задачи» и «Чаты».

Задача с файлом:

  1. POST /api/tasks — поля taskTypeSlug, params (JSON-строка), files. Запомните data.id.
  2. GET /api/tasks/:id — опрос раз в 5 секунд, пока status не станет done, partial или failed.
  3. GET /api/tasks/:id/files — список файлов.
  4. GET /api/tasks/:id/files/:fileId/download — по одному запросу на каждый файл с direction: "output".

Чат:

  1. POST /api/chat-sessions — поле taskTypeId. Запомните data.id.
  2. POST /api/chat-sessions/:id/messages — поля text и clientMessageId (новый UUID на каждое сообщение). Держите соединение открытым, пока портал не завершит ход.
  3. GET /api/chat-sessions/:id — опрос раз в 3 секунды, пока последнее сообщение не станет role: "assistant" с финальным статусом.
  4. Уточнение — повтор шагов 6–7 в ту же сессию.

Правила:

  • Ключ — в заголовке Authorization: Bearer … каждого запроса.
  • 429 — пауза в тексте error.message («через N минут», «N сек»); заголовка Retry-After нет.
  • 401 и 403 — ключ недействителен или не личный; повтор без смены ключа не поможет.
  • Таймаут на каждый запрос; повторять безопасно только GET.

На этой странице