Таза Алматы — интеграция для приложений-партнёров

Almaty Air Initiative · Сергек · обновлено 3 ноября 2026

Ваше приложение может принимать жалобы жителей на коптящие машины и дымящие трубы и передавать их в «Таза Алматы» — по тем же правилам и с той же модерацией, что у Telegram-бота @aai_sergek_bot. О решении специалиста ваш сервер получает событие и сам отправляет пуш.

Как это работает

Заявка из вашего приложения проходит те же правила и модерацию, что и в ботеЖительснимает машинуили трубуВаше приложениенаша страницаили ваш экран«Таза Алматы»правила как у бота:лимит, дубли, 7 днейМодераторсмотрит фото,принимает решениеВебхукподписанное событиеreport.updatedВаш серверпроверяет подписьи шлёт пушПуш жителюкод заявкии решение
Путь заявки: от снимка до пуша о решении

Заявка из вашего приложения попадает в общую очередь с заявками из 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 _ . : @ -.

Подпись запросов (вариант B)

Каждый запрос с вашего сервера несёт четыре заголовка. Подпись — HMAC-SHA256 секретом партнёра, в hex.

Заголовок Что в нём
X-Partner-Id ваш partner_id
X-Partner-User id пользователя
X-Partner-Timestamp unix-время в секундах
X-Partner-Signature подпись, см. ниже

Подписываемая строка — шесть полей через перевод строки \n:

  1. v1
  2. то же время, что в X-Partner-Timestamp
  3. метод заглавными: GET или POST
  4. путь без домена и query, например /api/partner/v1/reports
  5. тот же id пользователя
  6. 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.

Ручки 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, не зашивайте в приложение: типы и формулировки меняются.

Подача заявки по шагам

Подача — два запроса: сначала файл, потом данные. Их разделяет человек: он подтверждает номер, который мы прочитали на кадре.

  1. Согласие. GET /me; если consent = false — покажите текст согласия и после кнопки отправьте POST /consent с currentConsentVersion. Без согласия /media и /reports отвечают 409 consent_required.
  2. Фото или видео. POST /media с файлом в теле. Видео 5–10 секунд лучше фото: пар тает за секунду, дым держится. Новый POST /media заменяет предыдущий неотправленный файл.
  3. Номер — на подтверждение. Если в ответе есть plate, покажите его человеку в редактируемом поле. Нет — попросите ввести. Кириллица и пробелы допустимы, мы приводим сами.
  4. Место. Если location пустое, возьмите геолокацию телефона. Без места заявку не принять.
  5. Когда это было. Одна кнопка из пяти: «Только что» now, «Сегодня, раньше» today, «Вчера» yesterday, «2–7 дней назад» days, «Больше недели» older. По older заявка не принимается, файл удаляется.
  6. Отправка. 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 подтверждена. …"
}

Проверка подписи. Заголовки: 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.

Обязательные условия

Без этих пунктов интеграцию не включаем.

  1. Согласие — от имени Almaty Air Initiative и Сергека, не вашего приложения. Человек должен понимать, кому уходит фото. Текст согласия даём мы; в варианте A наша страница показывает его сама. Изменилась версия в GET /me — спросите согласие заново.
  2. Договор об обработке персональных данных между вашей компанией и оператором «Таза Алматы» — до запуска.
  3. Распознавание номера идёт в облаке Google (Vertex AI). Фото и видео покидают Казахстан; это сказано в согласии. До массового запуска распознавание переносится в контур Сергека.
  4. Секрет — только на сервере. Подпись запуска страницы тоже делает сервер.
  5. Удаление данных. Если человек удаляет аккаунт у вас — вызовите POST /delete-my-data от его имени.
  6. Не обещать штраф. Заявка — повод для проверки на экопосту, решение принимает уполномоченный орган. Номера чужих машин публиковать нельзя.

Чек-лист запуска

Вопросы, на которые нужен ответ партнёра до старта: