API телефонии Ringline

Получайте события о звонках в момент, когда они происходят, читайте справочник внутренних номеров, скачивайте записи разговоров и показывайте оператору карточку клиента, пока телефон ещё звонит.

Базовый адрес https://app.ringline.uz — один хост для всех эндпоинтов, только HTTPS. Машиночитаемая спецификация: openapi.json (OpenAPI 3.0 — импортируется в Postman, из неё генерируется клиент).

Что можно собрать

Ringline — облачная АТС. Стыковок с внешней системой четыре, и использовать можно любое подмножество:

ВозможностьНаправлениеКак
События звонков
звонит, ответили, завершён, пропущен
Ringline → вы Webhook с подписью HMAC-SHA256. См. типы событий.
Записи разговоров вы → Ringline В каждом завершённом звонке приходит ссылка на скачивание. См. записи.
Справочник номеров вы → Ringline Сопоставить своих пользователей с операторами и узнать, кто сейчас на линии.
Карточка звонящего
данные клиента на экране оператора
Ringline → вы Вы отдаёте один эндпоинт, мы дёргаем его на каждом входящем. См. карточка клиента.
Чего по API-ключу нет Инициировать исходящий звонок (click-to-call), читать историю звонков, менять статус оператора и отправлять SMS — всё это в Ringline есть, но в этот ключевой API не входит. Нужно что-то из перечисленного — скажите, это отдельный канал со своей аутентификацией.

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

  1. Получите ключ. Мы выдаём его и передаём отдельным каналом. Выглядит как rl_live_…, около 51 символа.
  2. Проверьте, что он работает.
    curl -H "Authorization: Bearer rl_live_ВАШ_КЛЮЧ" \
         https://app.ringline.uz/api/v1/account
    Код 200 и поле domain_name означают, что связь есть.
  3. Подпишите свой адрес. В ответе придёт signing_secret — сохраните его, больше он не показывается.
    curl -X POST https://app.ringline.uz/api/v1/webhooks \
         -H "Authorization: Bearer rl_live_ВАШ_КЛЮЧ" \
         -H "Content-Type: application/json" \
         -d '{"url":"https://crm.example.de/hooks/ringline","events":["*"]}'
  4. Убедитесь в доставке, не дожидаясь живого звонка.
    curl -X POST https://app.ringline.uz/api/v1/webhooks/test \
         -H "Authorization: Bearer rl_live_ВАШ_КЛЮЧ" \
         -H "Content-Type: application/json" \
         -d '{"event_type":"call.completed"}'
    В ответ придёт HTTP-код и тело, которые вернул ваш сервер, — видно, прошла ваша проверка подписи или нет.
  5. Обрабатывайте события. Проверьте подпись, затем разбирайте call.*. Отвечайте 2xx в пределах 10 секунд.

Аутентификация

В каждом запросе — постоянный API-ключ в виде bearer-токена:

Authorization: Bearer rl_live_4Vb9esauzSwtlt1js-Ati4KV0Nwtp6QirYeGkFgSzU4

Никакого OAuth, обновления токена и срока действия. Ключ живёт до отзыва в панели Ringline, и отзыв действует с первого же следующего запроса.

У нас хранится только SHA-256 от ключа, поэтому потерянный ключ восстановить нельзя — его отзывают и выдают новый. Обращайтесь с ним как с паролем: только на сервере, не в браузере, не в мобильном приложении и не в репозитории.

Области доступа

У ключа есть набор областей. Какие именно — покажет GET /account; на эндпоинт без нужной области придёт 403 insufficient_scope.

ОбластьНужна для
calls.readПолучения событий о звонках
recordings.readGET /api/v1/recordings/{call_uuid}
operators.readGET /api/v1/extensions
webhooks.manageПодписки, отписки, отправки тестового события

Песочница

Разработка идёт на отдельном арендаторе sandbox — на том же хосте, что и продакшен. В нём два внутренних номера (1001, 1002) и два оператора, живого трафика нет, так что испортить данные реального клиента оттуда невозможно.

Ключ песочницы запросите у нас. С ним доступно всё, кроме настоящих звонков: проверка ключа, чтение справочника, подписка и отправка образцов событий через POST /webhooks/test в любом количестве.

