REST API Bulko позволяет импортировать получателей, запускать managed email-кампании, получать статистику, читать классифицированные ответы и проверять баланс кредитов. В этом руководстве используются production endpoint’ы v1 с примерами на Python и cURL.

API предназначен для серверных интеграций. Не помещайте токен в браузерный код, мобильное приложение, публичный репозиторий или логи. Отправка зависит от баланса, suppression-проверок, content safety, Acceptable Use Policy и применимого законодательства.

Что поддерживает API

МетодEndpointНазначение
POST/api/v1/subscribers/importСоздать или дополнить список
POST/api/v1/campaigns/launchСоздать и поставить кампанию в очередь
GET/api/v1/campaigns/{id}/statsПолучить статистику
GET/api/v1/repliesПолучить ответы и классификацию
GET/api/v1/account/balanceПроверить кредиты
GET/api/v1/smtp/accessesПроверить отдельно купленный SMTP Access

Исходящие webhooks пока не доступны в production. Для автоматизации используйте polling статистики и ответов. Канонический контракт опубликован как OpenAPI 3.1.

1. Создайте API-токен

Войдите в аккаунт и откройте API keys. Назовите ключ и сразу скопируйте его: полный токен показывается только один раз. Формат заголовка — Authorization: Bearer bulko_live_.... Скомпрометированный ключ можно отозвать там же.

export BULKO_API_TOKEN='bulko_live_replace_me'

2. Проверьте авторизацию и баланс

cURL

curl --fail-with-body \
  -H "Authorization: Bearer $BULKO_API_TOKEN" \
  https://bulko.io/api/v1/account/balance

Python

import os
import requests

BASE_URL = "https://bulko.io/api/v1"
HEADERS = {
    "Authorization": f"Bearer {os.environ['BULKO_API_TOKEN']}",
    "Content-Type": "application/json",
}
response = requests.get(f"{BASE_URL}/account/balance", headers=HEADERS, timeout=30)
response.raise_for_status()
print(response.json()["balance"])

3. Импортируйте проверенный список

Один запрос принимает до 10 000 записей. Элемент может быть строкой либо объектом с email и name. Невалидные, повторные и находящиеся в глобальном suppression list адреса пропускаются и считаются отдельно.

payload = {
    "list_name": "Product demo requests",
    "emails": [
        {"email": "[email protected]", "name": "Alex"},
        "[email protected]",
    ],
}
response = requests.post(
    f"{BASE_URL}/subscribers/import",
    headers=HEADERS,
    json=payload,
    timeout=30,
)
response.raise_for_status()
list_id = response.json()["list"]["id"]

Техническая валидация не означает разрешение на контакт. Законно собирайте адреса, фиксируйте основание обработки и предоставляйте понятный opt-out.

4. Запустите кампанию

Передайте subscriber_list_id или массив recipients. Обязательные поля: name, subject и body. Успешный запрос возвращает HTTP 202, поскольку отправка выполняется асинхронно.

campaign_payload = {
    "name": "Requested product update",
    "subject": "A useful update for your team",
    "body": "<p>Hello, here is the update you requested.</p>",
    "subscriber_list_id": list_id,
    "reply_to": "[email protected]",
    "language": "en",
    "enable_tracking": True,
}
response = requests.post(
    f"{BASE_URL}/campaigns/launch",
    headers=HEADERS,
    json=campaign_payload,
    timeout=30,
)
response.raise_for_status()
campaign_id = response.json()["id"]

Ответ также содержит число допущенных и подавленных получателей и URL статистики. В API-кампаниях обработка отписки всегда остаётся включённой.

5. Проверяйте статус через polling

import time

for attempt in range(20):
    response = requests.get(
        f"{BASE_URL}/campaigns/{campaign_id}/stats",
        headers=HEADERS,
        timeout=30,
    )
    response.raise_for_status()
    stats = response.json()["campaign"]
    print(stats["status"], stats["sent"], stats["failed"])
    if stats["status"] in {"completed", "failed"}:
        break
    time.sleep(min(60, 5 * (attempt + 1)))

Используйте постепенное увеличение интервала. Текущий лимит — 60 запросов в минуту на токен; ответ HTTP 429 содержит заголовок Retry-After.

6. Получите ответы

params = {"status": "positive", "limit": 50, "offset": 0}
response = requests.get(
    f"{BASE_URL}/replies",
    headers=HEADERS,
    params=params,
    timeout=30,
)
response.raise_for_status()
for reply in response.json()["items"]:
    print(reply["campaign_id"], reply["from"], reply["status"])

Допустимые фильтры перечислены в OpenAPI. Токен получает ответы только собственных кампаний пользователя.

Обработка ошибок

КодПричинаДействие
400Ошибка JSON или поляИсправить запрос, не повторять без изменений
401Нет токена либо он отозванПроверить Bearer или заменить ключ
402Недостаточно email-кредитовПроверить и пополнить баланс
403Контент заблокированПроверить сообщение и правила
404Ресурс пользователя не найденПроверить ID и аккаунт
429Превышен rate limitПодождать Retry-After

Для сетевых ошибок и временных 5xx используйте ограниченные повторы с exponential backoff. Не повторяйте запуск кампании автоматически, пока система не подтвердила, что первый запрос не был принят.

Чек-лист production-интеграции

  • Храните токен в secret manager или защищённой переменной окружения.
  • Создавайте отдельные ключи для разных систем и отзывайте неиспользуемые.
  • Задавайте connection и read timeout для каждого запроса.
  • Логируйте ID кампаний, но не токены и полные базы получателей.
  • Учитывайте suppression и не возвращайте отписавшихся пользователей в базу.
  • Используйте OpenAPI для генерации типизированного клиента и проверки payload.

На странице API и интеграций есть примеры JavaScript и PHP, информация о SMTP Access, текущих ограничениях и roadmap webhooks.