API Today Express
Публичный API для маркетплейсов, интернет-магазинов, WMS и ERP. Создавайте заявки на доставку из своей системы, получайте статусы вебхуками и сверяйте выкуп (COD) — сервер↔сервер, без входа в панель. Эта страница — полная и единственная документация: всё, что нужно разработчику, здесь.
{base_url})Мы выдаём его вместе с API-ключом. Все пути в этой документации указаны относительно него.Обзор
Интеграция строится вокруг одной сущности — заявки на доставку (order). Типичный сценарий:
- Покупатель оформил заказ у вас → вы вызываете
POST /orders/и получаетеtracking_code. - Мы забираем посылку с вашего склада и везём получателю; на каждой смене статуса шлём вам вебхук.
- Если есть выкуп (COD) — курьер собирает деньги при вручении, вы получаете
ransom.collected. - Покупатель следит за посылкой по трек-коду на нашем сайте — вопросов «где заказ» меньше.
Быстрый старт
Три запроса от нуля до созданной заявки. Начинайте с тестового ключа (te_test_…).
# 1. Узнать id города получателя
curl -s {base_url}/reference/locations/ \
-H "X-Api-Key: te_test_ВАШ_КЛЮЧ"
# 2. Посчитать цену доставки до создания
curl -s -X POST {base_url}/orders/calculate/ \
-H "X-Api-Key: te_test_ВАШ_КЛЮЧ" -H "Content-Type: application/json" \
-d '{"to_location_id": 2, "weight": 2}'
# → {"to_location_id": 2, "price": 330.0}
# 3. Создать заявку (сохраните tracking_code из ответа)
curl -s -X POST {base_url}/orders/ \
-H "X-Api-Key: te_test_ВАШ_КЛЮЧ" -H "Content-Type: application/json" \
-H "Idempotency-Key: order-5567" \
-d '{
"external_id": "order-5567",
"to_location_id": 2,
"to_full_name": "Айбек Асанов",
"to_contact": "996700123456",
"to_address": "мкр Восток-5, д. 12, кв. 3"
}'Авторизация
Каждый запрос требует заголовок:
X-Api-Key: te_live_xxxxxxxxxxxxxxxxxxxxxxxx- Ключ выдаёт Today Express. Есть два вида:
te_test_…(sandbox) иte_live_…(боевой). - Ключ определяет вашу компанию, права (
read/write), тариф и режим расчётов. - В БД хранится только хэш ключа. Полный ключ показывается один раз при выдаче — сохраните его.
- Вы видите только свои заявки. Чужие данные недоступны (изоляция по компании).
Соглашения
| Правило | Тип | Обяз. | Описание |
|---|---|---|---|
| Слеш в конце | — | да | Все пути оканчиваются на /. Без него будет 301 редирект (и POST может потеряться). |
| Content-Type | — | да | Для POST/PATCH — application/json, тело в UTF-8. |
| Idempotency-Key | header | нет | Заголовок при POST /orders/: повтор с тем же значением вернёт ту же заявку, а не создаст дубль. |
| Телефоны | — | да | Формат 996XXXXXXXXX: начинается с 996, без + и без ведущего 0. Напр. 996700123456 (12 цифр). |
| Цена | — | нет | Всегда считает сервер. Поле price во входных данных игнорируется. |
| Даты | — | нет | В ответах — ISO-8601 с таймзоной. В фильтрах — YYYY-MM-DD. |
Справочники
Города и коды значений. Кэшируйте у себя — они меняются редко.
Список городов
/reference/locations/Возвращает массив городов компании. id используется в to_location_id.
[
{ "id": 2, "name": "Ош", "name_en": "Osh",
"price": 350.0, "latitude": 40.53, "longitude": 72.80 }
]Справочник значений
/reference/enums/Коды типов упаковки, веса, способов оплаты, типов выкупа и статусов (см. разделы ниже).
Тарифы по городам
/reference/tariffs/Итоговая цена доставки по каждому городу назначения с учётом скидки вашего ключа.
[ { "to_location_id": 2, "name": "Ош", "price": 330.0 } ]Расчёт цены
/orders/calculate/Узнать стоимость доставки до создания заявки.
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
to_location_id | integer | да | id города получателя |
weight | integer | нет | градация веса 1–5 (по умолчанию 5) |
view | integer | нет | тип упаковки 1–5 (по умолчанию 5) |
POST /orders/calculate/ → { "to_location_id": 2, "price": 330.0 }Заявки
Создать заявку
/orders/Создаёт заявку в статусе «Ожидание». Передавайте заголовок Idempotency-Key.
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
external_id | string | нет | ваш id заказа — вернётся в ответах и вебхуках |
to_location_id | integer | да | id города получателя (/reference/locations/) |
to_contact | string | да | телефон получателя в формате 996XXXXXXXXX (с 996, без + и без 0), напр. 996700123456 |
to_full_name | string | нет | ФИО получателя |
to_address | string | нет | адрес получателя |
to_company | string | нет | компания получателя |
view | integer | нет | тип упаковки 1–5 (по умолч. 5 — Другое) |
weight | integer | нет | градация веса 1–5 (по умолч. 5) |
amount | integer | нет | количество мест (по умолч. 1) |
ransom | integer | нет | тип выкупа 1–3 — только если COD включён для ключа |
ransom_price | number | нет | сумма выкупа (стоимость товара) |
comment | string | нет | комментарий к заявке |
to_contact передавайте как 996XXXXXXXXX — 12 цифр, начинается с 996, без + и без ведущего 0. Пример: 996700123456 (не 0700123456 и не +996700123456). В таком же виде номер хранится и возвращается в ответах и вебхуках.Ответ (201 Created; при повторе с тем же Idempotency-Key — 200 OK и та же заявка):
HTTP 201 Created
{
"id": 519995,
"tracking_code": "TEKG-202607-0F8B32CE",
"external_id": "order-5567",
"status": 1,
"status_name": "Ожидание",
"from_location": "Бишкек",
"from_address": "Дордой, проход 2, ангар 6",
"from_contact": "996999666777",
"to_location": "Ош",
"to_address": "мкр Восток-5, д. 12, кв. 3",
"to_full_name": "Айбек Асанов",
"to_contact": "996700123456",
"view": 5,
"weight": 2,
"amount": 1,
"price": 330.0,
"ransom": null,
"ransom_price": 0.0,
"ransom_paid": 0.0,
"is_sandbox": true,
"label_url": "{base_url}/orders/519995/label/",
"created_date": "2026-07-30T05:31:12+06:00",
"last_status_date": null
}Пакетное создание
/orders/bulk/До пачки заявок за раз. Возвращает 207 с результатом по каждой — частичный успех допускается.
POST /orders/bulk/
{
"items": [
{ "external_id": "A-1", "to_location_id": 2, "to_contact": "996700111222" },
{ "external_id": "A-2", "to_location_id": 999, "to_contact": "996700333444" }
]
}
HTTP 207 Multi-Status
{
"results": [
{ "external_id": "A-1", "ok": true, "id": 519993, "tracking_code": "TEKG-…" },
{ "external_id": "A-2", "ok": false, "error": "Локация id=999 не найдена у компании" }
]
}Список заявок
/orders/Пагинация. Фильтры (query): status (1–9), external_id, from_date, to_date (YYYY-MM-DD), page.
GET /orders/?status=1&from_date=2026-07-01&page=1
{
"count": 42,
"next": "{base_url}/orders/?page=2",
"previous": null,
"results": [ { /* объект заявки, как в ответе создания */ } ]
}Заявка по id / по трек-коду
/orders/{id}//orders/by-code/{tracking_code}/Возвращают тот же объект заявки, что и создание.
Изменить заявку (до забора)
/orders/{id}/Пока курьер не забрал посылку, можно поправить получателя и параметры места. Изменяемые поля: to_address, to_full_name, to_contact, comment, view, weight, amount.
Отменить заявку
/orders/{id}/cancel/Возможно только до отправки со склада. Статус станет «Отмена» (9).
Наклейка
/orders/{id}/label/Поля для печати наклейки; qr_value = tracking_code (кодируйте в QR).
{ "tracking_code": "TEKG-…", "qr_value": "TEKG-…",
"to_location": "Ош", "to_address": "…", "to_full_name": "…",
"to_contact": "996700123456", "from_location": "Бишкек",
"weight": 2, "amount": 1 }Подтверждение вручения
/orders/{id}/proof/{ "delivered": true, "status": 5, "status_name": "Получено",
"delivered_at": "2026-07-30T14:22:10+06:00", "image": "https://…" }Выкуп (COD) и выплаты
Выкуп (наложенный платёж) — опциональный модуль. Доступность включает Today Express на вашем ключе, не вы. Если включён: задаёте ransom и ransom_price при создании, курьер собирает деньги при вручении, а вы получаете событие ransom.collected. Если выключен — заявка с ransom_price отклоняется (400), а доставка оплачивается по вашему режиму расчётов (предоплата/постоплата).
Выкуп по заявке
/orders/{id}/ransom/{ "tracking_code": "TEKG-…", "ransom_price": 3500, "ransom_paid": 3500,
"ransom_duty": 0, "collected": true }Сводка по выкупу/выплатам
/payouts/Сколько собрано и сколько ещё к выплате за период. Фильтры: from_date, to_date.
{ "orders": 128, "ransom_total": 448000,
"ransom_collected": 430500, "to_payout": 17500 }/ransom/ и /payouts/ работают только для ключа в режиме выкупа (COD). Иначе — 400. Сами выплаты партнёру пока проводятся офлайн.Вебхуки
Вместо опроса — мы сами шлём POST на ваш URL при событиях. Зарегистрируйте подписку, проверяйте подпись, отвечайте 2xx.
Зарегистрировать подписку
/webhooks/POST /webhooks/
{
"url": "https://ваш-магазин.kg/api/te-webhook",
"secret": "случайная_строка_которую_знаете_только_вы",
"events": [] // [] = все события; можно ["order.status_changed"]
}
HTTP 201 Created
{ "id": 3, "url": "...", "events": [], "is_active": true, "created_date": "..." }Управление: GET /webhooks/ — список, DELETE /webhooks/{id}/ — удалить, GET /webhooks/deliveries/ — журнал последних доставок (статус, попытки, код ответа, ошибка).
Какие события мы шлём
| Событие | Тип | Обяз. | Описание |
|---|---|---|---|
order.created | — | нет | заявка создана |
order.status_changed | — | нет | статус изменился; конкретный статус — в data.status (1–9) и data.status_name |
ransom.collected | — | нет | выкуп полностью собран (однократно) |
Тело события и заголовки
POST на ваш URL
Content-Type: application/json
X-TE-Event: order.status_changed
X-TE-Delivery: 445e217b-d052-4ac2-a75c-f5161b0af563
X-TE-Signature: sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b...
{
"event_id": "445e217b-d052-4ac2-a75c-f5161b0af563",
"type": "order.status_changed",
"created_at": "2026-07-30T14:22:10+06:00",
"data": {
"id": 519995,
"tracking_code": "TEKG-202607-0F8B32CE",
"external_id": "order-5567",
"status": 5,
"status_name": "Получено",
"from_location": "Бишкек",
"to_location": "Ош",
"ransom_price": 3500,
"ransom_paid": 3500,
"ransom_collected": true
}
}X-TE-Signature—sha256=+ HMAC-SHA256 сырого тела вашимsecret.X-TE-Event— тип события,X-TE-Delivery— он жеevent_id.- Дедуплицируйте по
event_id: при ретраях одно событие может прийти повторно.
Проверка подписи
// Node.js (Express). Важно: подпись считается по СЫРОМУ телу запроса.
const crypto = require("crypto");
const SECRET = process.env.TE_WEBHOOK_SECRET;
app.post("/api/te-webhook",
express.raw({ type: "application/json" }), // получаем Buffer, не парсим заранее
(req, res) => {
const expected = "sha256=" +
crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
const got = req.header("X-TE-Signature") || "";
const ok = expected.length === got.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got));
if (!ok) return res.status(401).end();
const event = JSON.parse(req.body.toString("utf8"));
// TODO: дедуп по event.event_id, затем обработка
res.status(200).json({ ok: true });
});<?php // PHP
$secret = getenv('TE_WEBHOOK_SECRET');
$raw = file_get_contents('php://input'); // сырое тело
$expected = 'sha256=' . hash_hmac('sha256', $raw, $secret);
$got = $_SERVER['HTTP_X_TE_SIGNATURE'] ?? '';
if (!hash_equals($expected, $got)) { http_response_code(401); exit; }
$event = json_decode($raw, true);
// TODO: дедуп по $event['event_id'], затем обработка
http_response_code(200);
echo json_encode(['ok' => true]);# Python (Flask). Считаем HMAC по request.get_data() — сырому телу.
import hmac, hashlib
from flask import request, abort
SECRET = os.environ["TE_WEBHOOK_SECRET"].encode()
@app.post("/api/te-webhook")
def te_webhook():
raw = request.get_data()
expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-TE-Signature", "")):
abort(401)
event = request.get_json()
# TODO: дедуп по event["event_id"], затем обработка
return {"ok": True}, 200Ретраи и надёжность
- Ответьте кодом
2xxв течение 10 секунд — иначе доставка считается неуспешной. - До 6 попыток с нарастающей паузой:
1м → 5м → 30м → 2ч → 6ч. После — «мёртвая» доставка (dead-letter). - Обработку делайте идемпотентной (по
event_id) и быстрой; тяжёлое — в фоновую очередь у себя. - История попыток видна в
GET /webhooks/deliveries/.
Статусы заявки
Приходят в status (код) и status_name. Ориентируйтесь на код — названия для отображения.
| Код | Тип | Обяз. | Описание |
|---|---|---|---|
| 1 | Ожидание | нет | заявка создана, ждёт курьера |
| 2 | Забрал | нет | курьер забрал у отправителя |
| 3 | На складе | нет | принято на склад |
| 4 | Отправлено | нет | отправлено в город назначения |
| 5 | Получено | нет | прибыло / получено в городе назначения |
| 6 | Готов к доставке | нет | готово к вручению получателю |
| 7 | Завершено | нет | вручено, заявка закрыта |
| 8 | Возврат | нет | возврат отправителю |
| 9 | Отмена | нет | отменено |
Справочные значения
Тип упаковки (view)
| Код | Тип | Обяз. | Описание |
|---|---|---|---|
| 1 | Документ | нет | |
| 2 | Конверт | нет | |
| 3 | Коробка | нет | |
| 4 | Пакет | нет | |
| 5 | Другое | нет |
Вес (weight)
| Код | Тип | Обяз. | Описание |
|---|---|---|---|
| 1 | до 1 кг | нет | |
| 2 | до 5 кг | нет | |
| 3 | до 10 кг | нет | |
| 4 | до 15 кг | нет | |
| 5 | Другое | нет |
Способ оплаты
| Код | Тип | Обяз. | Описание |
|---|---|---|---|
| 1 | Наличные | нет | |
| 2 | Безнал | нет | |
| 3 | Получатель | нет |
Тип выкупа (ransom)
| Код | Тип | Обяз. | Описание |
|---|---|---|---|
| 1 | Наличные | нет | |
| 2 | Безнал | нет | |
| 3 | В долг | нет |
Ошибки
Единый формат тела: { "detail": "..." }. При ошибках валидации — детали по полям: { "to_contact": ["Обязательное поле."] }.
| Код | Тип | Обяз. | Описание |
|---|---|---|---|
| 400 | Bad Request | нет | ошибка валидации входных данных (или выкуп на не-COD ключе) |
| 401 | Unauthorized | нет | не передан или неверный X-Api-Key |
| 403 | Forbidden | нет | у ключа нет нужного права (scope) — напр. запись боевым read-ключом |
| 404 | Not Found | нет | ресурс не найден (в т.ч. чужая заявка — изоляция по компании) |
| 429 | Too Many Requests | нет | превышен лимит; см. заголовок Retry-After |
Лимиты и тестовый режим
Rate limiting
Лимит запросов считается на ключ (по умолчанию 120 запросов в минуту). При превышении — 429 с заголовком Retry-After (секунды до следующей попытки). Нужен лимит выше — напишите нам.
Sandbox (тестовый ключ)
Тестовый ключ (te_test_…) ходит в тот же API, но заявки помечаются is_sandbox: true и не попадают реальным курьерам и операторам. Идеально для отладки. Проверили всё — присылаете нам «готовы», переключаетесь на te_live_….
Получить ключ и поддержка
- Как получить доступ к API?
- Напишите нам в WhatsApp — мы заведём вас как партнёра и выдадим два ключа: тестовый (te_test_…) и боевой (te_live_…). Ключ определяет вашу компанию, права, тариф и режим расчётов; больше от вас ничего не нужно.
- Нужно ли опрашивать статусы заказов?
- Нет. Зарегистрируйте вебхук — мы сами шлём POST на ваш URL при каждом изменении статуса. Каждый вебхук подписан (HMAC), чтобы вы убедились, что запрос от нас.
- Можно ли протестировать перед боевым запуском?
- Да. Тестовый (sandbox) ключ ходит в тот же API, но заявки помечаются как тестовые (is_sandbox=true) и не уходят реальным курьерам/операторам. Убедились — переключаетесь на боевой ключ.
- Кто указывает адрес отправителя?
- Мы. Ваш склад (город, адрес, телефон) хранится в вашем профиле партнёра. В запросе на создание заявки вы передаёте только получателя и посылку — откуда забрать подставляем сами.
- Кто считает цену доставки?
- Сервер, по тарифу вашего ключа. Вы не передаёте цену — она приходит в ответе на создание и в POST /orders/calculate/. Индивидуальная скидка партнёра (в сомах) уже учтена.
- Получит ли покупатель уведомления с трекингом от Today Express?
- Нет. По заявкам, созданным через API, мы не отправляем получателю SMS/WhatsApp с трек-ссылкой — статусы вы показываете сами в своей системе (через вебхуки). Мы берём на себя только логистику. По обычным заявкам (не через API) уведомления получателю работают как раньше.
Готовы подключиться?
Напишите нам — заведём вас партнёром и выдадим тестовый и боевой ключи. Начать можно с sandbox, без обязательств.
Написать в WhatsApp
