API v1 · Public beta
Создайте временный inbox, отправьте на него письмо и получите результат через REST API. Все запросы и ответы используют JSON, кроме скачивания MIME и вложений.
Beta сохраняет URL и контракт alpha. Опциональные поля и методы могут добавляться в v1; breaking changes требуют новой major-версии API или пакета.
https://api.tstly.ruБыстрый старт
1. Выпустите API key
Войдите в dashboard, откройте «Ключи API» и сохраните secret: он показывается только один раз.
2. Создайте inbox
Сгенерируйте ключ операции один раз: export IDEMPOTENCY_KEY="$(uuidgen)". При сетевом retry повторяйте запрос с тем же ключом и тем же body.
curl -X POST https://api.tstly.ru/v1/inboxes \
-H "Authorization: Bearer $TSTLY_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{"localPart":"signup","retentionSeconds":86400}'{
"id": "019…",
"tenantId": "019…",
"localPart": "signup",
"address": "signup@acme.tstly.ru",
"retentionSeconds": 86400,
"createdAt": "2026-08-01T12:00:00Z"
}3. Отправьте письмо
Укажите возвращённый address как получателя в SMTP-клиенте вашего приложения.
SMTP host: smtp.tstly.ru
Port: 465 (SSL/TLS)
Alternative: 587 (STARTTLS)Порт 25 остаётся доступен для SMTP relay, но для приложений и CI используйте 465 или 587: провайдеры часто блокируют исходящий 25.
Remote registration для E2E
Для CI выпустите в dashboard ключ с preset E2E_INBOX. Его минимальные scopes: inboxes:read, inboxes:write, messages:read и messages:delete. Ключ храните только в secret TSTLY_API_KEY.
Полный lifecycle ниже регистрирует адрес, извлекает его поля, ожидает письмо и обязательно удаляет inbox. Нужны curl, jq и uuidgen.
export TSTLY_API_KEY='…'
export IDEMPOTENCY_KEY="$(uuidgen)"
INBOX="$(curl --fail-with-body -sS -X POST https://api.tstly.ru/v1/inboxes \
-H "Authorization: Bearer $TSTLY_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{"localPart":"e2e-signup","retentionSeconds":3600}')"
INBOX_ID="$(printf '%s' "$INBOX" | jq -r .id)"
INBOX_ADDRESS="$(printf '%s' "$INBOX" | jq -r .address)"
# Trigger the consumer flow so it sends mail to $INBOX_ADDRESS, then:
curl --fail-with-body -sS "https://api.tstly.ru/v1/inboxes/$INBOX_ID/messages/wait?subject=Confirm%20account&timeoutSeconds=30" \
-H "Authorization: Bearer $TSTLY_API_KEY"
curl --fail-with-body -sS -X DELETE "https://api.tstly.ru/v1/inboxes/$INBOX_ID" \
-H "Authorization: Bearer $TSTLY_API_KEY"Один registration intent использует один Idempotency-Key в течение всех повторов. Тот же key с другим body возвращает 409 IDEMPOTENCY_KEY_REUSED, а повтор после удаления inbox — 409 IDEMPOTENCY_RESOURCE_DELETED. Запись хранится 24 часа.
Для параллельных workers используйте читаемый prefix и случайный suffix в localPart, например pw-signup-2-0-a1b2c3d4. SMTP: smtp.tstly.ru:465 с implicit TLS либо smtp.tstly.ru:587 с STARTTLS.
Аутентификация
Передавайте API key как Bearer token. Ключ привязан к tenant и не даёт доступ к чужим inbox.
Authorization: Bearer $TSTLY_API_KEYХраните его как secret CI/CD и отзывайте в dashboard, если он мог быть раскрыт.
Получение писем
Сначала получите текущий список, затем используйте latestCursor как afterCursor в long polling. Без cursor запрос ждёт первое доступное сообщение.
curl "https://api.tstly.ru/v1/inboxes/$INBOX_ID/messages/wait?timeoutSeconds=30" \
-H "Authorization: Bearer $TSTLY_API_KEY"Ответ 204 No Content означает, что за выбранные 30 секунд нового письма не появилось. Повторите запрос; это нормальный результат, не ошибка.
Коды и ссылки из письма
Получите код подтверждения или ссылку из первого подходящего письма. Укажите длину кода либо разрешённый origin и префикс пути ссылки. Примеры ниже используют вымышленные значения.
const code = await tstly.messages.waitForCode(inbox.id, {
subject: "Confirm account", length: 6, timeoutSeconds: 30,
}); // строка: ведущие нули сохраняются
const link = await tstly.messages.waitForLink(inbox.id, {
subject: "Confirm account",
origin: "https://example.test", pathPrefix: "/confirm",
timeoutSeconds: 30,
});Один deadline 1–30 секунд охватывает ожидание и извлечение. Передайте signal вторым объектом options после input, чтобы отменить операцию. Несколько разных значений дают ошибку неоднозначности: SDK не выбирает первое наугад.
Из уже полученного письма
POST /v1/inboxes/{inboxId}/messages/{messageId}/extract
Authorization: Bearer $TSTLY_API_KEY
Content-Type: application/json
{"type":"code","length":6}
// 200: пример ответа
{"status":"success","candidates":[{"value":"001234","locations":[{"source":"plain","offset":6}]}]}Нужен scope messages:read. Статусы результата: success, not_found, ambiguous и limit_exceeded. При превышении лимита значения не возвращаются. Вложения не анализируются, ссылки не открываются.
CLI и адаптеры
tstly message wait-code "$INBOX_ID" --length 6 --subject "Confirm account" --timeout 30
# По умолчанию выводятся только status и candidateCount.
# --reveal явно разрешает вывести код или ссылку.
tstly message wait-link "$INBOX_ID" --origin https://example.test --path-prefix /confirm --revealPlaywright предоставляет tstly.waitForCode(input) и tstly.waitForLink(input). Cypress — cy.tstlyWaitForCode(inboxId, input) и cy.tstlyWaitForLink(inboxId, input): запрос выполняется в Node, запись команды в журнал отключена. API key храните только в Node/CI.
Не записывайте их в логи, отчёты, screenshots и traces. По умолчанию Playwright прикладывает только безопасные метаданные; снимки экрана вашего приложения могут раскрыть введённый код.
Код: 4–12 ASCII-цифр отдельным токеном. Ссылка: HTTP(S), один разрешённый origin с учётом порта и граница сегмента пути. Относительные href требуют явного baseUrl того же origin. До 1 Mi UTF-16 символов plain + HTML, 20 разных значений, 8 мест на значение. Заняты все четыре слота API — 429 EXTRACTION_BUSY с Retry-After: 1.
Поиск по письмам
Параметр q в списке писем ищет все слова в теме и тексте plain/HTML. Русский и английский поддерживаются без морфологии, поиска по части слова или вложениям. Длина запроса — от 1 до 256 символов UTF-16. Точные фильтры, например subject, применяются дополнительно.
const page = await tstly.messages.list(inbox.id, { q: "заявка ready", limit: 20 });
// CLI 0.1.5:
// tstly message list "$INBOX_ID" --query "заявка ready"Индекс создаётся асинхронно: search.indexing сообщает о незавершённой обработке,search.incomplete — о содержимом, превысившем ограничения обработки. Обновляйте результаты явно. Сохраняйте запрос и фильтры при переходе по cursor. Поисковый cursor и q нельзя использовать в точном ожидании messages.wait.
SDK 0.1.5 отклоняет ответ старого сервера без поисковых метаданных. Не записывайте запросы или содержимое писем в отчёты CI.
Создание тестового письма · Пока выключено
API-приём реализован, но пока закрыт в production: сервер отвечает503 CAPTURE_DISABLED. Для действующего подключения используйте SMTP. После включения письмо будет сохраняться только в вашем ящике tstly; получателям из заголовка To ничего не доставляется.
Кабинет работает через сессию входа и не требует вставлять API-ключ. Для приложения или CI нужен отдельный ключ с правом messages:write; существующие ключи не получают это право автоматически. Чтение требуетmessages:read.
# После включения API-приёма. Сохраните ключ для явного повтора того же тела.
cat > synthetic.json <<'JSON'
{"from":"app@example.test","to":["user@example.test"],"subject":"Sandbox check","text":"Synthetic content"}
JSON
export CAPTURE_KEY="$(uuidgen)"
curl --fail-with-body -X POST "https://api.tstly.ru/v1/inboxes/$INBOX_ID/messages" \
-H "Authorization: Bearer $TSTLY_API_KEY" \
-H "Idempotency-Key: $CAPTURE_KEY" \
-H "Content-Type: application/json" \
--data-binary @synthetic.jsonJSON поддерживает from, to, необязательную тему, text и/или html, вложения в base64. Для готового MIME используйтеContent-Type: message/rfc822 и --data-binary @synthetic.eml. Размер запроса — до 10 МиБ; до 20 вложений общим размером до 5 МиБ.
Автоматических повторов нет. При неизвестном результате повторите запрос явно с тем же ключом и неизменённым телом в течение 24 часов. Отмена ожидания не отменяет сохранение письма. Если письмо уже удалено или истёк срок хранения, повтор возвращает 410 без нового приёма.
# SDK/CLI 0.1.4, после включения серверной возможности
tstly message capture "$INBOX_ID" --json synthetic.json --idempotency-key "$CAPTURE_KEY"
export RAW_CAPTURE_KEY="$(uuidgen)"
tstly message capture "$INBOX_ID" --raw synthetic.eml --idempotency-key "$RAW_CAPTURE_KEY"TypeScript SDK и CLI
Официальный ESM-пакет @tstly/sdk работает в Node.js 22.12+. Клиент типизирует inbox, сообщения, вложения, usage и webhooks, поддерживает AbortSignal, timeout и безопасный retry чтения с учётом Retry-After.
SDK 0.1.7 и адаптеры 0.1.1 доступны как проверяемые tarball ниже. Публикация scope @tstly в npm registry ожидает внешней авторизации; обычная команда из registry пока не сработает.
curl -fLO https://tstly.ru/downloads/tstly-sdk-0.1.7.tgz
npm install ./tstly-sdk-0.1.7.tgz
import { Tstly } from "@tstly/sdk";
const tstly = new Tstly({ apiKey: process.env.TSTLY_API_KEY! });
const inbox = await tstly.inboxes.create({ retentionSeconds: 3600 });
const message = await tstly.messages.wait(inbox.id, {
subject: "Confirm account",
timeoutSeconds: 30,
});CLI для CI
Передавайте ключ только через TSTLY_API_KEY. JSON-данные пишутся в stdout, диагностика — в stderr. Timeout ожидания завершает команду с кодом 3.
export TSTLY_API_KEY='…'
tstly inbox create --local-part ci --retention 3600
tstly message wait "$INBOX_ID" --subject "Confirm account" --timeout 30Playwright и Cypress
Готовые адаптеры создают отдельный inbox на каждую попытку теста, изолируют параллельные workers и гарантируют cleanup. API key остаётся в Node-процессе и не передаётся странице или browser context.
Playwright fixture
curl -fLO https://tstly.ru/downloads/tstly-sdk-0.1.7.tgz
curl -fLO https://tstly.ru/downloads/tstly-playwright-0.1.1.tgz
npm install -D ./tstly-sdk-0.1.7.tgz ./tstly-playwright-0.1.1.tgz @playwright/test
import { test, expect } from "@tstly/playwright";
test("signup", async ({ page, tstly }) => {
await page.getByLabel("Email").fill(tstly.inbox.address);
expect(await tstly.wait({ subject: "Confirm account" })).not.toBeNull();
});Cypress tasks
// cypress.config.ts — Node process
import { setupTstly } from "@tstly/cypress/node";
// support/e2e.ts — browser commands, no API key
import { installTstlyCommands } from "@tstly/cypress/commands";При падении Playwright по умолчанию прикладывает только metadata. Режим artifactMode: "full" добавляет raw MIME и вложения — включайте его только для защищённых CI artifacts с ограниченным retention.
Webhooks
Подписка получает событие message.received как at-least-once доставку. Secret возвращается один раз. Проверяйте HMAC в X-Tstly-Signature, дедуплицируйте поX-Tstly-Event-Id и отвечайте 2xx.
curl -X POST https://api.tstly.ru/v1/webhooks \
-H "Authorization: Bearer $TSTLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.test/tstly","description":"CI events"}'История delivery, ротация secret и ручной redelivery доступны в API и dashboard. Redirect и private-network targets отклоняются.
Справочник API
Открыть интерактивную документацию Scalar
Скачать OpenAPI JSONОшибки
Ошибка возвращается в едином JSON-формате с кодом, сообщением, request ID и ошибками полей.
{
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"requestId": "…",
"fieldErrors": []
}400Некорректные параметры или JSON401Key отсутствует, неверен или отозван404Ресурс не найден в текущем tenant409Конфликт состояния или лимит активных inbox429Превышен rate/concurrency limitЛимиты beta
Матрица совместимости
Миграция alpha → beta
Смена base URL, API key и inbox address не требуется. Зафиксируйте SDK на ^0.1.7 и адаптеры на ^0.1.1, храните key только в Node/CI secret, обрабатывайте 204 long-poll timeout и 429с Retry-After. Для webhooks добавьте HMAC-проверку и идемпотентность event ID. HTML писем по-прежнему считается недоверенным содержимым.
HTTP-автотесты · В разработке
В кабинете откройте «Автотесты» → «Сценарии» → «Новый HTTP-сценарий». Редактор содержит вкладки «Запрос», «Проверки» и «Переменные», поддерживает светлую, тёмную и системную тему. Несохранённые черновики остаются только в памяти вкладки.
Сохранение и выполнение HTTP в production пока выключены. После отдельного включения запуск потребует сохранённую версию сценария и текущую ревизию окружения. До 20 последовательных шагов, общий таймаут до 300 секунд; только публичные HTTP(S) адреса на портах 80/443, без redirect и автоматического повтора запроса.
Доступны проверки статуса, заголовка, JSON по JSON Pointer и вхождения текста. Секреты привязываются по идентификатору и версии. Результат по умолчанию показывает статусы, длительность и размер ответа. Чувствительная диагностика требует явного opt-in и отдельного разрешения, ограничена TTL. Отмена не откатывает внешнее действие.
SDK 0.1.3 сохраняет v1 и добавляет явный namespace client.v2; CLI — tstly v2. Его наличие не означает включение HTTP на сервере.
Beta release notes
В beta входят quota/usage, cursor filters и long polling, подписанные webhooks, TypeScript SDK/CLI, Playwright и Cypress adapters, workspace roles/audit, operator abuse controls и external SMTP reliability monitoring.
Вход только через Яндекс; custom domains и IMAP отсутствуют; framework packages рассчитаны на Node.js ESM; webhook delivery at-least-once; npm scope ещё ожидает публикации, поэтому beta packages устанавливаются из versioned tarballs; сервис предназначен только для тестовых данных.
Безопасность, support и feedback
Не отправляйте реальные персональные или production-данные. Удаляйте inbox после теста и выбирайте минимальный retention. Уязвимости сообщайте приватно на security@tstly.ru, вопросы и feedback — на support@tstly.ru. Не прикладывайте API keys, raw MIME или содержимое писем.