Формат ключа везде один Продакшен-ключ отличается от ключа песочницы только арендатором, на который указывает. Переход на продакшен — это замена одной строки, без правок кода.

Эндпоинты

GET/api/v1/account любая область

Проверяет ключ и описывает арендатора, которому он принадлежит. Подходит для кнопки «проверить подключение».

{
  "domain_uuid": "0e0d5944-a54e-4af6-ab27-14f0757851df",
  "domain_name": "sandbox",
  "key": {
    "id": 5,
    "name": "Sandbox - integration partners",
    "prefix": "rl_live_KCYp",
    "scopes": ["calls.read", "recordings.read", "operators.read", "webhooks.manage"],
    "created_at": "2026-11-09 15:20:00+05",
    "last_used_at": "2026-11-09 15:29:24+05"
  },
  "webhook": null,
  "extensions": [
    {
      "number": "1001",
      "label": "Operator: Sandbox One",
      "enabled": true,
      "operator_id": 50,
      "operator_name": "Sandbox One",
      "operator_email": "op1@sandbox.local",
      "medflow_user_id": null
    }
  ]
}

domain_uuid идентифицирует арендатора и приходит в каждом событии, так что мультиарендная интеграция может маршрутизировать события по нему. Поле medflow_user_id игнорируйте — оно от другой интеграции.

GET/api/v1/extensions operators.read

Тот же справочник плюс текущая доступность. Для сопоставления своих пользователей с операторами берите operator_email — внутренние номера переназначают, адреса нет.

{
  "extensions": [
    {
      "number": "1001",
      "label": "Operator: Sandbox One",
      "enabled": true,
      "operator_id": 50,
      "operator_name": "Sandbox One",
      "operator_email": "op1@sandbox.local",
      "presence_mode": "full",
      "inbound_enabled": true,
      "outbound_enabled": false
    }
  ]
}
ПолеЗначение
presence_modefull — принимает звонки; outgoing_only и offline не звонят вообще.
inbound_enabledАдминистративный переключатель. False — оператор исключён из входящих.
enabledСуществует ли сам внутренний номер в АТС.

Оператор не в режиме full или с выключенными входящими никогда не появится в событии call.ringing. Это первое, что стоит проверить при вопросе «почему этому человеку не пришёл звонок».

GET/api/v1/webhooks webhooks.manage
{ "subscribed": true, "url": "https://crm.example.de/hooks/ringline", "events": ["*"] }

Секрет подписи здесь намеренно не возвращается: он показывается один раз, при подписке.

POST/api/v1/webhooks webhooks.manage

Регистрирует адрес для событий и отдаёт секрет, которым они подписываются.

// запрос
{ "url": "https://crm.example.de/hooks/ringline", "events": ["*"] }

// ответ
{
  "subscribed": true,
  "url": "https://crm.example.de/hooks/ringline",
  "events": ["*"],
  "signing_secret": "v6tZj-XCkc4pMOCL05vthvT8OTRmoYAP2AN_WdjI7-ulG15HSnF3y2l5C_BLFZuU"
}

На ключ приходится одна подписка: повторный POST заменяет адрес и список событий. Секрет привязан к ключу и не меняется — чтобы его сменить, нужно выдать новый ключ.

Требования к адресу, проверяются и при регистрации, и заново перед каждой отправкой:

Отсутствие events равнозначно ["*"]. Неизвестный тип события даёт 422, а не молчаливое игнорирование.

DELETE/api/v1/webhooks webhooks.manage

Прекращает доставку. Ключ остаётся действительным, секрет не меняется — подписаться заново можно без перенастройки.

POST/api/v1/webhooks/test webhooks.manage

Отправляет на ваш адрес одно подписанное событие сейчас и сообщает, что ответил ваш сервер. Так приёмник проверяется до того, как у клиента пойдёт первый звонок.

// запрос (тело необязательно, по умолчанию минимальный «ping»)
{ "event_type": "call.completed" }

