Перейти к содержимому
API-документация

Реквизиты карты и PCI

Как показать держателю номер карты, CVV и коды подтверждения и не нарушить PCI DSS.

Маскированный номер — всегда можно

masked_number из GET /cards/{id} (**** **** **** 4821) можно хранить, логировать и показывать где угодно. Для списка карт в интерфейсе, писем и поддержки его достаточно.

Полные реквизиты — по отдельному доступу

GET /cards/{id}/secret возвращает полный номер (PAN), CVV и срок действия.

Это вводит вас в периметр PCI DSS

Получая PAN и CVV, ваша система обрабатывает данные карт и должна соответствовать требованиям PCI DSS. Никогда не храните, не логируйте и не кэшируйте ответ этого эндпоинта — ни в базе, ни в логах веб-сервера, ни в системах мониторинга ошибок. Передайте реквизиты держателю и сразу забудьте.

Нужны оба условия:

  1. Ключ со scope cards:secret.
  2. Аккаунту открыт доступ к реквизитам — по договорённости с менеджером.

Без любого из них — 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. Не у каждой карты есть канал кодов: это зависит от эмитента за тарифом. Коды — живые секреты: правила те же, что для реквизитов.