Это руководство проведёт вашу CRM/PMS от нуля до первого подтверждённого запроса в песочнице emehmon.uz: получить ключ, собрать HMAC-подпись, отправить запрос и убедиться, что всё работает — прямо здесь, в браузере.
Каждый ответ помечен X-Sandbox: 1 — вы всегда знаете, что в тестовом контуре.
МВД/ОВИР, ГНИ, ГТК и аналитика не вызываются — очередь заглушена. Реальные службы не затрагиваются.
Иностранцы всегда «подтверждены» — реальный PERSON_NOT_FOUND не воспроизводится.
HMAC, окно времени, анти-replay, лимиты, идемпотентность и коды ошибок — идентичны боевым.
Освоив эти шаги здесь, вы готовы к проду — контракт совпадает.
Администратор заводит ключ командой crm:sandbox-seed и передаёт две строки:
• X-User-Name — идентификатор, напр. hotel-0000001
• secret — 64-символьный hex, хранится только у вас и по сети не передаётся.
К каждому запросу — четыре заголовка. Подпись это HMAC-SHA256 от склейки name + ts + request_id вашим секретом, hex (нижний регистр).
| Заголовок | Значение |
|---|---|
| X-User-Name | Ваш идентификатор обязателен |
| X-User-Ts | Unix-время в секундах. Окно ±300 c — синхронизируйте часы (NTP) обязателен |
| X-User-Request-Id | UUID, уникальный на каждый запрос (анти-replay) обязателен |
| X-User-Hash | HMAC-SHA256(name + ts + request_id, secret) в hex обязателен |
# bash — подпись и запрос
USERNAME="hotel-0000001"
SECRET="ваш-секрет"
TS=$(date +%s)
REQ_ID=$(uuidgen)
HASH=$(printf '%s' "${USERNAME}${TS}${REQ_ID}" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $NF}')
curl -s 'http://sandbox-api.emehmon.uz/sync/v1/crm/dictionaries/countries' \
-H "X-User-Name: $USERNAME" \
-H "X-User-Ts: $TS" \
-H "X-User-Request-Id: $REQ_ID" \
-H "X-User-Hash: $HASH"# Python 3 — requests
import time, uuid, hmac, hashlib, requests
USERNAME = "hotel-0000001"
SECRET = "ваш-секрет"
ts = str(int(time.time()))
rid = str(uuid.uuid4())
sig = hmac.new(SECRET.encode(), (USERNAME+ts+rid).encode(), hashlib.sha256).hexdigest()
r = requests.get(
"http://sandbox-api.emehmon.uz/sync/v1/crm/dictionaries/countries",
headers={"X-User-Name":USERNAME,"X-User-Ts":ts,"X-User-Request-Id":rid,"X-User-Hash":sig})
print(r.status_code, r.headers.get("X-Sandbox"), r.json()["data"]["count"])// Node.js 18+ (fetch + crypto встроены)
import crypto from 'node:crypto';
const USERNAME='hotel-0000001', SECRET='ваш-секрет';
const ts=Math.floor(Date.now()/1000).toString(), rid=crypto.randomUUID();
const sig=crypto.createHmac('sha256',SECRET).update(USERNAME+ts+rid).digest('hex');
const r=await fetch('http://sandbox-api.emehmon.uz/sync/v1/crm/dictionaries/countries',{
headers:{'X-User-Name':USERNAME,'X-User-Ts':ts,'X-User-Request-Id':rid,'X-User-Hash':sig}});
console.log(r.status, r.headers.get('x-sandbox'));// PHP 8 — cURL
$username='hotel-0000001'; $secret='ваш-секрет';
$ts=(string)time(); $rid=bin2hex(random_bytes(16));
$sig=hash_hmac('sha256',$username.$ts.$rid,$secret);
$ch=curl_init('http://sandbox-api.emehmon.uz/sync/v1/crm/dictionaries/countries');
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_HTTPHEADER=>[
"X-User-Name: $username","X-User-Ts: $ts","X-User-Request-Id: $rid","X-User-Hash: $sig"]]);
echo curl_exec($ch);Начните со справочника — это read-only. GET /sync/v1/crm/dictionaries/countries вернёт 200, заголовок X-Sandbox: 1 и список стран с настоящими кодами.
Введите свой ключ — страница посчитает HMAC в браузере (Web Crypto) и отправит живой запрос. Тот же расчёт, что и в коде выше.
Когда чтение работает — переходите к записи. Ниже готовые тела запросов; заголовки HMAC — как в шаге 2.
POST /sync/v1/crm/listok · гражданин UZ/БГ — обязателен pinfl (14 цифр)
{
"surname": "Иванов", "firstname": "Иван",
"id_citizen": 173, "passportNumber": "AD1234567",
"datebirth": "1990-05-15", "datePassport": "2020-01-01",
"datevisiton": "2026-05-25T14:00:00Z", "wdays": 2,
"sex": "M", "id_visittype": 2, "id_guest": 1,
"pinfl": "32505901234567", "propiska": "101"
}
Ответ · 201
{ "code":201, "data":{ "listok_id":8826912, "regNum":"…-26", "registered_via":2, "foreign_duplicates":[] } }
POST /sync/v1/crm/listok · иностранец — обязателен id_person (строка!), pinfl не нужен
{
"surname": "Smith", "firstname": "John",
"id_citizen": 2, "id_country": 2, "id_countryFrom": 2,
"id_passporttype": 1, "passportNumber": "FR9876543",
"datebirth": "1985-03-20", "datePassport": "2021-06-01",
"datevisiton": "2026-05-25T14:00:00Z", "wdays": 2,
"sex": "M", "id_visittype": 2, "id_guest": 1,
"id_person": "987654321"
}
Ответ · 201
{ "code":201, "data":{ "listok_id":8826913, "regNum":"…-26", "registered_via":2 } }
PATCH /sync/v1/crm/listok/{id} · только в течение 5 мин после создания (grace)
Запрос — присылайте только меняющиеся поля{ "propiska": "202" }
Ответ · 200
{ "code":200, "data":{ "listok_id":8826912, "updated":true } }
После grace-периода — 409 LISTOK_GRACE_PERIOD_EXPIRED. Дети правятся только при создании (POST).
POST /sync/v1/crm/listok/{id}/checkout · выселение + оплата (расчёт турсбора на сервере)
Запрос —payment_type_id из справочника payment-types
{ "sum": 300000, "payment_type_id": 1 }
Ответ · 200
{ "code":200, "data":{ "listok_id":8826912, "checked_out":true,
"dateVisitOff":"2026-05-25T17:00:00+05:00" } }
Read-only GET-запросы, тело не нужно — только query-параметры year (обязателен) и month (опционален). Турсбор считается по дате выезда, регистрации и абонплата — по дате заезда.
GET /sync/v1/crm/finance/tourist-tax?year=2026&month=7 · без month — все 12 месяцев + итог
{ "code":200, "data":{ "basis":"checkout", "months":[ { "month":7, "listok_qty":412, "self_qty":8,
"gt_sum":17800000, "mt_sum":420000, "st_sum":230000, "total":18450000 } ],
"total":{ "total":18450000 } } }
GET /sync/v1/crm/finance/tourist-tax/details?year=2026&month=7&page=1&per_page=50 · month обязателен
{ "code":200, "data":{ "total":420, "items":[ { "source":"listok", "listok_id":8826912,
"surname":"SMITH", "citizenship":"Франция", "days":4, "prc":5, "mzrp":412000, "tax_sum":82400 } ] } }
GET /sync/v1/crm/finance/subscription?year=2026&month=7
Ответ · 200 — начисление появляется после закрытия месяца; пока его нет,accruals пуст
{ "code":200, "data":{ "accruals":[ { "period":"2026-07", "tariff":500000, "discount_percent":0,
"total":500000, "registrations":42 } ], "payments_total":250000, "current_deposit":1500000 } }
GET /sync/v1/crm/finance/registrations?year=2026&month=7 · за месяц — по дням, без month — по месяцам
registrations в абонплате
{ "code":200, "data":{ "basis":"checkin", "items":[ { "day":1, "listok_qty":14, "self_qty":1, "total":15 } ],
"total":{ "total":420 } } }
Живые данные тест-отеля и последние листовки, заведённые через API. Обновляется без перезагрузки.
Почти все проблемы на старте сводятся к подписи или времени.
| Ответ | Причина и что делать |
|---|---|
| 401 · MISSING_AUTH_HEADERS | Отправлены не все 4 заголовка. Проверьте X-User-Name/Ts/Request-Id/Hash. |
| 401 · INVALID_TIMESTAMP | Часы клиента разошлись больше чем на 5 минут. Синхронизируйте время; X-User-Ts — в секундах. |
| 401 · подпись | Хеш не сошёлся. Порядок склейки строго name + ts + request_id; hex в нижнем регистре; тот же секрет. |
| 422 · VALIDATION_ERROR | Смотрите meta.fields. Напр. id_person у иностранца должен быть строкой. |
| 429 · TOO_MANY_REQUESTS | Лимит 120 запросов/мин на ключ. Учитывайте Retry-After. |
| 502 · INTERNAL_GATEWAY_ERROR | Ошибка сервиса. В песочнице причина дублируется в meta.upstream_message. |