Таза Алматы — интеграция для приложений-партнёров
Ваше приложение может принимать жалобы жителей на коптящие машины и дымящие трубы и передавать их в «Таза Алматы» — по тем же правилам и с той же модерацией, что у Telegram-бота @aai_sergek_bot. О решении специалиста ваш сервер получает событие и сам отправляет пуш.
Как это работает
Заявка из вашего приложения попадает в общую очередь с заявками из Telegram. Решение модератора приходит на ваш сервер событием, а пуш человеку отправляете вы.
Два способа встройки
Ручки API у обоих способов одни и те же — различается только, кто рисует экран и как подписан запрос.
| Вариант A: наша страница | Вариант B: ваш экран | |
|---|---|---|
| Интерфейс | наш, открывается в WebView вашего приложения | ваш, нативный |
| Кто ходит в API | наша страница, с телефона | ваш сервер |
| Подпись | ваш сервер подписывает ссылку запуска, она живёт сутки | ваш сервер подписывает каждый запрос, окно 5 минут |
| Работа у вас | выдать подписанную ссылку и принять вебхук | экран подачи, подпись запросов, вебхук |
| Когда выбрать | быстрый запуск, без своей вёрстки | подача должна выглядеть как часть вашего приложения |
Начать проще с варианта A: от вас нужны только подпись ссылки и вебхук. На вариант B можно перейти позже — заявки и пользователи сохранятся.
Подключение
Интеграция включается на нашей стороне отдельно для каждого партнёра. До включения все ручки отвечают 401.
| Что | Кто даёт | Как |
|---|---|---|
partner_id — например, superapp |
мы | письмом |
| Секрет — не короче 32 символов | мы | по защищённому каналу, не в письме и не в чате |
| Адрес API | мы | письмом при подключении; ниже обозначен как https://<api> |
Адрес вебхука — только https:// |
вы | письмом; без него событий не будет |
| Технический контакт | обе стороны | письмом |
Секрет хранится только на вашем сервере. В мобильное приложение его класть нельзя: из приложения его достанут, и любой сможет подавать заявки от имени ваших пользователей. Если секрет утёк, сообщите — выдадим новый, старый перестанет работать.
Идентификатор пользователя
user — ваш постоянный непрозрачный id пользователя: до 128 символов из A–Z a–z 0–9 _ . : @ -.
- Телефон, ИИН, имя или e-mail в
userпередавать нельзя. Подойдёт внутренний id или его хэш. - id должен быть стабильным: по нему человек видит свои заявки, лимит в сутки и своё место в рейтинге. Сменился id — для нас это новый человек.
- У нас хранится только соль-хэш от пары «партнёр + id». Пользователь №1 у вас и №1 в Telegram или у другого партнёра — разные люди.
- Чтобы прислать вебхук, мы держим пару
partner_id:user. По команде удаления данных стирается и она. - Владелец машины, госорганы и публика не видят, кто прислал заявку.
Подпись запросов (вариант B)
Каждый запрос с вашего сервера несёт четыре заголовка. Подпись — HMAC-SHA256 секретом партнёра, в hex.
| Заголовок | Что в нём |
|---|---|
X-Partner-Id |
ваш partner_id |
X-Partner-User |
id пользователя |
X-Partner-Timestamp |
unix-время в секундах |
X-Partner-Signature |
подпись, см. ниже |
Подписываемая строка — шесть полей через перевод строки \n:
v1- то же время, что в
X-Partner-Timestamp - метод заглавными:
GETилиPOST - путь без домена и query, например
/api/partner/v1/reports - тот же id пользователя
- SHA-256 тела запроса в hex — от тех же байтов, что уходят в запрос; у
GET— от пустой строки
Время подписи не должно отличаться от нашего больше чем на 5 минут. Тело подписывается целиком: после подписи его нельзя пересериализовать или переформатировать.
Node.js:
import { createHash, createHmac } from "node:crypto";
function signedHeaders({ secret, partnerId, user, method, path, body = "" }) {
const ts = Math.floor(Date.now() / 1000);
const bodyHash = createHash("sha256").update(body).digest("hex");
const line = ["v1", ts, method.toUpperCase(), path, user, bodyHash].join("\n");
return {
"X-Partner-Id": partnerId,
"X-Partner-User": user,
"X-Partner-Timestamp": String(ts),
"X-Partner-Signature": createHmac("sha256", secret).update(line).digest("hex"),
};
}
Python:
import hashlib, hmac, time
def signed_headers(secret: str, partner_id: str, user: str, method: str, path: str, body: bytes = b"") -> dict:
ts = str(int(time.time()))
line = "\n".join(["v1", ts, method.upper(), path, user, hashlib.sha256(body).hexdigest()])
sig = hmac.new(secret.encode(), line.encode(), hashlib.sha256).hexdigest()
return {"X-Partner-Id": partner_id, "X-Partner-User": user,
"X-Partner-Timestamp": ts, "X-Partner-Signature": sig}
Проверочный пример — ваш код должен дать ровно эти значения:
| Поле | Значение |
|---|---|
| секрет | test-secret-0123456789abcdef0123456789 |
| время | 1793680000 |
| метод и путь | POST /api/partner/v1/reports |
| пользователь | u-777 |
| тело | {"kind":"exhaust","lat":43.238,"lon":76.945,"when":"now","plate":"123ABC02"} |
| SHA-256 тела | 966c287c1a4cf9e8d9f57e95d16372774fef71796d3eff21b6d0dbd88ede694b |
| подпись | fb83c69961c13dfe9d6994123a01f47635ad3bb9897aae10a786d3c010dea4f1 |
Запуск нашей страницы (вариант A)
Ваш сервер подписывает ссылку, приложение открывает её в WebView. Дальше страница сама ходит в API — от вас ничего больше не нужно.
Подписываемая строка — четыре поля через \n: launch, partner_id, id пользователя, unix-время в секундах.
const ts = Math.floor(Date.now() / 1000);
const sig = createHmac("sha256", secret).update(["launch", partnerId, user, ts].join("\n")).digest("hex");
const url = `https://<api>/p/report?` + new URLSearchParams({ partner: partnerId, uid: user, ts: String(ts), sig });
Проверочный пример: секрет test-secret-0123456789abcdef0123456789, partner_id = superapp, пользователь u-777, время 1793680000 → sig = 3d9e3a85a35462833b92a545c752ffda81fd0e9ddb4158c2701b006cbe0d5a01.
- Ссылка действует сутки. Каждый новый показ страницы — свежая подпись с вашего сервера.
- Подписывает только сервер, секрета в приложении нет.
- Страница сразу убирает параметры из адресной строки и дальше шлёт их заголовком
X-Partner-Launch. - WebView должен разрешать: камеру и выбор файла (
<input type=file>), геолокацию, JavaScript иsessionStorage. - Страница на русском, светлая, ширина от 360 px. Вес — около 70 КБ сжатых скриптов.
Ручки API
Все ручки — под https://<api>/api/partner/v1. Все требуют подписи и работают только с заявками самого пользователя. Ответы — JSON.
| Метод и путь | Тело | Что делает |
|---|---|---|
GET /kinds |
— | типы заявок с подписями, варианты «когда», лимиты |
GET /me |
— | дал ли человек согласие и текущая версия согласия |
POST /consent |
{"version":"2026-10-16"} |
записать согласие; версия — из GET /me |
POST /media |
файл сырым телом, Content-Type: image/* или video/*, до 20 МБ |
фото или видео заявки; в ответ — распознанный номер и координаты из фото, если есть |
POST /reports |
JSON, см. следующий раздел | подать заявку с файлом из последнего POST /media |
GET /reports |
— | свои заявки со статусами, последние 20 |
POST /delete-my-data |
— | удалить все заявки, файлы и адрес для вебхука |
Типы заявок:
kind |
Что это | Номер нужен |
|---|---|---|
exhaust |
машина дымит, воняет или сильно шумит | да |
chimney |
дымит труба или жгут мусор | нет |
Подписи для кнопок и текст «что будет дальше» берите из GET /kinds, не зашивайте в приложение: типы и формулировки меняются.
Подача заявки по шагам
Подача — два запроса: сначала файл, потом данные. Их разделяет человек: он подтверждает номер, который мы прочитали на кадре.
- Согласие.
GET /me; еслиconsent=false— покажите текст согласия и после кнопки отправьтеPOST /consentсcurrentConsentVersion. Без согласия/mediaи/reportsотвечают409 consent_required. - Фото или видео.
POST /mediaс файлом в теле. Видео 5–10 секунд лучше фото: пар тает за секунду, дым держится. НовыйPOST /mediaзаменяет предыдущий неотправленный файл. - Номер — на подтверждение. Если в ответе есть
plate, покажите его человеку в редактируемом поле. Нет — попросите ввести. Кириллица и пробелы допустимы, мы приводим сами. - Место. Если
locationпустое, возьмите геолокацию телефона. Без места заявку не принять. - Когда это было. Одна кнопка из пяти: «Только что»
now, «Сегодня, раньше»today, «Вчера»yesterday, «2–7 дней назад»days, «Больше недели»older. Поolderзаявка не принимается, файл удаляется. - Отправка.
POST /reports. Код из ответа покажите человеку вместе сoutcome— что будет дальше.
Ответ POST /media, 201:
{"plate": {"value": "123ABC02", "confidence": 0.92, "country": "KZ"},
"location": null}
Запрос POST /reports (lat и lon можно не передавать, если они пришли в location; plate — только для exhaust):
{"kind": "exhaust", "lat": 43.238, "lon": 76.945, "when": "now", "plate": "123ABC02"}
Ответ, 201:
{"code": "ECO-26-000123", "kind": "exhaust",
"outcome": "Проверим по камерам. Подтверждённые сообщения идут в городскую систему выявления…"}
Если человек исправил номер, прочитанный на кадре, заявка принимается как есть. Модератор увидит оба номера и сверит по фото или видео.
Ошибки
Ошибка приходит JSON-ом {"error": "<код>", …}. Правила приёма те же, что в Telegram-боте: текст для человека ниже совпадает с ботом.
| Код ответа | error |
Что сказать человеку |
|---|---|---|
| 401 | unauthorized |
ничего: это ошибка подписи или часов сервера; причина — в поле reason |
| 403 | blocked |
«Приём сообщений с этого аккаунта приостановлен» |
| 409 | consent_required |
показать экран согласия |
| 409 | media_required |
«Сначала прикрепите фото или видео» |
| 409 | own_duplicate |
«Вы уже сообщали об этой машине: заявка existingCode» — повтор своего номера за 30 дней |
| 415 | expected image/* or video/* body |
«Нужно фото или видео» |
| 413 | — | «Файл больше 20 МБ, снимите покороче» |
| 422 | too_old |
«Сообщение нужно отправить в течение 7 дней» |
| 422 | location_required, bad_location |
«Нужно место, где это было» |
| 422 | plate_required, bad_plate |
«Проверьте номер машины» |
| 422 | when_required, unknown_kind, consent_version_mismatch |
ничего: ошибка интеграции |
| 429 | rate_limited |
«Сегодня от вас уже 10 сообщений — это предел на сутки» |
После own_duplicate, too_old и rate_limited файл удаляется — заявка начинается заново. После остальных 4xx файл сохраняется, и POST /reports можно повторить с исправленными данными.
Вебхук о решении
Когда модератор принимает или отклоняет заявку или приходит итог проверки по камерам, мы отправляем POST на ваш адрес. Пуш человеку отправляете вы.
{
"event": "report.updated",
"user": "u-777",
"report": {
"code": "ECO-26-000123",
"kind": "exhaust",
"moderation_status": "approved",
"check_status": "camera_confirmed",
"created_at": "2026-11-03T05:00:00.000Z"
},
"message": "Заявка ECO-26-000123 подтверждена. …"
}
message— готовый текст для пуша, тот же, что получает житель в Telegram-боте. Можно написать свой по полямreport.moderation_status:pending— на проверке,approved— подтверждена,rejected— не принята.- По одной заявке событий бывает несколько: итог проверки и решение модератора приходят отдельно.
- Ответьте
2xxза 5 секунд. Повторной отправки нет: если событие потерялось, актуальный статус всегда есть вGET /reports.
Проверка подписи. Заголовки: X-Taza-Timestamp — unix-время, X-Taza-Signature — HMAC-SHA256 секретом от строки <время>.<сырое тело>, в hex. Считайте подпись от тела до разбора JSON и отбрасывайте события старше 5 минут.
import { createHmac, timingSafeEqual } from "node:crypto";
function isFromTaza(rawBody, headers, secret) {
const ts = headers["x-taza-timestamp"];
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const want = createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
const got = String(headers["x-taza-signature"] ?? "");
return got.length === want.length && timingSafeEqual(Buffer.from(got), Buffer.from(want));
}
Проверочный пример: секрет test-secret-0123456789abcdef0123456789, время 1793680000, тело {"event":"report.updated"} → подпись 17c144a6fa2660c3d7c41cc29e987833674c185f5a38c67fd09d2a0cc6d8f466.
Обязательные условия
Без этих пунктов интеграцию не включаем.
- Согласие — от имени Almaty Air Initiative и Сергека, не вашего приложения. Человек должен понимать, кому уходит фото. Текст согласия даём мы; в варианте A наша страница показывает его сама. Изменилась версия в
GET /me— спросите согласие заново. - Договор об обработке персональных данных между вашей компанией и оператором «Таза Алматы» — до запуска.
- Распознавание номера идёт в облаке Google (Vertex AI). Фото и видео покидают Казахстан; это сказано в согласии. До массового запуска распознавание переносится в контур Сергека.
- Секрет — только на сервере. Подпись запуска страницы тоже делает сервер.
- Удаление данных. Если человек удаляет аккаунт у вас — вызовите
POST /delete-my-dataот его имени. - Не обещать штраф. Заявка — повод для проверки на экопосту, решение принимает уполномоченный орган. Номера чужих машин публиковать нельзя.
Чек-лист запуска
- Получены
partner_id, секрет и адрес API - Нам передан адрес вебхука (
https://) - Подпись сходится с проверочными примерами из этого документа
- Вариант A: страница открывается в WebView, работают камера, выбор файла и геолокация
- Вариант B: согласие, файл, подтверждение номера, место, «когда» и ошибки показаны человеку
- Тестовая заявка прошла, модератор её увидел и решил, вебхук дошёл, пуш пришёл
-
POST /delete-my-dataвызывается при удалении аккаунта у вас - Подписан договор об обработке персональных данных
Вопросы, на которые нужен ответ партнёра до старта:
- Как ваша платформа передаёт пользователя в мини-приложение: есть свой токен, или подходит наша подпись ссылки?
- Разрешены ли в вашем WebView камера, выбор файла и геолокация?
- В каком формате вы принимаете события для пушей — подойдёт наш вебхук или нужен адаптер?