SPINE Developer API
Единый API платформы для внешних проектов: почта, SSL-сертификаты, DNS, ящики, домены, вебхуки и Telegram-боты, по одному ключу.
BASE ·https://api.spine.maxsystems.az
🔑 Аутентификация
Заголовок Authorization: Bearer spine_sk_… в каждом запросе. Ключ выдаётся в консоли, показывается один раз, отзывается в клик.
⚙️ Учёт и лимиты
Каждый ключ считает запросы; лимит по умолчанию 120/мин (настраивается). Превышение, 429. Суточные/месячные квоты, по желанию.
⚠️ Коды ошибок
401 ключ неверный · 403 нет права · 429 лимит · 400 ошибка запроса (поле error).
| Скоуп | Что разрешает |
|---|---|
mail:send | Отправка писем (одиночно и пакетом, статус и статистика отправленных) |
mail:read | Чтение писем своих ящиков |
mail:suppressions | Заблокированные адреса: смотреть, добавлять, снимать блок |
mail:mailboxes | Создание и управление ящиками |
mail:domains | Подключение и статус доменов |
mail:templates | Шаблоны писем проекта: смотреть, править, откатывать, слать тест |
notify:send | Уведомления одним запросом: почта, Telegram, вебхук, с запасными каналами |
mail:subscriptions | Списки рассылок и подписки: подписать, отписать, посмотреть согласия |
protection:verify | Проверка ответа Turnstile для форм вашего сайта |
domains:read | Сроки регистрации своих доменов: когда истекает, у какого регистратора |
mail:migrate | Перенос почты с другого хостинга по IMAP (задания и прогресс) |
cert:issue | Заказ и перевыпуск сертификатов |
cert:read | Статус и скачивание сертификатов, SPKI-пины |
cert:acme | ACME-доступ: пары EAB для certbot, acme.sh, cert-manager, файрволов |
cert:delegate | CNAME-делегации для доменов, чей DNS не у нас |
pki:issue | Выпуск из приватного CA: длинные серты и клиентские mTLS |
pki:read | Приватный CA: корневые сертификаты, цепочки, CRL |
dns:read | Чтение DNS-записей зоны |
dns:records | Управление DNS-записями зоны |
webhooks | Подписка на вебхуки (входящие письма, события доставки) |
telegram:bots | Управление Telegram-ботами проекта |
webmail:login | Ссылки авто-входа сотрудника в вебмейл (SSO для встраивания) |
brand:read | Бренд-кит проекта (логотипы, цвета, шрифты, файлы) |
project:read | Сводка проекта: домены, серты, ключи, боты, бренд, биллинг (self-view) |
audit:read | Журнал событий проекта (кто/что/когда менял) |
siteconfig:read | SEO / site-config проекта (raw-конфиг; артефакты отдаются публично) |
secrets:read | Читать секреты проекта из сейфа (карта/.env/по имени) |
secrets:write | Записывать/генерировать/удалять секреты проекта в сейфе |
security:read | Отчёт безопасности своих доменов: оценка и открытые находки |
otp:send | Верификация: выдать и доставить одноразовый код (SMS, Telegram) |
otp:verify | Верификация: проверить код, статус попытки и доставки |
otp:totp | Верификация: приложение-аутентификатор и резервные коды пользователей проекта |
Почта
/v1/mail/sendmail:sendОтправить письмо. Домен отправителя должен быть подключён к почте SPINE и принадлежать проекту.
from | адрес отправителя (обязателен) |
to | получатель (обязателен) |
subject | тема (или template) |
text / html | тело письма |
category | метка для статистики (необязательно) |
cc / bcc / replyTo | списки адресов (необязательно) |
attachments | вложения: [{ filename, content (base64), contentType }], до 15 МБ на письмо |
headers | свои заголовки письма, объект (необязательно) |
stream | transactional (по умолчанию) | marketing. Транзакционный поток не блокируется отписками и жалобами: платёжный документ получатель должен получить, даже если отказался от новостей. Жёсткий отказ (несуществующий ящик) действует в обоих потоках |
scope | владелец отношения с получателем (например slug мерчанта). Жалобы и отписки ложатся именно на него, а не на весь проект. Пусто = весь проект, как раньше |
campaign | метка кампании: по ней считается воронка в /v1/mail/stats |
track | true, считать открытия и клики (события mail.opened / mail.clicked) |
unsubscribe | true, добавить отписку в один клик (RFC 8058, требование Gmail и Yahoo) |
sendAt | дата ISO 8601, отложенная отправка (до 90 дней, отмена через DELETE) |
idempotencyKey | защита от дублей (необязательно) |
{
"from": "noreply@ваш-домен",
"to": "user@example.com",
"subject": "Привет",
"text": "Тело письма",
"html": "<p>Тело письма со <a href=\"https://example.com\">ссылкой</a></p>",
"campaign": "welcome-july",
"track": true,
"unsubscribe": true
}{ "id": "01K…", "status": "sent", "relay": "ses", "relayMessageId": "…" }
curl -X POST "https://api.spine.maxsystems.az/v1/mail/send" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"from":"noreply@ваш-домен","to":"user@example.com","subject":"Привет","text":"Тело письма","html":"<p>Тело письма со <a href=\"https://example.com\">ссылкой</a></p>","campaign":"welcome-july","track":true,"unsubscribe":true}'
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/send", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({
"from": "noreply@ваш-домен",
"to": "user@example.com",
"subject": "Привет",
"text": "Тело письма",
"html": "<p>Тело письма со <a href=\"https://example.com\">ссылкой</a></p>",
"campaign": "welcome-july",
"track": true,
"unsubscribe": true
})
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/mail/send",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"from":"noreply@ваш-домен","to":"user@example.com","subject":"Привет","text":"Тело письма","html":"<p>Тело письма со <a href=\"https://example.com\">ссылкой</a></p>","campaign":"welcome-july","track":true,"unsubscribe":true}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/send");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"from":"noreply@ваш-домен","to":"user@example.com","subject":"Привет","text":"Тело письма","html":"<p>Тело письма со <a href=\"https://example.com\">ссылкой</a></p>","campaign":"welcome-july","track":true,"unsubscribe":true}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/send-batchmail:sendОтправить письма пакетом: до 200 получателей за запрос, у каждого свои vars. Общие поля задаются на верхнем уровне.
messages | список писем: [{ to, vars, subject?, html?, attachments? }] (обязателен) |
from / subject / html / text / template | общие поля для всех писем пакета |
campaign / track / unsubscribe / sendAt | как в одиночной отправке, применяются ко всему пакету |
{
"from": "noreply@ваш-домен",
"subject": "Привет, {{name}}",
"html": "<p>Здравствуйте, {{name}}</p>",
"campaign": "july-news",
"track": true,
"messages": [
{ "to": "a@example.com", "vars": { "name": "Анна" } },
{ "to": "b@example.com", "vars": { "name": "Борис" } }
]
}{ "total": 2, "counts": { "sent": 2 }, "results": [ { "to": "a@example.com", "id": "01K…", "status": "sent" } ] }
curl -X POST "https://api.spine.maxsystems.az/v1/mail/send-batch" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"from":"noreply@ваш-домен","subject":"Привет, {{name}}","html":"<p>Здравствуйте, {{name}}</p>","campaign":"july-news","track":true,"messages":[{"to":"a@example.com","vars":{"name":"Анна"}},{"to":"b@example.com","vars":{"name":"Борис"}}]}'
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/send-batch", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({
"from": "noreply@ваш-домен",
"subject": "Привет, {{name}}",
"html": "<p>Здравствуйте, {{name}}</p>",
"campaign": "july-news",
"track": true,
"messages": [
{ "to": "a@example.com", "vars": { "name": "Анна" } },
{ "to": "b@example.com", "vars": { "name": "Борис" } }
]
})
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/mail/send-batch",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"from":"noreply@ваш-домен","subject":"Привет, {{name}}","html":"<p>Здравствуйте, {{name}}</p>","campaign":"july-news","track":true,"messages":[{"to":"a@example.com","vars":{"name":"Анна"}},{"to":"b@example.com","vars":{"name":"Борис"}}]}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/send-batch");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"from":"noreply@ваш-домен","subject":"Привет, {{name}}","html":"<p>Здравствуйте, {{name}}</p>","campaign":"july-news","track":true,"messages":[{"to":"a@example.com","vars":{"name":"Анна"}},{"to":"b@example.com","vars":{"name":"Борис"}}]}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/mailboxes/{id}/messagesmail:readПисьма ящика, новые сверху, с постраничным обходом. Возвращает total и hasMore, поэтому обойти весь ящик можно без догадок.
limit | сколько вернуть, 1..500 (по умолчанию 50) |
offset | сколько пропустить: обход страницами offset += limit |
since | письма СТРОГО позже этого момента, ISO 8601. ⚠️ Письмо той же секунды не вернётся |
before | письма раньше этого момента, ISO 8601 |
hasAttachment | true, только письма с вложениями, false, только без |
with | переписка с адресом: письма ОТ него или К нему |
from / to | точечный фильтр по отправителю или получателю |
{ "messages": [ { "id": "…", "subject": "…", "receivedAt": "2026-08-10T09:00:00Z", "hasAttachment": true, "rfcMessageId": "20260810020431.ade65@example.com" } ], "total": 1843, "offset": 0, "limit": 50, "hasMore": true }
curl -X GET "https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mailboxes/{id}/messages/by-message-idmail:readПисьмо по его Message-ID: нужно, чтобы сопоставить письмо с уже сохранённым у себя, не перебирая списки. Значение принимается и в угловых скобках, и без них. Если письмо не найдено, в тексте ошибки написано, сколько писем просмотрено и до какой даты дошёл поиск: «не нашли в просмотренном» и «не существует» это разные ответы.
value | Message-ID письма (обязательно) |
since / before | сузить период поиска, ISO 8601: ускоряет ответ на больших ящиках |
{ "message": { "id": "…", "subject": "…", "rfcMessageId": "…" }, "lookup": { "source": "index", "scanned": 0 } }
curl -X GET "https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/by-message-id" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/by-message-id", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/by-message-id",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/by-message-id");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/messagesmail:sendСписок своих отправленных писем со статусами.
limit | сколько вернуть (до 200, по умолчанию 50) |
status | фильтр: sent | delivered | bounced | complained | delayed | scheduled | suppressed | failed |
campaign | фильтр по метке кампании |
{ "messages": [ { "id": "01K…", "toAddr": "user@example.com", "status": "delivered", "opens": 1, "clicks": 0 } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/mail/messages" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/messages", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mail/messages",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/messages");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/messages/{id}mail:sendСтатус письма и вся его история: принято релеем, доставлено, открыто, клик по ссылке.
{ "message": { "id": "01K…", "status": "delivered", "relay": "ses", "opens": 1, "clicks": 1 },
"events": [ { "event": "sent" }, { "event": "delivered" }, { "event": "opened" }, { "event": "clicked" } ],
"links": [ { "idx": 1, "url": "https://example.com", "clicks": 1 } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/mail/messages/{id}" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/messages/{id}", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mail/messages/{id}",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/messages/{id}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/messages/{id}mail:sendОтменить отложенное письмо, пока оно не ушло.
{ "id": "01K…", "status": "canceled" }
curl -X DELETE "https://api.spine.maxsystems.az/v1/mail/messages/{id}" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/messages/{id}", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/mail/messages/{id}",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/messages/{id}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/statsmail:sendВоронка доставляемости за период: принято, доставлено, открыто, клики, отказы, жалобы. Проценты отказов и жалоб важно держать ниже порогов Amazon (5% и 0.1%).
days | период в днях (1..365, по умолчанию 30) |
campaign | только по одной кампании |
{ "days": 30, "total": 120,
"funnel": { "accepted": 118, "delivered": 115, "opened": 64, "clicked": 12, "bounced": 2, "complained": 0 },
"rates": { "bounce": 1.69, "complaint": 0, "delivery": 97.46, "open": 55.65, "click": 10.43 } }
curl -X GET "https://api.spine.maxsystems.az/v1/mail/stats" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/stats", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mail/stats",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/stats");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/validatemail:sendПроверить адрес ДО отправки: синтаксис, есть ли у домена почтовые серверы, одноразовая ли почта, нет ли опечатки в известном домене (gmial вместо gmail). Заведомо негодный адрес получает verdict invalid, сомнительный warn с объяснением. Помогает не набирать жёсткие отказы, которые бьют по репутации домена.
{ "emails": ["user@gmial.com", "someone@example.com"] }{ "total": 2, "counts": { "warn": 1, "ok": 1 },
"results": [ { "email": "user@gmial.com", "verdict": "warn", "suggestion": "user@gmail.com",
"reasons": ["похоже на опечатку в домене"], "hasMx": true, "disposable": false } ] }
curl -X POST "https://api.spine.maxsystems.az/v1/mail/validate" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"emails":["user@gmial.com","someone@example.com"]}'
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/validate", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "emails": ["user@gmial.com", "someone@example.com"] })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/mail/validate",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"emails":["user@gmial.com","someone@example.com"]}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/validate");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"emails":["user@gmial.com","someone@example.com"]}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/checkmail:sendЧек-лист письма перед отправкой: нет текстовой части, картинка вместо текста, крик в теме, короткие ссылки, скрытый текст, массовое письмо без отписки. Это наш чек-лист причин попасть в спам, а не вердикт спам-фильтра.
{ "subject": "СРОЧНО!!", "html": "<img src=…>", "category": "marketing" }{ "score": 8, "verdict": "bad",
"issues": [ { "weight": 3, "text": "нет текстовой части: письмо только в HTML" } ] }
curl -X POST "https://api.spine.maxsystems.az/v1/mail/check" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"subject":"СРОЧНО!!","html":"<img src=…>","category":"marketing"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/check", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "subject": "СРОЧНО!!", "html": "<img src=…>", "category": "marketing" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/mail/check",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"subject":"СРОЧНО!!","html":"<img src=…>","category":"marketing"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/check");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"subject":"СРОЧНО!!","html":"<img src=…>","category":"marketing"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/mailboxes/{id}/messages/{msgId}/rawmail:readПисьмо целиком в исходном виде (RFC 5322, message/rfc822). Нужно, когда своего разбора хочется больше, чем нашего: вы получаете ровно то, что лежит на сервере. ⚠️ Политика проекта и антивирус применяются и здесь: письмо с запрещённым или заражённым вложением не отдаётся целиком, иначе через сырой файл проверку можно было бы обойти. Выдача пишется в журнал проекта: это доступ ко всей переписке письма.
(поток message/rfc822, имя файла <id письма>.eml)
curl -X GET "https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/{msgId}/raw" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/{msgId}/raw", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/{msgId}/raw",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/{msgId}/raw");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/suppressionsmail:suppressionsЗаблокированные адреса проекта: жёсткие отказы, жалобы на спам, отписки. Письма на них не отправляются.
email | проверить один адрес |
scope | блокировки этого владельца плюс общепроектные (они действуют на всех) |
stream | transactional, показать только то, что блокирует платёжные письма |
limit | сколько вернуть (до 500) |
{ "suppressions": [ { "email": "user@example.com", "reason": "hard_bounce", "scope": null, "ts": "2026-07-25T10:00:00Z" } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/mail/suppressions" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/suppressions", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mail/suppressions",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/suppressions");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/suppressionsmail:suppressionsЗаблокировать адрес вручную (например, по просьбе получателя).
email | адрес (обязателен) |
reason | hard_bounce | complaint | manual | unsubscribe (по умолчанию manual) |
scope | чья это блокировка: пусто = весь проект |
{ "email": "user@example.com", "reason": "manual", "scope": "мерчант" }{ "email": "user@example.com", "reason": "manual", "tenant": "ваш-проект", "scope": "мерчант" }
curl -X POST "https://api.spine.maxsystems.az/v1/mail/suppressions" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","reason":"manual","scope":"мерчант"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/suppressions", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "email": "user@example.com", "reason": "manual", "scope": "мерчант" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/mail/suppressions",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"email":"user@example.com","reason":"manual","scope":"мерчант"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/suppressions");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"email":"user@example.com","reason":"manual","scope":"мерчант"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/suppressions/{email}mail:suppressionsСнять блокировку с адреса (например, человек починил свой ящик и снова хочет письма). С параметром scope снимается блокировка ровно этого владельца, без него, все блокировки адреса в проекте.
scope | снять блокировку только этого владельца |
{ "email": "user@example.com", "removed": 1, "scope": "мерчант" }
curl -X DELETE "https://api.spine.maxsystems.az/v1/mail/suppressions/{email}" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/suppressions/{email}", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/mail/suppressions/{email}",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/suppressions/{email}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/policymail:sendСвои лимиты и правила: размеры вложений, запрещённые типы файлов, режим антивируса, потолки отправки. Полезно, чтобы не гадать, почему письмо отклонили.
{ "project": "ваш-проект",
"attachments": { "maxBytesOutbound": 15728640, "maxBytesInbound": 26214400,
"blockedExtensions": ["exe", "js", "bat", "…"], "antivirus": "отклоняем заражённое" },
"sending": { "maxRecipientsPerBatch": 200, "dailyCap": null } }
curl -X GET "https://api.spine.maxsystems.az/v1/mail/policy" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/policy", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mail/policy",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/policy");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
Подписки
/v1/mail/listsmail:subscriptionsСписки рассылок проекта («счета», «новости»). Отписка идёт от списка, а не от всей почты сразу: человек, отказавшийся от новостей, продолжает получать счета.
{ "lists": [ { "slug": "news", "name": "Новости", "doubleOptIn": true,
"confirmed": 128, "pending": 4, "unsubscribed": 11 } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/mail/lists" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/lists", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mail/lists",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/lists");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/listsmail:subscriptionsСоздать список рассылки. doubleOptIn (по умолчанию true) значит, что адрес считается подписанным только после подтверждения по ссылке из письма: так требуют Gmail и Yahoo, и так чужой адрес нельзя подписать за человека.
{ "slug": "news", "name": "Новости", "description": "раз в месяц", "doubleOptIn": true }{ "id": "…", "slug": "news", "name": "Новости", "doubleOptIn": true }
curl -X POST "https://api.spine.maxsystems.az/v1/mail/lists" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"slug":"news","name":"Новости","description":"раз в месяц","doubleOptIn":true}'
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/lists", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "slug": "news", "name": "Новости", "description": "раз в месяц", "doubleOptIn": true })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/mail/lists",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"slug":"news","name":"Новости","description":"раз в месяц","doubleOptIn":true}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/lists");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"slug":"news","name":"Новости","description":"раз в месяц","doubleOptIn":true}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/lists/{slug}mail:subscriptionsУдалить список вместе со всеми подписками на него. Уже отправленные письма это не затрагивает.
{ "deleted": "news" }
curl -X DELETE "https://api.spine.maxsystems.az/v1/mail/lists/{slug}" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/lists/{slug}", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/mail/lists/{slug}",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/lists/{slug}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/subscriptionsmail:subscriptionsПодписать адрес на список. При двойном подтверждении платформа сразу отправляет письмо со ссылкой и возвращает status pending: до подтверждения письма списка на этот адрес НЕ уходят. Если согласие уже получено у вас (галочка при заказе), можно передать confirmed: true.
{ "email": "user@example.com", "list": "news", "source": "форма на сайте" }{ "email": "user@example.com", "list": "news", "status": "pending", "confirmationSent": true }
curl -X POST "https://api.spine.maxsystems.az/v1/mail/subscriptions" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","list":"news","source":"форма на сайте"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/subscriptions", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "email": "user@example.com", "list": "news", "source": "форма на сайте" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/mail/subscriptions",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"email":"user@example.com","list":"news","source":"форма на сайте"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/subscriptions");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"email":"user@example.com","list":"news","source":"форма на сайте"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/subscriptionsmail:subscriptionsСлаг списка во всех методах называется list; поле listSlug оставлено синонимом ради совместимости. Согласия адреса по всем спискам проекта и готовая ссылка на его центр подписок. Ссылку можно положить в подвал письма: на ней человек сам управляет подписками.
email | адрес, который проверяем |
{ "email": "user@example.com",
"subscriptions": [ { "list": "news", "status": "confirmed", "confirmedAt": "2026-08-01T10:00:00Z" } ],
"prefsUrl": "https://t.example.com/s/…" }
curl -X GET "https://api.spine.maxsystems.az/v1/mail/subscriptions" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/subscriptions", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mail/subscriptions",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/subscriptions");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/subscriptionsmail:subscriptionsОтписать адрес от списка. Без параметра list отпишет от всех списков проекта.
email | адрес |
list | слаг списка (необязательно) |
{ "email": "user@example.com", "list": "news", "unsubscribed": 1 }
curl -X DELETE "https://api.spine.maxsystems.az/v1/mail/subscriptions" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/subscriptions", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/mail/subscriptions",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/subscriptions");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
Шаблоны
/v1/mail/templatesmail:templatesШаблоны писем проекта. Текст письма перестаёт жить в коде проекта: правится здесь, версии сохраняются, откат делается одним запросом.
{ "templates": [ { "slug": "invoice", "name": "Счёт", "version": 3, "variables": ["name","number"] } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/mail/templates" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/templates", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mail/templates",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/templates");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/templatesmail:templatesСоздать шаблон. Переменные вида {{name}} платформа находит сама и показывает списком.
{ "slug": "invoice", "name": "Счёт", "subject": "Счёт {{number}}",
"html": "<p>Здравствуйте, {{name}}. Счёт {{number}} на {{sum}}.</p>" }{ "slug": "invoice", "version": 1, "variables": ["name","number","sum"] }
curl -X POST "https://api.spine.maxsystems.az/v1/mail/templates" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"slug":"invoice","name":"Счёт","subject":"Счёт {{number}}","html":"<p>Здравствуйте, {{name}}. Счёт {{number}} на {{sum}}.</p>"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/templates", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "slug": "invoice", "name": "Счёт", "subject": "Счёт {{number}}",
"html": "<p>Здравствуйте, {{name}}. Счёт {{number}} на {{sum}}.</p>" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/mail/templates",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"slug":"invoice","name":"Счёт","subject":"Счёт {{number}}","html":"<p>Здравствуйте, {{name}}. Счёт {{number}} на {{sum}}.</p>"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/templates");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"slug":"invoice","name":"Счёт","subject":"Счёт {{number}}","html":"<p>Здравствуйте, {{name}}. Счёт {{number}} на {{sum}}.</p>"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/templates/{slug}mail:templatesИзменить шаблон. Прежняя версия сохраняется в истории, номер версии растёт.
{ "subject": "Счёт {{number}} к оплате" }{ "slug": "invoice", "version": 2 }
curl -X PUT "https://api.spine.maxsystems.az/v1/mail/templates/{slug}" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"subject":"Счёт {{number}} к оплате"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/templates/{slug}", {
method: "PUT",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "subject": "Счёт {{number}} к оплате" })
});
const data = await res.json();
import requests
r = requests.put(
"https://api.spine.maxsystems.az/v1/mail/templates/{slug}",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"subject":"Счёт {{number}} к оплате"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/templates/{slug}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"subject":"Счёт {{number}} к оплате"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/templates/{slug}/rollbackmail:templatesВернуть содержимое прошлой версии. История не переписывается: откат создаёт новую версию с прежним текстом.
{ "version": 1 }{ "slug": "invoice", "version": 3, "rolledBackFrom": 1 }
curl -X POST "https://api.spine.maxsystems.az/v1/mail/templates/{slug}/rollback" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"version":1}'
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/templates/{slug}/rollback", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "version": 1 })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/mail/templates/{slug}/rollback",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"version":1}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/templates/{slug}/rollback");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"version":1}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/templates/{slug}/previewmail:templatesПредпросмотр с подстановкой переменных плюс чек-лист письма.
{ "vars": { "name": "Михаил", "number": "SPINE-2026-0007", "sum": "59 AZN" } }{ "subject": "Счёт SPINE-2026-0007", "html": "…", "check": { "score": 0, "verdict": "ok" } }
curl -X POST "https://api.spine.maxsystems.az/v1/mail/templates/{slug}/preview" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"vars":{"name":"Михаил","number":"SPINE-2026-0007","sum":"59 AZN"}}'
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/templates/{slug}/preview", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "vars": { "name": "Михаил", "number": "SPINE-2026-0007", "sum": "59 AZN" } })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/mail/templates/{slug}/preview",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"vars":{"name":"Михаил","number":"SPINE-2026-0007","sum":"59 AZN"}}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/templates/{slug}/preview");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"vars":{"name":"Михаил","number":"SPINE-2026-0007","sum":"59 AZN"}}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/templates/{slug}/testmail:templatesТестовая отправка шаблона на указанный адрес. Идёт тем же путём, что и настоящее письмо, с метками template-test.
{ "to": "me@example.com", "vars": { "name": "Михаил" } }{ "id": "01K…", "status": "sent" }
curl -X POST "https://api.spine.maxsystems.az/v1/mail/templates/{slug}/test" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"to":"me@example.com","vars":{"name":"Михаил"}}'
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/templates/{slug}/test", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "to": "me@example.com", "vars": { "name": "Михаил" } })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/mail/templates/{slug}/test",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"to":"me@example.com","vars":{"name":"Михаил"}}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail/templates/{slug}/test");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"to":"me@example.com","vars":{"name":"Михаил"}}',
]);
$data = json_decode(curl_exec($ch), true);
Уведомления
/v1/notifynotify:sendУведомление одним запросом с запасными каналами: пробуем по очереди, пока один не сработает. Порядок и переход держим мы, вам не нужно городить «не дошло в Telegram, шлю письмо» у себя. В ответе видно, какой канал сработал и что пробовали до него.
{ "title": "Платёж прошёл", "body": "Счёт SPINE-2026-0007 оплачен",
"channels": [ { "channel": "telegram", "to": "12345678" }, { "channel": "email", "to": "user@example.com" } ] }{ "channel": "email", "to": "user@example.com", "status": "sent",
"tried": [ { "channel": "telegram", "error": "Telegram HTTP 400" } ] }
curl -X POST "https://api.spine.maxsystems.az/v1/notify" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"title":"Платёж прошёл","body":"Счёт SPINE-2026-0007 оплачен","channels":[{"channel":"telegram","to":"12345678"},{"channel":"email","to":"user@example.com"}]}'
const res = await fetch("https://api.spine.maxsystems.az/v1/notify", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "title": "Платёж прошёл", "body": "Счёт SPINE-2026-0007 оплачен",
"channels": [ { "channel": "telegram", "to": "12345678" }, { "channel": "email", "to": "user@example.com" } ] })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/notify",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"title":"Платёж прошёл","body":"Счёт SPINE-2026-0007 оплачен","channels":[{"channel":"telegram","to":"12345678"},{"channel":"email","to":"user@example.com"}]}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/notify");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"title":"Платёж прошёл","body":"Счёт SPINE-2026-0007 оплачен","channels":[{"channel":"telegram","to":"12345678"},{"channel":"email","to":"user@example.com"}]}',
]);
$data = json_decode(curl_exec($ch), true);
Сертификаты
/v1/certs/issuecert:issueЗаказать публичный SSL-сертификат. Домен должен быть в управляемой зоне либо иметь CNAME-делегацию, причём делегация нужна на КАЖДОЕ имя сертификата: владение проверяется по каждому имени отдельно, по своему _acme-challenge.<имя> (исключение, wildcard, он проверяется по записи домена). Выпуск 15-30 сек. Срок задаёт не клиент: по правилам CA/Browser Forum публичный сертификат живёт 199 дней (с 2027, 100, с 2029, 47). Нужен длинный срок, смотрите POST /v1/pki/issue. ⚠️ Поле pinWarning в ответе означает, что по этим доменам уже есть ДРУГОЙ сертификат с вшитыми пинами: у нового свой ключ, значит и пин другой. Нужен прежний пин, перевыпускайте существующий сертификат, а не заказывайте второй.
domains | список доменов, можно wildcard *.домен (обязателен) |
csr | свой CSR, тогда приватный ключ остаётся у вас (необязательно) |
autoRenew | автопродление, по умолчанию true |
{ "domains": ["app.ваш-домен", "*.ваш-домен"], "autoRenew": true }{ "id": "…", "subject": "app.ваш-домен", "sans": [...], "status": "pending",
"pinWarning": ["Банк X (Java-приложение)"] }
curl -X POST "https://api.spine.maxsystems.az/v1/certs/issue" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"domains":["app.ваш-домен","*.ваш-домен"],"autoRenew":true}'
const res = await fetch("https://api.spine.maxsystems.az/v1/certs/issue", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "domains": ["app.ваш-домен", "*.ваш-домен"], "autoRenew": true })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/certs/issue",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"domains":["app.ваш-домен","*.ваш-домен"],"autoRenew":true}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/certs/issue");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"domains":["app.ваш-домен","*.ваш-домен"],"autoRenew":true}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/certs/{id}/statuscert:readСтатус сертификата (pending → active | failed).
{ "status": "active", "notAfter": "…", "daysLeft": 89 }
curl -X GET "https://api.spine.maxsystems.az/v1/certs/{id}/status" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/certs/{id}/status", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/certs/{id}/status",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/certs/{id}/status");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/certs/{id}/downloadcert:readСкачать сертификат (fullchain + приватный ключ; для сертов по CSR privkey=null).
{ "fullchain": "-----BEGIN CERTIFICATE-----…", "privkey": "…" }
curl -X GET "https://api.spine.maxsystems.az/v1/certs/{id}/download" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/certs/{id}/download", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/certs/{id}/download",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/certs/{id}/download");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/certs/{id}/pinscert:readSPKI-пины сертификата. Это то, что вшивают у себя партнёры вместо самого сертификата: платформа сохраняет приватный ключ ЭТОГО сертификата, поэтому пин переживает и автопродление, и ручной перевыпуск. ⚠️ Пин сменится, если завести ВТОРОЙ сертификат на те же домены вместо перевыпуска первого, удалить и создать заново, выключить «Сохранять ключ» или запросить ротацию ключа. Отдаём текущий и запасной, плюс готовые строки для curl и OkHttp.
{ "spkiPin": "…", "backupSpkiPin": "…", "keepKey": true,
"formats": { "curl": "curl --pinnedpubkey \"sha256//…\" https://api.example.com/" } }
curl -X GET "https://api.spine.maxsystems.az/v1/certs/{id}/pins" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/certs/{id}/pins", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/certs/{id}/pins",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/certs/{id}/pins");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/certs/{id}/pins/registercert:readОтметить, что пин где-то вшит. Запись попадает в реестр, и перед сменой ключа платформа предупредит, кого это заденет.
{ "party": "Партнёр X", "system": "Java-приложение", "contact": "dev@partner.com" }{ "id": "…", "registered": true, "pin": "…" }
curl -X POST "https://api.spine.maxsystems.az/v1/certs/{id}/pins/register" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"party":"Партнёр X","system":"Java-приложение","contact":"dev@partner.com"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/certs/{id}/pins/register", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "party": "Партнёр X", "system": "Java-приложение", "contact": "dev@partner.com" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/certs/{id}/pins/register",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"party":"Партнёр X","system":"Java-приложение","contact":"dev@partner.com"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/certs/{id}/pins/register");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"party":"Партнёр X","system":"Java-приложение","contact":"dev@partner.com"}',
]);
$data = json_decode(curl_exec($ch), true);
PKI и ACME
/v1/acme/eabcert:acmeВыдать себе пару External Account Binding для нашего ACME-сервера. После этого с платформой работает любой стандартный клиент без нашего кода: certbot, acme.sh, cert-manager, Caddy, Traefik, FortiGate 7.6.3+, Cisco ASA 9.23.1+. HMAC-ключ возвращается ОДИН раз.
label | метка для себя (необязательно) |
allowedDomains | ограничить домены (пусто = любые доступные) |
issuer | letsencrypt (по умолчанию) или private |
policy | auto (мы проходим проверку сами) или verify (проверяет клиент) |
{ "label": "prod-cluster", "allowedDomains": ["example.com"] }{ "kid": "…", "hmacKey": "…",
"directoryUrl": "https://api.spine.maxsystems.az/acme/directory",
"snippets": { "certbot": "certbot register --server … --eab-kid … --eab-hmac-key …" } }
curl -X POST "https://api.spine.maxsystems.az/v1/acme/eab" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"label":"prod-cluster","allowedDomains":["example.com"]}'
const res = await fetch("https://api.spine.maxsystems.az/v1/acme/eab", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "label": "prod-cluster", "allowedDomains": ["example.com"] })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/acme/eab",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"label":"prod-cluster","allowedDomains":["example.com"]}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/acme/eab");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"label":"prod-cluster","allowedDomains":["example.com"]}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/acme/delegationscert:delegateСоздать делегацию проверки владения по CNAME. Нужна для доменов, чей DNS ведём не мы: клиент один раз ставит одну запись, дальше все проверки выполняем мы у себя, доступ к его DNS не требуется.
{ "domain": "client-domain.com" }{ "domain": "client-domain.com", "record": { "type": "CNAME",
"name": "_acme-challenge.client-domain.com", "value": "<метка>.acme.spine.maxsystems.az", "ttl": 300 } }
curl -X POST "https://api.spine.maxsystems.az/v1/acme/delegations" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"domain":"client-domain.com"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/acme/delegations", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "domain": "client-domain.com" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/acme/delegations",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"domain":"client-domain.com"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/acme/delegations");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"domain":"client-domain.com"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/acme/delegations/{id}/verifycert:delegateПроверить, что клиент поставил CNAME. Только читает DNS, ничего не меняет.
{ "ok": true, "expected": "…", "found": ["…"] }
curl -X POST "https://api.spine.maxsystems.az/v1/acme/delegations/{id}/verify" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/acme/delegations/{id}/verify", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/acme/delegations/{id}/verify",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/acme/delegations/{id}/verify");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/acme/delegationscert:delegateСписок делегаций проекта с их состоянием.
{ "delegations": [{ "domain": "client-domain.com", "verified": true }] }
curl -X GET "https://api.spine.maxsystems.az/v1/acme/delegations" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/acme/delegations", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/acme/delegations",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/acme/delegations");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/pki/issuepki:issueВыпустить сертификат приватным CA платформы. Правила CA/Browser Forum на приватный CA не распространяются, поэтому срок задаёте вы: до 3650 дней. Годится для партнёрских интеграций, внутренних сервисов и клиентских сертификатов mTLS.
domains | домены для серверного сертификата |
commonName | имя владельца для клиентского сертификата mTLS |
days | срок в днях, по умолчанию 397, максимум 3650 |
certType | server (по умолчанию) или client |
csr | свой CSR, приватный ключ остаётся у вас (необязательно) |
{ "domains": ["internal.example.com"], "days": 1095, "certType": "server" }{ "id": "…", "subject": "internal.example.com", "status": "pending", "lifetimeDays": 1095 }
curl -X POST "https://api.spine.maxsystems.az/v1/pki/issue" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"domains":["internal.example.com"],"days":1095,"certType":"server"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/pki/issue", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "domains": ["internal.example.com"], "days": 1095, "certType": "server" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/pki/issue",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"domains":["internal.example.com"],"days":1095,"certType":"server"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/pki/issue");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"domains":["internal.example.com"],"days":1095,"certType":"server"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/pki/rootpki:readКорневой сертификат приватного CA и цепочка. Это то, что партнёр один раз ставит себе в truststore и живёт с ним годы: перевыпуск конечных сертификатов его не затрагивает.
{ "name": "SPINE Partner CA Root", "rootPem": "-----BEGIN CERTIFICATE-----…",
"notAfter": "2036-07-29T…", "crlUrl": "https://api.spine.maxsystems.az/v1/pki/…/crl.pem" }
curl -X GET "https://api.spine.maxsystems.az/v1/pki/root" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/pki/root", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/pki/root",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/pki/root");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/pki/crlpki:readСписок отозванных сертификатов приватного CA (CRL) в формате PEM.
curl -X GET "https://api.spine.maxsystems.az/v1/pki/crl" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/pki/crl", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/pki/crl",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/pki/crl");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/pki/authoritiespki:readЦентры сертификации, доступные проекту: их имена, тип и срок действия.
{ "cas": [{ "name": "SPINE Partner CA Issuing", "kind": "intermediate",
"notAfter": "2031-07-28T…", "isDefault": true }] }
curl -X GET "https://api.spine.maxsystems.az/v1/pki/authorities" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/pki/authorities", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/pki/authorities",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/pki/authorities");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
Ящики
/v1/mailboxesmail:mailboxesСоздать почтовый ящик на своём домене.
name | имя ящика до @ (2-40, [a-z0-9._-]) |
domain | домен проекта (обязателен) |
password | пароль ящика (мин. 8) |
description | описание (необязательно) |
{ "name": "info", "domain": "ваш-домен", "password": "…" }{ "email": "info@ваш-домен", "id": "…" }
curl -X POST "https://api.spine.maxsystems.az/v1/mailboxes" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"name":"info","domain":"ваш-домен","password":"…"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "name": "info", "domain": "ваш-домен", "password": "…" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/mailboxes",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"name":"info","domain":"ваш-домен","password":"…"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mailboxes");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"name":"info","domain":"ваш-домен","password":"…"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/mailboxesmail:mailboxesСписок ящиков и групп на доменах проекта. Поле type: personal (личный), shared (общий, доступ у нескольких людей), group (распределительная группа). У групп поле membersCount.
{ "mailboxes": [ { "id": "…", "email": "ivan@ваш-домен", "type": "personal", "usedBytes": 0 }, { "id": "…", "email": "team@ваш-домен", "type": "group", "membersCount": 4 } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/mailboxes" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mailboxes",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mailboxes");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mailboxes/{id}mail:mailboxesУдалить ящик (только на своём домене).
{ "deleted": true }
curl -X DELETE "https://api.spine.maxsystems.az/v1/mailboxes/{id}" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/mailboxes/{id}",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mailboxes/{id}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mailboxes/{id}/accessmail:mailboxesКто из сотрудников работает с общим ящиком под своим логином. Уровни: read (только читать), write (читать и отвечать), full (плюс управление папками и удаление).
{ "access": [ { "accountId": "u", "email": "tamara@example.com", "level": "write", "folders": 5 } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/mailboxes/{id}/access" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/access", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mailboxes/{id}/access",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mailboxes/{id}/access");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mailboxes/{id}/accessmail:mailboxesОткрыть сотруднику доступ к общему ящику. Отдельный пароль ему не нужен: ящик появится в его почтовой программе сам, в разделе «Shared Folders». Доступ выдаётся на все папки ящика; если позже появятся новые папки, вызвать повторно.
{ "mailbox": "tamara@example.com", "level": "write" }{ "ok": true, "mailbox": "tamara@example.com", "level": "write", "folders": 5 }
curl -X POST "https://api.spine.maxsystems.az/v1/mailboxes/{id}/access" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"mailbox":"tamara@example.com","level":"write"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/access", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "mailbox": "tamara@example.com", "level": "write" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/mailboxes/{id}/access",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"mailbox":"tamara@example.com","level":"write"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mailboxes/{id}/access");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"mailbox":"tamara@example.com","level":"write"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/mailboxes/{id}/access/{granteeId}mail:mailboxesЗакрыть сотруднику доступ к общему ящику во всех папках. granteeId, это accountId из выдачи GET /v1/mailboxes/{id}/access.
{ "ok": true, "folders": 5 }
curl -X DELETE "https://api.spine.maxsystems.az/v1/mailboxes/{id}/access/{granteeId}" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/access/{granteeId}", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/mailboxes/{id}/access/{granteeId}",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mailboxes/{id}/access/{granteeId}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mailboxes/{id}/messagesmail:readСписок последних писем в ящике (свежие сверху). Можно фильтровать по адресу, удобно для панели «переписка с контактом».
limit | сколько писем вернуть, до 100 (по умолчанию 20) |
with | адрес контакта: письма ОТ или К нему (переписка с контрагентом) |
from | фильтр по отправителю (подстрока адреса) |
to | фильтр по получателю (подстрока адреса) |
{ "messages": [ { "id": "eaaaaab", "messageId": ["<abc@bank.az>"], "subject": "…", "from": [{"email":"…"}], "receivedAt": "…", "preview": "…" } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mailboxes/{id}/messages/{msgId}mail:readПолное письмо: тело, RFC Message-ID/In-Reply-To/References (для тредирования), сырые заголовки и список вложений с id для скачивания. Внимание: id в пути, per-mailbox (не глобальный); глобальный ключ, messageId. Чужой ящик отвечает 404, а не 403.
{ "id": "eaaaaab", "messageId": "<abc@bank.az>", "inReplyTo": ["<prev@you.az>"], "references": ["<root@…>"], "subject": "…", "from": […], "to": […], "receivedAt": "…", "text": "тело письма", "headers": [{"name":"Message-ID","value":"<abc@bank.az>"}, …] }
curl -X GET "https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/{msgId}" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/{msgId}", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/{msgId}",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/{msgId}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mailboxes/{id}/messages/{msgId}/attachmentsmail:readВложения письма: имя, тип, размер, id для скачивания и вердикт антивируса. Файл тянуть не нужно, чтобы узнать, что он заражён или запрещён политикой.
{ "attachments": [ {
"id": "cp73raw…", "name": "akt-sverki.pdf", "contentType": "application/pdf",
"size": 1665, "disposition": "attachment", "cid": null,
"blocked": false, "blockedReason": null,
"av": { "status": "clean", "threat": null, "detail": null, "scannedAt": "2026-07-26T10:48:38Z" }
} ] }
curl -X GET "https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/{msgId}/attachments" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/{msgId}/attachments", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/{msgId}/attachments",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/{msgId}/attachments");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mailboxes/{id}/messages/{msgId}/attachments/{attId}mail:readСкачать вложение. Отдаётся потоком самим файлом (не base64), с заголовками типа и имени. Перед отдачей файл проверяется антивирусом: заражённый и запрещённый политикой не отдаются.
attId | id вложения из списка вложений этого же письма |
HTTP 200 Content-Type: application/pdf Content-Disposition: attachment; filename*=UTF-8''akt-sverki.pdf Content-Length: 1665 <байты файла> # 409, вложение заражено или его тип запрещён политикой проекта # 413, вложение больше лимита проекта # 503, антивирус недоступен: файл не отдаём (fail-closed), пробуйте позже
curl -X GET "https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/{msgId}/attachments/{attId}" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/{msgId}/attachments/{attId}", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/{msgId}/attachments/{attId}",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/{msgId}/attachments/{attId}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mailboxes/{id}/unreadmail:readЧисло непрочитанных писем в ящике, для бейджа-счётчика в вашем приложении (напр. сайдбар CRM). В пути можно указать и id ящика, и полный адрес сотрудника, так не нужно заранее знать id. Поле unread, непрочитанные во «Входящих» (число для бейджа), total, по всем папкам, folders, разбивка по папкам, где есть непрочитанные.
{ "unread": 3, "total": 5, "folders": [ { "name": "Inbox", "role": "inbox", "unread": 3 }, { "name": "Черновики", "role": null, "unread": 2 } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/mailboxes/{id}/unread" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/unread", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mailboxes/{id}/unread",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mailboxes/{id}/unread");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
Перенос почты
/v1/mail-migrationsmail:migrateЗавести перенос одного ящика с другого хостинга по IMAP. Ящик у нас должен быть создан заранее: письма кладём в него по IMAP, поэтому нужен его пароль. sourceHost принимает и готовый пресет (vk, yandex, google, outlook), и адрес сервера. Для VK WorkSpace и Mail.ru нужен пароль для внешних приложений: обычный пароль их сервер отклоняет.
targetEmail | ящик у нас, куда переносим |
targetPassword | пароль этого ящика |
sourceHost | пресет (vk|yandex|google|outlook) или адрес IMAP-сервера |
sourcePort | порт источника (по умолчанию 993) |
sourceSecure | true = TLS сразу, false = STARTTLS |
sourceUser | логин на источнике (по умолчанию равен targetEmail) |
sourcePassword | пароль на источнике (для VK и Mail.ru, пароль приложения) |
{ "targetEmail": "info@ваш-домен", "targetPassword": "…", "sourceHost": "vk", "sourcePassword": "…" }{ "id": "…", "targetEmail": "info@ваш-домен", "status": "pending", "copiedMessages": 0 }
curl -X POST "https://api.spine.maxsystems.az/v1/mail-migrations" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"targetEmail":"info@ваш-домен","targetPassword":"…","sourceHost":"vk","sourcePassword":"…"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/mail-migrations", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "targetEmail": "info@ваш-домен", "targetPassword": "…", "sourceHost": "vk", "sourcePassword": "…" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/mail-migrations",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"targetEmail":"info@ваш-домен","targetPassword":"…","sourceHost":"vk","sourcePassword":"…"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail-migrations");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"targetEmail":"info@ваш-домен","targetPassword":"…","sourceHost":"vk","sourcePassword":"…"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail-migrationsmail:migrateЗадания переноса проекта и готовые пресеты популярных хостингов.
{ "presets": { "vk": { "host": "imap.mail.ru", "port": 993 } }, "migrations": [ { "id": "…", "status": "done", "copiedMessages": 4210 } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/mail-migrations" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail-migrations", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mail-migrations",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail-migrations");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail-migrations/{id}mail:migrateПодробности задания: счётчики по каждой папке и журнал прогона.
{ "migration": { "status": "running" }, "folders": [ { "sourcePath": "Отправленные", "targetPath": "Sent", "copied": 812, "total": 812 } ], "log": ["…"] }
curl -X GET "https://api.spine.maxsystems.az/v1/mail-migrations/{id}" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail-migrations/{id}", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mail-migrations/{id}",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail-migrations/{id}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail-migrations/startmail:migrateЗапустить перенос по списку заданий. Повторный запуск догоняет только новые письма (помним последний перенесённый UID в каждой папке), поэтому его безопасно гонять после переключения MX.
ids | список id заданий |
includeTrash | переносить корзину (по умолчанию да) |
includeJunk | переносить спам (по умолчанию нет) |
{ "ids": ["…"], "includeTrash": true, "includeJunk": false }{ "queued": 1 }
curl -X POST "https://api.spine.maxsystems.az/v1/mail-migrations/start" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"ids":["…"],"includeTrash":true,"includeJunk":false}'
const res = await fetch("https://api.spine.maxsystems.az/v1/mail-migrations/start", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "ids": ["…"], "includeTrash": true, "includeJunk": false })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/mail-migrations/start",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"ids":["…"],"includeTrash":true,"includeJunk":false}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail-migrations/start");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"ids":["…"],"includeTrash":true,"includeJunk":false}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail-migrations/{id}mail:migrateУдалить задание переноса вместе с сохранёнными доступами.
{ "deleted": true }
curl -X DELETE "https://api.spine.maxsystems.az/v1/mail-migrations/{id}" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail-migrations/{id}", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/mail-migrations/{id}",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/mail-migrations/{id}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
Группы
/v1/groupsmail:mailboxesСписок распределительных групп проекта. Группа, это адрес team@домен, письмо на который разлетается всем участникам в их личные ящики.
{ "groups": [ { "id": "…", "email": "team@ваш-домен", "membersCount": 4 } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/groups" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/groups", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/groups",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/groups");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/groupsmail:mailboxesСоздать распределительную группу name@домен. Домен обязан быть на вашем проекте.
domain | домен проекта (обязателен) |
name | имя адреса, 2-40 символов [a-z0-9._-] (обязателен) |
description | описание группы (необязательно) |
{ "domain": "ваш-домен.az", "name": "team", "description": "Вся команда" }{ "id": "…", "email": "team@ваш-домен.az" }
curl -X POST "https://api.spine.maxsystems.az/v1/groups" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"domain":"ваш-домен.az","name":"team","description":"Вся команда"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/groups", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "domain": "ваш-домен.az", "name": "team", "description": "Вся команда" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/groups",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"domain":"ваш-домен.az","name":"team","description":"Вся команда"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/groups");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"domain":"ваш-домен.az","name":"team","description":"Вся команда"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/groups/{id}mail:mailboxesУдалить группу.
{ "deleted": true }
curl -X DELETE "https://api.spine.maxsystems.az/v1/groups/{id}" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/groups/{id}", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/groups/{id}",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/groups/{id}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/groups/{id}/membersmail:mailboxesУчастники группы (ящики, в которые разлетается почта).
{ "members": [ { "id": "…", "email": "ivan@ваш-домен.az" } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/groups/{id}/members" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/groups/{id}/members", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/groups/{id}/members",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/groups/{id}/members");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/groups/{id}/membersmail:mailboxesДобавить ящик в группу по его адресу.
{ "mailbox": "ivan@ваш-домен.az" }{ "ok": true, "member": { "id": "…", "email": "ivan@ваш-домен.az" } }
curl -X POST "https://api.spine.maxsystems.az/v1/groups/{id}/members" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"mailbox":"ivan@ваш-домен.az"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/groups/{id}/members", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "mailbox": "ivan@ваш-домен.az" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/groups/{id}/members",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"mailbox":"ivan@ваш-домен.az"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/groups/{id}/members");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"mailbox":"ivan@ваш-домен.az"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/groups/{id}/members/{userId}mail:mailboxesУбрать участника из группы.
{ "ok": true }
curl -X DELETE "https://api.spine.maxsystems.az/v1/groups/{id}/members/{userId}" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/groups/{id}/members/{userId}", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/groups/{id}/members/{userId}",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/groups/{id}/members/{userId}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
Вебмейл (SSO)
/v1/webmail/change-passwordпароль ящикаСотрудник меняет пароль СВОЕГО ящика сам. Ключ API не нужен: запрос подтверждается текущим паролем, который проверяется попыткой входа. Минимум 8 символов, словарные пароли почтовый сервер отклоняет. Защита от перебора: 5 попыток на ящик и 20 на адрес источника за 15 минут.
{ "email": "ivan@ваш-домен.az", "currentPassword": "…", "newPassword": "…" }{ "ok": true }
curl -X POST "https://api.spine.maxsystems.az/v1/webmail/change-password" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"email":"ivan@ваш-домен.az","currentPassword":"…","newPassword":"…"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/webmail/change-password", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "email": "ivan@ваш-домен.az", "currentPassword": "…", "newPassword": "…" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/webmail/change-password",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"email":"ivan@ваш-домен.az","currentPassword":"…","newPassword":"…"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/webmail/change-password");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"email":"ivan@ваш-домен.az","currentPassword":"…","newPassword":"…"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/webmail/submit-asвход ящика (Basic/SSO)Отправить подготовленный черновик ОТ ИМЕНИ общего ящика (support@, info@). Нужно потому, что почтовый сервер даёт читать общий ящик, но отправлять от его имени не даёт. Платформа проверяет, что у отправителя есть доступ на запись к этому ящику, и пишет каждую отправку в аудит.
{ "accountId": "id ящика", "emailId": "id черновика" }{ "sent": true }
curl -X POST "https://api.spine.maxsystems.az/v1/webmail/submit-as" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"accountId":"id ящика","emailId":"id черновика"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/webmail/submit-as", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "accountId": "id ящика", "emailId": "id черновика" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/webmail/submit-as",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"accountId":"id ящика","emailId":"id черновика"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/webmail/submit-as");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"accountId":"id ящика","emailId":"id черновика"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/webmail/scheduledвход ящика (Basic/SSO)Отправить письмо позже. Письмо лежит обычным черновиком в ящике, платформа отправляет его в назначенное время (проверка раз в 30 секунд). Не раньше чем через минуту и не дальше чем на год. GET возвращает свои запланированные, DELETE /v1/webmail/scheduled/{id} отменяет.
{ "accountId": "id ящика", "emailId": "id черновика", "sendAt": 1785400000000 }{ "id": "…", "sendAt": "2026-07-30T06:00:00.000Z", "status": "pending" }
curl -X POST "https://api.spine.maxsystems.az/v1/webmail/scheduled" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"accountId":"id ящика","emailId":"id черновика","sendAt":1785400000000}'
const res = await fetch("https://api.spine.maxsystems.az/v1/webmail/scheduled", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "accountId": "id ящика", "emailId": "id черновика", "sendAt": 1785400000000 })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/webmail/scheduled",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"accountId":"id ящика","emailId":"id черновика","sendAt":1785400000000}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/webmail/scheduled");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"accountId":"id ящика","emailId":"id черновика","sendAt":1785400000000}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/webmail/login-linkwebmail:loginВыдать сотруднику одноразовую ссылку авто-входа в ЕГО ящик в вебмейле, чтобы встроить почту в ваше приложение (напр. CRM). Ссылка живёт 60 секунд и одноразовая: открыв её (в новой вкладке или в iframe), сотрудник попадает в почту без второго логина. Ящик обязан быть на вашем домене.
mailbox | полный адрес ящика сотрудника на вашем домене (обязателен) |
{ "mailbox": "ivan@ваш-домен.az" }{ "url": "https://webmail.ваш-домен.az/?login_token=…", "email": "ivan@ваш-домен.az", "expiresIn": 60 }
curl -X POST "https://api.spine.maxsystems.az/v1/webmail/login-link" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"mailbox":"ivan@ваш-домен.az"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/webmail/login-link", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "mailbox": "ivan@ваш-домен.az" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/webmail/login-link",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"mailbox":"ivan@ваш-домен.az"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/webmail/login-link");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"mailbox":"ivan@ваш-домен.az"}',
]);
$data = json_decode(curl_exec($ch), true);
Домены
/v1/maildomainsmail:domainsПодключить домен к почте (создаёт домен, при возможности сам публикует DNS).
{ "name": "ваш-домен" }{ "id": "…", "name": "ваш-домен", "autoDns": { "created": 6 }, "records": [...], "ready": false }
curl -X POST "https://api.spine.maxsystems.az/v1/maildomains" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"name":"ваш-домен"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/maildomains", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "name": "ваш-домен" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/maildomains",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"name":"ваш-домен"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/maildomains");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"name":"ваш-домен"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/maildomainsmail:domainsСписок подключённых доменов проекта.
{ "domains": [ { "id": "…", "name": "ваш-домен" } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/maildomains" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/maildomains", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/maildomains",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/maildomains");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/maildomains/{domain}mail:domainsDNS-записи и статус верификации домена.
{ "name": "ваш-домен", "records": [...], "ready": true }
curl -X GET "https://api.spine.maxsystems.az/v1/maildomains/{domain}" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/maildomains/{domain}", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/maildomains/{domain}",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/maildomains/{domain}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/maildomains/{domain}/catch-allmail:domainsПриём писем на несуществующие адреса домена: включён ли он, в какой ящик идёт и отвечаем ли отправителю. Поле warning появляется, когда приём настроен на личный ящик человека.
{ "domain": "ваш-домен", "enabled": true, "mailbox": "catchall@ваш-домен", "autoReply": true }
curl -X GET "https://api.spine.maxsystems.az/v1/maildomains/{domain}/catch-all" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/maildomains/{domain}/catch-all", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/maildomains/{domain}/catch-all",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/maildomains/{domain}/catch-all");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/maildomains/{domain}/catch-allmail:domainsВключить, изменить или выключить приём на несуществующие адреса. Ящик обязан существовать и быть на этом же домене. Пока приём включён, отправителю уходит автоответ на английском: такого адреса нет, отвечать сюда бессмысленно, вот адрес поддержки домена. Автоответ уходит только живым людям: рассылкам, роботам и отчётам о недоставке не отвечаем, одному адресу пишем не чаще раза в сутки.
mailbox | ящик приёма на этом домене, либо null чтобы выключить (тогда письма на несуществующие адреса отбиваются с отказом) |
autoReply | отвечать ли отправителю, что адреса нет (по умолчанию true) |
{ "mailbox": "catchall@ваш-домен", "autoReply": true }{ "domain": "ваш-домен", "enabled": true, "mailbox": "catchall@ваш-домен", "autoReply": true }
curl -X PUT "https://api.spine.maxsystems.az/v1/maildomains/{domain}/catch-all" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"mailbox":"catchall@ваш-домен","autoReply":true}'
const res = await fetch("https://api.spine.maxsystems.az/v1/maildomains/{domain}/catch-all", {
method: "PUT",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "mailbox": "catchall@ваш-домен", "autoReply": true })
});
const data = await res.json();
import requests
r = requests.put(
"https://api.spine.maxsystems.az/v1/maildomains/{domain}/catch-all",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"mailbox":"catchall@ваш-домен","autoReply":true}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/maildomains/{domain}/catch-all");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"mailbox":"catchall@ваш-домен","autoReply":true}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/domains/minedomains:readСроки регистрации доменов проекта: сколько дней осталось, у какого регистратора, где домен используется. Поле source говорит, откуда дата: rdap или whois, из реестра, manual, вбита в консоли (у части зон, например .az, машинного источника нет вовсе). Если даты нет, expiresAt = null и state = unknown.
{ "domains": [ { "domain": "ваш-домен", "registrar": "…", "expiresAt": "2027-03-01T00:00:00.000Z", "daysLeft": 208, "state": "ok", "uses": ["почта"], "source": "rdap" } ], "alertDays": [40, 30, 15, 5, 1] }
curl -X GET "https://api.spine.maxsystems.az/v1/domains/mine" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/domains/mine", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/domains/mine",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/domains/mine");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
Защита
/v1/protection/turnstile/verifyprotection:verifyПроверить ответ Turnstile из формы вашего сайта. Секрет хранится у нас и наружу не отдаётся: класть его в браузер нельзя, иначе проверку обойдут за минуту. Вы присылаете только токен, который вернул виджет, и домен, для которого он выдан.
token | значение поля cf-turnstile-response из вашей формы (обязательно) |
domain | домен, для которого настроены ключи Turnstile |
{ "token": "0.abc…", "domain": "ваш-домен" }{ "ok": true, "errors": [] }
curl -X POST "https://api.spine.maxsystems.az/v1/protection/turnstile/verify" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"token":"0.abc…","domain":"ваш-домен"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/protection/turnstile/verify", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "token": "0.abc…", "domain": "ваш-домен" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/protection/turnstile/verify",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"token":"0.abc…","domain":"ваш-домен"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/protection/turnstile/verify");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"token":"0.abc…","domain":"ваш-домен"}',
]);
$data = json_decode(curl_exec($ch), true);
Безопасность
/v1/security/reportsecurity:readОтчёт безопасности своих доменов: оценка A-F и открытые находки (TLS, DNS, почтовая аутентификация, HTTP-заголовки). Только чтение, сканы отсюда не запускаются. Находки со статусом «не проверено» (unknown) на оценку не влияют: не проверено это не «чисто».
{ "project": "ваш-проект", "grade": "B", "score": 82, "open": 3, "findings": [ { "target": "ваш-домен", "category": "mail", "severity": "medium", "title": "DMARC в режиме наблюдения (p=none)", "detail": "…" } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/security/report" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/security/report", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/security/report",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/security/report");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
DNS
/v1/dns/{zone}/recordsdns:readСписок DNS-записей зоны (зона должна быть привязана к проекту).
{ "zone": "ваш-домен", "records": [ { "id": "…", "type": "A", "name": "@", "data": "1.2.3.4" } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/dns/{zone}/records" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/dns/{zone}/records", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/dns/{zone}/records",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/dns/{zone}/records");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/dns/{zone}/recordsdns:recordsСоздать DNS-запись в зоне проекта.
type | A | AAAA | TXT | MX | CNAME | NS | SRV | CAA |
name | имя записи ('@' для корня) |
data | значение |
priority / ttl | для MX / TTL (необязательно) |
{ "type": "CNAME", "name": "app", "data": "target.example.com" }{ "created": true }
curl -X POST "https://api.spine.maxsystems.az/v1/dns/{zone}/records" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"type":"CNAME","name":"app","data":"target.example.com"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/dns/{zone}/records", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "type": "CNAME", "name": "app", "data": "target.example.com" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/dns/{zone}/records",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"type":"CNAME","name":"app","data":"target.example.com"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/dns/{zone}/records");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"type":"CNAME","name":"app","data":"target.example.com"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/dns/{zone}/records/{id}dns:recordsУдалить DNS-запись. Для GoDaddy добавь ?data=<значение>.
{ "deleted": true }
curl -X DELETE "https://api.spine.maxsystems.az/v1/dns/{zone}/records/{id}" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/dns/{zone}/records/{id}", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/dns/{zone}/records/{id}",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/dns/{zone}/records/{id}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
Вебхуки
/v1/webhookswebhooksПодписаться на события: платформа будет слать POST на ваш URL. secret в ответе, один раз.
url | ваш https-адрес приёма (обязателен) |
events | письмо в ящике: mail.received (получено), mail.sent_folder (сотрудник отправил из ящика) · отправка через API: mail.sent, mail.delivered, mail.bounced, mail.complained, mail.delayed, mail.rejected · вовлечённость (при track:true): mail.opened, mail.clicked, mail.unsubscribed |
{ "url": "https://ваш-сервер/webhook", "events": ["mail.received", "mail.bounced"] }{ "id": "…", "url": "…", "events": [...], "secret": "whsec_…" }
curl -X POST "https://api.spine.maxsystems.az/v1/webhooks" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"url":"https://ваш-сервер/webhook","events":["mail.received","mail.bounced"]}'
const res = await fetch("https://api.spine.maxsystems.az/v1/webhooks", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "url": "https://ваш-сервер/webhook", "events": ["mail.received", "mail.bounced"] })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/webhooks",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"url":"https://ваш-сервер/webhook","events":["mail.received","mail.bounced"]}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/webhooks");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"url":"https://ваш-сервер/webhook","events":["mail.received","mail.bounced"]}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/webhookswebhooksСписок ваших вебхуков.
{ "webhooks": [ { "id": "…", "url": "…", "events": [...], "active": true } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/webhooks" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/webhooks", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/webhooks",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/webhooks");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/webhooks/{id}webhooksИзменить подписку: список событий, адрес приёма, включить или выключить. СЕКРЕТ НЕ МЕНЯЕТСЯ, поэтому ваш верификатор подписи трогать не нужно. Передавайте только те поля, которые меняете.
url | новый https-адрес приёма (необязательно) |
events | новый список событий целиком (необязательно) |
active | false, приостановить доставку, не теряя подписку и лог (необязательно) |
{ "events": ["mail.received", "mail.sent_folder", "mail.bounced"] }{ "id": "…", "url": "…", "events": [...], "active": true }
curl -X PATCH "https://api.spine.maxsystems.az/v1/webhooks/{id}" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"events":["mail.received","mail.sent_folder","mail.bounced"]}'
const res = await fetch("https://api.spine.maxsystems.az/v1/webhooks/{id}", {
method: "PATCH",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "events": ["mail.received", "mail.sent_folder", "mail.bounced"] })
});
const data = await res.json();
import requests
r = requests.patch(
"https://api.spine.maxsystems.az/v1/webhooks/{id}",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"events":["mail.received","mail.sent_folder","mail.bounced"]}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/webhooks/{id}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "PATCH",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"events":["mail.received","mail.sent_folder","mail.bounced"]}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/webhooks/{id}/rotate-secretwebhooksСменить секрет подписи (утечка, уход сотрудника). Новый показывается ОДИН раз. Старый перестаёт работать сразу: события пойдут с новой подписью, и до подмены секрета у себя вы будете их отбивать.
{ "id": "…", "secret": "whsec_…" }
curl -X POST "https://api.spine.maxsystems.az/v1/webhooks/{id}/rotate-secret" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/webhooks/{id}/rotate-secret", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/webhooks/{id}/rotate-secret",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/webhooks/{id}/rotate-secret");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/webhooks/{id}webhooksУдалить вебхук. Чтобы просто поменять события или адрес, используйте PATCH: пересоздание выдаст новый секрет.
{ "deleted": true }
curl -X DELETE "https://api.spine.maxsystems.az/v1/webhooks/{id}" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/webhooks/{id}", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/webhooks/{id}",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/webhooks/{id}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/webhooks/{id}/deliverieswebhooksЛог доставок вебхука: статус (pending/delivered/failed), коды, попытки и точное подписанное тело, удобно для отладки подписи.
{ "deliveries": [ { "id": 42, "event": "mail.received", "status": "failed", "httpCode": 401, "attempts": 7, "payload": "{…}" } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/webhooks/{id}/deliveries" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/webhooks/{id}/deliveries", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/webhooks/{id}/deliveries",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/webhooks/{id}/deliveries");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/webhooks/{id}/deliveries/{deliveryId}/redeliverwebhooksПереотправить конкретную доставку сейчас (например, после того как починили верификатор подписи).
{ "status": "delivered", "httpCode": 200 }
curl -X POST "https://api.spine.maxsystems.az/v1/webhooks/{id}/deliveries/{deliveryId}/redeliver" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/webhooks/{id}/deliveries/{deliveryId}/redeliver", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/webhooks/{id}/deliveries/{deliveryId}/redeliver",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/webhooks/{id}/deliveries/{deliveryId}/redeliver");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
Telegram-боты
/v1/telegram/mybotstelegram:botsПодключить существующего бота по токену (из BotFather) к платформе.
{ "token": "123456:ABC-…", "note": "бот проекта" }{ "id": "…", "botId": 123456, "username": "MyBot", "name": "My Bot" }
curl -X POST "https://api.spine.maxsystems.az/v1/telegram/mybots" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"token":"123456:ABC-…","note":"бот проекта"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/telegram/mybots", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "token": "123456:ABC-…", "note": "бот проекта" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/telegram/mybots",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"token":"123456:ABC-…","note":"бот проекта"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/telegram/mybots");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"token":"123456:ABC-…","note":"бот проекта"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/telegram/mybots/bulktelegram:botsМассовый импорт: подключить сразу много ботов по списку токенов (из BotFather). tokens, массив или текст (по токену на строку).
{ "tokens": ["123456:AAA…", "234567:BBB…"] }{ "total": 2, "added": 2, "failed": 0, "results": [ { "ok": true, "username": "MyBot", "botId": 123456 } ] }
curl -X POST "https://api.spine.maxsystems.az/v1/telegram/mybots/bulk" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"tokens":["123456:AAA…","234567:BBB…"]}'
const res = await fetch("https://api.spine.maxsystems.az/v1/telegram/mybots/bulk", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "tokens": ["123456:AAA…", "234567:BBB…"] })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/telegram/mybots/bulk",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"tokens":["123456:AAA…","234567:BBB…"]}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/telegram/mybots/bulk");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"tokens":["123456:AAA…","234567:BBB…"]}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/telegram/mybotstelegram:botsСписок ботов проекта.
{ "bots": [ { "id": "…", "username": "MyBot", "webhookSet": true } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/telegram/mybots" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/telegram/mybots", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/telegram/mybots",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/telegram/mybots");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/telegram/mybots/{id}telegram:botsЖивой статус бота (getMe + вебхук + команды).
{ "username": "MyBot", "me": {...}, "webhook": {...}, "commands": [...] }
curl -X GET "https://api.spine.maxsystems.az/v1/telegram/mybots/{id}" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/telegram/mybots/{id}", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/telegram/mybots/{id}",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/telegram/mybots/{id}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/telegram/mybots/{id}/webhooktelegram:botsНаправить апдейты бота на ваш сервер (SPINE принимает от Telegram и пересылает вам).
{ "forwardUrl": "https://ваш-сервер/tg" }{ "hookUrl": "https://api.spine.maxsystems.az/v1/tg/hook/123456", "forwardUrl": "…" }
curl -X POST "https://api.spine.maxsystems.az/v1/telegram/mybots/{id}/webhook" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"forwardUrl":"https://ваш-сервер/tg"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/telegram/mybots/{id}/webhook", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "forwardUrl": "https://ваш-сервер/tg" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/telegram/mybots/{id}/webhook",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"forwardUrl":"https://ваш-сервер/tg"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/telegram/mybots/{id}/webhook");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"forwardUrl":"https://ваш-сервер/tg"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/telegram/mypresetstelegram:botsПресеты «фабрика под ключ», доступные проекту (общие + свои): команды, вебхук, профиль.
{ "presets": [ { "id": "…", "name": "Поддержка", "forwardUrl": "https://…" } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/telegram/mypresets" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/telegram/mypresets", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/telegram/mypresets",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/telegram/mypresets");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/telegram/mybots/create-linktelegram:botsФабрика: дип-линк создания нового бота. Пользователь подтверждает в Telegram, бот сам подключится к проекту. С presetId бот сразу разворачивается «под ключ» (команды + вебхук + профиль).
{ "suggestedUsername": "myproject_bot", "presetId": "…(необязательно)" }{ "link": "https://t.me/newbot/spinemsbot/myproject_bot" }
curl -X POST "https://api.spine.maxsystems.az/v1/telegram/mybots/create-link" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"suggestedUsername":"myproject_bot","presetId":"…(необязательно)"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/telegram/mybots/create-link", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "suggestedUsername": "myproject_bot", "presetId": "…(необязательно)" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/telegram/mybots/create-link",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"suggestedUsername":"myproject_bot","presetId":"…(необязательно)"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/telegram/mybots/create-link");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"suggestedUsername":"myproject_bot","presetId":"…(необязательно)"}',
]);
$data = json_decode(curl_exec($ch), true);
Бренд
/v1/brandbrand:readБренд-кит вашего проекта: название, слоган, палитра цветов, шрифты, ссылки и логотипы/файлы (с прямыми URL для встраивания). Полезно для авто-подстановки фирменного стиля в письма, сайты и приложения.
{
"tenant": "acme",
"displayName": "Acme Inc.",
"tagline": "Строим будущее",
"colors": [{ "name": "Основной", "hex": "#5b8def", "role": "primary" }],
"fonts": [{ "name": "Inter", "role": "текст" }],
"links": [{ "label": "Сайт", "url": "https://acme.example" }],
"logos": [{ "variant": "primary", "format": "svg", "downloadUrl": "https://api.spine.maxsystems.az/v1/public/brand/…" }],
"files": [{ "label": "Брендбук", "format": "pdf", "downloadUrl": "https://api.spine.maxsystems.az/v1/public/brand/…" }]
}
curl -X GET "https://api.spine.maxsystems.az/v1/brand" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/brand", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/brand",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/brand");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
Проект
/v1/projectproject:readКарточка вашего проекта одним запросом: что подключено и активно (домены, ящики, сертификаты со сроками, API-ключи и скоупы, вебхуки, Telegram-боты, бренд-кит, тарифы и счета) и что требует внимания (истекающие серты, просроченные счета). Поле client это ваш клиент-владелец: у одного клиента может быть несколько проектов. Self-view: ключ видит только СВОЙ проект.
{
"tenant": { "slug": "acme", "name": "Acme Inc.", "status": "active" },
"client": { "slug": "acme-group", "name": "Acme Group", "status": "active" },
"domains": [{ "domain": "acme.az", "whitelabel": true, "branding": true }],
"mail": { "domains": 1, "mailboxes": 8 },
"certificates": { "total": 2, "list": [{ "subject": "*.acme.az", "status": "active", "notAfter": "2026-10-14", "autoRenew": true }] },
"dns": { "managedZones": 1, "monitors": 0 },
"api": { "clients": 1, "keys": 2, "scopes": ["mail:send","mail:read"], "requests": 1240, "webhooks": 1 },
"telegram": { "bots": 1 },
"brand": { "hasProfile": true, "assets": 5 },
"billing": { "customers": 1, "activeSubscriptions": 2, "openInvoices": 1, "dueMinor": 5900, "currency": "AZN" },
"attention": [{ "kind": "cert-expiring", "text": "Сертификат *.acme.az истекает через 12 дн." }]
}
curl -X GET "https://api.spine.maxsystems.az/v1/project" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/project", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/project",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/project");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/project/auditaudit:readЖурнал событий вашего проекта: кто, что и когда менял (сертификаты, DNS, API-ключи, бренд, счета). Свежие сверху. Параметр limit, до 500.
limit | сколько событий вернуть, до 500 (по умолчанию 100) |
{ "events": [ { "ts": "2026-07-22T18:40:00Z", "action": "cert.issue", "target": "*.acme.az", "actor": "apikey:spine_sk_ab12cd", "source": "api" } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/project/audit" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/project/audit", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/project/audit",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/project/audit");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
Сайт / SEO
/v1/site-configsiteconfig:readСырой SEO/site-конфиг вашего проекта (meta по языкам, OG, аналитика, robots, sitemap, JSON-LD, verification). Нужен, только если рендерите <head> сами. Проще, брать готовые артефакты ниже (их отдаёт SPINE, ключ не нужен).
{ "tenant": "acme", "meta": { "en": { "title": "Acme", "description": "…" } }, "og": { "siteName": "Acme" }, "analytics": { "ga4": "G-XXXX" }, "robots": { "disallow": ["/admin"] } }
curl -X GET "https://api.spine.maxsystems.az/v1/site-config" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/site-config", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/site-config",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/site-config");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/site/{tenant}/robots.txtГотовый robots.txt проекта (публично, ключ не нужен). Проксируйте /robots.txt вашего сайта сюда, правим в консоли, у вас применяется без редеплоя.
User-agent: * Disallow: /admin Sitemap: https://acme.az/sitemap.xml
curl -X GET "https://api.spine.maxsystems.az/v1/site/{tenant}/robots.txt"
const res = await fetch("https://api.spine.maxsystems.az/v1/site/{tenant}/robots.txt", {
method: "GET"
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/site/{tenant}/robots.txt"
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/site/{tenant}/robots.txt");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);
/v1/site/{tenant}/sitemap.xmlГотовый sitemap.xml из URL проекта (публично). Проксируйте /sitemap.xml сюда.
<?xml version="1.0" encoding="UTF-8"?><urlset>…</urlset>
curl -X GET "https://api.spine.maxsystems.az/v1/site/{tenant}/sitemap.xml"
const res = await fetch("https://api.spine.maxsystems.az/v1/site/{tenant}/sitemap.xml", {
method: "GET"
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/site/{tenant}/sitemap.xml"
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/site/{tenant}/sitemap.xml");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);
/v1/site/{tenant}/headГотовый <head>-фрагмент: meta, canonical, Open Graph, Twitter, JSON-LD, verification-метки и сниппеты аналитики (GA4/Метрика/Pixel). Вставьте один include в <head>, SPINE рендерит всё сам. Параметры: path (текущий путь для canonical/og:url), lang (язык meta).
path | путь страницы для canonical и og:url (например /events/123) |
lang | язык meta-тегов (ключ из секции meta, например ru/en/az) |
<title>Acme</title> <meta name="description" content="…"> <meta property="og:image" content="…/og.png"> <script>…GA4…</script>
curl -X GET "https://api.spine.maxsystems.az/v1/site/{tenant}/head"
const res = await fetch("https://api.spine.maxsystems.az/v1/site/{tenant}/head", {
method: "GET"
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/site/{tenant}/head"
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/site/{tenant}/head");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);
/v1/site/{tenant}/og.pngБрендированная OG-картинка 1200×630, сгенерированная из бренд-кита проекта (лого, цвета). Параметры title/subtitle. Ссылку выдаёт /head автоматически, отдельно нужна, если хотите свою подпись на превью страницы.
title | заголовок на картинке (по умолчанию название бренда) |
subtitle | подзаголовок (необязательно) |
(image/png, картинка 1200×630)
curl -X GET "https://api.spine.maxsystems.az/v1/site/{tenant}/og.png"
const res = await fetch("https://api.spine.maxsystems.az/v1/site/{tenant}/og.png", {
method: "GET"
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/site/{tenant}/og.png"
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/site/{tenant}/og.png");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);
/v1/site/{tenant}/favicon.pngFavicon-набор из логотипа проекта: favicon.png/ico (32), apple-touch-icon.png (180), icon-192/512.png и site.webmanifest. Ссылки на них /head тоже проставляет сам. Меняете лого в бренд-ките, favicon у проекта обновляется.
(image/png, иконка)
curl -X GET "https://api.spine.maxsystems.az/v1/site/{tenant}/favicon.png"
const res = await fetch("https://api.spine.maxsystems.az/v1/site/{tenant}/favicon.png", {
method: "GET"
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/site/{tenant}/favicon.png"
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/site/{tenant}/favicon.png");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);
Секреты
/v1/secretssecrets:readВсе секреты проекта в окружении одной картой, грузите как переменные окружения при старте. Значения шифруются у нас (AES-256-GCM), отдаются только вашему ключу по TLS. Параметр env (по умолчанию prod).
env | окружение: prod|staging|dev|любое (по умолчанию prod) |
{ "env": "prod", "secrets": { "DB_PASSWORD": "…", "STRIPE_KEY": "…" } }
curl -X GET "https://api.spine.maxsystems.az/v1/secrets" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/secrets", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/secrets",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/secrets");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/secrets.envsecrets:readГотовый .env-файл со всеми секретами окружения (NAME="value"). Проект пишет его к себе одним запросом.
env | окружение (по умолчанию prod) |
DB_PASSWORD="…" STRIPE_KEY="…"
curl -X GET "https://api.spine.maxsystems.az/v1/secrets.env" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/secrets.env", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/secrets.env",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/secrets.env");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/secrets/getsecrets:readОдно значение по имени (имя в теле, имена могут быть путём вида db/password).
{ "env": "prod", "name": "db/password" }{ "name": "db/password", "env": "prod", "value": "…" }
curl -X POST "https://api.spine.maxsystems.az/v1/secrets/get" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"env":"prod","name":"db/password"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/secrets/get", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "env": "prod", "name": "db/password" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/secrets/get",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"env":"prod","name":"db/password"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/secrets/get");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"env":"prod","name":"db/password"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/secretssecrets:writeЗаписать свой секрет (создаёт новую версию, старые хранятся для отката).
{ "env": "prod", "name": "stripe/key", "value": "sk_live_…", "description": "ключ Stripe" }{ "name": "stripe/key", "env": "prod", "version": 2 }
curl -X PUT "https://api.spine.maxsystems.az/v1/secrets" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"env":"prod","name":"stripe/key","value":"sk_live_…","description":"ключ Stripe"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/secrets", {
method: "PUT",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "env": "prod", "name": "stripe/key", "value": "sk_live_…", "description": "ключ Stripe" })
});
const data = await res.json();
import requests
r = requests.put(
"https://api.spine.maxsystems.az/v1/secrets",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"env":"prod","name":"stripe/key","value":"sk_live_…","description":"ключ Stripe"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/secrets");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"env":"prod","name":"stripe/key","value":"sk_live_…","description":"ключ Stripe"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/secrets/generatesecrets:writeСгенерировать стойкий секрет и сохранить его. kind: base64|hex|alnum|uuid, length до 512. Значение возвращается один раз.
{ "env": "prod", "name": "jwt/secret", "kind": "base64", "length": 48 }{ "name": "jwt/secret", "env": "prod", "version": 1, "value": "…сгенерировано…" }
curl -X POST "https://api.spine.maxsystems.az/v1/secrets/generate" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"env":"prod","name":"jwt/secret","kind":"base64","length":48}'
const res = await fetch("https://api.spine.maxsystems.az/v1/secrets/generate", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "env": "prod", "name": "jwt/secret", "kind": "base64", "length": 48 })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/secrets/generate",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"env":"prod","name":"jwt/secret","kind":"base64","length":48}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/secrets/generate");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"env":"prod","name":"jwt/secret","kind":"base64","length":48}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/secrets/leasesecrets:readКороткоживущая выдача: одноразовый токен-ссылка на один секрет (или всю карту), чтобы передать значение процессу/человеку, не светя долгоживущий ключ. ttl в секундах (30..86400, по умолчанию 300), singleUse по умолчанию true.
{ "env": "prod", "name": "db/password", "ttl": 120 }{ "token": "spine_lease_…", "url": "https://api.spine.maxsystems.az/v1/secrets/lease/spine_lease_…", "expiresAt": "…", "singleUse": true }
curl -X POST "https://api.spine.maxsystems.az/v1/secrets/lease" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"env":"prod","name":"db/password","ttl":120}'
const res = await fetch("https://api.spine.maxsystems.az/v1/secrets/lease", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "env": "prod", "name": "db/password", "ttl": 120 })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/secrets/lease",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"env":"prod","name":"db/password","ttl":120}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/secrets/lease");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"env":"prod","name":"db/password","ttl":120}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/secrets/lease/{token}Погасить lease по токену (сам токен, доступ, ключ не нужен). Работает один раз до истечения TTL. Возвращает значение секрета (или карту, если lease на всю).
{ "env": "prod", "name": "db/password", "value": "…" }
curl -X GET "https://api.spine.maxsystems.az/v1/secrets/lease/{token}"
const res = await fetch("https://api.spine.maxsystems.az/v1/secrets/lease/{token}", {
method: "GET"
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/secrets/lease/{token}"
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/secrets/lease/{token}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);
Верификация
/v1/otp/sendotp:sendВыдать одноразовый код и доставить его: SMS или Telegram. Код у нас хранится только в виде отпечатка и не отдаётся никому, кроме получателя. Ключ с пометкой «песочница» ничего никуда не шлёт и возвращает код прямо в ответе, чтобы интеграцию можно было собрать без расхода денег. Условия канала при этом проверяются как в бою: нет учётки SMS - 503, пользователь не привязал Telegram - 409.
channel | sms (по умолчанию) | telegram |
to | номер телефона в международном виде (994501234567) или ваш идентификатор пользователя для Telegram |
purpose | зачем код: login, payment, reset. Попадает в журнал (необязательно) |
ttlSeconds | срок жизни кода, 60..3600 (по умолчанию из настроек проекта) |
codeLength | длина кода, 4..8 (по умолчанию 6) |
{ "channel": "sms", "to": "994501234567", "purpose": "login" }{ "verificationId": "3f2a…", "channel": "sms", "to": "994501234567", "expiresAt": "…", "resendAfter": "…", "attemptsLeft": 5, "delivery": "sent", "sandbox": false }
curl -X POST "https://api.spine.maxsystems.az/v1/otp/send" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"channel":"sms","to":"994501234567","purpose":"login"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/send", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "channel": "sms", "to": "994501234567", "purpose": "login" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/otp/send",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"channel":"sms","to":"994501234567","purpose":"login"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/otp/send");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"channel":"sms","to":"994501234567","purpose":"login"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/otp/verifyotp:verifyПроверить код. Правильный путь, по verificationId; можно и по получателю, тогда берётся его последний активный код. Успех гасит код. Ответ всегда объясняет причину отказа: no_active_code, expired, wrong_code, too_many_attempts.
verificationId | идентификатор из ответа на send (рекомендуется) |
to | получатель, если verificationId не сохраняли |
channel | нужен вместе с to, чтобы одинаково нормализовать номер |
code | код от пользователя (обязателен) |
{ "verificationId": "3f2a…", "code": "123456" }{ "valid": true, "verificationId": "3f2a…", "channel": "sms", "to": "994501234567", "purpose": "login" }
curl -X POST "https://api.spine.maxsystems.az/v1/otp/verify" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"verificationId":"3f2a…","code":"123456"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/verify", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "verificationId": "3f2a…", "code": "123456" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/otp/verify",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"verificationId":"3f2a…","code":"123456"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/otp/verify");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"verificationId":"3f2a…","code":"123456"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/otp/resendotp:sendПовторная отправка в рамках той же верификации: выдаётся новый код, счётчик неверных попыток сохраняется. Пауза между повторами своя (по умолчанию 60 секунд), раньше срока приходит 429.
{ "verificationId": "3f2a…" }{ "verificationId": "3f2a…", "expiresAt": "…", "resendAfter": "…", "delivery": "sent" }
curl -X POST "https://api.spine.maxsystems.az/v1/otp/resend" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"verificationId":"3f2a…"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/resend", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "verificationId": "3f2a…" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/otp/resend",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"verificationId":"3f2a…"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/otp/resend");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"verificationId":"3f2a…"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/otp/verifications/{id}otp:verifyСостояние попытки: подтверждена или нет, сколько попыток осталось, что с доставкой. Кода здесь нет.
{ "verificationId": "3f2a…", "verified": false, "attemptsLeft": 4, "delivery": "sent", "sandbox": false }
curl -X GET "https://api.spine.maxsystems.az/v1/otp/verifications/{id}" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/verifications/{id}", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/otp/verifications/{id}",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/otp/verifications/{id}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/otp/verifications/{id}/deliveryotp:verifyСпросить оператора, что стало с сообщением. «Приняли в очередь» и «доставлено» это разные вещи, поэтому статус доставки отдельный: delivered, pending, failed или unknown.
{ "delivery": "delivered", "note": "доставлено" }
curl -X GET "https://api.spine.maxsystems.az/v1/otp/verifications/{id}/delivery" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/verifications/{id}/delivery", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/otp/verifications/{id}/delivery",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/otp/verifications/{id}/delivery");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/otp/settingsotp:verifyЧто доступно вашему проекту: включённые каналы, длина кода, срок жизни, пауза между повторами и действующие лимиты. Полезно показать пользователю честный таймер вместо отказа наугад.
{ "project": "ваш-проект", "channels": ["sms","telegram","totp"], "codeLength": 6, "ttlSeconds": 300, "limits": { "perIdentifierHour": 5, "perIdentifierDay": 15, "perProjectDay": 500, "perIpHour": 20 } }
curl -X GET "https://api.spine.maxsystems.az/v1/otp/settings" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/settings", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/otp/settings",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/otp/settings");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/otp/telegram/linkotp:sendСсылка привязки Telegram. Бот не может написать человеку первым, поэтому сначала пользователь открывает эту ссылку и нажимает «Старт» у бота вашего проекта, и только после этого коды доставляются в Telegram. Ссылка одноразовая и живёт 15 минут. Если пользователь уже привязан, вернётся linked: true.
{ "identifier": "user-42" }{ "url": "https://t.me/вашбот?start=otp_…", "bot": "вашбот", "expiresAt": "…", "linked": false }
curl -X POST "https://api.spine.maxsystems.az/v1/otp/telegram/link" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"identifier":"user-42"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/telegram/link", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "identifier": "user-42" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/otp/telegram/link",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"identifier":"user-42"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/otp/telegram/link");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"identifier":"user-42"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/otp/telegram/linkotp:verifyПривязан ли пользователь к Telegram (параметр identifier). Спрашивайте до отправки, чтобы показать человеку кнопку привязки, а не ошибку.
{ "linked": true, "username": "ivan", "linkedAt": "…", "revoked": false }
curl -X GET "https://api.spine.maxsystems.az/v1/otp/telegram/link" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/telegram/link", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/otp/telegram/link",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/otp/telegram/link");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/otp/telegram/linkotp:sendОтвязать Telegram у пользователя (identifier в теле или в query).
{ "removed": true }
curl -X DELETE "https://api.spine.maxsystems.az/v1/otp/telegram/link" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/telegram/link", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/otp/telegram/link",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/otp/telegram/link");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/otp/totp/enrollotp:totpНачать подключение приложения-аутентификатора (Google Authenticator, 1Password и любое другое по RFC 6238). Возвращает секрет и otpauth-ссылку для QR-кода. До подтверждения кодом TOTP выключен: иначе человек запишет секрет с ошибкой и потеряет доступ, думая, что всё настроил.
{ "identifier": "user-42", "issuer": "Ваш проект" }{ "secret": "JBSWY3DP…", "otpauthUrl": "otpauth://totp/…", "enabled": false }
curl -X POST "https://api.spine.maxsystems.az/v1/otp/totp/enroll" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"identifier":"user-42","issuer":"Ваш проект"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/totp/enroll", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "identifier": "user-42", "issuer": "Ваш проект" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/otp/totp/enroll",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"identifier":"user-42","issuer":"Ваш проект"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/otp/totp/enroll");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"identifier":"user-42","issuer":"Ваш проект"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/otp/totp/confirmotp:totpПодтвердить подключение кодом из приложения и включить TOTP.
{ "identifier": "user-42", "code": "123456" }{ "enabled": true }
curl -X POST "https://api.spine.maxsystems.az/v1/otp/totp/confirm" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"identifier":"user-42","code":"123456"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/totp/confirm", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "identifier": "user-42", "code": "123456" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/otp/totp/confirm",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"identifier":"user-42","code":"123456"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/otp/totp/confirm");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"identifier":"user-42","code":"123456"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/otp/totp/verifyotp:totpПроверить код из приложения либо резервный код. Один и тот же код второй раз не принимается (reason: code_already_used), резервный код после использования вычёркивается.
{ "identifier": "user-42", "code": "123456" }{ "valid": true }
curl -X POST "https://api.spine.maxsystems.az/v1/otp/totp/verify" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"identifier":"user-42","code":"123456"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/totp/verify", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "identifier": "user-42", "code": "123456" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/otp/totp/verify",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"identifier":"user-42","code":"123456"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/otp/totp/verify");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"identifier":"user-42","code":"123456"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/otp/totp/backup-codesotp:totpВыпустить резервные коды (10 штук). Показываются один раз, у нас хранятся только отпечатки. Прежний список при этом перестаёт работать.
{ "identifier": "user-42" }{ "codes": ["A1B2C3D4E5", "…"] }
curl -X POST "https://api.spine.maxsystems.az/v1/otp/totp/backup-codes" \
-H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{"identifier":"user-42"}'
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/totp/backup-codes", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ",
"Content-Type": "application/json"
},
body: JSON.stringify({ "identifier": "user-42" })
});
const data = await res.json();
import requests
r = requests.post(
"https://api.spine.maxsystems.az/v1/otp/totp/backup-codes",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type": "application/json"},
data='''{"identifier":"user-42"}'''
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/otp/totp/backup-codes");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"identifier":"user-42"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/otp/totpotp:totpСостояние TOTP пользователя (параметр identifier): подключён, включён, сколько резервных кодов осталось.
{ "enrolled": true, "enabled": true, "backupLeft": 8, "backupUsed": 2 }
curl -X GET "https://api.spine.maxsystems.az/v1/otp/totp" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/totp", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/otp/totp",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/otp/totp");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/otp/totpotp:totpОтключить TOTP у пользователя и удалить его секрет.
{ "removed": true }
curl -X DELETE "https://api.spine.maxsystems.az/v1/otp/totp" \ -H "Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/totp", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/otp/totp",
headers={"Authorization": "Bearer spine_sk_ВАШ_КЛЮЧ"}
)
print(r.status_code, r.json())
$ch = curl_init("https://api.spine.maxsystems.az/v1/otp/totp");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer spine_sk_ВАШ_КЛЮЧ"],
]);
$data = json_decode(curl_exec($ch), true);
Проверка подписи и доставкаКаждая доставка, POST с заголовками X-Spine-Event (тип события) и X-Spine-Signature: sha256=<hex>. Тело: {"event":"…","ts":"…","data":{…}}.
Подпись = HMAC-SHA256 от сырого тела запроса (байты как пришли, до разбора JSON). Ключ, секрет whsec_… как есть, строкой целиком: НЕ декодировать из base64 и НЕ срезать префикс (это не Svix). Результат, hex, с префиксом sha256=. Сравнивайте в константное время.
const crypto = require("crypto");
function verifySpineWebhook(secret, rawBody, sigHeader) {
// secret, строка ЦЕЛИКОМ, включая префикс whsec_ (не декодировать/не обрезать)
// rawBody, СЫРОЕ тело запроса (Buffer/строка), до JSON.parse
// sigHeader- значение заголовка X-Spine-Signature ("sha256=…")
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(sigHeader || ""), b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Одна немедленная попытка; при неуспехе, durable-ретраи с экспоненциальным бэкоффом (≈ через 1 мин, 5 мин, 30 мин, 2 ч, 6 ч, 12 ч, до 7 попыток за ~21 час), очередь переживает перезапуск. Успех = ваш ответ 2xx. Лог доставок, GET /v1/webhooks/{id}/deliveries (видно статус, коды и точное подписанное тело); переотправить конкретную, POST /v1/webhooks/{id}/deliveries/{deliveryId}/redeliver. Отвечайте быстро, тяжёлую обработку, в фон (доставка «хотя бы один раз», делайте обработчик идемпотентным).
Для входящего письма отдаём его метаданные (message), RFC-заголовки треда (rfc) и сигналы фрода (security):
"data": {
"mailbox": "you@ваш-домен.az", "mailboxId": "c",
"folder": "Inbox", "mailboxRole": "inbox", // где лежит письмо (роль: inbox|junk|trash|archive|null)
"message": {
"id": "eaaaaab", // per-mailbox id (НЕ глобальный, см. ниже)
"from": [{ "email": "sender@bank.az" }], "to": [{ "email": "you@ваш-домен.az" }],
"subject": "…", "receivedAt": "…", "preview": "…"
},
"rfc": {
"messageId": "abc123@bank.az", // строка или null: ключ письма, БЕЗ угловых скобок
"inReplyTo": ["prev@you.az"], // ВСЕГДА массив (пустой = письмо не ответ)
"references": ["root@…", "prev@you.az"] // ВСЕГДА массив: цепочка треда от корня
},
"delivery": {
"type": "inbound", // local | inbound | imported
"internal": false, // true = письмо не выходило за пределы платформы
"receivedBy": "mail.spine.maxsystems.az" // наш узел, принявший письмо (только у inbound)
},
"security": {
"spam": { "isSpam": false, "score": 2.9, "status": "No", "label": "ham", "symbols": "DKIM_VALID(-0.5), …" },
"auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass" }
}
}
Папки. mail.received приходит только на ПОЛУЧЕННЫЕ письма: «Входящие», «Спам», «Архив» и свои папки сотрудника. По «Черновикам» и «Корзине» событий нет ни при какой подписке (черновик, который почтовый клиент сохраняет каждые 25 секунд, это не письмо, а в корзину письмо попадает уже после того, как о нём сообщили). Письмо, отправленное сотрудником из ящика, приходит отдельным событием mail.sent_folder с mailboxRole: "sent": подпишитесь на него, если ведёте переписку целиком, и не подписывайтесь, если нужны только входящие.
delivery. У письма между ящиками ОДНОГО проекта нет ни SPF, ни DKIM, ни DMARC: письмо никуда не уходило, проверять было нечего, и security.auth будет пустым. Чтобы такое письмо не путать с подделкой вашего домена извне, смотрите delivery.internal. Признак строится на ПУТИ письма, а не на адресе отправителя: любое письмо снаружи получает от нашего узла строку приёма, подделать её отправитель не может. imported, письмо заведено переносом с прежнего хостинга, а не получено почтой.
Идентификаторы: message.id, короткий per-mailbox id (для чтения того же письма через API), НЕ глобальный. Как глобально-уникальный ключ (идемпотентность, тредирование) используйте rfc.messageId; связывайте ответы в тред по rfc.inReplyTo/rfc.references.
security: score, оценка спам-фильтра (стиль rspamd; больше = подозрительнее), isSpam, превысил ли порог; spf/dkim/dmarc, подлинность отправителя (pass/fail/none…). Если письмо без этих заголовков, поле может быть null.
Жизненный путь письма: mail.sent (релей принял) → mail.delivered (сервер получателя принял) либо mail.bounced / mail.complained / mail.delayed. Ключ письма, messageId (тот же id, что вернула отправка).
"data": {
"messageId": "01KYCGV14SS0FPTFD4VRK7TF0S", // id письма в SPINE (вернулся при отправке)
"to": "user@example.com", "from": "noreply@ваш-домен",
"subject": "…", "campaign": "welcome-july",
"relay": "ses", // каким релеем ушло
"relayMessageId": "0107019f…", // id письма у релея
"bounce": { "type": "Permanent", "subType": "General", "diagnostic": "smtp; 550 5.1.1 …" }
}
Жёсткий отказ и жалоба блокируют адрес: он попадает в блок-лист проекта (GET /v1/mail/suppressions), и следующие письма на него возвращают статус suppressed вместо отправки, с полями reason и scope. Это защищает репутацию домена: у Amazon отказы должны быть ниже 5%, жалобы ниже 0.1%. Область блокировки зависит от причины: несуществующий ящик закрывается для всего проекта и обоих потоков, а жалоба и отписка, только для marketing и только у своего scope, поэтому квитанция об оплате уйдёт даже тому, кто отказался от новостей.
Открытия и клики считает сама платформа: в HTML подставляется прозрачный пиксель, ссылки заменяются на наш редирект (домен трекинга ваш, если включён t.ваш-домен). Поле automated: true означает, что открытие сделал не человек, а прокси или сканер почтового провайдера, такие обычно не считают.
"data": {
"messageId": "01KYCGV14SS0FPTFD4VRK7TF0S",
"to": "user@example.com", "campaign": "welcome-july",
"url": "https://example.com/страница", // только у mail.clicked
"count": 1, // какое это по счёту открытие/клик
"automated": false, // true = прокси или сканер, не человек
"userAgent": "…", "ip": "85.132.72.134", // адрес открывшего; null, если определить не вышло
"geo": { "country": "AZ", "countryName": "Azerbaijan", "continent": "AS",
"city": "Baku", "region": "Baki", "lat": 40.37, "lon": 50.08 }
}
Про ip и geo: страну определяет сама платформа, вам не нужна своя база: в событии сразу приходит geo с кодом страны, названием и континентом (null, если определить не вышло). Готовый разрез по странам есть и в GET /v1/mail/stats, поле byCountry. Есть и город с регионом, и приблизительные координаты для карты: это центр населённого пункта, а не точка человека, и на бесплатной базе точность на уровне города или области. Для географии считайте в первую очередь клики. У открытий адрес часто принадлежит прокси почтового провайдера (Gmail грузит картинки через свои серверы), и по нему вы увидите дата-центр, а не город получателя, такие открытия помечены automated: true и в byCountry они не попадают. Данные о странах: IP Geolocation by DB-IP (CC BY 4.0).
mail.unsubscribed приходит, когда получатель нажал отписку (в том числе кнопкой самого Gmail или Yahoo, отписка в один клик по RFC 8058, включается полем unsubscribe: true). Адрес сразу попадает в блок-лист.
https://api.spine.maxsystems.az · машинное описание /v1/openapi.json