Atlorium

Антифрод на входе платежа: что можно понять о заказе до того, как деньги ушли

Чарджбэк стоит дороже заказа: возврат, штраф платёжной системы, испорченная статистика. Разбираем, какие сигналы доступны за копейки ещё до подтверждения платежа.

11 минут чтения e-commerce · финтех · риск-менеджер · разработчик

Заказ упал в три сорок ночи: два флагманских смартфона, доставка курьером в Подмосковье, оплата картой прошла с первого раза. Утром менеджер увидел в админке зелёную галочку и отправил заказ в сборку.

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

Если открыть этот заказ задним числом, видно почти всё. Карта выпущена банком в Юго-Восточной Азии. Она предоплаченная и непополняемая. Платёж пришёл с адреса, который принадлежит дата-центру в Германии. Ни один из этих фактов в карточке заказа не хранился — хотя каждый можно было получить до того, как платёж ушёл на авторизацию, одним HTTP-запросом.

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

Где именно течёт

Платёжный шлюз отвечает на один вопрос: прошла авторизация или нет. У него нет мнения о том, похож заказ на настоящий или нет — это ваша зона ответственности. А данных, на которых можно составить мнение, в типовой карточке заказа нет: маскированный номер карты, сумма, адрес доставки и IP в логе nginx, на который никто не смотрит.

Страна карты и страна плательщика разошлись

Что происходит
В админке видно «оплата прошла» и последние четыре цифры. Что карта выпущена банком другой страны, а платёж пришёл из третьей, не видит никто.
Во что обходится
Чарджбэк по неавторизованной операции — это возврат всей суммы, себестоимость уехавшего товара и комиссия за диспут поверх.
Чем закрывается
Проверка BIN возвращает страну и код страны эмитента, а если передать IP плательщика — ещё и страну IP с готовым флагом ipMatchesBin: сравнивать руками не нужно.

Предоплаченная карта, за которой никого нет

Что происходит
Карту купили в переходе, пополнили ровно на сумму заказа и выбросили. Держателя не существует, обратной связи с банком нет.
Во что обходится
Вернуть деньги некуда, спросить не с кого. Такие заказы почти всегда идут на ликвидный товар: электронику, подарочные сертификаты, топливные карты.
Чем закрывается
В ответе BIN есть isPrepaid и isReloadable. Одноразовая предоплаченная карта на дорогой заказ от нового клиента — повод притормозить, а не отказать сразу.

Плательщик пришёл через дата-центр или VPN

Что происходит
IP в логе выглядит обычным набором цифр ровно до момента, когда посмотришь, кому принадлежит сеть.
Во что обходится
Автоматизированный перебор краденых карт идёт именно с хостинга: он дешёвый, быстрый и легко масштабируется на тысячи попыток за ночь.
Чем закрывается
Расширенный профиль IP отдаёт privacy.isVpn, isProxy, isTor, isResidentialProxy, флаг flags.isHosting и отметку о публичных жалобах на адрес.

Каждая из этих трёх проверок стоит доли копейки и укладывается в сотни миллисекунд. Ни одна из них не требует полного номера карты: BIN — это первые 6, 8 или 10 цифр, и больше сервису передавать нечего.

Сколько стоит ошибка — в обе стороны

Разговор про антифрод обычно застревает на первом же вопросе: «а сколько отсекать?». Ответ «всё подозрительное» стоит дороже, чем кажется, потому что у ошибки есть вторая сторона — живой покупатель, которому отказали. Он не пишет в поддержку, он уходит к конкуренту и рассказывает об этом друзьям.

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

Величина В примере Откуда берётся у вас
Средний чек 25 000 ₽ Аналитика заказов за квартал
Маржа 12%, то есть 3 000 ₽ с заказа Финансовая модель
Себестоимость товара 22 000 ₽ Закупка плюс логистика
Комиссия за диспут около 1 500 ₽ Договор с эквайером — там же лимиты по доле чарджбэков
Стоимость ручной проверки около 100 ₽ (5 минут оператора) Фонд оплаты труда, делённый на пропускную способность

Мошеннический заказ, который прошёл: товар уехал (минус 22 000 ₽ себестоимости), деньги вернулись держателю карты, комиссия за диспут осталась на вас. Потеря — примерно 23 500 ₽. Выручку в потерю не считаем: её просто не было.

Ложный отказ: не заработали 3 000 ₽ маржи. Но покупатель, которому отказали в оплате без объяснений, чаще всего не возвращается. Допустим, каждый второй уходит навсегда и вместе с ним уходят будущие покупки — тогда цена ложного отказа выходит на 6 000 ₽. Множитель спорный, и это нормально: подставьте свой, формула от этого не сломается.

