Привязка карты
Наша страница привязки или своя форма — и почему номер карты не должен попадать на ваш сервер
Клиент подтверждает карту кодом из SMS один раз — дальше вы списываете без его участия.
Оба способа начинаются одинаково: вы создаёте сессию привязки своим
секретным ключом на сервере. В ответе — ссылка url для первого способа
и clientToken для второго.
Токен сессии живёт 15 минут и разрешает только привязку одной карты. Он не позволяет списывать деньги и смотреть другие операции, поэтому его безопасно передать в браузер или приложение — в отличие от секретного ключа.
/subscribe/v1/card_sessionscurl -X POST https://api.assalam.uz/subscribe/v1/card_sessions \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"returnUrl": "https://your.app/cards/return",
"customerRef": "user_1024"
}'{
"id": "cmsr9f2c",
"url": "https://checkout.assalam.uz/s/6a3f...",
"clientToken": "6a3f...",
"expiresAt": "2026-08-14T10:15:00.000Z",
"livemode": false
}# когда клиент вернулся на returnUrl
curl https://api.assalam.uz/subscribe/v1/card_sessions/cmsr9f2c \
-H "Authorization: Bearer sk_test_..."
# → {"status":"COMPLETED","paymentMethodId":"cmsr7d1e"}Способ 1 — наша страница
Откройте url в WebView приложения или редиректом на сайте. Клиент вводит
карту и код на нашей странице, после чего возвращается на ваш returnUrl.
Ни ваш бэкенд, ни ваше приложение не видят номер карты. Это самый быстрый способ подключиться и самый простой с точки зрения требований PCI DSS.
Способ 2 — своя форма
Вы рисуете форму ввода карты сами — внутри мобильного приложения или на своём сайте. Наша страница не открывается вовсе.
Ключевое отличие от обычной интеграции: эти два запроса делает не ваш сервер, а само приложение или страница клиента. Номер карты уходит от клиента прямо к нам, минуя ваш бэкенд — поэтому он и не попадает в PCI-scope.
Порядок вызовов:
- Ваш сервер создаёт сессию и получает
clientToken. - Вы передаёте
clientTokenв приложение или на страницу. - Клиент вводит карту →
POST /checkout/v1/sessions/{token}/card. - Клиент вводит код из SMS →
POST /checkout/v1/sessions/{token}/code. - Ваш сервер забирает
paymentMethodIdпо id сессии.
# 6a3f... — это clientToken из ответа выше.
# Запрос уходит ИЗ приложения или страницы клиента,
# минуя ваш бэкенд. Ключ продукта здесь не нужен.
curl -X POST \
https://api.assalam.uz/checkout/v1/sessions/6a3f.../card \
-H "Content-Type: application/json" \
-d '{
"pan": "8600000000001234",
"expiry": "3001",
"phone": "998901234567"
}'
# → {"status":"AWAITING_CODE","maskedPan":"860000******1234"}
# клиенту приходит SMS с кодом# тот же токен, код клиент вводит в вашей форме
curl -X POST \
https://api.assalam.uz/checkout/v1/sessions/6a3f.../code \
-H "Content-Type: application/json" \
-d '{"code": "123456"}'
# → {"status":"COMPLETED", ...} — карта привязана# снова с вашего сервера, по id сессии
curl https://api.assalam.uz/subscribe/v1/card_sessions/cmsr9f2c \
-H "Authorization: Bearer sk_test_..."
# → {"status":"COMPLETED","paymentMethodId":"cmsr7d1e"}Секретный ключ в приложение не передаётся
Обратите внимание: в запросах клиента нет заголовка Authorization.
Авторизацией служит clientToken прямо в адресе — он разрешает только эту
привязку и живёт 15 минут. Секретный ключ продукта в приложение передавать
нельзя.
Обязательные правила
- Никогда не отправляйте номер карты на свой бэкенд — ни для валидации, ни для логов. Только напрямую в Assalam.
- Не сохраняйте и не логируйте его: ни в базе, ни в аналитике, ни в crash-репортах.
- Не спрашивайте CVV — банк его не принимает.
- Никаких сторонних скриптов на странице с формой: любой из них может прочитать поле ввода.
Для веб-формы укажите домены в настройках продукта — запрос с другого origin будет отклонён. Мобильным приложениям это не нужно: там границей служит сам токен сессии.
Число попыток ограничено: 3 на ввод карты и 5 на код. После этого сессия закрывается — создайте новую.
Получение результата
Когда клиент вернулся, запросите сессию со своего сервера: статус
COMPLETED и paymentMethodId — идентификатор карты для списаний. Сохраните
его у себя рядом со своим пользователем.
/subscribe/v1/card_sessions/{id}Сохраните paymentMethodId у себя
Это единственное, что нужно для будущих списаний. Положите его в свою базу
рядом с пользователем — например в поле user.assalamPaymentMethodId.
Полезно сохранить и маску карты, чтобы показывать её клиенту.
Если не сохранили — карты клиента находятся по вашей же метке:
GET /subscribe/v1/cards?customerRef=user_1024.
Подробнее о том, кто что хранит и как связаны идентификаторы — в разделе «Как это устроено».
Что известно о карте
При опознании карты банк сообщает нам её маску, банк-эмитент, процессинг и ФИО держателя. Всё это возвращается в объекте карты; полного номера среди этих полей нет и быть не может — он не хранится нигде.
maskedPan— маска вида860012******1234. Показывайте её клиенту, чтобы он понимал, какой картой платит.holderName— ФИО держателя. Банк заполняет его не всегда, поэтому поле может быть пустым.bankName,processingSystem— банк-эмитент и платёжная система (UZCARD, HUMO, PULLY).
ФИО держателя — персональные данные
Показывайте его только самому держателю — например, чтобы он отличил свою карту от чужой в списке. Не передавайте третьим лицам и не показывайте другим пользователям вашего продукта. Банк раскрывает эти сведения нам как агенту, а не для дальнейшего распространения.
Остатка на карте банк не сообщает
Метода запроса баланса в действующем контракте нет. Узнать, хватает ли
средств, можно только попыткой списания: при нехватке приходит отказ с кодом
INSUFFICIENT_FUNDS. Учитывайте это в сценариях, где хотелось бы
предупредить клиента заранее.
Статусы карты
REQUIRES_VERIFICATION— карта опознана, ждём код из SMS.ACTIVE— можно списывать.DISABLED— отключена вами или клиентом.FAILED— привязка не удалась.
Отключить карту: POST /subscribe/v1/cards/{id}/disable. После этого списания
по ней невозможны.
Дальше по теме
Списания
Списание с привязанной карты, статусы.
Как это устроено
Объекты, идентификаторы и кто что хранит.
Тестирование
Тестовые карты и сценарии отказов.
Остаток на карте
Нужно отдельное разрешение
Метод доступен, только если мы включили его вашему продукту. Напишите нам,
если он нужен: остаток — персональные финансовые данные клиента,
а не справочный атрибут карты вроде маски номера, поэтому доступ выдаётся
поимённо и под задачу. Без разрешения ответ — 403 CARD_BALANCE_DISABLED.
GET /subscribe/v1/cards/{card}/balance{
"object": "card_balance",
"card": "cmv93eb6h0005wi01kqvnbhhk",
"balance": 999500,
"currency": "UZS"
}Сумма в тийинах, как и везде в API: 999500 — это 9 995 сум.
Зачем это бывает нужно: перед списанием по подписке проверить, хватит ли денег, и не портить клиенту историю отказом. Чего делать не стоит:
- опрашивать остаток в цикле. Каждый вызов идёт в банк и делится общим лимитом обращений с вашими платежами — опрос остатка способен затормозить собственные списания;
- хранить остаток у себя. Он меняется без нашего участия, и сохранённое значение устаревает сразу же;
- показывать остаток кому-либо, кроме держателя карты.
Метод работает не на всякой рельсе: если банк, через который привязана карта,
остаток не отдаёт, ответ — NOT_SUPPORTED. Это не ошибка интеграции.