Реквизиты карты и PCI
Как показать держателю номер карты, CVV и коды подтверждения и не нарушить PCI DSS.
Маскированный номер — всегда можно
masked_number из GET /cards/{id} (**** **** **** 4821) можно хранить, логировать и показывать где угодно. Для списка карт в интерфейсе, писем и поддержки его достаточно.
Полные реквизиты — по отдельному доступу
GET /cards/{id}/secret возвращает полный номер (PAN), CVV и срок действия.
Это вводит вас в периметр PCI DSS
Получая PAN и CVV, ваша система обрабатывает данные карт и должна соответствовать требованиям PCI DSS. Никогда не храните, не логируйте и не кэшируйте ответ этого эндпоинта — ни в базе, ни в логах веб-сервера, ни в системах мониторинга ошибок. Передайте реквизиты держателю и сразу забудьте.
Нужны оба условия:
- Ключ со scope
cards:secret. - Аккаунту открыт доступ к реквизитам — по договорённости с менеджером.
Без любого из них — 403 card_secret_not_allowed. У эндпоинта свой строгий лимит (по умолчанию 10 запросов в минуту на ключ), каждый вызов записывается.
CARD=9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d
TS=$(date +%s)
SIG=$(printf '%s\n%s\n%s\n%s' "$TS" "GET" "/api/partner/v1/cards/$CARD/secret" "" \
| openssl dgst -sha256 -hmac "$PP_SECRET" -hex | awk '{print $NF}')
curl -sS "https://api.plativputi.com/api/partner/v1/cards/$CARD/secret" \
-H "X-Key-Id: $PP_KEY_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG"// Отдать держателю и забыть: без логов, без кэша.
const secret = await pp("GET", `/cards/${cardId}/secret`);
res.set("Cache-Control", "no-store").json(secret);# Отдать держателю и забыть: без логов, без кэша.
@app.get("/my-cards/<card_id>/details")
def card_details(card_id):
secret = pp("GET", f"/cards/{card_id}/secret")
return secret, 200, {"Cache-Control": "no-store"}// Отдать держателю и забыть: без логов, без кэша.
$secret = pp('GET', "/cards/$cardId/secret");
header('Cache-Control: no-store');
echo json_encode($secret);{
"pan": "4000001234564821",
"cvv": "123",
"expiry_month": "10",
"expiry_year": "2029",
"masked_number": "**** **** **** 4821"
}| Ответ | Что делать |
|---|---|
403 card_secret_not_allowed | Нет scope или доступа — обратитесь к менеджеру |
429 card_secret_rate_limited | Подождите минуту |
503 card_secret_reveal_disabled | Выдача временно выключена — повторите позже |
Коды подтверждения
Банк может запросить у держателя одноразовый код:
- 3-D Secure (
type: 3ds) — при оплате в интернете; - токенизация (
type: tokenization) — при добавлении карты в Apple Pay или Google Pay.
Код живёт около двух минут. Push-канала для него нет: пока держатель на экране подтверждения, опрашивайте GET /cards/{id}/otp раз в 3–5 секунд.
const { items } = await pp("GET", `/cards/${cardId}/otp`);
const fresh = items
.filter((c) => new Date(c.expires_at) > new Date())
.sort((a, b) => b.issued_at.localeCompare(a.issued_at))[0];from datetime import datetime, timezone
items = pp("GET", f"/cards/{card_id}/otp")["items"]
now = datetime.now(timezone.utc).isoformat()
fresh = max((c for c in items if c["expires_at"] > now), key=lambda c: c["issued_at"], default=None)$items = pp('GET', "/cards/$cardId/otp")['items'];
$live = array_filter($items, fn ($c) => strtotime($c['expires_at']) > time());
usort($live, fn ($a, $b) => strcmp($b['issued_at'], $a['issued_at']));
$fresh = $live[0] ?? null;{
"items": [
{
"type": "3ds",
"code": "482913",
"amount": "12.99",
"currency": "USD",
"merchant_name": "SPOTIFY",
"last4": "4821",
"issued_at": "2026-10-07T18:43:52Z",
"expires_at": "2026-10-07T18:45:52Z"
}
]
}Если кодов нет — 200 и пустой items. Не у каждой карты есть канал кодов: это зависит от эмитента за тарифом. Коды — живые секреты: правила те же, что для реквизитов.