// ответ
{
  "delivered": true,
  "url": "https://crm.example.de/hooks/ringline",
  "event_type": "call.completed",
  "http_status": 200,
  "response_body": "{\"received\":true}",
  "sent_payload": { "...": "ровно то, что было подписано и отправлено" }
}
ПолеЗначение
deliveredTrue, если ваш сервер ответил 2xx.
http_statusВаш код ответа. 0 означает, что не удалось само соединение — DNS, TLS или таймаут.
response_bodyПервые 2000 символов вашего ответа — обычно этого хватает, чтобы увидеть собственное сообщение об ошибке.
Тестовые события помечены В корне объекта стоит "test": true, а образец звонка использует номера +998000000000 и +998000000001, которые не выдаются абонентам. В журнал доставок такое событие не пишется и не повторяется.
GET/api/v1/recordings/{call_uuid} recordings.read

Отдаёт аудио завершённого звонка. call_uuid — это call.call_uuid из события; готовая ссылка лежит в call.recording.url.

curl -H "Authorization: Bearer rl_live_ВАШ_КЛЮЧ" \
     -o call.mp3 \
     https://app.ringline.uz/api/v1/recordings/71dc4322-a21b-48d7-8adf-b743b4ac4e19

В ответе — само аудио, не JSON:

HTTP/1.1 200 OK
Content-Type: audio/mpeg
Content-Disposition: attachment; filename="71dc4322-a21b-48d7-8adf-b743b4ac4e19.mp3"
Accept-Ranges: bytes
Записи удаляются через 90 дней Хранение — скользящее окно в 90 дней. Дальше звонок остаётся в истории, а аудио нет, и эндпоинт отвечает 404. Если записи нужны дольше, скачивайте их по событию call.completed и храните у себя.

Типы событий

Четыре события, все про звонки. Подпишитесь на ["*"], чтобы получать всё, или перечислите нужные.

СобытиеКогдаТипичное применение
call.ringing Телефон начал звонить. Показать карточку клиента на экране, запустить таймер.
call.answered Оператор снял трубку. Отметить начало разговора и того, кто его взял.
call.completed Отвеченный звонок закончился. Записать в историю, забрать запись разговора.
call.missed Звонок закончился, оператор так и не ответил. Создать задачу на обратный звонок.
Только эти четыре Отдельного события о готовности записи нет — запись приезжает внутри call.completed. События о смене статуса оператора в этом канале тоже нет. Подписка на что-либо вне списка отклоняется с кодом 422, а не принимается с тем, чтобы никогда не прийти.

Групповой обзвон даёт ветки

Когда входящий звонит нескольким операторам сразу, вы получаете сам звонок плюс по одной ветке на каждое плечо оператора. У веток is_branch: true и parent_call_uuid, указывающий на родительский звонок.

Если нужна одна запись на разговор — обычный случай — игнорируйте все события с is_branch: true. Разбирайте ветки только если вам действительно важно знать, какие именно телефоны звонили.

Состав события

У всех событий одинаковая оболочка; поля, которые ещё неизвестны, равны null:

{
  "event_id": "evt_71dc4322-a21b-48d7-8adf-b743b4ac4e19_callcompleted_k5",
  "event_type": "call.completed",
  "occurred_at": "2026-11-09T15:24:45+05:00",
  "domain_uuid": "0e0d5944-a54e-4af6-ab27-14f0757851df",
  "call": {
    "call_uuid": "71dc4322-a21b-48d7-8adf-b743b4ac4e19",
    "is_branch": false,
    "parent_call_uuid": null,
    "direction": "inbound",
    "from_number": "+4930123456",
    "to_number": "+998781136247",
    "customer_number": "+4930123456",
    "status": "completed",
    "answered_by_operator": true,
    "ringing_at": "2026-11-09T15:24:21+05:00",
    "answered_at": "2026-11-09T15:24:26+05:00",
    "ended_at": "2026-11-09T15:24:45+05:00",
    "wait_seconds": 5,
    "talk_seconds": 19,
    "total_seconds": 24,
    "operator": {
      "extension": "1001",
      "name": "Sandbox One",
      "email": "op1@sandbox.local"
    },
    "recording": {
      "available": true,
      "url": "https://app.ringline.uz/api/v1/recordings/71dc4322-a21b-48d7-8adf-b743b4ac4e19",
      "format": "mp3",
      "duration_seconds": 19
    },
    "hangup_cause": "NORMAL_CLEARING"
  }
}
ПолеПояснение
event_idУникален и постоянен для пары (звонок, тип события). Используйте как ключ идемпотентности — повтор доставки несёт тот же event_id.
occurred_atISO 8601 со смещением арендатора (Узбекистан — +05:00, перехода на летнее время нет).
domain_uuidКакому арендатору принадлежит звонок.
customer_numberСопоставляйте свои записи по этому полю. Это номер человека вне компании независимо от направления, так что ветвиться по direction не нужно.
from_number / to_numberНомер звонящего и набранный номер в том виде, в каком их видела АТС.
answered_by_operatorTrue только когда ответил живой человек. Ответ автоинформатора или голосовой почты не считается, и тогда answered_at равен null, а talk_seconds — нулю.
wait_secondsСколько звонил телефон до ответа — показатель качества обслуживания.
talk_secondsДлительность разговора. Ноль, если никто не ответил.
total_secondsВесь звонок, включая время дозвона.
operatorКто обработал звонок. Null у неотвеченного группового звонка — назвать одного из тех, кто просто не снял трубку, было бы неверно.
recording.availableFalse, когда записи нет, а также для файлов короче 0,5 с — это артефакты тишины, а не разговоры.
hangup_causeПричина от FreeSWITCH/SIP: NORMAL_CLEARING, USER_BUSY, NO_ANSWER, ORIGINATOR_CANCEL, …
Отклонённый звонок выглядит как занято Когда оператор нажимает «отклонить» в софтфоне, АТС сообщает об этом так же, как о занятой линии (USER_BUSY). Отличить намеренный отказ от занятого телефона невозможно — не стройте логику на этом различии.

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

