Документация API

Отправка писем, таблицы, шаблоны и статистика по HTTP

MailSharks API — то же самое, что делает приложение, но из вашей программы: отправить письмо, подключить таблицу, завести рассылку, забрать статистику. Всё поверх обычного HTTPS и JSON, без SDK.

Базовый адрес: https://api.mailsharks.org/v1

С чего начать

  1. Откройте приложение → API.
  2. Подключите почту для API. Это отдельный Google-аккаунт: у него свой дневной лимит, поэтому интеграция не съедает лимит вашей живой работы. Один и тот же ящик нельзя подключить дважды — ни в два слота, ни в два аккаунта Telegram.
  3. Выпустите ключ. Он показывается один раз: в базе хранится только его SHA-256, и подсмотреть ключ позже нельзя — только перевыпустить.

Аутентификация

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

Authorization: Bearer ms_live_ВАШ_КЛЮЧ

Ключ даёт полный доступ к вашей почте для API. Держите его на сервере: из браузера и мобильного приложения его видно всем, кто откроет исходники.

Лимиты

Эндпоинты

МетодПутьЧто делает
GET/accountПочта, дневной лимит и остаток на сегодня
POST/messagesОтправить одно письмо
GET/messagesИстория одиночных отправок
GET/messages/{id}Одно письмо со статусом
GET/sheets/inspectЛисты и заголовки таблицы по ссылке
POST/sheetsПодключить таблицу
GET/sheetsПодключённые таблицы
DELETE/sheets/{id}Отключить таблицу
GET/templatesШаблоны писем
POST/templatesСоздать шаблон
PATCH/templates/{id}Изменить шаблон
POST/templates/{id}/renderСобрать письмо, не отправляя
POST/campaignsРассылка по таблице — стартует сразу
GET/campaigns/{id}Статус и статистика рассылки
POST/campaigns/{id}/pauseПауза
POST/campaigns/{id}/resumeПродолжить
GET/campaigns/{id}/recipientsПолучатели постранично
GET/eventsЧто произошло с указанного момента
GET/unsubscribesСтоп-лист аккаунта
POST/unsubscribesДобавить адрес в стоп-лист

Отправить письмо

Одиночная отправка синхронная: ответ приходит, когда письмо уже принято Gmail. Заголовок Idempotency-Key необязателен, но с ним повтор запроса после оборвавшейся сети вернёт прежний ответ, а не отправит письмо второй раз.

curl -X POST https://api.mailsharks.org/v1/messages \
  -H "Authorization: Bearer ms_live_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-confirm" \
  -d '{
    "to": "client@example.com",
    "subject": "Заказ {{order}} принят",
    "body_html": "<p>Здравствуйте, {{name}}! Мы получили заказ {{order}}.</p>",
    "variables": {"name": "Анна", "order": "A-1042"}
  }'
import httpx

response = httpx.post(
    "https://api.mailsharks.org/v1/messages",
    headers={
        "Authorization": "Bearer ms_live_ВАШ_КЛЮЧ",
        "Idempotency-Key": "order-1042-confirm",
    },
    json={
        "to": "client@example.com",
        "subject": "Заказ {{order}} принят",
        "body_html": "<p>Здравствуйте, {{name}}! Мы получили заказ {{order}}.</p>",
        "variables": {"name": "Анна", "order": "A-1042"},
    },
    timeout=30,
)
response.raise_for_status()
print(response.json()["id"])
const response = await fetch("https://api.mailsharks.org/v1/messages", {
  method: "POST",
  headers: {
    "Authorization": "Bearer ms_live_ВАШ_КЛЮЧ",
    "Content-Type": "application/json",
    "Idempotency-Key": "order-1042-confirm",
  },
  body: JSON.stringify({
    to: "client@example.com",
    subject: "Заказ {{order}} принят",
    body_html: "<p>Здравствуйте, {{name}}! Мы получили заказ {{order}}.</p>",
    variables: { name: "Анна", order: "A-1042" },
  }),
});
if (!response.ok) throw new Error((await response.json()).error.message);
const { id } = await response.json();

Переменные

В теме и в теле работают подстановки вида {{name}}. Имена нечувствительны к регистру. Значения берутся, по возрастанию важности, из:

  1. поля variables шаблона — значения по умолчанию;
  2. поля variables запроса — общие для всей отправки;
  3. колонок таблицы, если это рассылка: {{city}} берётся из столбца city и у каждого получателя своё.

Если подстановку нечем закрыть — ни переменной, ни колонки — запрос отклоняется с 400 missing_variables. Проверка делается один раз, при создании: пустая ячейка у отдельной строки подставляется пустой и рассылку не рушит.

Посмотреть результат, ничего не отправляя, можно через POST /templates/{id}/render.

Шаблоны заводятся в приложении: API → Шаблоны. У каждого есть короткий номер вида a7Kd93Xz — его копируют кнопкой и передают как template_id. Шаблоны из обычного раздела «Шаблоны писем» этим ключом не работают: они принадлежат другой почте, с другим лимитом и своими таблицами.

Рассылка по таблице

Таблица должна быть открыта почте для API — это отдельный аккаунт, и доступ, выданный вашей основной почте, ему не наследуется. Кампания стартует сразу после создания, поэтому все проверки — почта, лимит, доступ к таблице, полнота переменных — делаются до первого письма.