А теперь то же самое для ручной проверки. Она стоит 100 ₽ и отменяет заказ только тогда, когда оператор что-то нашёл. Отправлять заказ на проверку выгодно, если f × 23 500 > 100, то есть при f > 0,43%.

Между 0,43% и 20% — почти два порядка. В этот зазор попадает подавляющее большинство сигналов, которые обсуждают под словом «антифрод». Несовпадение страны карты и страны IP даёт долю фрода в единицы процентов — для автоблока мало, для ручной проверки более чем достаточно. Отсюда вывод, который стоит выписать на стену перед тем, как садиться писать правила: по умолчанию сигнал ведёт в жёлтый, а не в красный. Красный нужно заслужить.

Что можно узнать до подтверждения платежа

Первый источник — сама карта, точнее её префикс. Проверка BIN (/api/bin/{bin}) принимает 6, 8 или 10 цифр; в платёжной индустрии идёт переход с шестизначных BIN на восьмизначные, поэтому передавайте 8 и больше — точность выше.

Что приходит в ответе:

  • valid — нашёлся ли BIN в справочнике. Если false, реквизиты будут пустыми.
  • cardBrand, cardType (DEBIT / CREDIT / CHARGE CARD), cardCategory (CLASSIC, GOLD, PLATINUM, BUSINESS и другие).
  • country, countryCode, countryCode3 и currencyCode — страна и валюта эмитента.
  • issuer, issuerWebsite, issuerPhone — банк, выпустивший карту.
  • isCommercial, isPrepaid, isReloadable — корпоративная, предоплаченная, пополняемая.

Если добавить к запросу ?customerIp=, в тот же ответ приезжает антифрод-блок по плательщику: hasCustomerIp, ipMatchesBin (совпала ли страна IP со страной эмитента), ipCountry, ipRegion, ipCity, а также ipBlocklisted и список ipBlocklists — в каких именно списках блокировок засветился адрес. Это один запрос к сервису, а не два: сравнение стран уже сделано за вас.

Второй источник — Профиль IP (/api/ipinfo/{ip}). Здесь важно не перепутать два режима, потому что они отвечают на разные вопросы.

Базовый режим отдаёт географию: город, регион, страну, координаты, часовой пояс, радиус точности accuracyRadiusKm. Если внутренние базы расходятся в значениях, сервис вернёт все варианты под обезличенными метками «Источник 1», «Источник 2» — чтобы вы видели, что данные спорные, а не получали одно уверенное неверное значение. Никаких признаков VPN в базовом режиме нет и быть не может: это локальные адресные базы.

Расширенный режим (?extended=true) — это то, ради чего антифрод вообще смотрит на IP:

  • privacy: isVpn, isProxy, isTor, isRelay, isHosting, isResidentialProxy, иногда имя конкретного сервиса анонимизации в serviceName, а также lastSeen и percentDaysSeen — насколько свежий сигнал и как часто адрес наблюдался как прокси.
  • flags: isHosting (дата-центр), isMobile, isAnycast, isSatellite.
  • asn и company — кто держит сеть: номер AS, оператор, тип (isp / hosting / business).
  • abuseReports.flagged — есть ли на адрес публичные жалобы, и detailsUrl со ссылкой на детали.
  • abuse — контакт для жалоб у владельца сети, hostname и hostedDomains.total.

Поле tier в ответе всегда говорит, что именно внутри: Basic или Extended. Резидентный прокси, кстати, самый неприятный из признаков: трафик прячется за адресами обычных домашних пользователей, и по географии он выглядит совершенно нормально.

Светофор вместо стоп-крана

Правило, написанное как «если VPN — отказ», убивает конверсию у половины корпоративных клиентов: у них весь офисный трафик выходит через корпоративный шлюз, и он честно детектируется как VPN. Правило, написанное как сумма весов с тремя порогами, ведёт себя сильно приличнее.