В каждой доставке приходят такие заголовки:

X-Ringline-Event: call.completed
X-Ringline-Timestamp: 1794219829
X-Ringline-Signature: t=1794219829,v1=1f6b1093b401382ef7bae0a324e6d1bb2bb9a5fa9ae7bd86fb7366cb45160ab8
User-Agent: Ringline-Webhook/1.0

Подпись — это HMAC-SHA256 от строки "<t>.<сырое тело запроса>" на вашем signing_secret, в шестнадцатеричном виде.

Подписывайте сырые байты Считайте HMAC от тела ровно в том виде, в каком оно пришло, до разбора JSON и повторной сериализации. Пересборка меняет пробелы и порядок ключей, и подпись не сойдётся. Заодно отклоняйте метку времени старше 5 минут, чтобы перехваченный запрос нельзя было воспроизвести позже.
<?php
$secret = getenv('RINGLINE_SIGNING_SECRET');
$raw    = file_get_contents('php://input');          // сырые байты, не разобранные
$header = $_SERVER['HTTP_X_RINGLINE_SIGNATURE'] ?? '';

$parts = [];
foreach (explode(',', $header) as $p) {
    [$k, $v] = array_pad(explode('=', trim($p), 2), 2, '');
    $parts[$k] = $v;
}

$ts = (int) ($parts['t'] ?? 0);
if (abs(time() - $ts) > 300) {                       // окно воспроизведения
    http_response_code(400);
    exit('stale timestamp');
}

$expected = hash_hmac('sha256', $ts . '.' . $raw, $secret);
if (!hash_equals($expected, $parts['v1'] ?? '')) {   // сравнение за одно время
    http_response_code(401);
    exit('bad signature');
}

$event = json_decode($raw, true);
// ... обработка $event, затем ответ 2xx
http_response_code(200);
echo '{"received":true}';
import hashlib, hmac, time
from flask import Flask, request

app = Flask(__name__)
SECRET = b"ваш-signing-secret"

