SPINE Developer API
One platform API for external projects: mail, SSL certificates, DNS, mailboxes, domains, webhooks and Telegram bots, all under a single key.
BASE ·https://api.spine.maxsystems.az
🔑 Authentication
The Authorization: Bearer spine_sk_… header in every request. The key is issued in the console, shown once, and revoked in one click.
⚙️ Metering and limits
Each key counts requests; the default limit is 120/min (configurable). On overflow, 429. Daily/monthly quotas are optional.
⚠️ Error codes
401 invalid key · 403 missing scope · 429 rate limit · 400 bad request (the error field).
| Scope | What it allows |
|---|---|
mail:send | Sending emails (single and batch, status and stats of what was sent) |
mail:read | Reading emails from your mailboxes |
mail:suppressions | Blocked addresses: view, add, remove the block |
mail:mailboxes | Creating and managing mailboxes |
mail:domains | Connecting domains and their status |
mail:templates | Шаблоны писем проекта: смотреть, править, откатывать, слать тест |
notify:send | Уведомления одним запросом: почта, Telegram, вебхук, с запасными каналами |
mail:subscriptions | Списки рассылок и подписки: подписать, отписать, посмотреть согласия |
protection:verify | Проверка ответа Turnstile для форм вашего сайта |
domains:read | Сроки регистрации своих доменов: когда истекает, у какого регистратора |
mail:migrate | Перенос почты с другого хостинга по IMAP (задания и прогресс) |
cert:issue | Ordering and reissuing certificates |
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 | Reading the zone DNS records |
dns:records | Managing the zone DNS records |
webhooks | Subscribing to webhooks (incoming emails, delivery events) |
telegram:bots | Managing the project Telegram bots |
webmail:login | Employee auto-login links to webmail (SSO for embedding) |
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:sendSend an email. The sender domain must be connected to SPINE mail and belong to the project.
from | sender address (required) |
to | recipient (required) |
subject | subject (or template) |
text / html | email body |
category | label for statistics (optional) |
cc / bcc / replyTo | lists of addresses (optional) |
attachments | attachments: [{ filename, content (base64), contentType }], up to 15 MB per message |
headers | your own message headers, an object (optional) |
stream | transactional (по умолчанию) | marketing. Транзакционный поток не блокируется отписками и жалобами: платёжный документ получатель должен получить, даже если отказался от новостей. Жёсткий отказ (несуществующий ящик) действует в обоих потоках |
scope | владелец отношения с получателем (например slug мерчанта). Жалобы и отписки ложатся именно на него, а не на весь проект. Пусто = весь проект, как раньше |
campaign | campaign label: the funnel in /v1/mail/stats is grouped by it |
track | true means count opens and clicks (events mail.opened / mail.clicked) |
unsubscribe | true adds one-click unsubscribe (RFC 8058, required by Gmail and Yahoo) |
sendAt | an ISO 8601 date for scheduled sending (up to 90 days, cancel via DELETE) |
idempotencyKey | duplicate protection (optional) |
{
"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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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:sendSend a batch: up to 200 recipients per request, each with its own vars. Shared fields go at the top level.
messages | list of messages: [{ to, vars, subject?, html?, attachments? }] (required) |
from / subject / html / text / template | shared fields for every message in the batch |
campaign / track / unsubscribe / sendAt | same as in a single send, applied to the whole batch |
{
"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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/by-message-id", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
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_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/messagesmail:sendList of your sent emails with their statuses.
limit | how many to return (up to 200, 50 by default) |
status | filter: sent | delivered | bounced | complained | delayed | scheduled | suppressed | failed |
campaign | filter by campaign label |
{ "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/messages", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mail/messages",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/messages/{id}mail:sendMessage status and its full history: accepted by the relay, delivered, opened, link clicked.
{ "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/messages/{id}", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mail/messages/{id}",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/messages/{id}mail:sendCancel a scheduled email while it has not been sent yet.
{ "id": "01K…", "status": "canceled" }
curl -X DELETE "https://api.spine.maxsystems.az/v1/mail/messages/{id}" \
-H "Authorization: Bearer spine_sk_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/messages/{id}", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/mail/messages/{id}",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/statsmail:sendDeliverability funnel for a period: accepted, delivered, opened, clicks, bounces, complaints. Keep bounce and complaint rates below the Amazon thresholds (5% and 0.1%).
days | period in days (1..365, 30 by default) |
campaign | for a single campaign only |
{ "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/stats", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mail/stats",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/{msgId}/raw", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
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_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/suppressionsmail:suppressionsBlocked addresses of the project: hard bounces, spam complaints, unsubscribes. Messages to them are not sent.
email | check a single address |
scope | блокировки этого владельца плюс общепроектные (они действуют на всех) |
stream | transactional, показать только то, что блокирует платёжные письма |
limit | how many to return (up to 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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/suppressions", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mail/suppressions",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/suppressionsmail:suppressionsBlock an address manually (for example, at the recipient's request).
email | address (required) |
reason | hard_bounce | complaint | manual | unsubscribe (manual by default) |
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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/suppressions/{email}", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/mail/suppressions/{email}",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mail/policymail:sendYour own limits and rules: attachment sizes, blocked file types, the antivirus mode, sending caps. Handy so you do not have to guess why a message was rejected.
{ "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/policy", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mail/policy",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/lists", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mail/lists",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/lists/{slug}", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/mail/lists/{slug}",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/subscriptions", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mail/subscriptions",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/subscriptions", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/mail/subscriptions",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail/templates", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mail/templates",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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);
Certificates
/v1/certs/issuecert:issueOrder a public SSL certificate. The domain must be in a managed zone or have a CNAME delegation, and the delegation is needed for EVERY name in the certificate: ownership is verified per name, each via its own _acme-challenge.<name> (the exception is a wildcard, verified via the domain record). Issuance takes 15-30 sec. The lifetime is not up to the client: under CA/Browser Forum rules a public certificate lives 199 days (100 from 2027, 47 from 2029). Need a longer one, see POST /v1/pki/issue. ⚠️ A pinWarning field in the response means these domains already have ANOTHER certificate with embedded pins: the new one has its own key, so its pin differs. If you need the previous pin, reissue the existing certificate instead of ordering a second one.
domains | list of domains, wildcard *.domain allowed (required) |
csr | your own CSR, then the private key stays with you (optional) |
autoRenew | auto-renewal, true by default |
{ "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"domains":["app.ваш-домен","*.ваш-домен"],"autoRenew":true}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/certs/{id}/statuscert:readCertificate status (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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/certs/{id}/status", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/certs/{id}/status",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/certs/{id}/downloadcert:readDownload the certificate (fullchain + private key; for CSR-based certs privkey=null).
{ "fullchain": "-----BEGIN CERTIFICATE-----…", "privkey": "…" }
curl -X GET "https://api.spine.maxsystems.az/v1/certs/{id}/download" \
-H "Authorization: Bearer spine_sk_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/certs/{id}/download", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/certs/{id}/download",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/certs/{id}/pinscert:readSPKI pins of the certificate. This is what partners embed instead of the certificate itself: the platform keeps the private key OF THIS certificate, so the pin survives both auto-renewal and a manual reissue. ⚠️ The pin changes if you create a SECOND certificate for the same domains instead of reissuing the first, delete and recreate it, turn off Keep key, or request a key rotation. We return the current and the backup pin, plus ready-made strings for curl and 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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/certs/{id}/pins", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/certs/{id}/pins",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/certs/{id}/pins/registercert:readRecord that the pin is embedded somewhere. The entry goes into the registry, and before a key change the platform warns you who will be affected.
{ "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"party":"Партнёр X","system":"Java-приложение","contact":"dev@partner.com"}',
]);
$data = json_decode(curl_exec($ch), true);
PKI and ACME
/v1/acme/eabcert:acmeIssue yourself an External Account Binding pair for our ACME server. After that any standard client works with the platform without our code: certbot, acme.sh, cert-manager, Caddy, Traefik, FortiGate 7.6.3+, Cisco ASA 9.23.1+. The HMAC key is returned ONCE.
label | a label for yourself (optional) |
allowedDomains | restrict domains (empty = any available) |
issuer | letsencrypt (default) or private |
policy | auto (we pass the check ourselves) or verify (the client passes it) |
{ "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"label":"prod-cluster","allowedDomains":["example.com"]}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/acme/delegationscert:delegateCreate a CNAME ownership validation delegation. Needed for domains whose DNS we do not run: the client adds one record once, after that we run every check on our side and need no access to their 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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"domain":"client-domain.com"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/acme/delegations/{id}/verifycert:delegateCheck that the client added the CNAME. Read-only DNS lookup, changes nothing.
{ "ok": true, "expected": "…", "found": ["…"] }
curl -X POST "https://api.spine.maxsystems.az/v1/acme/delegations/{id}/verify" \
-H "Authorization: Bearer spine_sk_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/acme/delegations/{id}/verify", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
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_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/acme/delegationscert:delegateList of the project delegations with their state.
{ "delegations": [{ "domain": "client-domain.com", "verified": true }] }
curl -X GET "https://api.spine.maxsystems.az/v1/acme/delegations" \ -H "Authorization: Bearer spine_sk_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/acme/delegations", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/acme/delegations",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/pki/issuepki:issueIssue a certificate from the platform private CA. CA/Browser Forum rules do not apply to a private CA, so you set the lifetime: up to 3650 days. Suitable for partner integrations, internal services and mTLS client certificates.
domains | domains for a server certificate |
commonName | holder name for an mTLS client certificate |
days | lifetime in days, 397 by default, 3650 maximum |
certType | server (default) or client |
csr | your own CSR: the private key stays with you (optional) |
{ "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"domains":["internal.example.com"],"days":1095,"certType":"server"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/pki/rootpki:readThe root certificate of the private CA and its chain. This is what a partner installs into their truststore once and lives with for years: reissuing end certificates does not affect it.
{ "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/pki/root", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/pki/root",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/pki/crlpki:readThe revoked certificate list (CRL) of the private CA in PEM format.
curl -X GET "https://api.spine.maxsystems.az/v1/pki/crl" \ -H "Authorization: Bearer spine_sk_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/pki/crl", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/pki/crl",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/pki/authoritiespki:readCertificate authorities available to the project: names, kind and validity.
{ "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/pki/authorities", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/pki/authorities",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
Mailboxes
/v1/mailboxesmail:mailboxesCreate a mailbox on your own domain.
name | mailbox name before @ (2-40, [a-z0-9._-]) |
domain | project domain (required) |
password | mailbox password (min. 8) |
description | description (optional) |
{ "name": "info", "domain": "ваш-домен", "password": "…" }{ "email": "info@ваш-домен", "id": "…" }
curl -X POST "https://api.spine.maxsystems.az/v1/mailboxes" \
-H "Authorization: Bearer spine_sk_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"name":"info","domain":"ваш-домен","password":"…"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/mailboxesmail:mailboxesList of mailboxes and groups on the project domains. The type field: personal, shared (several people have access), group (distribution group). Groups have a membersCount field.
{ "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mailboxes",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mailboxes/{id}mail:mailboxesDelete a mailbox (only on your own domain).
{ "deleted": true }
curl -X DELETE "https://api.spine.maxsystems.az/v1/mailboxes/{id}" \
-H "Authorization: Bearer spine_sk_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/mailboxes/{id}",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/access", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mailboxes/{id}/access",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/access/{granteeId}", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
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_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mailboxes/{id}/messagesmail:readList of the latest emails in a mailbox (newest first). You can filter by address, handy for a contact conversation panel.
limit | how many emails to return, up to 100 (20 by default) |
with | contact address: emails FROM or TO them (conversation with a counterparty) |
from | filter by sender (address substring) |
to | filter by recipient (address substring) |
{ "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mailboxes/{id}/messages/{msgId}mail:readThe full email: body, RFC Message-ID/In-Reply-To/References (for threading), raw headers and the list of attachments with ids to download. Note: the id in the path is per-mailbox (not global); the global key is messageId. A mailbox of another project answers 404, not 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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/{msgId}", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
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_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mailboxes/{id}/messages/{msgId}/attachmentsmail:readMessage attachments: name, type, size, the id to download and the antivirus verdict. You do not need to fetch the file to learn that it is infected or blocked by policy.
{ "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/{msgId}/attachments", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
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_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/mailboxes/{id}/messages/{msgId}/attachments/{attId}mail:readDownload an attachment. It is streamed as the file itself (not base64), with type and name headers. Before serving, the file is scanned: infected files and types blocked by policy are not served.
attId | the attachment id from the attachment list of the same message |
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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/messages/{msgId}/attachments/{attId}", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
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_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mailboxes/{id}/unread", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mailboxes/{id}/unread",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail-migrations", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mail-migrations",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail-migrations/{id}", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/mail-migrations/{id}",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/mail-migrations/{id}", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/mail-migrations/{id}",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
Groups
/v1/groupsmail:mailboxesList of the project distribution groups. A group is an address team@domain; an email sent to it fans out to all members in their personal mailboxes.
{ "groups": [ { "id": "…", "email": "team@ваш-домен", "membersCount": 4 } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/groups" \ -H "Authorization: Bearer spine_sk_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/groups", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/groups",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/groupsmail:mailboxesCreate a distribution group name@domain. The domain must belong to your project.
domain | project domain (required) |
name | address name, 2-40 characters [a-z0-9._-] (required) |
description | group description (optional) |
{ "domain": "ваш-домен.az", "name": "team", "description": "Вся команда" }{ "id": "…", "email": "team@ваш-домен.az" }
curl -X POST "https://api.spine.maxsystems.az/v1/groups" \
-H "Authorization: Bearer spine_sk_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"domain":"ваш-домен.az","name":"team","description":"Вся команда"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/groups/{id}mail:mailboxesDelete a group.
{ "deleted": true }
curl -X DELETE "https://api.spine.maxsystems.az/v1/groups/{id}" \
-H "Authorization: Bearer spine_sk_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/groups/{id}", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/groups/{id}",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/groups/{id}/membersmail:mailboxesGroup members (mailboxes the mail fans out to).
{ "members": [ { "id": "…", "email": "ivan@ваш-домен.az" } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/groups/{id}/members" \
-H "Authorization: Bearer spine_sk_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/groups/{id}/members", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/groups/{id}/members",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/groups/{id}/membersmail:mailboxesAdd a mailbox to the group by its address.
{ "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"mailbox":"ivan@ваш-домен.az"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/groups/{id}/members/{userId}mail:mailboxesRemove a member from the group.
{ "ok": true }
curl -X DELETE "https://api.spine.maxsystems.az/v1/groups/{id}/members/{userId}" \
-H "Authorization: Bearer spine_sk_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/groups/{id}/members/{userId}", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
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_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
Webmail (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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"accountId":"id ящика","emailId":"id черновика","sendAt":1785400000000}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/webmail/login-linkwebmail:loginIssue an employee a one-time auto-login link to THEIR own webmail mailbox, so you can embed mail into your app (e.g. a CRM). The link lives 60 seconds and is single-use: on opening it (in a new tab or in an iframe) the employee lands in mail without a second login. The mailbox must be on your domain.
mailbox | the employee full mailbox address on your domain (required) |
{ "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"mailbox":"ivan@ваш-домен.az"}',
]);
$data = json_decode(curl_exec($ch), true);
Domains
/v1/maildomainsmail:domainsConnect a domain to mail (creates the domain and, when possible, publishes DNS itself).
{ "name": "ваш-домен" }{ "id": "…", "name": "ваш-домен", "autoDns": { "created": 6 }, "records": [...], "ready": false }
curl -X POST "https://api.spine.maxsystems.az/v1/maildomains" \
-H "Authorization: Bearer spine_sk_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"name":"ваш-домен"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/maildomainsmail:domainsList of the project connected domains.
{ "domains": [ { "id": "…", "name": "ваш-домен" } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/maildomains" \ -H "Authorization: Bearer spine_sk_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/maildomains", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/maildomains",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/maildomains/{domain}mail:domainsDNS records and the domain verification status.
{ "name": "ваш-домен", "records": [...], "ready": true }
curl -X GET "https://api.spine.maxsystems.az/v1/maildomains/{domain}" \
-H "Authorization: Bearer spine_sk_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/maildomains/{domain}", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/maildomains/{domain}",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/maildomains/{domain}/catch-allmail:domainsIntake of mail sent to addresses that do not exist on the domain: whether it is on, which mailbox it goes to, and whether we reply to the sender. A warning field appears when the intake points at a person's own mailbox.
{ "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/maildomains/{domain}/catch-all", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
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_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/maildomains/{domain}/catch-allmail:domainsTurn the intake on, change it or turn it off. The mailbox must exist and belong to this same domain. While the intake is on, the sender gets an automatic reply in English: there is no such address, answering it makes no sense, here is the support address of the domain. Only real people get that reply: mailing lists, robots and bounce reports get nothing, and one address hears from us at most once a day.
mailbox | the intake mailbox on this domain, or null to turn it off (then mail to non-existent addresses is rejected outright) |
autoReply | whether to tell the sender that the address does not exist (true by default) |
{ "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/domains/mine", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/domains/mine",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/security/report", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/security/report",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
DNS
/v1/dns/{zone}/recordsdns:readList of the zone DNS records (the zone must be linked to the project).
{ "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/dns/{zone}/records", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/dns/{zone}/records",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/dns/{zone}/recordsdns:recordsCreate a DNS record in the project zone.
type | A | AAAA | TXT | MX | CNAME | NS | SRV | CAA |
name | record name ('@' for the root) |
data | value |
priority / ttl | for MX / TTL (optional) |
{ "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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:recordsDelete a DNS record. For GoDaddy add ?data=<value>.
{ "deleted": true }
curl -X DELETE "https://api.spine.maxsystems.az/v1/dns/{zone}/records/{id}" \
-H "Authorization: Bearer spine_sk_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/dns/{zone}/records/{id}", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
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_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
Webhooks
/v1/webhookswebhooksSubscribe to events: the platform will send a POST to your URL. The secret in the response is shown once.
url | your https receiving address (required) |
events | mail in a mailbox: mail.received (received), mail.sent_folder (an employee sent it from the mailbox) · sending via API: mail.sent, mail.delivered, mail.bounced, mail.complained, mail.delayed, mail.rejected · engagement (with 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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"url":"https://ваш-сервер/webhook","events":["mail.received","mail.bounced"]}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/webhookswebhooksList of your webhooks.
{ "webhooks": [ { "id": "…", "url": "…", "events": [...], "active": true } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/webhooks" \ -H "Authorization: Bearer spine_sk_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/webhooks", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/webhooks",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/webhooks/{id}webhooksUpdate the subscription: event list, receiving URL, enable or disable. THE SECRET DOES NOT CHANGE, so your signature verifier stays as is. Send only the fields you are changing.
url | new https receiving URL (optional) |
events | the full new event list (optional) |
active | false pauses delivery without losing the subscription and its log (optional) |
{ "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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-secretwebhooksRotate the signing secret (leak, staff change). The new one is shown ONCE. The old one stops working immediately: events are signed with the new secret, and until you replace it on your side you will reject them.
{ "id": "…", "secret": "whsec_…" }
curl -X POST "https://api.spine.maxsystems.az/v1/webhooks/{id}/rotate-secret" \
-H "Authorization: Bearer spine_sk_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/webhooks/{id}/rotate-secret", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
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_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/webhooks/{id}webhooksDelete a webhook. To simply change events or the URL use PATCH: re-creating issues a new secret.
{ "deleted": true }
curl -X DELETE "https://api.spine.maxsystems.az/v1/webhooks/{id}" \
-H "Authorization: Bearer spine_sk_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/webhooks/{id}", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/webhooks/{id}",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/webhooks/{id}/deliverieswebhooksWebhook delivery log: status (pending/delivered/failed), codes, attempts and the exact signed body, handy for debugging the signature.
{ "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/webhooks/{id}/deliveries", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/webhooks/{id}/deliveries",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/webhooks/{id}/deliveries/{deliveryId}/redeliverwebhooksRedeliver a specific delivery now (for example, after you have fixed the signature verifier).
{ "status": "delivered", "httpCode": 200 }
curl -X POST "https://api.spine.maxsystems.az/v1/webhooks/{id}/deliveries/{deliveryId}/redeliver" \
-H "Authorization: Bearer spine_sk_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/webhooks/{id}/deliveries/{deliveryId}/redeliver", {
method: "POST",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
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_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
Telegram bots
/v1/telegram/mybotstelegram:botsConnect an existing bot to the platform by its token (from 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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"token":"123456:ABC-…","note":"бот проекта"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/telegram/mybots/bulktelegram:botsBulk import: connect many bots at once from a list of tokens (from BotFather). tokens is an array or text (one token per line).
{ "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"tokens":["123456:AAA…","234567:BBB…"]}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/telegram/mybotstelegram:botsList of the project bots.
{ "bots": [ { "id": "…", "username": "MyBot", "webhookSet": true } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/telegram/mybots" \ -H "Authorization: Bearer spine_sk_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/telegram/mybots", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/telegram/mybots",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/telegram/mybots/{id}telegram:botsLive bot status (getMe + webhook + commands).
{ "username": "MyBot", "me": {...}, "webhook": {...}, "commands": [...] }
curl -X GET "https://api.spine.maxsystems.az/v1/telegram/mybots/{id}" \
-H "Authorization: Bearer spine_sk_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/telegram/mybots/{id}", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/telegram/mybots/{id}",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/telegram/mybots/{id}/webhooktelegram:botsRoute the bot updates to your server (SPINE receives them from Telegram and forwards them to you).
{ "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"forwardUrl":"https://ваш-сервер/tg"}',
]);
$data = json_decode(curl_exec($ch), true);
/v1/telegram/mypresetstelegram:botsTurnkey factory presets available to the project (shared + your own): commands, webhook, profile.
{ "presets": [ { "id": "…", "name": "Поддержка", "forwardUrl": "https://…" } ] }
curl -X GET "https://api.spine.maxsystems.az/v1/telegram/mypresets" \ -H "Authorization: Bearer spine_sk_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/telegram/mypresets", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/telegram/mypresets",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
/v1/telegram/mybots/create-linktelegram:botsFactory: a deep link to create a new bot. The user confirms in Telegram and the bot connects to the project by itself. With presetId the bot is deployed turnkey right away (commands + webhook + profile).
{ "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => '{"suggestedUsername":"myproject_bot","presetId":"…(необязательно)"}',
]);
$data = json_decode(curl_exec($ch), true);
Brand
/v1/brandbrand:readYour project brand kit: name, tagline, color palette, fonts, links and logos/files (with direct URLs to embed). Handy to auto-apply the brand style to emails, websites and apps.
{
"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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/brand", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/brand",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/project", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/project",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/project/audit", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/project/audit",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/site-config", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/site-config",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/secrets", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/secrets",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/secrets.env", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/secrets.env",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/verifications/{id}", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/otp/verifications/{id}",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/verifications/{id}/delivery", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
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_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/settings", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/otp/settings",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/telegram/link", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/otp/telegram/link",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/telegram/link", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/otp/telegram/link",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY" \
-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_YOUR_KEY",
"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_YOUR_KEY", "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_YOUR_KEY", "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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/totp", {
method: "GET",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.get(
"https://api.spine.maxsystems.az/v1/otp/totp",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$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_YOUR_KEY"
const res = await fetch("https://api.spine.maxsystems.az/v1/otp/totp", {
method: "DELETE",
headers: {
"Authorization": "Bearer spine_sk_YOUR_KEY"
}
});
const data = await res.json();
import requests
r = requests.delete(
"https://api.spine.maxsystems.az/v1/otp/totp",
headers={"Authorization": "Bearer spine_sk_YOUR_KEY"}
)
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_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
Signature verification and deliveryEvery delivery is a POST with the headers X-Spine-Event (event type) and X-Spine-Signature: sha256=<hex>. Body: {"event":"…","ts":"…","data":{…}}.
The signature = HMAC-SHA256 of the raw request body (bytes as received, before parsing JSON). The key is the whsec_… secret as is, the whole string: do NOT base64-decode it and do NOT strip the prefix (this is not Svix). The result is hex, with the sha256= prefix. Compare in constant time.
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);
}
One immediate attempt; on failure, durable retries with exponential backoff (≈ after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, up to 7 attempts over ~21 hours); the queue survives a restart. Success = your 2xx response. Delivery log: GET /v1/webhooks/{id}/deliveries (shows status, codes and the exact signed body); redeliver a specific one: POST /v1/webhooks/{id}/deliveries/{deliveryId}/redeliver. Respond quickly and push heavy work to the background (delivery is at-least-once, so make your handler idempotent).
For an incoming email we return its metadata (message), the thread RFC headers (rfc) and fraud signals (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" }
}
}
Folders. mail.received fires only for RECEIVED mail: Inbox, Junk, Archive and the employee's own folders. Drafts and Trash produce no events under any subscription (a draft your mail client saves every 25 seconds is not a message, and mail reaches Trash only after it was already reported). Mail an employee sent from the mailbox arrives as a separate event mail.sent_folder with mailboxRole: "sent": subscribe to it if you keep the whole conversation, skip it if you only need incoming mail.
delivery. Mail between mailboxes of the SAME project has no SPF, DKIM or DMARC: it never left the platform, so there was nothing to check and security.auth will be empty. To tell such mail apart from someone spoofing your domain from outside, use delivery.internal. The flag is derived from the message PATH, not from the sender address: anything arriving from outside gets a received line from our node, and the sender cannot forge it. imported means the message was brought in by a migration from a previous host rather than received as mail.
Identifiers: message.id is a short per-mailbox id (to read the same email via the API), NOT global. As a globally unique key (idempotency, threading) use rfc.messageId; link replies into a thread by rfc.inReplyTo/rfc.references.
security: score is the spam-filter score (rspamd style; higher = more suspicious), isSpam is whether it crossed the threshold; spf/dkim/dmarc are the sender authenticity (pass/fail/none…). If the email has no such headers, the field may be null.
The life of a message: mail.sent (relay accepted) → mail.delivered (recipient server accepted) or mail.bounced / mail.complained / mail.delayed. The key of a message is messageId, the same id that the send call returned.
"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, поэтому квитанция об оплате уйдёт даже тому, кто отказался от новостей.
Opens and clicks are counted by the platform itself: a transparent pixel is inserted into the HTML and links are replaced with our redirect (the tracking domain is yours when t.your-domain is enabled). The field automated: true means the open came from a proxy or a mailbox provider scanner rather than a person, and such opens are usually not counted.
"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 arrives when the recipient unsubscribed, including through the Gmail or Yahoo button itself (one-click unsubscribe per RFC 8058, enabled by the field unsubscribe: true). The address goes to the blocklist right away.
https://api.spine.maxsystems.az · machine description /v1/openapi.json