[{"data":1,"prerenderedAt":3869},["ShallowReactive",2],{"blog-api-podtverzhdeniya-nomera":3,"blog-related-api-podtverzhdeniya-nomera":2687,"mdc-nkscbq-key":3778,"mdc-knfr0x-key":3810,"mdc--rvocxp-key":3826,"mdc-u461xl-key":3835,"mdc-6oai6c-key":3851,"mdc--n457a9-key":3860},{"id":4,"title":5,"body":6,"category":2650,"cta":2651,"description":2656,"draft":2657,"extension":2658,"faq":2659,"h1":2675,"meta":2676,"navigation":486,"order":697,"path":2677,"published":2678,"related":2679,"seo":2683,"stem":2684,"summary":2685,"updated":2678,"__hash__":2686},"blog\u002Fblog\u002Fapi-podtverzhdeniya-nomera.md","API подтверждения номера телефона — REST с примерами",{"type":7,"value":8,"toc":2633},"minimark",[9,13,18,84,91,104,108,160,163,199,212,215,286,290,296,312,316,328,405,411,414,436,440,445,1031,1035,1198,1202,1350,1354,1361,1365,1893,1897,2501,2505,2557,2561,2618,2621,2629],[10,11,12],"p",{},"Интеграция подтверждения номера обычно занимает не больше рабочего дня. Ниже — весь путь: от первого запроса до обработки граничных случаев, с примерами, которые можно копировать.",[14,15,17],"h2",{"id":16},"kak-ustroen-api","Как устроен API",[19,20,21,33,39,57,67],"ul",{},[22,23,24,28,29],"li",{},[25,26,27],"strong",{},"Базовый адрес:"," ",[30,31,32],"code",{},"https:\u002F\u002Fflashcall.ru\u002Fapi\u002Fv1",[22,34,35,38],{},[25,36,37],{},"Формат:"," JSON, обычный REST.",[22,40,41,44,45,48,49,52,53,56],{},[25,42,43],{},"Авторизация:"," ключ проекта передаётся полем ",[30,46,47],{},"token"," — в теле ",[30,50,51],{},"POST \u002Fget-phone"," или в query-строке ",[30,54,55],{},"GET \u002Fstatus",". Отдельных заголовков авторизации нет.",[22,58,59,62,63,66],{},[25,60,61],{},"Формат номера:"," E.164 — знак плюс, код страны и номер без разделителей: ",[30,64,65],{},"+79161234567",".",[22,68,69,28,72,75,76,79,80,83],{},[25,70,71],{},"Статусы проверки:",[30,73,74],{},"PENDING"," — ждём звонок, ",[30,77,78],{},"CONFIRMED"," — номер подтверждён, ",[30,81,82],{},"EXPIRED"," — за 60 секунд звонка не было.",[10,85,86,87,90],{},"Всего два запроса. Третий эндпоинт, ",[30,88,89],{},"POST \u002Finbound-call",", вызывает оператор телефонии при поступлении звонка — со стороны интеграции он не используется.",[10,92,93,94,99,100,66],{},"Если механизм в целом ещё не знаком, начните с разбора в статье про ",[95,96,98],"a",{"href":97},"\u002Fblog\u002Fpodtverzhdenie-nomera-zvonkom","подтверждение номера телефона звонком",": дальше по тексту предполагается, что схема «выдали номер — пользователь позвонил — забрали статус» уже понятна. Модель тарификации и критерии выбора провайдера — в материале про ",[95,101,103],{"href":102},"\u002Fblog\u002Fservis-podtverzhdeniya-nomera","сервисы подтверждения номера",[14,105,107],{"id":106},"shag-1-sozdat-proverku","Шаг 1. Создать проверку",[109,110,115],"pre",{"className":111,"code":112,"language":113,"meta":114,"style":114},"language-bash shiki shiki-themes github-light github-dark","curl -X POST \"https:\u002F\u002Fflashcall.ru\u002Fapi\u002Fv1\u002Fget-phone\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{ \"token\": \"YOUR_API_KEY\", \"phone\": \"+79161234567\" }'\n","bash","",[30,116,117,140,151],{"__ignoreMap":114},[118,119,122,126,130,134,137],"span",{"class":120,"line":121},"line",1,[118,123,125],{"class":124},"sScJk","curl",[118,127,129],{"class":128},"sj4cs"," -X",[118,131,133],{"class":132},"sZZnC"," POST",[118,135,136],{"class":132}," \"https:\u002F\u002Fflashcall.ru\u002Fapi\u002Fv1\u002Fget-phone\"",[118,138,139],{"class":128}," \\\n",[118,141,143,146,149],{"class":120,"line":142},2,[118,144,145],{"class":128},"  -H",[118,147,148],{"class":132}," \"Content-Type: application\u002Fjson\"",[118,150,139],{"class":128},[118,152,154,157],{"class":120,"line":153},3,[118,155,156],{"class":128},"  -d",[118,158,159],{"class":132}," '{ \"token\": \"YOUR_API_KEY\", \"phone\": \"+79161234567\" }'\n",[10,161,162],{},"Ответ:",[109,164,168],{"className":165,"code":166,"language":167,"meta":114,"style":114},"language-json shiki shiki-themes github-light github-dark","{ \"id\": \"c1x8f2k9b0001\", \"phone\": \"+74950000000\" }\n","json",[30,169,170],{"__ignoreMap":114},[118,171,172,176,179,182,185,188,191,193,196],{"class":120,"line":121},[118,173,175],{"class":174},"sVt8B","{ ",[118,177,178],{"class":128},"\"id\"",[118,180,181],{"class":174},": ",[118,183,184],{"class":132},"\"c1x8f2k9b0001\"",[118,186,187],{"class":174},", ",[118,189,190],{"class":128},"\"phone\"",[118,192,181],{"class":174},[118,194,195],{"class":132},"\"+74950000000\"",[118,197,198],{"class":174}," }\n",[10,200,201,204,205,208,209,211],{},[30,202,203],{},"id"," — идентификатор проверки, по нему запрашивается статус. ",[30,206,207],{},"phone"," — выделенный номер, на который пользователь должен позвонить. Проверка создаётся в статусе ",[30,210,74],{}," и живёт 60 секунд.",[10,213,214],{},"Возможные ошибки:",[216,217,220],"div",{"className":218},[219],"md-table",[221,222,223,239],"table",{},[224,225,226],"thead",{},[227,228,229,233,236],"tr",{},[230,231,232],"th",{},"Код",[230,234,235],{},"Причина",[230,237,238],{},"Что делать",[240,241,242,254,264,275],"tbody",{},[227,243,244,248,251],{},[245,246,247],"td",{},"400",[245,249,250],{},"неверный формат номера",[245,252,253],{},"привести к E.164 до отправки",[227,255,256,258,261],{},[245,257,247],{},[245,259,260],{},"недостаточно средств на балансе",[245,262,263],{},"пополнить, заранее завести оповещение",[227,265,266,269,272],{},[245,267,268],{},"404",[245,270,271],{},"ключ проекта не найден",[245,273,274],{},"проверить ключ и окружение",[227,276,277,280,283],{},[245,278,279],{},"503",[245,281,282],{},"нет доступных номеров в пуле",[245,284,285],{},"повторить попытку, показать запасной способ",[14,287,289],{"id":288},"shag-2-pokazat-nomer-polzovatelyu","Шаг 2. Показать номер пользователю",[10,291,292,293,295],{},"Во фронтенд отдаются только ",[30,294,203],{}," и выделенный номер — ключ проекта наружу не уходит.",[10,297,298,299,302,303,307,308,66],{},"Что должно быть на экране: номер крупно и с разделителями, на мобильных — ссылкой ",[30,300,301],{},"tel:","; таймер на 60 секунд; пояснение, что трубку брать не нужно; подсказка, что звонить надо именно с подтверждаемого номера. Разбор интерфейса — в материалах про ",[95,304,306],{"href":305},"\u002Fblog\u002Fpodtverzhdenie-nomera-na-sayte","подтверждение на сайте"," и ",[95,309,311],{"href":310},"\u002Fblog\u002Fpodtverzhdenie-nomera-v-prilozhenii","в мобильном приложении",[14,313,315],{"id":314},"shag-3-poluchit-rezultat","Шаг 3. Получить результат",[109,317,319],{"className":111,"code":318,"language":113,"meta":114,"style":114},"curl \"https:\u002F\u002Fflashcall.ru\u002Fapi\u002Fv1\u002Fstatus?token=YOUR_API_KEY&id=c1x8f2k9b0001\"\n",[30,320,321],{"__ignoreMap":114},[118,322,323,325],{"class":120,"line":121},[118,324,125],{"class":124},[118,326,327],{"class":132}," \"https:\u002F\u002Fflashcall.ru\u002Fapi\u002Fv1\u002Fstatus?token=YOUR_API_KEY&id=c1x8f2k9b0001\"\n",[109,329,331],{"className":165,"code":330,"language":167,"meta":114,"style":114},"{\n  \"id\": \"c1x8f2k9b0001\",\n  \"status\": \"PENDING\",\n  \"callerPhone\": \"+79161234567\",\n  \"cost\": \"0.20\",\n  \"createdAt\": \"2026-08-08T18:00:00.000Z\"\n}\n",[30,332,333,338,350,362,375,388,399],{"__ignoreMap":114},[118,334,335],{"class":120,"line":121},[118,336,337],{"class":174},"{\n",[118,339,340,343,345,347],{"class":120,"line":142},[118,341,342],{"class":128},"  \"id\"",[118,344,181],{"class":174},[118,346,184],{"class":132},[118,348,349],{"class":174},",\n",[118,351,352,355,357,360],{"class":120,"line":153},[118,353,354],{"class":128},"  \"status\"",[118,356,181],{"class":174},[118,358,359],{"class":132},"\"PENDING\"",[118,361,349],{"class":174},[118,363,365,368,370,373],{"class":120,"line":364},4,[118,366,367],{"class":128},"  \"callerPhone\"",[118,369,181],{"class":174},[118,371,372],{"class":132},"\"+79161234567\"",[118,374,349],{"class":174},[118,376,378,381,383,386],{"class":120,"line":377},5,[118,379,380],{"class":128},"  \"cost\"",[118,382,181],{"class":174},[118,384,385],{"class":132},"\"0.20\"",[118,387,349],{"class":174},[118,389,391,394,396],{"class":120,"line":390},6,[118,392,393],{"class":128},"  \"createdAt\"",[118,395,181],{"class":174},[118,397,398],{"class":132},"\"2026-08-08T18:00:00.000Z\"\n",[118,400,402],{"class":120,"line":401},7,[118,403,404],{"class":174},"}\n",[10,406,407,410],{},[25,408,409],{},"Вебхуков нет"," — результат забирается опросом. Это заметно упрощает интеграцию: не нужен публично доступный обработчик, не нужна проверка подписи, не нужна защита от повторной доставки.",[10,412,413],{},"Правила опроса:",[19,415,416,419,427,430],{},[22,417,418],{},"Интервал одна-две секунды. Чаще бессмысленно, реже — заметная задержка на экране.",[22,420,421,422,187,424,426],{},"Три условия остановки: ",[30,423,78],{},[30,425,82],{}," и истечение локального таймера. Без последнего цикл может не завершиться никогда.",[22,428,429],{},"Решение «номер подтверждён» принимается на сервере по ответу API, а не по тому, что прислал браузер.",[22,431,432,433,435],{},"Привязка номера должна отрабатывать один раз, даже если ",[30,434,78],{}," увиден несколькими запросами подряд.",[14,437,439],{"id":438},"primery-integracii","Примеры интеграции",[441,442,444],"h3",{"id":443},"node-js","Node.js",[109,446,450],{"className":447,"code":448,"language":449,"meta":114,"style":114},"language-js shiki shiki-themes github-light github-dark","const API = 'https:\u002F\u002Fflashcall.ru\u002Fapi\u002Fv1'\nconst TOKEN = process.env.FLASHCALL_TOKEN\n\nexport async function createVerification(phone) {\n  const res = await fetch(`${API}\u002Fget-phone`, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application\u002Fjson' },\n    body: JSON.stringify({ token: TOKEN, phone }),\n  })\n  if (!res.ok) throw new Error(`get-phone: ${res.status}`)\n\n  \u002F\u002F callTo показываем пользователю — на него нужно позвонить\n  const { id, phone: callTo } = await res.json()\n  return { id, callTo }\n}\n\nexport async function getStatus(id) {\n  const url = `${API}\u002Fstatus?token=${encodeURIComponent(TOKEN)}&id=${encodeURIComponent(id)}`\n  const res = await fetch(url)\n  if (!res.ok) throw new Error(`status: ${res.status}`)\n  return (await res.json()).status\n}\n\n\u002F** Ждём результат, пока проверка жива: TTL 60 секунд. *\u002F\nexport async function waitForCall(id, timeoutMs = 60_000) {\n  const deadline = Date.now() + timeoutMs\n  while (Date.now() \u003C deadline) {\n    const status = await getStatus(id)\n    if (status === 'CONFIRMED') return true\n    if (status === 'EXPIRED') return false\n    await new Promise((r) => setTimeout(r, 2000))\n  }\n  return false\n}\n","js",[30,451,452,467,482,488,511,541,551,567,590,596,639,644,651,686,695,700,705,723,765,781,813,830,835,840,846,874,899,918,936,960,979,1013,1019,1026],{"__ignoreMap":114},[118,453,454,458,461,464],{"class":120,"line":121},[118,455,457],{"class":456},"szBVR","const",[118,459,460],{"class":128}," API",[118,462,463],{"class":456}," =",[118,465,466],{"class":132}," 'https:\u002F\u002Fflashcall.ru\u002Fapi\u002Fv1'\n",[118,468,469,471,474,476,479],{"class":120,"line":142},[118,470,457],{"class":456},[118,472,473],{"class":128}," TOKEN",[118,475,463],{"class":456},[118,477,478],{"class":174}," process.env.",[118,480,481],{"class":128},"FLASHCALL_TOKEN\n",[118,483,484],{"class":120,"line":153},[118,485,487],{"emptyLinePlaceholder":486},true,"\n",[118,489,490,493,496,499,502,505,508],{"class":120,"line":364},[118,491,492],{"class":456},"export",[118,494,495],{"class":456}," async",[118,497,498],{"class":456}," function",[118,500,501],{"class":124}," createVerification",[118,503,504],{"class":174},"(",[118,506,207],{"class":507},"s4XuR",[118,509,510],{"class":174},") {\n",[118,512,513,516,519,521,524,527,529,532,535,538],{"class":120,"line":377},[118,514,515],{"class":456},"  const",[118,517,518],{"class":128}," res",[118,520,463],{"class":456},[118,522,523],{"class":456}," await",[118,525,526],{"class":124}," fetch",[118,528,504],{"class":174},[118,530,531],{"class":132},"`${",[118,533,534],{"class":128},"API",[118,536,537],{"class":132},"}\u002Fget-phone`",[118,539,540],{"class":174},", {\n",[118,542,543,546,549],{"class":120,"line":390},[118,544,545],{"class":174},"    method: ",[118,547,548],{"class":132},"'POST'",[118,550,349],{"class":174},[118,552,553,556,559,561,564],{"class":120,"line":401},[118,554,555],{"class":174},"    headers: { ",[118,557,558],{"class":132},"'Content-Type'",[118,560,181],{"class":174},[118,562,563],{"class":132},"'application\u002Fjson'",[118,565,566],{"class":174}," },\n",[118,568,570,573,576,578,581,584,587],{"class":120,"line":569},8,[118,571,572],{"class":174},"    body: ",[118,574,575],{"class":128},"JSON",[118,577,66],{"class":174},[118,579,580],{"class":124},"stringify",[118,582,583],{"class":174},"({ token: ",[118,585,586],{"class":128},"TOKEN",[118,588,589],{"class":174},", phone }),\n",[118,591,593],{"class":120,"line":592},9,[118,594,595],{"class":174},"  })\n",[118,597,599,602,605,608,611,614,617,620,622,625,628,630,633,636],{"class":120,"line":598},10,[118,600,601],{"class":456},"  if",[118,603,604],{"class":174}," (",[118,606,607],{"class":456},"!",[118,609,610],{"class":174},"res.ok) ",[118,612,613],{"class":456},"throw",[118,615,616],{"class":456}," new",[118,618,619],{"class":124}," Error",[118,621,504],{"class":174},[118,623,624],{"class":132},"`get-phone: ${",[118,626,627],{"class":174},"res",[118,629,66],{"class":132},[118,631,632],{"class":174},"status",[118,634,635],{"class":132},"}`",[118,637,638],{"class":174},")\n",[118,640,642],{"class":120,"line":641},11,[118,643,487],{"emptyLinePlaceholder":486},[118,645,647],{"class":120,"line":646},12,[118,648,650],{"class":649},"sJ8bj","  \u002F\u002F callTo показываем пользователю — на него нужно позвонить\n",[118,652,654,656,659,661,663,665,667,670,673,676,678,681,683],{"class":120,"line":653},13,[118,655,515],{"class":456},[118,657,658],{"class":174}," { ",[118,660,203],{"class":128},[118,662,187],{"class":174},[118,664,207],{"class":507},[118,666,181],{"class":174},[118,668,669],{"class":128},"callTo",[118,671,672],{"class":174}," } ",[118,674,675],{"class":456},"=",[118,677,523],{"class":456},[118,679,680],{"class":174}," res.",[118,682,167],{"class":124},[118,684,685],{"class":174},"()\n",[118,687,689,692],{"class":120,"line":688},14,[118,690,691],{"class":456},"  return",[118,693,694],{"class":174}," { id, callTo }\n",[118,696,698],{"class":120,"line":697},15,[118,699,404],{"class":174},[118,701,703],{"class":120,"line":702},16,[118,704,487],{"emptyLinePlaceholder":486},[118,706,708,710,712,714,717,719,721],{"class":120,"line":707},17,[118,709,492],{"class":456},[118,711,495],{"class":456},[118,713,498],{"class":456},[118,715,716],{"class":124}," getStatus",[118,718,504],{"class":174},[118,720,203],{"class":507},[118,722,510],{"class":174},[118,724,726,728,731,733,736,738,741,744,746,748,751,754,756,758,760,762],{"class":120,"line":725},18,[118,727,515],{"class":456},[118,729,730],{"class":128}," url",[118,732,463],{"class":456},[118,734,735],{"class":132}," `${",[118,737,534],{"class":128},[118,739,740],{"class":132},"}\u002Fstatus?token=${",[118,742,743],{"class":124},"encodeURIComponent",[118,745,504],{"class":132},[118,747,586],{"class":128},[118,749,750],{"class":132},")",[118,752,753],{"class":132},"}&id=${",[118,755,743],{"class":124},[118,757,504],{"class":132},[118,759,203],{"class":174},[118,761,750],{"class":132},[118,763,764],{"class":132},"}`\n",[118,766,768,770,772,774,776,778],{"class":120,"line":767},19,[118,769,515],{"class":456},[118,771,518],{"class":128},[118,773,463],{"class":456},[118,775,523],{"class":456},[118,777,526],{"class":124},[118,779,780],{"class":174},"(url)\n",[118,782,784,786,788,790,792,794,796,798,800,803,805,807,809,811],{"class":120,"line":783},20,[118,785,601],{"class":456},[118,787,604],{"class":174},[118,789,607],{"class":456},[118,791,610],{"class":174},[118,793,613],{"class":456},[118,795,616],{"class":456},[118,797,619],{"class":124},[118,799,504],{"class":174},[118,801,802],{"class":132},"`status: ${",[118,804,627],{"class":174},[118,806,66],{"class":132},[118,808,632],{"class":174},[118,810,635],{"class":132},[118,812,638],{"class":174},[118,814,816,818,820,823,825,827],{"class":120,"line":815},21,[118,817,691],{"class":456},[118,819,604],{"class":174},[118,821,822],{"class":456},"await",[118,824,680],{"class":174},[118,826,167],{"class":124},[118,828,829],{"class":174},"()).status\n",[118,831,833],{"class":120,"line":832},22,[118,834,404],{"class":174},[118,836,838],{"class":120,"line":837},23,[118,839,487],{"emptyLinePlaceholder":486},[118,841,843],{"class":120,"line":842},24,[118,844,845],{"class":649},"\u002F** Ждём результат, пока проверка жива: TTL 60 секунд. *\u002F\n",[118,847,849,851,853,855,858,860,862,864,867,869,872],{"class":120,"line":848},25,[118,850,492],{"class":456},[118,852,495],{"class":456},[118,854,498],{"class":456},[118,856,857],{"class":124}," waitForCall",[118,859,504],{"class":174},[118,861,203],{"class":507},[118,863,187],{"class":174},[118,865,866],{"class":507},"timeoutMs",[118,868,463],{"class":456},[118,870,871],{"class":128}," 60_000",[118,873,510],{"class":174},[118,875,877,879,882,884,887,890,893,896],{"class":120,"line":876},26,[118,878,515],{"class":456},[118,880,881],{"class":128}," deadline",[118,883,463],{"class":456},[118,885,886],{"class":174}," Date.",[118,888,889],{"class":124},"now",[118,891,892],{"class":174},"() ",[118,894,895],{"class":456},"+",[118,897,898],{"class":174}," timeoutMs\n",[118,900,902,905,908,910,912,915],{"class":120,"line":901},27,[118,903,904],{"class":456},"  while",[118,906,907],{"class":174}," (Date.",[118,909,889],{"class":124},[118,911,892],{"class":174},[118,913,914],{"class":456},"\u003C",[118,916,917],{"class":174}," deadline) {\n",[118,919,921,924,927,929,931,933],{"class":120,"line":920},28,[118,922,923],{"class":456},"    const",[118,925,926],{"class":128}," status",[118,928,463],{"class":456},[118,930,523],{"class":456},[118,932,716],{"class":124},[118,934,935],{"class":174},"(id)\n",[118,937,939,942,945,948,951,954,957],{"class":120,"line":938},29,[118,940,941],{"class":456},"    if",[118,943,944],{"class":174}," (status ",[118,946,947],{"class":456},"===",[118,949,950],{"class":132}," 'CONFIRMED'",[118,952,953],{"class":174},") ",[118,955,956],{"class":456},"return",[118,958,959],{"class":128}," true\n",[118,961,963,965,967,969,972,974,976],{"class":120,"line":962},30,[118,964,941],{"class":456},[118,966,944],{"class":174},[118,968,947],{"class":456},[118,970,971],{"class":132}," 'EXPIRED'",[118,973,953],{"class":174},[118,975,956],{"class":456},[118,977,978],{"class":128}," false\n",[118,980,982,985,987,990,993,996,998,1001,1004,1007,1010],{"class":120,"line":981},31,[118,983,984],{"class":456},"    await",[118,986,616],{"class":456},[118,988,989],{"class":128}," Promise",[118,991,992],{"class":174},"((",[118,994,995],{"class":507},"r",[118,997,953],{"class":174},[118,999,1000],{"class":456},"=>",[118,1002,1003],{"class":124}," setTimeout",[118,1005,1006],{"class":174},"(r, ",[118,1008,1009],{"class":128},"2000",[118,1011,1012],{"class":174},"))\n",[118,1014,1016],{"class":120,"line":1015},32,[118,1017,1018],{"class":174},"  }\n",[118,1020,1022,1024],{"class":120,"line":1021},33,[118,1023,691],{"class":456},[118,1025,978],{"class":128},[118,1027,1029],{"class":120,"line":1028},34,[118,1030,404],{"class":174},[441,1032,1034],{"id":1033},"php","PHP",[109,1036,1039],{"className":1037,"code":1038,"language":1033,"meta":114,"style":114},"language-php shiki shiki-themes github-light github-dark","\u003C?php\nconst FLASHCALL_API = 'https:\u002F\u002Fflashcall.ru\u002Fapi\u002Fv1';\n\nfunction flashcallRequest(string $url, ?array $body = null): array {\n    $ch = curl_init($url);\n    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n    curl_setopt($ch, CURLOPT_TIMEOUT, 10);\n    if ($body !== null) {\n        curl_setopt($ch, CURLOPT_POST, true);\n        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));\n        curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application\u002Fjson']);\n    }\n    $response = curl_exec($ch);\n    $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);\n    curl_close($ch);\n\n    if ($code >= 400) {\n        throw new RuntimeException(\"FlashCall API вернул $code: $response\");\n    }\n    return json_decode($response, true);\n}\n\nfunction createVerification(string $phone): array {\n    return flashcallRequest(FLASHCALL_API . '\u002Fget-phone', [\n        'token' => getenv('FLASHCALL_TOKEN'),\n        'phone' => $phone,\n    ]);\n}\n\nfunction getStatus(string $id): string {\n    $query = http_build_query(['token' => getenv('FLASHCALL_TOKEN'), 'id' => $id]);\n    return flashcallRequest(FLASHCALL_API . '\u002Fstatus?' . $query)['status'];\n}\n",[30,1040,1041,1046,1051,1055,1060,1065,1070,1075,1080,1085,1090,1095,1100,1105,1110,1115,1119,1124,1129,1133,1138,1142,1146,1151,1156,1161,1166,1171,1175,1179,1184,1189,1194],{"__ignoreMap":114},[118,1042,1043],{"class":120,"line":121},[118,1044,1045],{},"\u003C?php\n",[118,1047,1048],{"class":120,"line":142},[118,1049,1050],{},"const FLASHCALL_API = 'https:\u002F\u002Fflashcall.ru\u002Fapi\u002Fv1';\n",[118,1052,1053],{"class":120,"line":153},[118,1054,487],{"emptyLinePlaceholder":486},[118,1056,1057],{"class":120,"line":364},[118,1058,1059],{},"function flashcallRequest(string $url, ?array $body = null): array {\n",[118,1061,1062],{"class":120,"line":377},[118,1063,1064],{},"    $ch = curl_init($url);\n",[118,1066,1067],{"class":120,"line":390},[118,1068,1069],{},"    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n",[118,1071,1072],{"class":120,"line":401},[118,1073,1074],{},"    curl_setopt($ch, CURLOPT_TIMEOUT, 10);\n",[118,1076,1077],{"class":120,"line":569},[118,1078,1079],{},"    if ($body !== null) {\n",[118,1081,1082],{"class":120,"line":592},[118,1083,1084],{},"        curl_setopt($ch, CURLOPT_POST, true);\n",[118,1086,1087],{"class":120,"line":598},[118,1088,1089],{},"        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));\n",[118,1091,1092],{"class":120,"line":641},[118,1093,1094],{},"        curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application\u002Fjson']);\n",[118,1096,1097],{"class":120,"line":646},[118,1098,1099],{},"    }\n",[118,1101,1102],{"class":120,"line":653},[118,1103,1104],{},"    $response = curl_exec($ch);\n",[118,1106,1107],{"class":120,"line":688},[118,1108,1109],{},"    $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);\n",[118,1111,1112],{"class":120,"line":697},[118,1113,1114],{},"    curl_close($ch);\n",[118,1116,1117],{"class":120,"line":702},[118,1118,487],{"emptyLinePlaceholder":486},[118,1120,1121],{"class":120,"line":707},[118,1122,1123],{},"    if ($code >= 400) {\n",[118,1125,1126],{"class":120,"line":725},[118,1127,1128],{},"        throw new RuntimeException(\"FlashCall API вернул $code: $response\");\n",[118,1130,1131],{"class":120,"line":767},[118,1132,1099],{},[118,1134,1135],{"class":120,"line":783},[118,1136,1137],{},"    return json_decode($response, true);\n",[118,1139,1140],{"class":120,"line":815},[118,1141,404],{},[118,1143,1144],{"class":120,"line":832},[118,1145,487],{"emptyLinePlaceholder":486},[118,1147,1148],{"class":120,"line":837},[118,1149,1150],{},"function createVerification(string $phone): array {\n",[118,1152,1153],{"class":120,"line":842},[118,1154,1155],{},"    return flashcallRequest(FLASHCALL_API . '\u002Fget-phone', [\n",[118,1157,1158],{"class":120,"line":848},[118,1159,1160],{},"        'token' => getenv('FLASHCALL_TOKEN'),\n",[118,1162,1163],{"class":120,"line":876},[118,1164,1165],{},"        'phone' => $phone,\n",[118,1167,1168],{"class":120,"line":901},[118,1169,1170],{},"    ]);\n",[118,1172,1173],{"class":120,"line":920},[118,1174,404],{},[118,1176,1177],{"class":120,"line":938},[118,1178,487],{"emptyLinePlaceholder":486},[118,1180,1181],{"class":120,"line":962},[118,1182,1183],{},"function getStatus(string $id): string {\n",[118,1185,1186],{"class":120,"line":981},[118,1187,1188],{},"    $query = http_build_query(['token' => getenv('FLASHCALL_TOKEN'), 'id' => $id]);\n",[118,1190,1191],{"class":120,"line":1015},[118,1192,1193],{},"    return flashcallRequest(FLASHCALL_API . '\u002Fstatus?' . $query)['status'];\n",[118,1195,1196],{"class":120,"line":1021},[118,1197,404],{},[441,1199,1201],{"id":1200},"python","Python",[109,1203,1206],{"className":1204,"code":1205,"language":1200,"meta":114,"style":114},"language-python shiki shiki-themes github-light github-dark","import os, time, requests\n\nAPI = \"https:\u002F\u002Fflashcall.ru\u002Fapi\u002Fv1\"\nTOKEN = os.environ[\"FLASHCALL_TOKEN\"]\n\n\ndef create_verification(phone: str) -> tuple[str, str]:\n    r = requests.post(f\"{API}\u002Fget-phone\", json={\"token\": TOKEN, \"phone\": phone}, timeout=10)\n    r.raise_for_status()\n    data = r.json()\n    # второй элемент — номер, на который звонит пользователь\n    return data[\"id\"], data[\"phone\"]\n\n\ndef get_status(check_id: str) -> str:\n    r = requests.get(f\"{API}\u002Fstatus\", params={\"token\": TOKEN, \"id\": check_id}, timeout=10)\n    r.raise_for_status()\n    return r.json()[\"status\"]\n\n\ndef wait_for_call(check_id: str, timeout: float = 60.0) -> bool:\n    deadline = time.monotonic() + timeout\n    while time.monotonic() \u003C deadline:\n        status = get_status(check_id)\n        if status == \"CONFIRMED\":\n            return True\n        if status == \"EXPIRED\":\n            return False\n        time.sleep(2)\n    return False\n",[30,1207,1208,1213,1217,1222,1227,1231,1235,1240,1245,1250,1255,1260,1265,1269,1273,1278,1283,1287,1292,1296,1300,1305,1310,1315,1320,1325,1330,1335,1340,1345],{"__ignoreMap":114},[118,1209,1210],{"class":120,"line":121},[118,1211,1212],{},"import os, time, requests\n",[118,1214,1215],{"class":120,"line":142},[118,1216,487],{"emptyLinePlaceholder":486},[118,1218,1219],{"class":120,"line":153},[118,1220,1221],{},"API = \"https:\u002F\u002Fflashcall.ru\u002Fapi\u002Fv1\"\n",[118,1223,1224],{"class":120,"line":364},[118,1225,1226],{},"TOKEN = os.environ[\"FLASHCALL_TOKEN\"]\n",[118,1228,1229],{"class":120,"line":377},[118,1230,487],{"emptyLinePlaceholder":486},[118,1232,1233],{"class":120,"line":390},[118,1234,487],{"emptyLinePlaceholder":486},[118,1236,1237],{"class":120,"line":401},[118,1238,1239],{},"def create_verification(phone: str) -> tuple[str, str]:\n",[118,1241,1242],{"class":120,"line":569},[118,1243,1244],{},"    r = requests.post(f\"{API}\u002Fget-phone\", json={\"token\": TOKEN, \"phone\": phone}, timeout=10)\n",[118,1246,1247],{"class":120,"line":592},[118,1248,1249],{},"    r.raise_for_status()\n",[118,1251,1252],{"class":120,"line":598},[118,1253,1254],{},"    data = r.json()\n",[118,1256,1257],{"class":120,"line":641},[118,1258,1259],{},"    # второй элемент — номер, на который звонит пользователь\n",[118,1261,1262],{"class":120,"line":646},[118,1263,1264],{},"    return data[\"id\"], data[\"phone\"]\n",[118,1266,1267],{"class":120,"line":653},[118,1268,487],{"emptyLinePlaceholder":486},[118,1270,1271],{"class":120,"line":688},[118,1272,487],{"emptyLinePlaceholder":486},[118,1274,1275],{"class":120,"line":697},[118,1276,1277],{},"def get_status(check_id: str) -> str:\n",[118,1279,1280],{"class":120,"line":702},[118,1281,1282],{},"    r = requests.get(f\"{API}\u002Fstatus\", params={\"token\": TOKEN, \"id\": check_id}, timeout=10)\n",[118,1284,1285],{"class":120,"line":707},[118,1286,1249],{},[118,1288,1289],{"class":120,"line":725},[118,1290,1291],{},"    return r.json()[\"status\"]\n",[118,1293,1294],{"class":120,"line":767},[118,1295,487],{"emptyLinePlaceholder":486},[118,1297,1298],{"class":120,"line":783},[118,1299,487],{"emptyLinePlaceholder":486},[118,1301,1302],{"class":120,"line":815},[118,1303,1304],{},"def wait_for_call(check_id: str, timeout: float = 60.0) -> bool:\n",[118,1306,1307],{"class":120,"line":832},[118,1308,1309],{},"    deadline = time.monotonic() + timeout\n",[118,1311,1312],{"class":120,"line":837},[118,1313,1314],{},"    while time.monotonic() \u003C deadline:\n",[118,1316,1317],{"class":120,"line":842},[118,1318,1319],{},"        status = get_status(check_id)\n",[118,1321,1322],{"class":120,"line":848},[118,1323,1324],{},"        if status == \"CONFIRMED\":\n",[118,1326,1327],{"class":120,"line":876},[118,1328,1329],{},"            return True\n",[118,1331,1332],{"class":120,"line":901},[118,1333,1334],{},"        if status == \"EXPIRED\":\n",[118,1336,1337],{"class":120,"line":920},[118,1338,1339],{},"            return False\n",[118,1341,1342],{"class":120,"line":938},[118,1343,1344],{},"        time.sleep(2)\n",[118,1346,1347],{"class":120,"line":962},[118,1348,1349],{},"    return False\n",[14,1351,1353],{"id":1352},"frontend","Фронтенд",[10,1355,1356,1357,1360],{},"Оба примера ходят в ",[25,1358,1359],{},"ваш"," эндпоинт, а не в API напрямую: ключ проекта в браузере размещать нельзя.",[441,1362,1364],{"id":1363},"vue","Vue",[109,1366,1369],{"className":1367,"code":1368,"language":1363,"meta":114,"style":114},"language-vue shiki shiki-themes github-light github-dark","\u003Cscript setup>\nimport { ref, onUnmounted } from 'vue'\n\nconst callTo = ref('')\nconst secondsLeft = ref(0)\nconst done = ref(false)\nlet timer = null\n\nasync function start(phone) {\n  const res = await fetch('\u002Fapi\u002Fverification\u002Fstart', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application\u002Fjson' },\n    body: JSON.stringify({ phone }),\n  })\n  const { id, callTo: number } = await res.json()\n\n  callTo.value = number\n  secondsLeft.value = 60\n\n  timer = setInterval(async () => {\n    secondsLeft.value--\n    const { status } = await (await fetch(`\u002Fapi\u002Fverification\u002F${id}`)).json()\n\n    if (status === 'CONFIRMED') {\n      done.value = true\n      stop()\n    } else if (status === 'EXPIRED' || secondsLeft.value \u003C= 0) {\n      stop()\n    }\n  }, 2000)\n}\n\nfunction stop() {\n  clearInterval(timer)\n  timer = null\n}\n\nonUnmounted(stop)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Ca v-if=\"callTo && !done\" :href=\"`tel:${callTo}`\">Позвонить на {{ callTo }}\u003C\u002Fa>\n  \u003Cp v-if=\"secondsLeft > 0\">Ждём звонок: {{ secondsLeft }} с\u003C\u002Fp>\n\u003C\u002Ftemplate>\n",[30,1370,1371,1385,1399,1403,1422,1440,1458,1471,1475,1491,1510,1518,1530,1543,1547,1576,1580,1590,1600,1604,1626,1634,1670,1674,1686,1695,1702,1733,1739,1743,1752,1756,1760,1771,1779,1788,1793,1798,1807,1817,1822,1832,1863,1884],{"__ignoreMap":114},[118,1372,1373,1375,1379,1382],{"class":120,"line":121},[118,1374,914],{"class":174},[118,1376,1378],{"class":1377},"s9eBZ","script",[118,1380,1381],{"class":124}," setup",[118,1383,1384],{"class":174},">\n",[118,1386,1387,1390,1393,1396],{"class":120,"line":142},[118,1388,1389],{"class":456},"import",[118,1391,1392],{"class":174}," { ref, onUnmounted } ",[118,1394,1395],{"class":456},"from",[118,1397,1398],{"class":132}," 'vue'\n",[118,1400,1401],{"class":120,"line":153},[118,1402,487],{"emptyLinePlaceholder":486},[118,1404,1405,1407,1410,1412,1415,1417,1420],{"class":120,"line":364},[118,1406,457],{"class":456},[118,1408,1409],{"class":128}," callTo",[118,1411,463],{"class":456},[118,1413,1414],{"class":124}," ref",[118,1416,504],{"class":174},[118,1418,1419],{"class":132},"''",[118,1421,638],{"class":174},[118,1423,1424,1426,1429,1431,1433,1435,1438],{"class":120,"line":377},[118,1425,457],{"class":456},[118,1427,1428],{"class":128}," secondsLeft",[118,1430,463],{"class":456},[118,1432,1414],{"class":124},[118,1434,504],{"class":174},[118,1436,1437],{"class":128},"0",[118,1439,638],{"class":174},[118,1441,1442,1444,1447,1449,1451,1453,1456],{"class":120,"line":390},[118,1443,457],{"class":456},[118,1445,1446],{"class":128}," done",[118,1448,463],{"class":456},[118,1450,1414],{"class":124},[118,1452,504],{"class":174},[118,1454,1455],{"class":128},"false",[118,1457,638],{"class":174},[118,1459,1460,1463,1466,1468],{"class":120,"line":401},[118,1461,1462],{"class":456},"let",[118,1464,1465],{"class":174}," timer ",[118,1467,675],{"class":456},[118,1469,1470],{"class":128}," null\n",[118,1472,1473],{"class":120,"line":569},[118,1474,487],{"emptyLinePlaceholder":486},[118,1476,1477,1480,1482,1485,1487,1489],{"class":120,"line":592},[118,1478,1479],{"class":456},"async",[118,1481,498],{"class":456},[118,1483,1484],{"class":124}," start",[118,1486,504],{"class":174},[118,1488,207],{"class":507},[118,1490,510],{"class":174},[118,1492,1493,1495,1497,1499,1501,1503,1505,1508],{"class":120,"line":598},[118,1494,515],{"class":456},[118,1496,518],{"class":128},[118,1498,463],{"class":456},[118,1500,523],{"class":456},[118,1502,526],{"class":124},[118,1504,504],{"class":174},[118,1506,1507],{"class":132},"'\u002Fapi\u002Fverification\u002Fstart'",[118,1509,540],{"class":174},[118,1511,1512,1514,1516],{"class":120,"line":641},[118,1513,545],{"class":174},[118,1515,548],{"class":132},[118,1517,349],{"class":174},[118,1519,1520,1522,1524,1526,1528],{"class":120,"line":646},[118,1521,555],{"class":174},[118,1523,558],{"class":132},[118,1525,181],{"class":174},[118,1527,563],{"class":132},[118,1529,566],{"class":174},[118,1531,1532,1534,1536,1538,1540],{"class":120,"line":653},[118,1533,572],{"class":174},[118,1535,575],{"class":128},[118,1537,66],{"class":174},[118,1539,580],{"class":124},[118,1541,1542],{"class":174},"({ phone }),\n",[118,1544,1545],{"class":120,"line":688},[118,1546,595],{"class":174},[118,1548,1549,1551,1553,1555,1557,1559,1561,1564,1566,1568,1570,1572,1574],{"class":120,"line":697},[118,1550,515],{"class":456},[118,1552,658],{"class":174},[118,1554,203],{"class":128},[118,1556,187],{"class":174},[118,1558,669],{"class":507},[118,1560,181],{"class":174},[118,1562,1563],{"class":128},"number",[118,1565,672],{"class":174},[118,1567,675],{"class":456},[118,1569,523],{"class":456},[118,1571,680],{"class":174},[118,1573,167],{"class":124},[118,1575,685],{"class":174},[118,1577,1578],{"class":120,"line":702},[118,1579,487],{"emptyLinePlaceholder":486},[118,1581,1582,1585,1587],{"class":120,"line":707},[118,1583,1584],{"class":174},"  callTo.value ",[118,1586,675],{"class":456},[118,1588,1589],{"class":174}," number\n",[118,1591,1592,1595,1597],{"class":120,"line":725},[118,1593,1594],{"class":174},"  secondsLeft.value ",[118,1596,675],{"class":456},[118,1598,1599],{"class":128}," 60\n",[118,1601,1602],{"class":120,"line":767},[118,1603,487],{"emptyLinePlaceholder":486},[118,1605,1606,1609,1611,1614,1616,1618,1621,1623],{"class":120,"line":783},[118,1607,1608],{"class":174},"  timer ",[118,1610,675],{"class":456},[118,1612,1613],{"class":124}," setInterval",[118,1615,504],{"class":174},[118,1617,1479],{"class":456},[118,1619,1620],{"class":174}," () ",[118,1622,1000],{"class":456},[118,1624,1625],{"class":174}," {\n",[118,1627,1628,1631],{"class":120,"line":815},[118,1629,1630],{"class":174},"    secondsLeft.value",[118,1632,1633],{"class":456},"--\n",[118,1635,1636,1638,1640,1642,1644,1646,1648,1650,1652,1654,1656,1659,1661,1663,1666,1668],{"class":120,"line":832},[118,1637,923],{"class":456},[118,1639,658],{"class":174},[118,1641,632],{"class":128},[118,1643,672],{"class":174},[118,1645,675],{"class":456},[118,1647,523],{"class":456},[118,1649,604],{"class":174},[118,1651,822],{"class":456},[118,1653,526],{"class":124},[118,1655,504],{"class":174},[118,1657,1658],{"class":132},"`\u002Fapi\u002Fverification\u002F${",[118,1660,203],{"class":174},[118,1662,635],{"class":132},[118,1664,1665],{"class":174},")).",[118,1667,167],{"class":124},[118,1669,685],{"class":174},[118,1671,1672],{"class":120,"line":837},[118,1673,487],{"emptyLinePlaceholder":486},[118,1675,1676,1678,1680,1682,1684],{"class":120,"line":842},[118,1677,941],{"class":456},[118,1679,944],{"class":174},[118,1681,947],{"class":456},[118,1683,950],{"class":132},[118,1685,510],{"class":174},[118,1687,1688,1691,1693],{"class":120,"line":848},[118,1689,1690],{"class":174},"      done.value ",[118,1692,675],{"class":456},[118,1694,959],{"class":128},[118,1696,1697,1700],{"class":120,"line":876},[118,1698,1699],{"class":124},"      stop",[118,1701,685],{"class":174},[118,1703,1704,1707,1710,1713,1715,1717,1719,1722,1725,1728,1731],{"class":120,"line":901},[118,1705,1706],{"class":174},"    } ",[118,1708,1709],{"class":456},"else",[118,1711,1712],{"class":456}," if",[118,1714,944],{"class":174},[118,1716,947],{"class":456},[118,1718,971],{"class":132},[118,1720,1721],{"class":456}," ||",[118,1723,1724],{"class":174}," secondsLeft.value ",[118,1726,1727],{"class":456},"\u003C=",[118,1729,1730],{"class":128}," 0",[118,1732,510],{"class":174},[118,1734,1735,1737],{"class":120,"line":920},[118,1736,1699],{"class":124},[118,1738,685],{"class":174},[118,1740,1741],{"class":120,"line":938},[118,1742,1099],{"class":174},[118,1744,1745,1748,1750],{"class":120,"line":962},[118,1746,1747],{"class":174},"  }, ",[118,1749,1009],{"class":128},[118,1751,638],{"class":174},[118,1753,1754],{"class":120,"line":981},[118,1755,404],{"class":174},[118,1757,1758],{"class":120,"line":1015},[118,1759,487],{"emptyLinePlaceholder":486},[118,1761,1762,1765,1768],{"class":120,"line":1021},[118,1763,1764],{"class":456},"function",[118,1766,1767],{"class":124}," stop",[118,1769,1770],{"class":174},"() {\n",[118,1772,1773,1776],{"class":120,"line":1028},[118,1774,1775],{"class":124},"  clearInterval",[118,1777,1778],{"class":174},"(timer)\n",[118,1780,1782,1784,1786],{"class":120,"line":1781},35,[118,1783,1608],{"class":174},[118,1785,675],{"class":456},[118,1787,1470],{"class":128},[118,1789,1791],{"class":120,"line":1790},36,[118,1792,404],{"class":174},[118,1794,1796],{"class":120,"line":1795},37,[118,1797,487],{"emptyLinePlaceholder":486},[118,1799,1801,1804],{"class":120,"line":1800},38,[118,1802,1803],{"class":124},"onUnmounted",[118,1805,1806],{"class":174},"(stop)\n",[118,1808,1810,1813,1815],{"class":120,"line":1809},39,[118,1811,1812],{"class":174},"\u003C\u002F",[118,1814,1378],{"class":1377},[118,1816,1384],{"class":174},[118,1818,1820],{"class":120,"line":1819},40,[118,1821,487],{"emptyLinePlaceholder":486},[118,1823,1825,1827,1830],{"class":120,"line":1824},41,[118,1826,914],{"class":174},[118,1828,1829],{"class":1377},"template",[118,1831,1384],{"class":174},[118,1833,1835,1838,1840,1843,1845,1848,1851,1853,1856,1859,1861],{"class":120,"line":1834},42,[118,1836,1837],{"class":174},"  \u003C",[118,1839,95],{"class":1377},[118,1841,1842],{"class":124}," v-if",[118,1844,675],{"class":174},[118,1846,1847],{"class":132},"\"callTo && !done\"",[118,1849,1850],{"class":124}," :href",[118,1852,675],{"class":174},[118,1854,1855],{"class":132},"\"`tel:${callTo}`\"",[118,1857,1858],{"class":174},">Позвонить на {{ callTo }}\u003C\u002F",[118,1860,95],{"class":1377},[118,1862,1384],{"class":174},[118,1864,1866,1868,1870,1872,1874,1877,1880,1882],{"class":120,"line":1865},43,[118,1867,1837],{"class":174},[118,1869,10],{"class":1377},[118,1871,1842],{"class":124},[118,1873,675],{"class":174},[118,1875,1876],{"class":132},"\"secondsLeft > 0\"",[118,1878,1879],{"class":174},">Ждём звонок: {{ secondsLeft }} с\u003C\u002F",[118,1881,10],{"class":1377},[118,1883,1384],{"class":174},[118,1885,1887,1889,1891],{"class":120,"line":1886},44,[118,1888,1812],{"class":174},[118,1890,1829],{"class":1377},[118,1892,1384],{"class":174},[441,1894,1896],{"id":1895},"react","React",[109,1898,1902],{"className":1899,"code":1900,"language":1901,"meta":114,"style":114},"language-jsx shiki shiki-themes github-light github-dark","import { useEffect, useRef, useState } from 'react'\n\nexport function CallVerification({ phone, onConfirmed }) {\n  const [callTo, setCallTo] = useState('')\n  const [secondsLeft, setSecondsLeft] = useState(0)\n  const idRef = useRef(null)\n\n  useEffect(() => {\n    let cancelled = false\n\n    async function start() {\n      const res = await fetch('\u002Fapi\u002Fverification\u002Fstart', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application\u002Fjson' },\n        body: JSON.stringify({ phone }),\n      })\n      const data = await res.json()\n      if (cancelled) return\n\n      idRef.current = data.id\n      setCallTo(data.callTo)\n      setSecondsLeft(60)\n    }\n\n    start()\n    return () => { cancelled = true }\n  }, [phone])\n\n  useEffect(() => {\n    if (!idRef.current || secondsLeft \u003C= 0) return\n\n    const timer = setTimeout(async () => {\n      const { status } = await (await fetch(`\u002Fapi\u002Fverification\u002F${idRef.current}`)).json()\n      if (status === 'CONFIRMED') onConfirmed()\n      else setSecondsLeft((s) => s - 2)\n    }, 2000)\n\n    return () => clearTimeout(timer)\n  }, [secondsLeft, onConfirmed])\n\n  if (!callTo) return null\n  return (\n    \u003C>\n      \u003Ca href={`tel:${callTo}`}>Позвонить на {callTo}\u003C\u002Fa>\n      {secondsLeft > 0 && \u003Cp>Ждём звонок: {secondsLeft} с\u003C\u002Fp>}\n    \u003C\u002F>\n  )\n}\n","jsx",[30,1903,1904,1916,1920,1942,1970,1996,2015,2019,2031,2043,2047,2058,2077,2086,2099,2112,2117,2134,2145,2149,2159,2167,2179,2183,2187,2194,2213,2218,2222,2232,2257,2261,2282,2322,2338,2366,2375,2379,2392,2397,2401,2416,2423,2428,2457,2484,2490,2496],{"__ignoreMap":114},[118,1905,1906,1908,1911,1913],{"class":120,"line":121},[118,1907,1389],{"class":456},[118,1909,1910],{"class":174}," { useEffect, useRef, useState } ",[118,1912,1395],{"class":456},[118,1914,1915],{"class":132}," 'react'\n",[118,1917,1918],{"class":120,"line":142},[118,1919,487],{"emptyLinePlaceholder":486},[118,1921,1922,1924,1926,1929,1932,1934,1936,1939],{"class":120,"line":153},[118,1923,492],{"class":456},[118,1925,498],{"class":456},[118,1927,1928],{"class":124}," CallVerification",[118,1930,1931],{"class":174},"({ ",[118,1933,207],{"class":507},[118,1935,187],{"class":174},[118,1937,1938],{"class":507},"onConfirmed",[118,1940,1941],{"class":174}," }) {\n",[118,1943,1944,1946,1949,1951,1953,1956,1959,1961,1964,1966,1968],{"class":120,"line":364},[118,1945,515],{"class":456},[118,1947,1948],{"class":174}," [",[118,1950,669],{"class":128},[118,1952,187],{"class":174},[118,1954,1955],{"class":128},"setCallTo",[118,1957,1958],{"class":174},"] ",[118,1960,675],{"class":456},[118,1962,1963],{"class":124}," useState",[118,1965,504],{"class":174},[118,1967,1419],{"class":132},[118,1969,638],{"class":174},[118,1971,1972,1974,1976,1979,1981,1984,1986,1988,1990,1992,1994],{"class":120,"line":377},[118,1973,515],{"class":456},[118,1975,1948],{"class":174},[118,1977,1978],{"class":128},"secondsLeft",[118,1980,187],{"class":174},[118,1982,1983],{"class":128},"setSecondsLeft",[118,1985,1958],{"class":174},[118,1987,675],{"class":456},[118,1989,1963],{"class":124},[118,1991,504],{"class":174},[118,1993,1437],{"class":128},[118,1995,638],{"class":174},[118,1997,1998,2000,2003,2005,2008,2010,2013],{"class":120,"line":390},[118,1999,515],{"class":456},[118,2001,2002],{"class":128}," idRef",[118,2004,463],{"class":456},[118,2006,2007],{"class":124}," useRef",[118,2009,504],{"class":174},[118,2011,2012],{"class":128},"null",[118,2014,638],{"class":174},[118,2016,2017],{"class":120,"line":401},[118,2018,487],{"emptyLinePlaceholder":486},[118,2020,2021,2024,2027,2029],{"class":120,"line":569},[118,2022,2023],{"class":124},"  useEffect",[118,2025,2026],{"class":174},"(() ",[118,2028,1000],{"class":456},[118,2030,1625],{"class":174},[118,2032,2033,2036,2039,2041],{"class":120,"line":592},[118,2034,2035],{"class":456},"    let",[118,2037,2038],{"class":174}," cancelled ",[118,2040,675],{"class":456},[118,2042,978],{"class":128},[118,2044,2045],{"class":120,"line":598},[118,2046,487],{"emptyLinePlaceholder":486},[118,2048,2049,2052,2054,2056],{"class":120,"line":641},[118,2050,2051],{"class":456},"    async",[118,2053,498],{"class":456},[118,2055,1484],{"class":124},[118,2057,1770],{"class":174},[118,2059,2060,2063,2065,2067,2069,2071,2073,2075],{"class":120,"line":646},[118,2061,2062],{"class":456},"      const",[118,2064,518],{"class":128},[118,2066,463],{"class":456},[118,2068,523],{"class":456},[118,2070,526],{"class":124},[118,2072,504],{"class":174},[118,2074,1507],{"class":132},[118,2076,540],{"class":174},[118,2078,2079,2082,2084],{"class":120,"line":653},[118,2080,2081],{"class":174},"        method: ",[118,2083,548],{"class":132},[118,2085,349],{"class":174},[118,2087,2088,2091,2093,2095,2097],{"class":120,"line":688},[118,2089,2090],{"class":174},"        headers: { ",[118,2092,558],{"class":132},[118,2094,181],{"class":174},[118,2096,563],{"class":132},[118,2098,566],{"class":174},[118,2100,2101,2104,2106,2108,2110],{"class":120,"line":697},[118,2102,2103],{"class":174},"        body: ",[118,2105,575],{"class":128},[118,2107,66],{"class":174},[118,2109,580],{"class":124},[118,2111,1542],{"class":174},[118,2113,2114],{"class":120,"line":702},[118,2115,2116],{"class":174},"      })\n",[118,2118,2119,2121,2124,2126,2128,2130,2132],{"class":120,"line":707},[118,2120,2062],{"class":456},[118,2122,2123],{"class":128}," data",[118,2125,463],{"class":456},[118,2127,523],{"class":456},[118,2129,680],{"class":174},[118,2131,167],{"class":124},[118,2133,685],{"class":174},[118,2135,2136,2139,2142],{"class":120,"line":725},[118,2137,2138],{"class":456},"      if",[118,2140,2141],{"class":174}," (cancelled) ",[118,2143,2144],{"class":456},"return\n",[118,2146,2147],{"class":120,"line":767},[118,2148,487],{"emptyLinePlaceholder":486},[118,2150,2151,2154,2156],{"class":120,"line":783},[118,2152,2153],{"class":174},"      idRef.current ",[118,2155,675],{"class":456},[118,2157,2158],{"class":174}," data.id\n",[118,2160,2161,2164],{"class":120,"line":815},[118,2162,2163],{"class":124},"      setCallTo",[118,2165,2166],{"class":174},"(data.callTo)\n",[118,2168,2169,2172,2174,2177],{"class":120,"line":832},[118,2170,2171],{"class":124},"      setSecondsLeft",[118,2173,504],{"class":174},[118,2175,2176],{"class":128},"60",[118,2178,638],{"class":174},[118,2180,2181],{"class":120,"line":837},[118,2182,1099],{"class":174},[118,2184,2185],{"class":120,"line":842},[118,2186,487],{"emptyLinePlaceholder":486},[118,2188,2189,2192],{"class":120,"line":848},[118,2190,2191],{"class":124},"    start",[118,2193,685],{"class":174},[118,2195,2196,2199,2201,2203,2206,2208,2211],{"class":120,"line":876},[118,2197,2198],{"class":456},"    return",[118,2200,1620],{"class":174},[118,2202,1000],{"class":456},[118,2204,2205],{"class":174}," { cancelled ",[118,2207,675],{"class":456},[118,2209,2210],{"class":128}," true",[118,2212,198],{"class":174},[118,2214,2215],{"class":120,"line":901},[118,2216,2217],{"class":174},"  }, [phone])\n",[118,2219,2220],{"class":120,"line":920},[118,2221,487],{"emptyLinePlaceholder":486},[118,2223,2224,2226,2228,2230],{"class":120,"line":938},[118,2225,2023],{"class":124},[118,2227,2026],{"class":174},[118,2229,1000],{"class":456},[118,2231,1625],{"class":174},[118,2233,2234,2236,2238,2240,2243,2246,2249,2251,2253,2255],{"class":120,"line":962},[118,2235,941],{"class":456},[118,2237,604],{"class":174},[118,2239,607],{"class":456},[118,2241,2242],{"class":174},"idRef.current ",[118,2244,2245],{"class":456},"||",[118,2247,2248],{"class":174}," secondsLeft ",[118,2250,1727],{"class":456},[118,2252,1730],{"class":128},[118,2254,953],{"class":174},[118,2256,2144],{"class":456},[118,2258,2259],{"class":120,"line":981},[118,2260,487],{"emptyLinePlaceholder":486},[118,2262,2263,2265,2268,2270,2272,2274,2276,2278,2280],{"class":120,"line":1015},[118,2264,923],{"class":456},[118,2266,2267],{"class":128}," timer",[118,2269,463],{"class":456},[118,2271,1003],{"class":124},[118,2273,504],{"class":174},[118,2275,1479],{"class":456},[118,2277,1620],{"class":174},[118,2279,1000],{"class":456},[118,2281,1625],{"class":174},[118,2283,2284,2286,2288,2290,2292,2294,2296,2298,2300,2302,2304,2306,2309,2311,2314,2316,2318,2320],{"class":120,"line":1021},[118,2285,2062],{"class":456},[118,2287,658],{"class":174},[118,2289,632],{"class":128},[118,2291,672],{"class":174},[118,2293,675],{"class":456},[118,2295,523],{"class":456},[118,2297,604],{"class":174},[118,2299,822],{"class":456},[118,2301,526],{"class":124},[118,2303,504],{"class":174},[118,2305,1658],{"class":132},[118,2307,2308],{"class":174},"idRef",[118,2310,66],{"class":132},[118,2312,2313],{"class":174},"current",[118,2315,635],{"class":132},[118,2317,1665],{"class":174},[118,2319,167],{"class":124},[118,2321,685],{"class":174},[118,2323,2324,2326,2328,2330,2332,2334,2336],{"class":120,"line":1028},[118,2325,2138],{"class":456},[118,2327,944],{"class":174},[118,2329,947],{"class":456},[118,2331,950],{"class":132},[118,2333,953],{"class":174},[118,2335,1938],{"class":124},[118,2337,685],{"class":174},[118,2339,2340,2343,2346,2348,2351,2353,2355,2358,2361,2364],{"class":120,"line":1781},[118,2341,2342],{"class":456},"      else",[118,2344,2345],{"class":124}," setSecondsLeft",[118,2347,992],{"class":174},[118,2349,2350],{"class":507},"s",[118,2352,953],{"class":174},[118,2354,1000],{"class":456},[118,2356,2357],{"class":174}," s ",[118,2359,2360],{"class":456},"-",[118,2362,2363],{"class":128}," 2",[118,2365,638],{"class":174},[118,2367,2368,2371,2373],{"class":120,"line":1790},[118,2369,2370],{"class":174},"    }, ",[118,2372,1009],{"class":128},[118,2374,638],{"class":174},[118,2376,2377],{"class":120,"line":1795},[118,2378,487],{"emptyLinePlaceholder":486},[118,2380,2381,2383,2385,2387,2390],{"class":120,"line":1800},[118,2382,2198],{"class":456},[118,2384,1620],{"class":174},[118,2386,1000],{"class":456},[118,2388,2389],{"class":124}," clearTimeout",[118,2391,1778],{"class":174},[118,2393,2394],{"class":120,"line":1809},[118,2395,2396],{"class":174},"  }, [secondsLeft, onConfirmed])\n",[118,2398,2399],{"class":120,"line":1819},[118,2400,487],{"emptyLinePlaceholder":486},[118,2402,2403,2405,2407,2409,2412,2414],{"class":120,"line":1824},[118,2404,601],{"class":456},[118,2406,604],{"class":174},[118,2408,607],{"class":456},[118,2410,2411],{"class":174},"callTo) ",[118,2413,956],{"class":456},[118,2415,1470],{"class":128},[118,2417,2418,2420],{"class":120,"line":1834},[118,2419,691],{"class":456},[118,2421,2422],{"class":174}," (\n",[118,2424,2425],{"class":120,"line":1865},[118,2426,2427],{"class":174},"    \u003C>\n",[118,2429,2430,2433,2435,2438,2440,2443,2446,2448,2450,2453,2455],{"class":120,"line":1886},[118,2431,2432],{"class":174},"      \u003C",[118,2434,95],{"class":1377},[118,2436,2437],{"class":124}," href",[118,2439,675],{"class":456},[118,2441,2442],{"class":174},"{",[118,2444,2445],{"class":132},"`tel:${",[118,2447,669],{"class":174},[118,2449,635],{"class":132},[118,2451,2452],{"class":174},"}>Позвонить на {callTo}\u003C\u002F",[118,2454,95],{"class":1377},[118,2456,1384],{"class":174},[118,2458,2460,2463,2466,2468,2471,2474,2476,2479,2481],{"class":120,"line":2459},45,[118,2461,2462],{"class":174},"      {secondsLeft ",[118,2464,2465],{"class":456},">",[118,2467,1730],{"class":128},[118,2469,2470],{"class":456}," &&",[118,2472,2473],{"class":174}," \u003C",[118,2475,10],{"class":1377},[118,2477,2478],{"class":174},">Ждём звонок: {secondsLeft} с\u003C\u002F",[118,2480,10],{"class":1377},[118,2482,2483],{"class":174},">}\n",[118,2485,2487],{"class":120,"line":2486},46,[118,2488,2489],{"class":174},"    \u003C\u002F>\n",[118,2491,2493],{"class":120,"line":2492},47,[118,2494,2495],{"class":174},"  )\n",[118,2497,2499],{"class":120,"line":2498},48,[118,2500,404],{"class":174},[14,2502,2504],{"id":2503},"tipichnye-oshibki-pri-integracii","Типичные ошибки при интеграции",[19,2506,2507,2515,2521,2527,2533,2539,2545,2551],{},[22,2508,2509,2514],{},[25,2510,2511,2512,66],{},"Не обрабатывают ",[30,2513,82],{}," Пользователь видит вечный спиннер вместо кнопки «Позвонить ещё раз». Самая частая ошибка.",[22,2516,2517,2520],{},[25,2518,2519],{},"Держат ключ во фронтенде."," Ключ забирают из исходников страницы и тратят ваш баланс.",[22,2522,2523,2526],{},[25,2524,2525],{},"Опрашивают статус слишком часто."," Десять запросов в секунду ничего не ускоряют.",[22,2528,2529,2532],{},[25,2530,2531],{},"Не останавливают опрос."," Без условия по таймеру цикл продолжается после ухода пользователя со страницы.",[22,2534,2535,2538],{},[25,2536,2537],{},"Не проверяют баланс."," При нулевом балансе проверка не создастся, и форма перестанет работать молча.",[22,2540,2541,2544],{},[25,2542,2543],{},"Не ограничивают частоту на своём эндпоинте."," Создание проверок стоит денег — лимит на попытки обязателен.",[22,2546,2547,2550],{},[25,2548,2549],{},"Не приводят номер к E.164."," Номер с восьмёркой, скобками и дефисами не пройдёт валидацию.",[22,2552,2553,2556],{},[25,2554,2555],{},"Забывают предупредить, что звонить надо с подтверждаемого номера."," Звонок с рабочего телефона не подтвердится, а причина будет неочевидна.",[14,2558,2560],{"id":2559},"skolko-eto-zanimaet","Сколько это занимает",[216,2562,2564],{"className":2563},[219],[221,2565,2566,2576],{},[224,2567,2568],{},[227,2569,2570,2573],{},[230,2571,2572],{},"Шаг",[230,2574,2575],{},"Время",[240,2577,2578,2586,2594,2603,2610],{},[227,2579,2580,2583],{},[245,2581,2582],{},"Регистрация, проект, ключ",[245,2584,2585],{},"5 минут",[227,2587,2588,2591],{},[245,2589,2590],{},"Два эндпоинта на бэкенде",[245,2592,2593],{},"1–2 часа",[227,2595,2596,2601],{},[245,2597,2598,2599],{},"Экран ожидания с таймером и ",[30,2600,301],{},[245,2602,2593],{},[227,2604,2605,2608],{},[245,2606,2607],{},"Обработка граничных случаев и лимиты",[245,2609,2593],{},[227,2611,2612,2615],{},[245,2613,2614],{},"Тестирование на реальных номерах",[245,2616,2617],{},"1 час",[10,2619,2620],{},"Итого рабочий день на разработчика. Дольше обычно идёт не техническая часть, а согласование интерфейса и сравнение воронок.",[10,2622,2623,2624,2628],{},"Актуальные цены — на странице ",[95,2625,2627],{"href":2626},"\u002Fpricing","тарифов",". Инструкция с вашим собственным ключом открывается в кабинете на странице проекта сразу после регистрации.",[2630,2631,2632],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":114,"searchDepth":142,"depth":142,"links":2634},[2635,2636,2637,2638,2639,2644,2648,2649],{"id":16,"depth":142,"text":17},{"id":106,"depth":142,"text":107},{"id":288,"depth":142,"text":289},{"id":314,"depth":142,"text":315},{"id":438,"depth":142,"text":439,"children":2640},[2641,2642,2643],{"id":443,"depth":153,"text":444},{"id":1033,"depth":153,"text":1034},{"id":1200,"depth":153,"text":1201},{"id":1352,"depth":142,"text":1353,"children":2645},[2646,2647],{"id":1363,"depth":153,"text":1364},{"id":1895,"depth":153,"text":1896},{"id":2503,"depth":142,"text":2504},{"id":2559,"depth":142,"text":2560},"Реализация",{"title":2652,"text":2653,"label":2654,"href":2655},"Получите ключ и попробуйте","Проект и ключ создаются сразу после регистрации. Инструкция с вашим ключом открывается в кабинете.","Получить API-ключ","\u002Fregister","Как интегрировать подтверждение номера через REST API: создание проверки, опрос статуса, обработка ошибок и примеры на Node.js, PHP, Python, Vue и React.",false,"md",[2660,2663,2666,2669,2672],{"q":2661,"a":2662},"Есть ли вебхуки?","Нет. Результат забирается опросом `GET \u002Fstatus`. Это упрощает интеграцию:\nне нужен публично доступный обработчик, проверка подписи и защита\nот повторной доставки. Если вебхуки появятся, опрос продолжит работать.\n",{"q":2664,"a":2665},"Можно ли вызывать API прямо из браузера?","Нет. Запрос содержит ключ проекта, а всё, что попало во фронтенд, доступно\nпользователю. Оба запроса должны идти с вашего сервера, а браузер общается\nс вашим собственным эндпоинтом.\n",{"q":2667,"a":2668},"Сколько живёт проверка?","60 секунд с момента создания. После этого она переходит в `EXPIRED`,\nи звонок, поступивший позже, к ней уже не привяжется. Создавайте новую\nпроверку вместо продления старой.\n",{"q":2670,"a":2671},"Что вернётся, если на балансе нет денег?","Создание проверки завершится ошибкой о недостатке средств — проверка\nне будет создана. Уже существующие проверки при этом дорабатывают штатно.\n",{"q":2673,"a":2674},"Нужен ли SDK?","Нет. Это обычный REST с JSON: два запроса, которые делаются штатным\nHTTP-клиентом любого языка. Отдельной библиотеки не требуется.\n","API подтверждения номера телефона: интеграция за час",{},"\u002Fblog\u002Fapi-podtverzhdeniya-nomera","2026-08-08",[2680,2681,2682],"podtverzhdenie-nomera-na-sayte","podtverzhdenie-nomera-v-prilozhenii","servis-podtverzhdeniya-nomera",{"title":5,"description":2656},"blog\u002Fapi-podtverzhdeniya-nomera","Интеграция состоит из двух запросов: создать проверку и узнать её статус. Первый\nвозвращает выделенный номер, который нужно показать пользователю, второй — одно из трёх\nсостояний: `PENDING`, `CONFIRMED` или `EXPIRED`. Вебхуков нет, результат забирается\nопросом — поэтому не нужен публичный обработчик, проверка подписи и защита от повторных\nдоставок. Ключ проекта используется только с сервера.\n","FCFlMRQUYKfE8axfA1BS8q2zx_WEzpDY3DgBePLh8Ho",[2688,3028,3323],{"id":2689,"title":2690,"body":2691,"category":2650,"cta":3001,"description":3005,"draft":2657,"extension":2658,"faq":3006,"h1":3019,"meta":3020,"navigation":486,"order":401,"path":305,"published":2678,"related":3021,"seo":3024,"stem":3025,"summary":3026,"updated":2678,"__hash__":3027},"blog\u002Fblog\u002Fpodtverzhdenie-nomera-na-sayte.md","Подтверждение номера телефона на сайте — как реализовать",{"type":7,"value":2692,"toc":2994},[2693,2696,2700,2732,2739,2743,2808,2811,2821,2825,2866,2872,2876,2879,2929,2932,2936,2939,2942],[10,2694,2695],{},"Форма с подтверждением телефона нужна в трёх местах почти любого сайта: при регистрации, при оформлении заказа и при публикации объявления. Разбираем, какие есть варианты реализации, сколько они стоят и как собрать сценарий со звонком.",[14,2697,2699],{"id":2698},"gde-na-sayte-nuzhno-podtverzhdat-nomer","Где на сайте нужно подтверждать номер",[19,2701,2702,2708,2714,2720,2726],{},[22,2703,2704,2707],{},[25,2705,2706],{},"Регистрация."," Отсечь фейковые аккаунты и получить рабочий контакт.",[22,2709,2710,2713],{},[25,2711,2712],{},"Оформление заказа."," Убедиться, что курьер дозвонится, и отсечь ложные заказы — особенно при оплате при получении.",[22,2715,2716,2719],{},[25,2717,2718],{},"Публикация объявления."," Подтверждённый контакт продавца снижает долю мошеннических размещений.",[22,2721,2722,2725],{},[25,2723,2724],{},"Восстановление доступа."," Телефон как доказательство, что аккаунт принадлежит вам.",[22,2727,2728,2731],{},[25,2729,2730],{},"Подтверждение операции."," Смена реквизитов, крупная покупка, изменение доставки.",[10,2733,2734,2735,66],{},"Разбор задач по отраслям — в статье ",[95,2736,2738],{"href":2737},"\u002Fblog\u002Fgde-nuzhna-verifikaciya-telefona","где нужна верификация телефона",[14,2740,2742],{"id":2741},"varianty-realizacii-i-chto-oni-stoyat","Варианты реализации и что они стоят",[216,2744,2746],{"className":2745},[219],[221,2747,2748,2764],{},[224,2749,2750],{},[227,2751,2752,2755,2758,2761],{},[230,2753,2754],{},"Вариант",[230,2756,2757],{},"Срок внедрения",[230,2759,2760],{},"За что платите",[230,2762,2763],{},"Что ломается",[240,2765,2766,2780,2794],{},[227,2767,2768,2771,2774,2777],{},[245,2769,2770],{},"Своя интеграция с SMS-агрегатором",[245,2772,2773],{},"от недели",[245,2775,2776],{},"за отправку, независимо от доставки",[245,2778,2779],{},"доставляемость, фильтры операторов, роуминг",[227,2781,2782,2785,2788,2791],{},[245,2783,2784],{},"Готовый виджет с кодом",[245,2786,2787],{},"день-два",[245,2789,2790],{},"по тарифу виджета, обычно за попытку",[245,2792,2793],{},"чужой интерфейс на вашем сайте, ограниченная кастомизация",[227,2795,2796,2799,2802,2805],{},[245,2797,2798],{},"Подтверждение звонком",[245,2800,2801],{},"день",[245,2803,2804],{},"только за состоявшийся звонок",[245,2806,2807],{},"не проходит при скрытом номере и у зарубежных SIM",[10,2809,2810],{},"У первых двух вариантов общая слабость: результат зависит от доставки сообщения, на которую вы не влияете. У третьего зависимость другая — от того, передаётся ли номер вызывающего абонента. Она затрагивает меньшую долю аудитории и, в отличие от доставляемости, предсказуема.",[10,2812,2813,2814,2816,2817,2820],{},"Как именно устроена проверка звонком — в статье про ",[95,2815,98],{"href":97},". Если ваш продукт живёт не только в вебе, посмотрите отличия мобильного сценария в материале про ",[95,2818,2819],{"href":310},"подтверждение номера в приложении",": там набор открывается одним касанием, и экран ожидания строится иначе.",[14,2822,2824],{"id":2823},"kak-vstroit-podtverzhdenie-zvonkom-5-shagov","Как встроить подтверждение звонком: 5 шагов",[2826,2827,2828,2834,2840,2848,2854],"ol",{},[22,2829,2830,2833],{},[25,2831,2832],{},"Зарегистрируйтесь и создайте проект."," Он получит собственный ключ. Ключ живёт в переменных окружения на сервере — не в репозитории и не во фронтенде.",[22,2835,2836,2839],{},[25,2837,2838],{},"Добавьте эндпоинт «начать проверку» на своём бэкенде."," Он принимает номер от фронтенда, валидирует его, вызывает создание проверки и возвращает во фронтенд только идентификатор и выделенный номер.",[22,2841,2842,2845,2846,66],{},[25,2843,2844],{},"Покажите номер пользователю."," Крупно, с таймером на 60 секунд, на мобильных — ссылкой ",[30,2847,301],{},[22,2849,2850,2853],{},[25,2851,2852],{},"Добавьте эндпоинт «статус проверки»."," Фронтенд опрашивает его, а он проксирует запрос с ключом. Наружу отдавайте только статус — ни ключ, ни лишние поля клиенту не нужны.",[22,2855,2856,2859,2860,2862,2863,2865],{},[25,2857,2858],{},"Обработайте результат на бэкенде."," По ",[30,2861,78],{}," — помечайте номер подтверждённым и продолжайте сценарий, по ",[30,2864,82],{}," — предлагайте повторить.",[10,2867,2868,2869,66],{},"Запросы, форматы ответов и примеры кода — в материале про ",[95,2870,2871],{"href":2677},"API подтверждения номера",[14,2873,2875],{"id":2874},"chto-pokazat-polzovatelyu-v-forme","Что показать пользователю в форме",[10,2877,2878],{},"Экран ожидания звонка решает больше, чем кажется: именно на нём пользователь либо делает нужное действие, либо закрывает вкладку.",[19,2880,2881,2891,2897,2905,2911,2917,2923],{},[22,2882,2883,2886,2887,2890],{},[25,2884,2885],{},"Номер — самый крупный элемент экрана."," С разделителями, в привычном виде: ",[30,2888,2889],{},"+7 495 000-00-00",". Его будут набирать руками с другого устройства.",[22,2892,2893,2896],{},[25,2894,2895],{},"Кнопка копирования"," рядом с номером — на десктопе она нужнее, чем кажется.",[22,2898,2899,2904],{},[25,2900,2901,2902],{},"Ссылка ",[30,2903,301],{}," на мобильных: одно касание вместо переписывания.",[22,2906,2907,2910],{},[25,2908,2909],{},"Понятная инструкция."," «Позвоните на этот номер с телефона, который подтверждаете. Трубку брать не нужно — вызов оборвётся сам». Без слова «верификация».",[22,2912,2913,2916],{},[25,2914,2915],{},"Таймер на 60 секунд."," Показывает, что процесс идёт, и задаёт рамку ожидания.",[22,2918,2919,2922],{},[25,2920,2921],{},"Автопереход при подтверждении."," Пользователь не должен нажимать «Проверить» — экран меняется сам.",[22,2924,2925,2928],{},[25,2926,2927],{},"После истечения таймера"," — «Позвонить ещё раз» и ссылка «Подтвердить другим способом».",[10,2930,2931],{},"Отдельно про звонок с того же номера: предупредите об этом заранее, одной строкой под номером. Иначе часть пользователей позвонит с рабочего телефона, проверка не подтвердится, и причина будет неочевидна.",[14,2933,2935],{"id":2934},"kak-obrabotat-rezultat-na-bekende","Как обработать результат на бэкенде",[10,2937,2938],{},"Вебхуков в сервисе нет — результат забирается опросом статуса. Это упрощает интеграцию: не нужен публично доступный обработчик, не нужна проверка подписи, не нужно думать о повторных доставках.",[10,2940,2941],{},"Что учесть:",[19,2943,2944,2950,2961,2967,2976,2982,2988],{},[22,2945,2946,2949],{},[25,2947,2948],{},"Интервал опроса."," Раз в одну-две секунды достаточно. Чаще — бессмысленно, реже — заметная задержка на экране.",[22,2951,2952,2955,2956,187,2958,2960],{},[25,2953,2954],{},"Условия остановки."," Три штуки: ",[30,2957,78],{},[30,2959,82],{},", истёк локальный таймер. Без явных условий опрос продолжится после ухода пользователя со страницы.",[22,2962,2963,2966],{},[25,2964,2965],{},"Доверять только своему серверу."," Решение «номер подтверждён» принимается на бэкенде по ответу API, а не по тому, что прислал фронтенд.",[22,2968,2969,2972,2973,2975],{},[25,2970,2971],{},"Идемпотентность на своей стороне."," Один и тот же ",[30,2974,78],{}," может быть увиден несколькими запросами подряд — привязка номера должна отрабатывать один раз.",[22,2977,2978,2981],{},[25,2979,2980],{},"Проверка баланса."," Если средств не хватает, проверка не создастся и API вернёт ошибку. Обработайте её явно и заведите оповещение о низком балансе: иначе форма молча перестанет работать.",[22,2983,2984,2987],{},[25,2985,2986],{},"Ограничение частоты на своём эндпоинте."," Создание проверок стоит денег, поэтому лимит на число попыток с одного адреса или сессии нужен обязательно.",[22,2989,2990,2993],{},[25,2991,2992],{},"Валидация номера до отправки."," Приведите введённое к формату E.164 на своей стороне — так вы не потратите запрос на заведомо некорректный номер и покажете пользователю понятную ошибку сразу в форме.",{"title":114,"searchDepth":142,"depth":142,"links":2995},[2996,2997,2998,2999,3000],{"id":2698,"depth":142,"text":2699},{"id":2741,"depth":142,"text":2742},{"id":2823,"depth":142,"text":2824},{"id":2874,"depth":142,"text":2875},{"id":2934,"depth":142,"text":2935},{"title":3002,"text":3003,"label":3004,"href":2655},"Добавьте подтверждение на свой сайт","Два запроса к REST API, без SDK и лишних зависимостей. Ключ проекта выдаётся сразу после регистрации.","Начать бесплатно","Как добавить на сайт подтверждение номера телефона: варианты реализации и их цена, сценарий со звонком, что показывать в форме и как обработать результат.",[3007,3010,3013,3016],{"q":3008,"a":3009},"Пользователь на десктопе — как он позвонит?","С телефона, глядя на экран компьютера. Поэтому номер нужно показывать крупно\nи с разделителями, чтобы его было легко набрать вручную, а не мелким шрифтом\nв одну строку. На мобильных браузерах номер должен быть ссылкой `tel:`.\n",{"q":3011,"a":3012},"Можно ли обойтись без бэкенда?","Нет. Ключ проекта нельзя размещать во фронтенде — из браузера его заберёт кто\nугодно и начнёт тратить ваш баланс. Создание проверки и опрос статуса должны\nидти через ваш собственный сервер.\n",{"q":3014,"a":3015},"Как часто опрашивать статус?","Достаточно раза в пару секунд: сценарий укладывается в 60 секунд, и более\nчастый опрос ничего не ускоряет. Прекращать опрос нужно по `CONFIRMED`,\nпо `EXPIRED` и по истечении таймера, иначе запросы будут идти вечно.\n",{"q":3017,"a":3018},"Что делать с пользователями, у которых звонок не проходит?","Оставить в форме ссылку «Подтвердить другим способом». Скрытый номер,\nзарубежная SIM и корпоративная АТС встречаются нечасто, но без запасного\nварианта такие пользователи не пройдут дальше.\n","Как сделать подтверждение номера телефона на сайте",{},[3022,2681,3023],"podtverzhdenie-nomera-zvonkom","api-podtverzhdeniya-nomera",{"title":2690,"description":3005},"blog\u002Fpodtverzhdenie-nomera-na-sayte","Подтверждение номера на сайте делается тремя способами: своя интеграция с SMS-агрегатором,\nготовый виджет или проверка звонком. Последний вариант не требует доставки сообщения —\nпользователь сам звонит на выделенный номер, и вводить ему нечего. На бэкенде это два\nHTTP-запроса и три статуса, во фронтенде — кликабельный номер и таймер на 60 секунд.\nПодтверждение стоит 0,20 ₽, несостоявшееся не тарифицируется.\n","E4A1c-OYXZBIRTAoVrLxmuUT_fr8E9_Q658zLnHCN8E",{"id":3029,"title":3030,"body":3031,"category":2650,"cta":3298,"description":3301,"draft":2657,"extension":2658,"faq":3302,"h1":3315,"meta":3316,"navigation":486,"order":569,"path":310,"published":2678,"related":3317,"seo":3319,"stem":3320,"summary":3321,"updated":2678,"__hash__":3322},"blog\u002Fblog\u002Fpodtverzhdenie-nomera-v-prilozhenii.md","Подтверждение номера в приложении — как сделать без SMS",{"type":7,"value":3032,"toc":3291},[3033,3036,3040,3047,3050,3053,3056,3060,3092,3095,3099,3102,3114,3123,3133,3147,3151,3223,3227,3230,3285],[10,3034,3035],{},"На мобильных сценарий подтверждения звонком раскрывается лучше всего: телефон, с которого нужно позвонить, — это то же устройство, где открыто приложение. Пользователю не приходится ничего переносить с экрана на экран.",[14,3037,3039],{"id":3038},"chem-podtverzhdenie-v-prilozhenii-otlichaetsya-ot-sayta","Чем подтверждение в приложении отличается от сайта",[10,3041,3042,3043,3046],{},"Главное отличие одно, и оно решающее: ",[25,3044,3045],{},"приложение может открыть набор номера в одно касание",". Пользователь нажимает кнопку — открывается системный диалер с уже подставленным номером, остаётся нажать вызов.",[10,3048,3049],{},"На сайте с десктопа так не получится: номер придётся переносить на телефон руками, и это худшая точка сценария. В приложении её просто нет.",[10,3051,3052],{},"Второе отличие — контроль над экраном. Приложение точно знает, когда пользователь вернулся из диалера, и может проверить статус именно в этот момент, а не по таймеру. Переход к следующему шагу получается мгновенным.",[10,3054,3055],{},"Третье — обратная сторона: приложение уходит на задний план во время вызова, и это надо учитывать в логике, а не считать сбоем.",[14,3057,3059],{"id":3058},"scenariy-so-zvonkom-chto-vidit-polzovatel","Сценарий со звонком: что видит пользователь",[2826,3061,3062,3068,3074,3080,3086],{},[22,3063,3064,3067],{},[25,3065,3066],{},"Экран ввода номера."," Поле в международном формате, подсказка про код страны, кнопка «Продолжить».",[22,3069,3070,3073],{},[25,3071,3072],{},"Экран подтверждения."," Крупная кнопка «Позвонить», под ней — номер текстом на случай, если диалер не откроется. Пояснение: трубку брать не нужно, вызов оборвётся сам. Таймер на 60 секунд.",[22,3075,3076,3079],{},[25,3077,3078],{},"Системный диалер."," Открывается с подставленным номером. Пользователь нажимает вызов, соединение сбрасывается через мгновение.",[22,3081,3082,3085],{},[25,3083,3084],{},"Возврат в приложение."," Статус уже подтверждён — экран меняется сам, нажимать ничего не нужно.",[22,3087,3088,3091],{},[25,3089,3090],{},"Если не сложилось."," По истечении таймера — кнопка «Позвонить ещё раз» и ссылка на запасной способ.",[10,3093,3094],{},"Формулировки на втором экране важнее, чем кажется. «Позвоните на этот номер, трубку брать не нужно» пользователь понимает сразу. «Пройдите верификацию» — нет.",[14,3096,3098],{"id":3097},"realizaciya-na-ios-i-android","Реализация на iOS и Android",[10,3100,3101],{},"Логика одинаковая, различается только открытие диалера.",[10,3103,3104,3107,3108,3110,3111,66],{},[25,3105,3106],{},"Общая часть."," Бэкенд создаёт проверку и получает выделенный номер; приложение показывает его и опрашивает статус. Сам механизм проверки разобран в статье про ",[95,3109,98],{"href":97}," — он одинаков для всех платформ. Ключ проекта в приложении хранить нельзя: его место на сервере, а мобильный клиент ходит в ваш собственный API. Веб-версия того же сценария описана в материале про ",[95,3112,3113],{"href":305},"подтверждение номера на сайте",[10,3115,3116,3119,3120,3122],{},[25,3117,3118],{},"Открытие набора."," На iOS — системная ссылка вида ",[30,3121,301],{},", на Android — намерение набора номера. В обоих случаях открывается штатный диалер с подставленным номером; сам вызов инициирует пользователь. Автоматически совершать звонок за него не нужно и не следует: это требует отдельного разрешения и выглядит подозрительно.",[10,3124,3125,3128,3129,66],{},[25,3126,3127],{},"Разрешения не нужны."," Здесь ключевое отличие от обычного flash call, при котором приложение считывает номер входящего вызова из журнала. Такой доступ ограничен политиками магазинов: в Google Play выдача разрешения на журнал вызовов требует обоснования, а в iOS публичного API для чтения журнала нет вовсе. При обратном сценарии читать нечего — звонок видит сервис на своей стороне. Разбор разницы — в статье ",[95,3130,3132],{"href":3131},"\u002Fblog\u002Fchto-takoe-flash-call","что такое flash call",[10,3134,3135,3138,3139,3141,3142,3144,3145,66],{},[25,3136,3137],{},"Опрос статуса."," Периодический запрос с разумным интервалом плюс обязательная внеочередная проверка при возврате приложения на передний план. Опрос прекращается по ",[30,3140,78],{},", по ",[30,3143,82],{}," или по истечении таймера. Детали запросов — в материале про ",[95,3146,2871],{"href":2677},[14,3148,3150],{"id":3149},"obrabotka-granichnyh-sluchaev","Обработка граничных случаев",[216,3152,3154],{"className":3153},[219],[221,3155,3156,3165],{},[224,3157,3158],{},[227,3159,3160,3163],{},[230,3161,3162],{},"Ситуация",[230,3164,238],{},[240,3166,3167,3175,3183,3191,3199,3207,3215],{},[227,3168,3169,3172],{},[245,3170,3171],{},"Приложение свёрнуто во время вызова",[245,3173,3174],{},"проверять статус при возврате на передний план, не только по таймеру",[227,3176,3177,3180],{},[245,3178,3179],{},"Пользователь отменил вызов в диалере",[245,3181,3182],{},"оставить экран активным, таймер продолжает идти — он может позвонить снова",[227,3184,3185,3188],{},[245,3186,3187],{},"Звонок с другого номера",[245,3189,3190],{},"проверка не подтвердится; в подсказке заранее написать, что звонить нужно с указанного номера",[227,3192,3193,3196],{},[245,3194,3195],{},"Нет сигнала сотовой сети",[245,3197,3198],{},"предложить запасной способ подтверждения",[227,3200,3201,3204],{},[245,3202,3203],{},"Диалер не открылся",[245,3205,3206],{},"номер показан текстом, дать кнопку копирования",[227,3208,3209,3212],{},[245,3210,3211],{},"Истекли 60 секунд",[245,3213,3214],{},"показать «Позвонить ещё раз», создать новую проверку",[227,3216,3217,3220],{},[245,3218,3219],{},"Приложение закрыто до подтверждения",[245,3221,3222],{},"при следующем запуске начинать сценарий заново",[14,3224,3226],{"id":3225},"chto-proverit-pered-otpravkoy-v-magazin","Что проверить перед отправкой в магазин",[10,3228,3229],{},"Короткий чек-лист по местам, где мобильная реализация обычно ломается.",[19,3231,3232,3238,3244,3250,3261,3267,3273,3279],{},[22,3233,3234,3237],{},[25,3235,3236],{},"Диалер открывается на реальном устройстве",", а не только в эмуляторе. На части прошивок поведение отличается.",[22,3239,3240,3243],{},[25,3241,3242],{},"Номер показан текстом рядом с кнопкой."," Если системный набор не открылся, пользователю нужен запасной путь — скопировать и набрать вручную.",[22,3245,3246,3249],{},[25,3247,3248],{},"Статус проверяется при возврате на передний план."," Без этого пользователь возвращается на экран ожидания, который уже неактуален.",[22,3251,3252,3255,3256,187,3258,3260],{},[25,3253,3254],{},"Опрос останавливается."," Три условия: ",[30,3257,78],{},[30,3259,82],{},", истёкший таймер. Иначе запросы продолжатся в фоне и посадят батарею.",[22,3262,3263,3266],{},[25,3264,3265],{},"Ключ проекта не лежит в приложении."," Даже в обфусцированном виде: мобильный клиент разбирается, а ключ тратит ваш баланс. Приложение ходит в ваш собственный API.",[22,3268,3269,3272],{},[25,3270,3271],{},"Экран переживает поворот и сворачивание."," Идентификатор проверки и остаток таймера должны сохраняться.",[22,3274,3275,3278],{},[25,3276,3277],{},"Есть запасной способ подтверждения"," для тех, у кого номер не определяется.",[22,3280,3281,3284],{},[25,3282,3283],{},"Тексты понятны без терминов."," «Позвоните на этот номер, трубку брать не нужно» вместо «пройдите верификацию».",[10,3286,3287,3288,3290],{},"Отдельно про повторные попытки: они ничего не стоят. Тарифицируется только состоявшийся звонок, проверка в статусе ",[30,3289,82],{}," не списывает баланс — поэтому предлагать позвонить ещё раз можно без оглядки на расходы.",{"title":114,"searchDepth":142,"depth":142,"links":3292},[3293,3294,3295,3296,3297],{"id":3038,"depth":142,"text":3039},{"id":3058,"depth":142,"text":3059},{"id":3097,"depth":142,"text":3098},{"id":3149,"depth":142,"text":3150},{"id":3225,"depth":142,"text":3226},{"title":3299,"text":3300,"label":3004,"href":2655},"Встройте подтверждение в своё приложение","Два HTTP-запроса, никаких SDK и разрешений. Ключ проекта выдаётся сразу после регистрации.","Как реализовать подтверждение номера в мобильном приложении: сценарий со звонком вместо SMS, работа без разрешений, экраны, обработка граничных случаев.",[3303,3306,3309,3312],{"q":3304,"a":3305},"Нужны ли разрешения на чтение SMS или журнала вызовов?","Нет, и это ключевое отличие сценария. Приложение только открывает системный\nдиалер и опрашивает статус проверки по сети. Читать сообщения и журнал\nвызовов не требуется, поэтому и запрашивать соответствующие разрешения\nне нужно.\n",{"q":3307,"a":3308},"Пройдёт ли такое приложение модерацию в магазинах?","С этой стороны придираться не к чему: чувствительные разрешения не запрашиваются,\nфоновой активности нет, всё общение с сервисом идёт обычными HTTP-запросами.\nИменно из-за разрешений обычный flash call часто и не проходит.\n",{"q":3310,"a":3311},"Что если пользователь свернул приложение во время звонка?","Так и будет — набор номера сворачивает приложение по определению. Поэтому\nстатус нужно проверять и при возврате на передний план, а не только по таймеру:\nк этому моменту проверка обычно уже подтверждена.\n",{"q":3313,"a":3314},"Нужен ли отдельный код для iOS и Android?","Отличается только способ открыть диалер — системная ссылка на iOS и намерение\nнабора на Android. Вся логика проверки одинаковая: создать, показать номер,\nопросить статус.\n","Подтверждение номера телефона в мобильном приложении",{},[3022,2680,3318],"chto-takoe-flash-call",{"title":3030,"description":3301},"blog\u002Fpodtverzhdenie-nomera-v-prilozhenii","В мобильном приложении подтверждение номера звонком удобнее, чем где-либо ещё: набор\nоткрывается одним касанием через системный диалер, пользователю не нужно ничего\nпереписывать и вводить. Главное преимущество перед обычным flash call — приложению\nне требуются разрешения на чтение SMS и журнала вызовов, а их выдачу ограничивают\nи Google Play, и App Store. Проверка живёт 60 секунд, стоит 0,20 ₽ за состоявшийся звонок.\n","SzYBtnQ2PPpUxp6ImUb7w9yWpfYtOF4vzcvV-g9wfsk",{"id":3324,"title":3325,"body":3326,"category":3749,"cta":3750,"description":3753,"draft":2657,"extension":2658,"faq":3754,"h1":3770,"meta":3771,"navigation":486,"order":153,"path":102,"published":2678,"related":3772,"seo":3774,"stem":3775,"summary":3776,"updated":2678,"__hash__":3777},"blog\u002Fblog\u002Fservis-podtverzhdeniya-nomera.md","Сервис подтверждения номера телефона — выбор и цены",{"type":7,"value":3327,"toc":3740},[3328,3331,3334,3338,3341,3418,3424,3427,3432,3436,3442,3448,3454,3460,3466,3472,3478,3482,3489,3497,3562,3568,3573,3577,3580,3594,3597,3601,3638,3647,3651,3654,3704,3708],[10,3329,3330],{},"Рынок подтверждения номера выглядит однородно: почти все продают «верификацию по цене X за штуку». Разница вылезает позже — когда оказывается, что списывают за попытку, а не за результат, что при недоставке деньги всё равно ушли, а на интеграцию нужен не день, а две недели.",[10,3332,3333],{},"Ниже — критерии, которые видно до подписания договора, и честный расчёт по FlashCall.",[14,3335,3337],{"id":3336},"sposoby-podtverzhdeniya-i-ih-sebestoimost","Способы подтверждения и их себестоимость",[10,3339,3340],{},"Первое решение — не «какой провайдер», а «каким способом». От него зависит и цена, и что именно вы покупаете.",[216,3342,3344],{"className":3343},[219],[221,3345,3346,3362],{},[224,3347,3348],{},[227,3349,3350,3353,3356,3359],{},[230,3351,3352],{},"Способ",[230,3354,3355],{},"За что списывают",[230,3357,3358],{},"От чего зависит успех",[230,3360,3361],{},"Что делает пользователь",[240,3363,3364,3377,3391,3404],{},[227,3365,3366,3369,3372,3374],{},[245,3367,3368],{},"SMS-код",[245,3370,3371],{},"за отправку сообщения",[245,3373,2779],{},[245,3375,3376],{},"ждёт, читает, вводит код",[227,3378,3379,3382,3385,3388],{},[245,3380,3381],{},"Голосовой звонок с кодом",[245,3383,3384],{},"за совершённый вызов",[245,3386,3387],{},"дозвон, качество распознавания на слух",[245,3389,3390],{},"принимает вызов, слушает, вводит код",[227,3392,3393,3396,3398,3401],{},[245,3394,3395],{},"Обычный flash call",[245,3397,3384],{},[245,3399,3400],{},"разрешение приложению на журнал вызовов",[245,3402,3403],{},"ничего либо вводит цифры номера",[227,3405,3406,3409,3412,3415],{},[245,3407,3408],{},"Обратный звонок",[245,3410,3411],{},"за состоявшийся звонок",[245,3413,3414],{},"определяемость номера вызывающего",[245,3416,3417],{},"нажимает вызов",[10,3419,3420,3423],{},[25,3421,3422],{},"О ценах конкурентов."," Публичные прайсы на SMS зависят от оператора получателя, объёма и типа трафика, поэтому единой цифры не существует, а «средняя цена SMS» из статей обычно ничем не подтверждена. Если сравниваете — берите свой счёт за прошлый месяц и делите на число доставленных сообщений. Это единственная цифра, которая имеет отношение к вашей ситуации.",[10,3425,3426],{},"Обратите внимание на третий столбец таблицы. Он объясняет, почему два сервиса с одинаковой ценой за штуку дают разный итоговый счёт: у способов разная доля успеха, и оплачиваете вы в одном случае попытки, в другом — результат.",[10,3428,3429,3430,66],{},"Разбор терминологии и разницы между обычным и обратным flash call — в статье ",[95,3431,3132],{"href":3131},[14,3433,3435],{"id":3434},"sem-kriteriev-vybora-servisa","Семь критериев выбора сервиса",[10,3437,3438,3441],{},[25,3439,3440],{},"1. Модель тарификации: за попытку или за результат."," Главный критерий, который почти никогда не стоит первым в презентации. При оплате за отправку вы платите и за недоставленные сообщения, и за пользователей, которые передумали. При оплате за результат несостоявшаяся проверка стоит ноль. На объёме разница между этими моделями больше, чем разница в цене за штуку.",[10,3443,3444,3447],{},[25,3445,3446],{},"2. Прозрачность статусов."," Вы должны в любой момент видеть, что происходит с конкретной проверкой, и различать «ещё ждём», «подтверждено» и «не сложилось». Если API отдаёт только «успех\u002Fнеуспех» без промежуточного состояния, вы не сможете нормально нарисовать интерфейс ожидания.",[10,3449,3450,3453],{},[25,3451,3452],{},"3. REST API без SDK."," Проприетарная библиотека — это зависимость, которую придётся обновлять, и ограничение по языкам. Обычный HTTP-интерфейс встраивается в любой стек и не устаревает.",[10,3455,3456,3459],{},[25,3457,3458],{},"4. Время интеграции."," Считается не по документации, а по числу сущностей, которые надо завести на своей стороне. Два запроса и три статуса — это день работы. Очередь, подписи, отдельный обработчик и своя таблица состояний — это спринт.",[10,3461,3462,3465],{},[25,3463,3464],{},"5. Отсутствие абонентской платы и минимального объёма."," Обязательный ежемесячный платёж превращает эксперимент в обязательство. Если вы не уверены, что способ подойдёт вашей аудитории, стартовать надо там, где неудачная гипотеза стоит только потраченного времени.",[10,3467,3468,3471],{},[25,3469,3470],{},"6. Документы и юридическая сторона."," Оферта, счёт для юрлиц и ИП, корректный НДС по налоговому статусу. Проверяется до интеграции, а не после, когда бухгалтерия откажется проводить платёж.",[10,3473,3474,3477],{},[25,3475,3476],{},"7. Поддержка живыми людьми."," На этапе внедрения важнее тарифа: половина вопросов возникает в первые два дня и снимается одним ответом.",[14,3479,3481],{"id":3480},"skolko-stoit-podtverzhdenie-v-flashcall","Сколько стоит подтверждение в FlashCall",[10,3483,3484,3485,3488],{},"Одно подтверждение — ",[25,3486,3487],{},"0,20 ₽",". Абонентской платы нет, минимального месячного объёма нет, плата за подключение отсутствует.",[10,3490,3491,3492,187,3494,66],{},"Списание происходит в момент, когда звонок зафиксирован. Проверка, по которой звонка не было и которая ушла в статус ",[30,3493,82],{},[25,3495,3496],{},"не тарифицируется вообще",[216,3498,3500],{"className":3499},[219],[221,3501,3502,3518],{},[224,3503,3504],{},[227,3505,3506,3509,3512,3515],{},[230,3507,3508],{},"Подтверждений в месяц",[230,3510,3511],{},"При 100 % дозвона",[230,3513,3514],{},"При 80 % дозвона",[230,3516,3517],{},"При 60 % дозвона",[240,3519,3520,3534,3548],{},[227,3521,3522,3525,3528,3531],{},[245,3523,3524],{},"1 000",[245,3526,3527],{},"200 ₽",[245,3529,3530],{},"160 ₽",[245,3532,3533],{},"120 ₽",[227,3535,3536,3539,3542,3545],{},[245,3537,3538],{},"10 000",[245,3540,3541],{},"2 000 ₽",[245,3543,3544],{},"1 600 ₽",[245,3546,3547],{},"1 200 ₽",[227,3549,3550,3553,3556,3559],{},[245,3551,3552],{},"100 000",[245,3554,3555],{},"20 000 ₽",[245,3557,3558],{},"16 000 ₽",[245,3560,3561],{},"12 000 ₽",[10,3563,3564,3565,66],{},"Столбцы справа — не скидка, а прямое следствие модели: платите за то, что состоялось. Формула для подстановки своих цифр: ",[30,3566,3567],{},"запущенные проверки × доля дозвона × 0,20 ₽",[10,3569,3570,3571,66],{},"Цены в таблице актуальны на дату публикации; источник правды — страница ",[95,3572,2627],{"href":2626},[14,3574,3576],{"id":3575},"kak-oplatit","Как оплатить",[10,3578,3579],{},"Баланс пополняется двумя способами:",[19,3581,3582,3588],{},[22,3583,3584,3587],{},[25,3585,3586],{},"Счёт"," — для юрлиц и ИП. Реквизиты подставляются из карточки в кабинете, счёт приходит готовым к оплате. Зачисление на баланс происходит автоматически, когда банк подтверждает поступление.",[22,3589,3590,3593],{},[25,3591,3592],{},"Картой"," — если удобнее платить сразу и без бухгалтерии.",[10,3595,3596],{},"Минимальная сумма пополнения по умолчанию — 100 ₽; фактическое значение показано в кабинете. Неизрасходованный баланс не сгорает.",[14,3598,3600],{"id":3599},"kak-podklyuchit","Как подключить",[2826,3602,3603,3608,3614,3620,3632],{},[22,3604,3605,3607],{},[25,3606,2706],{}," Занимает минуту, карта не требуется.",[22,3609,3610,3613],{},[25,3611,3612],{},"Проект."," Создаётся в кабинете, сразу получает собственный ключ. Ключ хранится на сервере — во фронтенде ему не место.",[22,3615,3616,3619],{},[25,3617,3618],{},"Тестовая проверка."," Запускается с первого же запроса, по реальному номеру.",[22,3621,3622,3625,3626,187,3628,187,3630,66],{},[25,3623,3624],{},"Интеграция."," Два запроса: создать проверку и получить её статус. Обработать три состояния — ",[30,3627,74],{},[30,3629,78],{},[30,3631,82],{},[22,3633,3634,3637],{},[25,3635,3636],{},"Запасной способ."," Оставьте пользователю альтернативу на случай скрытого номера или зарубежной SIM.",[10,3639,3640,3641,3643,3644,66],{},"Пошаговое описание запросов с примерами кода — в материале про ",[95,3642,2871],{"href":2677},". Как устроен сам механизм проверки — в статье про ",[95,3645,3646],{"href":97},"подтверждение номера звонком",[14,3648,3650],{"id":3649},"chek-list-pered-integraciey","Чек-лист перед интеграцией",[10,3652,3653],{},"Пройдите его до того, как разработчик начнёт писать код, — все пункты проверяются за час и снимают большую часть будущих сюрпризов.",[2826,3655,3656,3662,3668,3674,3680,3686,3692,3698],{},[22,3657,3658,3661],{},[25,3659,3660],{},"Посмотрите на модель списания в документации, а не в презентации."," Найдите, в какой момент уменьшается баланс. Если это момент отправки или создания запроса, вы платите за попытки.",[22,3663,3664,3667],{},[25,3665,3666],{},"Уточните, что происходит с неудачной проверкой."," Есть ли отдельный статус, тарифицируется ли она, видно ли её в статистике.",[22,3669,3670,3673],{},[25,3671,3672],{},"Проверьте формат номера."," E.164 — стандарт; если сервис требует собственный формат, это лишний слой преобразований и источник ошибок.",[22,3675,3676,3679],{},[25,3677,3678],{},"Найдите, где хранится ключ."," Он должен использоваться только с сервера. Если инструкция предлагает положить его во фронтенд, это плохой знак для всей архитектуры.",[22,3681,3682,3685],{},[25,3683,3684],{},"Оцените, что будет на пике."," Спросите про ограничения на число одновременных проверок: узкое место обычно там, а не в тарифе.",[22,3687,3688,3691],{},[25,3689,3690],{},"Прочитайте оферту до слова «ответственность»."," Особенно раздел про возврат неизрасходованного баланса.",[22,3693,3694,3697],{},[25,3695,3696],{},"Задайте один вопрос в поддержку."," Скорость и содержательность ответа на этапе выбора хорошо предсказывают, как будет во время инцидента.",[22,3699,3700,3703],{},[25,3701,3702],{},"Заложите запасной способ."," Ни один канал не работает у ста процентов аудитории; альтернатива в интерфейсе стоит дешевле, чем потерянные регистрации.",[14,3705,3707],{"id":3706},"na-chem-chasche-vsego-oshibayutsya-pri-vybore","На чём чаще всего ошибаются при выборе",[19,3709,3710,3716,3722,3728,3734],{},[22,3711,3712,3715],{},[25,3713,3714],{},"Сравнивают цену за штуку, а не стоимость успешного подтверждения."," Это разные величины, и расходятся они тем сильнее, чем хуже доставляемость.",[22,3717,3718,3721],{},[25,3719,3720],{},"Не считают стоимость потерянных регистраций."," Пользователь, не получивший код, стоит дороже, чем сам код.",[22,3723,3724,3727],{},[25,3725,3726],{},"Берут провайдера под текущий объём."," Проверять надо поведение на пике, а не на среднем.",[22,3729,3730,3733],{},[25,3731,3732],{},"Оставляют один способ подтверждения."," Любой канал иногда не срабатывает; запасной вариант дешевле, чем разбор инцидента.",[22,3735,3736,3739],{},[25,3737,3738],{},"Откладывают документы на потом."," Оферту и порядок выставления счетов проще посмотреть до интеграции.",{"title":114,"searchDepth":142,"depth":142,"links":3741},[3742,3743,3744,3745,3746,3747,3748],{"id":3336,"depth":142,"text":3337},{"id":3434,"depth":142,"text":3435},{"id":3480,"depth":142,"text":3481},{"id":3575,"depth":142,"text":3576},{"id":3599,"depth":142,"text":3600},{"id":3649,"depth":142,"text":3650},{"id":3706,"depth":142,"text":3707},"Выбор решения",{"title":3751,"text":3752,"label":3004,"href":2655},"Посчитайте на своём объёме","Регистрация бесплатная, абонентской платы нет. Тестовую проверку можно запустить сразу после создания проекта.","На что смотреть при выборе сервиса подтверждения номера: способ проверки, цена за подтверждение, модель списания, интеграция, документы и поддержка.",[3755,3758,3761,3764,3767],{"q":3756,"a":3757},"Есть ли минимальный платёж?","Абонентской платы и обязательного месячного объёма нет — вы платите только\nза подтверждения. Минимум есть у пополнения баланса: по умолчанию 100 ₽,\nактуальное значение показано в кабинете на странице пополнения.\n",{"q":3759,"a":3760},"Какие документы получит бухгалтерия?","Юрлицам и ИП выставляется счёт, отношения регулирует публичная оферта.\nРеквизиты компании подставляются в счёт из карточки в кабинете, а НДС\nуказывается согласно налоговому статусу исполнителя.\n",{"q":3762,"a":3763},"Можно ли протестировать до оплаты?","Да. Проект и ключ создаются сразу после регистрации, а стоимость проверки\nтакая, что полноценное тестирование сценария укладывается в минимальное\nпополнение. Отдельного песочного контура с фиктивными звонками нет —\nпроверки идут по реальному пулу номеров.\n",{"q":3765,"a":3766},"Что будет с неизрасходованным балансом?","Он не сгорает и остаётся на счёте: списание идёт по факту подтверждений,\nбез ежемесячного обнуления. Порядок возврата описан в оферте.\n",{"q":3768,"a":3769},"Что делать, если объём сильно больше типового?","Написать до интеграции. Крупные объёмы упираются не в тариф, а в ёмкость\nпула номеров, и это вопрос планирования, а не прайса.\n","Сервис подтверждения номера телефона: как выбрать и сколько стоит",{},[3022,3773,3023],"flash-call-ili-sms",{"title":3325,"description":3753},"blog\u002Fservis-podtverzhdeniya-nomera","При выборе сервиса подтверждения номера почти всё решают три вопроса: за что именно\nсписывают деньги — за попытку или за результат; что происходит, когда подтверждение\nне доходит до пользователя; за сколько это встраивается в ваш продукт. Цена за штуку\nвторична — на реальную стоимость сильнее влияет модель списания. Ниже разбор критериев\nи расчёт: в FlashCall подтверждение стоит 0,20 ₽, и платите вы только за состоявшиеся.\n","cKsNAXF6yYZKwaXx9i9qxM3vrxbBahCb-ceO101blQ0",{"data":3779,"body":3780},{},{"type":3781,"children":3782},"root",[3783],{"type":3784,"tag":10,"props":3785,"children":3786},"element",{},[3787,3790,3795,3796,3801,3803,3808],{"type":3788,"value":3789},"text","Интеграция состоит из двух запросов: создать проверку и узнать её статус. Первый\nвозвращает выделенный номер, который нужно показать пользователю, второй — одно из трёх\nсостояний: ",{"type":3784,"tag":30,"props":3791,"children":3793},{"className":3792},[],[3794],{"type":3788,"value":74},{"type":3788,"value":187},{"type":3784,"tag":30,"props":3797,"children":3799},{"className":3798},[],[3800],{"type":3788,"value":78},{"type":3788,"value":3802}," или ",{"type":3784,"tag":30,"props":3804,"children":3806},{"className":3805},[],[3807],{"type":3788,"value":82},{"type":3788,"value":3809},". Вебхуков нет, результат забирается\nопросом — поэтому не нужен публичный обработчик, проверка подписи и защита от повторных\nдоставок. Ключ проекта используется только с сервера.",{"data":3811,"body":3812},{},{"type":3781,"children":3813},[3814],{"type":3784,"tag":10,"props":3815,"children":3816},{},[3817,3819,3824],{"type":3788,"value":3818},"Нет. Результат забирается опросом ",{"type":3784,"tag":30,"props":3820,"children":3822},{"className":3821},[],[3823],{"type":3788,"value":55},{"type":3788,"value":3825},". Это упрощает интеграцию:\nне нужен публично доступный обработчик, проверка подписи и защита\nот повторной доставки. Если вебхуки появятся, опрос продолжит работать.",{"data":3827,"body":3828},{},{"type":3781,"children":3829},[3830],{"type":3784,"tag":10,"props":3831,"children":3832},{},[3833],{"type":3788,"value":3834},"Нет. Запрос содержит ключ проекта, а всё, что попало во фронтенд, доступно\nпользователю. Оба запроса должны идти с вашего сервера, а браузер общается\nс вашим собственным эндпоинтом.",{"data":3836,"body":3837},{},{"type":3781,"children":3838},[3839],{"type":3784,"tag":10,"props":3840,"children":3841},{},[3842,3844,3849],{"type":3788,"value":3843},"60 секунд с момента создания. После этого она переходит в ",{"type":3784,"tag":30,"props":3845,"children":3847},{"className":3846},[],[3848],{"type":3788,"value":82},{"type":3788,"value":3850},",\nи звонок, поступивший позже, к ней уже не привяжется. Создавайте новую\nпроверку вместо продления старой.",{"data":3852,"body":3853},{},{"type":3781,"children":3854},[3855],{"type":3784,"tag":10,"props":3856,"children":3857},{},[3858],{"type":3788,"value":3859},"Создание проверки завершится ошибкой о недостатке средств — проверка\nне будет создана. Уже существующие проверки при этом дорабатывают штатно.",{"data":3861,"body":3862},{},{"type":3781,"children":3863},[3864],{"type":3784,"tag":10,"props":3865,"children":3866},{},[3867],{"type":3788,"value":3868},"Нет. Это обычный REST с JSON: два запроса, которые делаются штатным\nHTTP-клиентом любого языка. Отдельной библиотеки не требуется.",1787687892258]