MailSharks API — то же самое, что делает приложение, но из вашей программы: отправить письмо, подключить таблицу, завести рассылку, забрать статистику. Всё поверх обычного HTTPS и JSON, без SDK.
Базовый адрес: https://api.mailsharks.org/v1
Ключ передаётся заголовком в каждом запросе:
Authorization: Bearer ms_live_ВАШ_КЛЮЧ
Ключ даёт полный доступ к вашей почте для API. Держите его на сервере: из браузера и мобильного приложения его видно всем, кто откроет исходники.
X-RateLimit-Remaining и
X-RateLimit-Reset, так что узнавать о лимите по ошибке 429 не обязательно.| Метод | Путь | Что делает |
|---|---|---|
| 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}}. Имена
нечувствительны к регистру. Значения берутся, по возрастанию важности, из:
variables шаблона — значения по умолчанию;variables запроса — общие для всей отправки;{{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 запросов в минуту."}}
| HTTP | code | Когда |
|---|---|---|
400 | invalid_request, invalid_email, missing_variables, empty_body | Запрос разобран, но им нельзя воспользоваться |
401 | missing_key, invalid_key | Ключа нет или он отозван |
404 | sheet_not_found, template_not_found, campaign_not_found | Объекта нет или он принадлежит другому аккаунту |
409 | google_not_connected, unsubscribed, not_running | Запрос верный, но состояние аккаунта не позволяет его выполнить |
429 | rate_limited, daily_limit_reached | Исчерпан лимит запросов в минуту или дневной лимит почты |
502 | gmail_error | Gmail отказался принять письмо; текст ошибки — в message |