Что видим в ответе Свет Почему так
Страна карты совпала со страной IP, карта дебетовая, ipBlocklisted: false, признаков анонимизации нет Зелёный Пропускаем молча, без единой лишней секунды в оформлении
ipMatchesBin: false — страны разошлись, остальное чисто Жёлтый Так выглядят экспаты, командировки, туристы и все, кто платит зарубежной картой. Доля фрода тут — единицы процентов, для автоблока мало
isPrepaid: true и isReloadable: false на дорогом заказе от нового клиента Жёлтый Одноразовая предоплаченная карта — законный платёжный инструмент. Подозрительна связка «одноразовая карта + ликвидный товар + первый заказ»
privacy.isVpn: true или flags.isHosting: true вместе с несовпадением стран Жёлтый, ближе к красному Ручная проверка со звонком. Два независимых сигнала уже сильно поднимают долю фрода
ipBlocklisted: true или privacy.isTor: true при крупной сумме Красный Единственный класс сигналов, где доля фрода правдоподобно перешагивает 20%. Всё равно смотрите на сумму: на заказе в 500 ₽ отказ не окупается
valid: false — BIN не нашёлся в справочнике Жёлтый Это не признак фрода. Это опечатка, свежий диапазон или редкий эмитент. Отказывать по такому — терять живые деньги

Отдельный случай — ответ 503: внешний источник расширенного профиля временно недоступен. Деньги за такой запрос не списываются, но решение принимать всё равно надо. Правильное поведение — не отказ, а жёлтый: заказ уходит на ручную проверку, покупатель ничего не замечает. Антифрод, который роняет оплату вместе с чужим API, стоит дороже, чем фрод, который он ловит.

Код: две проверки перед подтверждением

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

Шаг 1. BIN карты вместе с проверкой IP плательщика

curl -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
     "https://atlorium.com/api/bin/48334884?customerIp=8.8.8.8"
        

import requests

HEADERS = {"Authorization": "Bearer ak_sandbox_demo_mockdata_v1"}


def check_card(bin_prefix: str, customer_ip: str) -> dict:
    resp = requests.get(
        f"https://atlorium.com/api/bin/{bin_prefix}",
        params={"customerIp": customer_ip},
        headers=HEADERS,
        timeout=10,
    )
    resp.raise_for_status()
    return resp.json()


card = check_card("48334884", "8.8.8.8")

# Веса подобраны так, чтобы один сигнал не отправлял заказ в отказ.
score = 0
if card["hasCustomerIp"] and not card["ipMatchesBin"]:
    score += 2   # страна карты и страна плательщика разошлись
if card["isPrepaid"] and not card["isReloadable"]:
    score += 2   # одноразовая предоплаченная карта
if card["ipBlocklisted"]:
    score += 4   # адрес числится в списках блокировок
if not card["valid"]:
    score += 1   # BIN не нашёлся: опечатка или свежий диапазон

# 0-1 — пропускаем, 2-4 — ручная проверка, 5 и выше — отказ.
print(score, card["cardBrand"], card["country"], card["ipCountry"])
        

using System.Net.Http.Json;

var http = new HttpClient { BaseAddress = new Uri("https://atlorium.com") };
http.DefaultRequestHeaders.Add("Authorization", "Bearer ak_sandbox_demo_mockdata_v1");

var card = await http.GetFromJsonAsync<BinLookupResult>(
    "/api/bin/48334884?customerIp=8.8.8.8");

if (card is { HasCustomerIp: true, IpMatchesBin: false })
{
    // Не отказ: страны разошлись — заказ уходит на ручную проверку.
    Console.WriteLine($"Карта из {card.Country}, платёж из {card.IpCountry} — на проверку");
}

sealed record BinLookupResult(
    bool Valid, string? CardBrand, string? CardType, string? CardCategory,
    string? Country, string? CountryCode, string? CurrencyCode, string? Issuer,
    bool IsCommercial, bool IsPrepaid, bool IsReloadable,
    bool HasCustomerIp, bool IpMatchesBin, string? IpCountry, string? IpCity,
    bool IpBlocklisted, List<string> IpBlocklists);
        

{
  "valid": true,
  "binNumber": "48334884",
  "binLength": 8,
  "cardBrand": "VISA",
  "cardType": "CREDIT",
  "cardCategory": "PLATINUM",
  "country": "NEW ZEALAND",
  "countryCode": "NZ",
  "countryCode3": "NZL",
  "currencyCode": "NZD",
  "issuer": "KIWIBANK, LTD.",
  "issuerWebsite": "http://www.kiwibank.co.nz",
  "issuerPhone": null,
  "isCommercial": false,
  "isPrepaid": false,
  "isReloadable": true,
  "hasCustomerIp": true,
  "ipMatchesBin": false,
  "ipCountry": "UNITED STATES",
  "ipCountryCode": "US",
  "ipCountryCode3": "USA",
  "ipRegion": "California",
  "ipCity": "Mountain View",
  "ipBlocklisted": false,
  "ipBlocklists": [],
  "elapsedMs": 96
}
        