# 1. Посмотреть, какие листы и столбцы есть в таблице
curl -G https://api.mailsharks.org/v1/sheets/inspect \
  -H "Authorization: Bearer ms_live_ВАШ_КЛЮЧ" \
  --data-urlencode "link=https://docs.google.com/spreadsheets/d/1AbC.../edit"

# 2. Подключить её
curl -X POST https://api.mailsharks.org/v1/sheets \
  -H "Authorization: Bearer ms_live_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{"link": "https://docs.google.com/spreadsheets/d/1AbC.../edit",
       "email_column": "email", "name_column": "name"}'

# 3. Запустить рассылку (стартует сразу)
curl -X POST https://api.mailsharks.org/v1/campaigns \
  -H "Authorization: Bearer ms_live_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{"sheet_id": 42, "template_id": "a7Kd93Xz",
       "variables": {"sale": "20%"}, "daily_limit": 300}'
import httpx

api = httpx.Client(
    base_url="https://api.mailsharks.org/v1",
    headers={"Authorization": "Bearer ms_live_ВАШ_КЛЮЧ"},
    timeout=60,
)

sheet = api.post("/sheets", json={
    "link": "https://docs.google.com/spreadsheets/d/1AbC.../edit",
    "email_column": "email",
    "name_column": "name",
}).json()

campaign = api.post("/campaigns", json={
    "sheet_id": sheet["id"],
    "template_id": "a7Kd93Xz",
    "variables": {"sale": "20%"},   # общие для всех; колонки таблицы важнее
    "daily_limit": 300,
}).json()

print(campaign["id"], campaign["recipients"])
const headers = {
  "Authorization": "Bearer ms_live_ВАШ_КЛЮЧ",
  "Content-Type": "application/json",
};

const sheet = await (await fetch("https://api.mailsharks.org/v1/sheets", {
  method: "POST", headers,
  body: JSON.stringify({
    link: "https://docs.google.com/spreadsheets/d/1AbC.../edit",
    email_column: "email",
    name_column: "name",
  }),
})).json();

const campaign = await (await fetch("https://api.mailsharks.org/v1/campaigns", {
  method: "POST", headers,
  body: JSON.stringify({
    sheet_id: sheet.id,
    template_id: "a7Kd93Xz",
    variables: { sale: "20%" },
    daily_limit: 300,
  }),
})).json();

События вместо вебхуков

Открытия, переходы, отписки и ошибки собираются в одну ленту. Спрашивайте её с курсором since: ответ отсортирован по времени, и его next_since — курсор для следующего вызова.

curl -G https://api.mailsharks.org/v1/events \
  -H "Authorization: Bearer ms_live_ВАШ_КЛЮЧ" \
  --data-urlencode "since=2026-08-20T09:00:00Z"

# Ответ:
# {"data": [{"at": "2026-08-20T09:04:11Z", "type": "opened",
#             "email": "client@example.com", "campaign_id": 42,
#             "message_id": null}],
#  "next_since": "2026-08-20T09:04:11Z"}
import time, httpx

api = httpx.Client(base_url="https://api.mailsharks.org/v1",
                   headers={"Authorization": "Bearer ms_live_ВАШ_КЛЮЧ"})
since = "2026-08-20T09:00:00Z"

while True:
    page = api.get("/events", params={"since": since}).json()
    for event in page["data"]:
        handle(event)          # ваша обработка
    since = page["next_since"]  # курсор на следующий вызов
    time.sleep(60)              # раз в минуту: чаще нет смысла
let since = "2026-08-20T09:00:00Z";

setInterval(async () => {
  const url = new URL("https://api.mailsharks.org/v1/events");
  url.searchParams.set("since", since);
  const page = await (await fetch(url, {
    headers: { "Authorization": "Bearer ms_live_ВАШ_КЛЮЧ" },
  })).json();
  page.data.forEach(handle);
  since = page.next_since;
}, 60_000);

Отписки

У одиночных писем нет таблицы, поэтому их отписки попадают в стоп-лист аккаунта и действуют на всю почту этого отправителя, включая рассылки. Стоп-лист читается и пополняется через /unsubscribes.

Футер отписки и заголовок List-Unsubscribe включены по умолчанию; для транзакционных писем их можно выключить полями unsubscribe_link и check_suppression. Выключать их в рассылках не стоит: у Gmail это прямой путь в спам, и страдает репутация вашего же домена.

Ошибки

Формат один на все ошибки — разбирайте code, а не текст:

{"error": {"code": "rate_limited", "message": "Не больше 100 запросов в минуту."}}
HTTPcodeКогда
400invalid_request, invalid_email, missing_variables, empty_bodyЗапрос разобран, но им нельзя воспользоваться
401missing_key, invalid_keyКлюча нет или он отозван
404sheet_not_found, template_not_found, campaign_not_foundОбъекта нет или он принадлежит другому аккаунту
409google_not_connected, unsubscribed, not_runningЗапрос верный, но состояние аккаунта не позволяет его выполнить
429rate_limited, daily_limit_reachedИсчерпан лимит запросов в минуту или дневной лимит почты
502gmail_errorGmail отказался принять письмо; текст ошибки — в message
MailSharks