API Today Express

Публичный API для маркетплейсов, интернет-магазинов, WMS и ERP. Создавайте заявки на доставку из своей системы, получайте статусы вебхуками и сверяйте выкуп (COD) — сервер↔сервер, без входа в панель. Эта страница — полная и единственная документация: всё, что нужно разработчику, здесь.

Базовый URL ({base_url})Мы выдаём его вместе с API-ключом. Все пути в этой документации указаны относительно него.

Обзор

Интеграция строится вокруг одной сущности — заявки на доставку (order). Типичный сценарий:

  1. Покупатель оформил заказ у вас → вы вызываете POST /orders/ и получаете tracking_code.
  2. Мы забираем посылку с вашего склада и везём получателю; на каждой смене статуса шлём вам вебхук.
  3. Если есть выкуп (COD) — курьер собирает деньги при вручении, вы получаете ransom.collected.
  4. Покупатель следит за посылкой по трек-коду на нашем сайте — вопросов «где заказ» меньше.
Две вещи вы не передаёте: адрес отправителя (берём из вашего профиля партнёра) и цену доставки (считает сервер по вашему тарифу). Вы отвечаете только за получателя, посылку и, при необходимости, выкуп.

Быстрый старт

Три запроса от нуля до созданной заявки. Начинайте с тестового ключа (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/PATCHapplication/json, тело в UTF-8.
Idempotency-KeyheaderнетЗаголовок при POST /orders/: повтор с тем же значением вернёт ту же заявку, а не создаст дубль.
ТелефоныдаФормат 996XXXXXXXXX: начинается с 996, без + и без ведущего 0. Напр. 996700123456 (12 цифр).
ЦенанетВсегда считает сервер. Поле price во входных данных игнорируется.
ДатынетВ ответах — ISO-8601 с таймзоной. В фильтрах — YYYY-MM-DD.

Справочники

Города и коды значений. Кэшируйте у себя — они меняются редко.

Список городов

GET/reference/locations/

Возвращает массив городов компании. id используется в to_location_id.

[
  { "id": 2, "name": "Ош", "name_en": "Osh",
    "price": 350.0, "latitude": 40.53, "longitude": 72.80 }
]

Справочник значений

GET/reference/enums/

Коды типов упаковки, веса, способов оплаты, типов выкупа и статусов (см. разделы ниже).

Тарифы по городам

GET/reference/tariffs/

Итоговая цена доставки по каждому городу назначения с учётом скидки вашего ключа.

[ { "to_location_id": 2, "name": "Ош", "price": 330.0 } ]

Расчёт цены

POST/orders/calculate/

Узнать стоимость доставки до создания заявки.

ПолеТипОбяз.Описание
to_location_idintegerдаid города получателя
weightintegerнетградация веса 1–5 (по умолчанию 5)
viewintegerнеттип упаковки 1–5 (по умолчанию 5)
POST /orders/calculate/  →  { "to_location_id": 2, "price": 330.0 }

Заявки

Создать заявку

POST/orders/

Создаёт заявку в статусе «Ожидание». Передавайте заголовок Idempotency-Key.

ПолеТипОбяз.Описание
external_idstringнетваш id заказа — вернётся в ответах и вебхуках
to_location_idintegerдаid города получателя (/reference/locations/)
to_contactstringдателефон получателя в формате 996XXXXXXXXX (с 996, без + и без 0), напр. 996700123456
to_full_namestringнетФИО получателя
to_addressstringнетадрес получателя
to_companystringнеткомпания получателя
viewintegerнеттип упаковки 1–5 (по умолч. 5 — Другое)
weightintegerнетградация веса 1–5 (по умолч. 5)
amountintegerнетколичество мест (по умолч. 1)
ransomintegerнеттип выкупа 1–3 — только если COD включён для ключа
ransom_pricenumberнетсумма выкупа (стоимость товара)
commentstringнеткомментарий к заявке
Формат телефона. to_contact передавайте как 996XXXXXXXXX — 12 цифр, начинается с 996, без + и без ведущего 0. Пример: 996700123456 (не 0700123456 и не +996700123456). В таком же виде номер хранится и возвращается в ответах и вебхуках.

Ответ (201 Created; при повторе с тем же Idempotency-Key200 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
}

Пакетное создание

POST/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 не найдена у компании" }
  ]
}

Список заявок

GET/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 / по трек-коду

GET/orders/{id}/
GET/orders/by-code/{tracking_code}/

Возвращают тот же объект заявки, что и создание.

Изменить заявку (до забора)

PATCH/orders/{id}/

Пока курьер не забрал посылку, можно поправить получателя и параметры места. Изменяемые поля: to_address, to_full_name, to_contact, comment, view, weight, amount.

Отменить заявку

POST/orders/{id}/cancel/

Возможно только до отправки со склада. Статус станет «Отмена» (9).

Наклейка

GET/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 }

Подтверждение вручения

GET/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), а доставка оплачивается по вашему режиму расчётов (предоплата/постоплата).

Выкуп по заявке

GET/orders/{id}/ransom/
{ "tracking_code": "TEKG-…", "ransom_price": 3500, "ransom_paid": 3500,
  "ransom_duty": 0, "collected": true }

Сводка по выкупу/выплатам

GET/payouts/

Сколько собрано и сколько ещё к выплате за период. Фильтры: from_date, to_date.

{ "orders": 128, "ransom_total": 448000,
  "ransom_collected": 430500, "to_payout": 17500 }
Эндпоинты /ransom/ и /payouts/ работают только для ключа в режиме выкупа (COD). Иначе — 400. Сами выплаты партнёру пока проводятся офлайн.

Вебхуки

Вместо опроса — мы сами шлём POST на ваш URL при событиях. Зарегистрируйте подписку, проверяйте подпись, отвечайте 2xx.

Зарегистрировать подписку

POST/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-Signaturesha256= + HMAC-SHA256 сырого тела вашим secret.
  • X-TE-Event — тип события, X-TE-Delivery — он же event_id.
  • Дедуплицируйте по event_id: при ретраях одно событие может прийти повторно.

Проверка подписи

Считайте HMAC по сырому телу запроса (bytes), до любого парсинга/переформатирования JSON. Если распарсить и заново сериализовать — подпись не сойдётся.
// 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": ["Обязательное поле."] }.

КодТипОбяз.Описание
400Bad Requestнетошибка валидации входных данных (или выкуп на не-COD ключе)
401Unauthorizedнетне передан или неверный X-Api-Key
403Forbiddenнету ключа нет нужного права (scope) — напр. запись боевым read-ключом
404Not Foundнетресурс не найден (в т.ч. чужая заявка — изоляция по компании)
429Too 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