Тот же вызов на шести языках (Python, TypeScript, Go, Java, C#, PHP) — в репозитории bin-lookup-api-client. Примеры запускаются сразу: ключ в них уже стоит демонстрационный.

Второй шаг нужен не всегда. Гоняйте расширенный профиль IP только там, где он окупается: на заказах выше порога суммы, на первых заказах нового клиента, на всём, что уже получило жёлтый от первого шага.

Шаг 2. Расширенный профиль IP: VPN, дата-центр, жалобы

curl -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
     "https://atlorium.com/api/ipinfo/8.8.8.8?extended=true"
        

def ip_profile(ip: str) -> dict:
    resp = requests.get(
        f"https://atlorium.com/api/ipinfo/{ip}",
        params={"extended": "true"},
        headers=HEADERS,
        timeout=10,
    )
    if resp.status_code == 503:
        # Источник недоступен, деньги за запрос не списаны.
        # Платёж НЕ отклоняем: пустой профиль -> заказ уходит на ручную проверку.
        return {}
    resp.raise_for_status()
    return resp.json()


report = ip_profile("8.8.8.8")
privacy = report.get("privacy") or {}
flags = report.get("flags") or {}
reports = report.get("abuseReports") or {}

anonymized = any([
    privacy.get("isVpn"),
    privacy.get("isProxy"),
    privacy.get("isTor"),
    privacy.get("isResidentialProxy"),
])

if privacy.get("isTor") or reports.get("flagged"):
    score += 4          # сильные сигналы
elif anonymized or flags.get("isHosting"):
    score += 2          # плательщик прячет реальную сеть или сидит в дата-центре

decision = "pass" if score <= 1 else "review" if score <= 4 else "decline"
        

var ip = await http.GetFromJsonAsync<IpReport>("/api/ipinfo/8.8.8.8?extended=true");

// Признаки анонимизации приходят ТОЛЬКО при extended=true.
// В базовом режиме privacy будет null — не путайте отсутствие сигнала с его отсутствием у клиента.
bool anonymized = ip?.Privacy is { } p
                  && (p.IsVpn == true || p.IsProxy == true
                      || p.IsTor == true || p.IsResidentialProxy == true);

bool fromDataCenter = ip?.Flags?.IsHosting == true;
bool complained = ip?.AbuseReports?.Flagged == true;

sealed record IpReport(string Ip, string Tier, PrivacyInfo? Privacy,
                       NetworkFlags? Flags, AbuseReportsInfo? AbuseReports);
sealed record PrivacyInfo(bool? IsVpn, bool? IsProxy, bool? IsTor,
                          bool? IsRelay, bool? IsHosting, bool? IsResidentialProxy,
                          string? ServiceName, int? PercentDaysSeen);
sealed record NetworkFlags(bool? IsAnonymous, bool? IsAnycast, bool? IsHosting,
                           bool? IsMobile, bool? IsSatellite);
sealed record AbuseReportsInfo(bool Flagged, string? DetailsUrl);
        

{
  "ip": "8.8.8.8",
  "tier": "Extended",
  "isBogon": false,
  "hostname": "dns.google",
  "geo": {
    "city": "Mountain View",
    "region": "California",
    "countryCode": "US",
    "latitude": 37.4056,
    "longitude": -122.0775,
    "timezone": "America/Los_Angeles",
    "accuracyRadiusKm": 20
  },
  "asn": {
    "asn": "AS15169",
    "name": "Google LLC",
    "domain": "google.com",
    "route": "8.8.8.0/24",
    "type": "hosting"
  },
  "company": { "name": "Google LLC", "domain": "google.com", "type": "hosting" },
  "flags": {
    "isAnonymous": false,
    "isAnycast": true,
    "isHosting": true,
    "isMobile": false,
    "isSatellite": false
  },
  "privacy": {
    "isVpn": false,
    "isProxy": false,
    "isTor": false,
    "isRelay": false,
    "isHosting": true,
    "isResidentialProxy": false,
    "serviceName": null
  },
  "abuse": {
    "email": "network-abuse@google.com",
    "country": "US",
    "network": "8.8.8.0/24"
  },
  "hostedDomains": { "total": 0 },
  "abuseReports": { "flagged": false, "detailsUrl": null }
}
        

Тот же вызов на шести языках (Python, TypeScript, Go, Java, C#, PHP) — в репозитории ip-geolocation-api-client. Примеры запускаются сразу: ключ в них уже стоит демонстрационный.

Дальше — обычная таблица решений, которую вы кладёте в конфиг, а не в код, чтобы риск-менеджер мог двигать пороги без релиза:

Сумма баллов Решение Что видит покупатель
0–1 Пропустить Обычное оформление, никаких задержек
2–4 Ручная проверка «Заказ принят, подтвердим в течение часа» — платёж холдим, товар не резервируем
5 и выше Отклонить Предложение оплатить другим способом. Не пишите «вы похожи на мошенника»

Чего это не умеет

BIN — это справочник диапазонов. Он говорит, какой банк и какой страны выпустил карту с таким префиксом, и ничего больше. Украдена ли карта, жив ли её держатель, есть ли на ней деньги — не знает и знать не может. Единственный, кто ответит на эти вопросы, — банк-эмитент через авторизацию и 3-D Secure.

Несовпадение страны карты и страны IP — не улика. Так выглядит любой человек, который живёт не там, где открывал счёт: релокант, студент, командированный, владелец зарубежной карты в отпуске. Само по себе оно даёт долю фрода в единицы процентов и на автоблок не тянет.

Геоданные по IP обезличены. Это география сети, а не человека: страна, регион, город и радиус точности, который иногда измеряется сотнями километров. Персональные данные владельца адреса мы не собираем и не отдаём — определить личность плательщика по IP невозможно, и не стоит обещать этого своему руководству.

VPN не равно мошенник. За корпоративным шлюзом сидит целый офис, за публичным VPN — все, кому просто не нравится, когда за ними следят. Использовать этот флаг как единственное основание для отказа — гарантированно терять деньги.

Признаки анонимизации приходят только при extended=true. В базовом режиме блок privacy отсутствует, и пустой ответ здесь означает «не спрашивали», а не «чисто». Это самая частая ошибка при интеграции.

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

И общая рамка: BIN плюс IP — это дешёвый первый контур. Он не заменяет ни 3-D Secure, ни поведенческий скоринг, ни историю клиента, ни правила по адресу доставки. Он просто снимает самые грубые кейсы за доли копейки — те, где мошенник даже не пытался маскироваться.

Частые вопросы

Частые вопросы

Можно ли определить банк и страну по номеру карты?

Да, по первым 6, 8 или 10 цифрам — это BIN (он же IIN). По нему определяются платёжная система, тип и категория карты, банк-эмитент, страна выпуска и валюта. Полный номер карты для этого не нужен и передавать его не надо.

Помогает ли проверка BIN избежать чарджбэка?

Она не блокирует чарджбэк, а даёт сигналы до подтверждения платежа: страна эмитента, предоплаченная карта, совпадение страны карты со страной IP, попадание IP в списки блокировок. Решение по этим сигналам принимаете вы. Чарджбэк по неавторизованной операции всё равно возможен — снижается только его вероятность.

Как отличить VPN или прокси от обычного пользователя по IP?

Расширенный режим профиля IP возвращает признаки анонимизации: VPN, прокси, Tor, relay, резидентный прокси, а также принадлежность адреса дата-центру и отметку о публичных жалобах. Базовый режим таких признаков не содержит — там только география.

Что делать, если карта выпущена в одной стране, а платёж идёт из другой?

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

Передаёте ли вы куда-то номер карты и данные клиента?

Сервису передаётся только BIN — первые цифры карты, по которым нельзя восстановить номер. Персональные данные держателя карты и владельца IP-адреса мы не собираем и не отдаём: геоданные по IP обезличены и описывают сеть, а не человека.

Что произойдёт, если сервис ответит 503 прямо во время оплаты?

Ответ 503 означает, что внешний источник временно недоступен; деньги за такой запрос не списываются. Со стороны вашего кода правильная реакция — не отклонять платёж, а отправить заказ на ручную проверку: антифрод не должен ронять оплату вместе с чужим API.

Сервисы из этой статьи

Проверка BIN

Карта, банк-эмитент и страна по BIN/IIN: бренд, тип, категория, антифрод по IP

Расширенный профиль IP

Всё из базового плюс владелец сети (ASN/организация), VPN/прокси/Tor, контакт для жалоб и отметка о жалобах

Профиль IP

Геолокация IP-адреса по нескольким базам: город, регион, страна, координаты, часовой пояс

Попробовать прямо сейчас — без регистрации

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

curl -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
     https://atlorium.com/openapi/bin_ru.json
Полезно? Перешлите коллеге: Telegram VK

Восстанавливаем соединение…

Похоже, связь с сервером ненадолго прервалась. Переподключаемся автоматически — пожалуйста, подождите несколько секунд.

Не удалось переподключиться

Проверьте интернет-соединение. Можно повторить попытку или обновить страницу.

Сессия устарела

Соединение восстановлено, но сессию нужно перезагрузить. Обновите страницу, чтобы продолжить.