@app.post("/hooks/ringline")
def ringline():
    raw = request.get_data()                      # сырые байты, не request.json
    header = request.headers.get("X-Ringline-Signature", "")
    parts = dict(
        p.strip().split("=", 1) for p in header.split(",") if "=" in p
    )

    ts = int(parts.get("t", 0))
    if abs(time.time() - ts) > 300:               # окно воспроизведения
        return "stale timestamp", 400

    expected = hmac.new(
        SECRET, f"{ts}.".encode() + raw, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(expected, parts.get("v1", "")):
        return "bad signature", 401

    event = request.get_json()
    # ... обработка события
    return {"received": True}, 200
// ASP.NET Core minimal API
app.MapPost("/hooks/ringline", async (HttpRequest req) =>
{
    using var reader = new StreamReader(req.Body);
    var raw = await reader.ReadToEndAsync();          // сырое тело

    var header = req.Headers["X-Ringline-Signature"].ToString();
    var parts = header.Split(',')
        .Select(p => p.Trim().Split('=', 2))
        .Where(p => p.Length == 2)
        .ToDictionary(p => p[0], p => p[1]);

    var ts = long.Parse(parts.GetValueOrDefault("t", "0"));
    var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
    if (Math.Abs(now - ts) > 300)
        return Results.BadRequest("stale timestamp");

    var secret = Encoding.UTF8.GetBytes(
        Environment.GetEnvironmentVariable("RINGLINE_SIGNING_SECRET")!);
    using var hmac = new HMACSHA256(secret);
    var computed = Convert.ToHexString(
        hmac.ComputeHash(Encoding.UTF8.GetBytes($"{ts}.{raw}"))
    ).ToLowerInvariant();

    if (!CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(computed),
            Encoding.UTF8.GetBytes(parts.GetValueOrDefault("v1", ""))))
        return Results.Unauthorized();

    // ... обработка события
    return Results.Ok(new { received = true });
});
const crypto = require('crypto');
const express = require('express');
const app = express();

// нужно именно сырое тело — express.json() его потеряет
app.post('/hooks/ringline',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const header = req.get('X-Ringline-Signature') || '';
    const parts = Object.fromEntries(
      header.split(',').map(p => p.trim().split('='))
    );

    const ts = Number(parts.t || 0);
    if (Math.abs(Date.now() / 1000 - ts) > 300) {
      return res.status(400).send('stale timestamp');
    }

    const expected = crypto
      .createHmac('sha256', process.env.RINGLINE_SIGNING_SECRET)
      .update(`${ts}.`)
      .update(req.body)                    // Buffer, без преобразований
      .digest('hex');

    const ok = crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(parts.v1 || '')
    );
    if (!ok) return res.status(401).send('bad signature');

    const event = JSON.parse(req.body.toString('utf8'));
    // ... обработка события
    res.json({ received: true });
  });

Доставка и повторы

СвойствоЗначение
МетодPOST, Content-Type: application/json; charset=utf-8
Ожидаемый ответЛюбой 2xx в пределах 10 секунд
Первая попыткаСразу же, из демона событий АТС
ПовторыЧерез 1 мин, 5 мин, 30 мин, 2 ч, далее раз в 24 часа
Предел попыток10, после чего событие оставляется
Ваш ответ 4xxСчитается окончательным: пишется в журнал, не повторяется
5xx, таймаут, сбой TLS или DNSПовторяется по расписанию выше
ПорядокНе гарантирован. Повторно доставленный ringing может прийти после completed.
РедиректыНе отслеживаются

Что из этого следует для приёмника:

Карточка клиента из вашей CRM

Обратное направление: при входящем звонке Ringline спрашивает вашу систему и показывает результат оператору до того, как он снял трубку, — имя клиента, уровень членства, ближайшую бронь.

Вы отдаёте один HTTP-эндпоинт, запрос и сопоставление полей мы настраиваем у себя, отдельно для каждого арендатора. Под новую CRM ничего разрабатывать не нужно, так что настройка занимает минуты, как только ваш эндпоинт готов.

Что отправляем мы

Метод GET или POST, ваши заголовки (обычно токен) и ваш шаблон тела. В них подставляются такие метки:

МеткаВо что превращаетсяПример
{phone}Номер ровно в том виде, в каком его видела АТС+4930123456
{phone_e164}Приведён к E.164 с ведущим ++998901234567
{phone_local}Только цифры, без + и разделителей998901234567
{domain_uuid}Арендатор — пригодится, если вы обслуживаете несколько наших клиентов0e0d5944-…

Девятизначный узбекский номер дополняется до +998…; всё остальное передаётся как есть с ведущим +, так что зарубежные звонящие приходят без искажений.

Пример настроенного POST-запроса:

POST https://crm.example.de/api/phone-lookup
Authorization: Bearer ВАШ_ТОКЕН_CRM
Content-Type: application/json

{ "msisdn": "{phone_e164}", "tenant": "{domain_uuid}" }

Если выбран GET и метки в URL нет, мы сами добавим ?phone=….

Что возвращаете вы

Любой JSON, который у вас уже есть. Мы достаём из него значения простыми путями — $.customer.name, $.bookings[0].area — и раскладываем по карточке из пяти полей, которую софтфон показывает как есть:

Поле карточкиГде видноЧто уместно
titleЗаголовокИмя клиента
subtitleВторая строкаУровень членства, сегмент
info_1Строка деталейБлижайшая бронь: дата и зона
info_2Строка деталейЗадолженность, пометка, число визитов
urlСсылкаПрямая ссылка на клиента в вашей CRM

Для такого ответа:

{
  "success": true,
  "customer": {
    "id": 8821,
    "name": "Klaus Berger",
    "membership": { "level": "Gold", "valid_until": "2027-03-01" },
    "bookings": [
      { "date": "2026-11-12", "area": "Sauna Panorama", "guests": 2 }
    ],
    "profile_url": "https://crm.example.de/customers/8821"
  }
}

сопоставление будет таким:

title    = $.customer.name                 → «Klaus Berger»
subtitle = $.customer.membership.level     → «Gold»
info_1   = $.customer.bookings[0].area     → «Sauna Panorama»
info_2   = $.customer.bookings[0].date     → «2026-11-12»
url      = $.customer.profile_url          → ссылка
found    = $.success                       → false означает «такого клиента нет»

Синтаксис путей намеренно простой: $.a.b, $.a[0].b, $["ключ с пробелами"]. Без фильтров, подстановочных знаков и рекурсивного спуска.

Требования

Пришлите адрес эндпоинта, заголовок авторизации, пример ответа и то, какое поле в какую строку карточки положить. Мы настроим и проверим на живом эндпоинте прямо из панели.

Ошибки

Два разных формата ошибок Слой аутентификации и обработчики эндпоинтов отвечают по-разному. Поддержите оба.

Аутентификация и права — 401, 403:

{ "error": "invalid_token", "error_description": "api key invalid or revoked" }
{ "error": "insufficient_scope", "error_description": "required scope: recordings.read" }

Проверка данных и состояние — 409, 422:

{ "success": false, "message": "url must be an absolute https:// URL" }
КодЗначениеЧто делать
200Успех—
401Нет заголовка, ключ искажён или отозванПроверить заголовок, попросить перевыпуск
403Ключ верный, области не хватаетПопросить расширить области ключа
404Записи нет, она чужого арендатора или истёк срок храненияНе повторять запрос вслепую
409Действие требует подписки, которой нетСначала подписаться
422Неверный адрес, неизвестный тип событияПрочитать message и исправить запрос
5xxНаша сторонаПовторить с задержкой, сообщить нам, если не проходит

Ограничения и хранение

ПредметЗначение
Хранение записей90 дней, затем удаляются
Формат записейСейчас MP3 (8 кГц, ~32 кбит/с); старые звонки — WAV
Таймаут вебхука10 с на попытку
Число попыток10, растянуты примерно на 5 суток
Окно против воспроизведения5 минут
Срок жизни ключаНе ограничен до отзыва
Подписок на ключ1
Частота запросовЖёсткого предела нет; /account опрашивать не чаще раза в минуту
Часовой поясAsia/Tashkent, +05:00 круглый год, без перехода на летнее время

Эндпоинта для вытягивания истории звонков в этом канале нет, поэтому вебхуки — единственный источник данных о звонках. Если приёмник недоступен дольше окна повторов, те звонки отсюда уже не восстановить — напишите нам, мы можем выслать их повторно из журнала доставок.

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

  1. GET /api/v1/account с ключом песочницы отвечает 200.
  2. Ваш адрес подписан, а POST /api/v1/webhooks/test сообщает "delivered": true.
  3. Ваша проверка подписи отклоняет подменённое тело — проверьте, изменив один байт.
  4. Обработчик отсеивает дубли по event_id и игнорирует is_branch: true.
  5. Вы отвечаете 2xx заметно быстрее 10 секунд, а настоящую работу делаете асинхронно.
  6. Скачивание записи работает, и вы сохраняете то, что нужно, — помните про окно в 90 дней.
  7. Если используете карточку клиента: ваш эндпоинт отвечает быстрее 2 секунд и явно сообщает «не найдено».
  8. Запросите продакшен-ключ и замените строку. Больше ничего менять не нужно.