Get Card Info
Returns the card's status, balance, masked PAN by default.
When with_sensitive=true is sent, this same endpoint additionally returns the full PAN / CVV / expiry / cardholder name in the same response, via either of two authorization paths:
- User second-factor verification (default, self-service) — the same flow as viewing CVV on the web app: submit
email_code/pin/two_fa_code/sms_codeaccording to the account's configured transaction-auth methods. Codes are requested via/openapi/card/sensitive/send_code. No admin approval needed. - Verification-exempt credential (approval-gated) — for fully automated server-to-server flows, your credential can be granted an exemption so no per-request second factor is required. Disabled by default; see Sensitive Card Information below.
Use cases:
- Poll after async open until
status=2 (active) - Periodically sync balance on the customer side
- Investigate missed webhooks
- Display PAN / CVV in your own checkout / wallet UI (via either authorization path)
Endpoint
| Item | Value |
|---|---|
| Method | POST |
| Path | /api/v1/openapi/card/info |
| Auth | HMAC |
| Idempotency | Not required |
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
card_id | string | ✅ | card_<id> |
with_sensitive | bool | optional | When true, request full PAN / CVV / expiry / cardholder name. Defaults to false. Requires status=2 (active) plus one of the two authorization paths. |
email_code | string | conditional | Email verification code (when the account's auth methods include email). Sent via /openapi/card/sensitive/send_code. Not needed for verification-exempt credentials. |
sms_code | string | conditional | SMS verification code (when auth methods include SMS). |
pin | string | conditional | Transaction PIN (when auth methods include PIN; set by the user on the web app). |
two_fa_code | string | conditional | 2FA / Google Authenticator TOTP (when auth methods include 2FA). |
Example Request — default (status & balance only)
{ "card_id": "card_12345" }Example Request — sensitive fields (user second-factor path)
{
"card_id": "card_12345",
"with_sensitive": true,
"email_code": "482913"
}Example Request — sensitive fields (verification-exempt credential)
{
"card_id": "card_12345",
"with_sensitive": true
}Response Fields
Always returned
| Field | Type | Description |
|---|---|---|
card_id | string | Echo |
card_type | string | Lowercase: virtual_l / virtual_p / virtual_v / virtual_r / virtual_g / virtual_a |
card_brand | string | Brand display name |
currency | string | Card currency |
status | int | 1=pending 2=active 3=failed 4=closing 5=closed 6=frozen |
status_desc | string | English description |
masked_card_no | string | Masked PAN (first 6 + last 4), e.g. 424242******1234 |
last_four | string | Last 4 digits |
balance | string (decimal) | Available balance |
frozen_balance | string (decimal) | Frozen balance |
activated_at | string nullable | Activation time (after success) |
created_at | string | Creation time |
Returned only when with_sensitive=true AND authorization passes AND status=2
| Field | Type | Description |
|---|---|---|
card_number | string | Full PAN (no spaces / dashes) |
cvv | string | Card verification value (3 digits for Visa/Mastercard) |
expiry_date | string | Expiry, format MM/YY |
first_name | string | Cardholder first name (as provided at apply time) |
last_name | string | Cardholder last name |
These fields are entirely omitted from the response (not present at all, not null or empty) when any of the three preconditions is unmet.
Example Response — default
{
"code": 200,
"message": "OK",
"data": {
"card_id": "card_12345",
"card_type": "virtual_v",
"card_brand": "VISA",
"currency": "USD",
"status": 2,
"status_desc": "active",
"masked_card_no": "424242******1234",
"last_four": "1234",
"balance": "85.50",
"frozen_balance": "0",
"activated_at": "2026-04-29T11:00:30Z",
"created_at": "2026-04-29T11:00:12Z"
}
}Example Response — with sensitive fields
{
"code": 200,
"message": "OK",
"data": {
"card_id": "card_12345",
"card_type": "virtual_v",
"card_brand": "VISA",
"currency": "USD",
"status": 2,
"status_desc": "active",
"masked_card_no": "424242******1234",
"last_four": "1234",
"balance": "85.50",
"frozen_balance": "0",
"activated_at": "2026-04-29T11:00:30Z",
"created_at": "2026-04-29T11:00:12Z",
"card_number": "4242420000001234",
"cvv": "123",
"expiry_date": "12/28",
"first_name": "JOHN",
"last_name": "DOE"
}
}Status Codes
| Value | Name | Meaning |
|---|---|---|
| 1 | pending | Opening |
| 2 | active | Activated, usable |
| 3 | failed | Open failed (terminal) |
| 4 | closing | Closing |
| 5 | closed | Closed (terminal) |
| 6 | frozen | Frozen (user / risk) |
Sensitive Card Information
Sensitive fields (card_number / cvv / expiry_date / first_name / last_name) are gated behind one of two authorization paths:
| Path | Who can use it | Per-request requirement | Enablement |
|---|---|---|---|
| A. User second-factor verification | Every credential (default) | Submit email_code / pin / two_fa_code / sms_code per the account's transaction-auth settings — only when outside the grace window (see below) | None — self-service |
| B. Verification-exempt credential | Approved integrations only | None (no codes needed) | Admin approval required, off by default |
Path A uses the same transaction-auth configuration as the web app (Security Settings → Transaction Authentication), the same verification purpose, the same one-time codes. Path B exists for unattended server-to-server automation where a per-request human second factor is impossible.
Grace window — verify once, then no codes for a while
After one successful second-factor verification through this endpoint, the account enters a grace window (duration set by the platform administrator; default 30 minutes, 0 = disabled). While the window is active, further with_sensitive=true requests for your own cards succeed without any verification fields — so you can programmatically fetch PAN/CVV for multiple cards (e.g. when re-distributing cards to your downstream users) after a single human verification.
- The window is fixed, not sliding: it starts at the moment of successful verification and is not extended by subsequent no-code requests. When it expires, verify again.
- The window is per account, covering all cards owned by the credential.
send_codereturnsgrace_active=truewhile the window is live (and skips sending), so your integration can check cheaply whether codes are currently needed. Note: outside the window the same call actually sends a code and consumes the 60 s cooldown — don't poll it as a status probe.- The administrative ban always overrides the window.
Path A — user second-factor verification (default)
Step 1: Send the verification code
| Item | Value |
|---|---|
| Method | POST |
| Path | /api/v1/openapi/card/sensitive/send_code |
| Auth | HMAC |
| Idempotency | Not required |
| Request body | {} (empty JSON object) |
| Cooldown | 1 request per account per 60 s → excess returns 429 + Retry-After header |
The verification purpose is fixed server-side (get_card_sensitive) — this endpoint cannot be used to trigger codes for transfers, withdrawals, or any other operation, and codes issued for other purposes are not accepted here.
Response data:
| Field | Type | Description |
|---|---|---|
require_email | bool | Must submit email_code in Step 2 |
require_sms | bool | Must submit sms_code in Step 2 |
require_pin | bool | Must submit pin in Step 2 (no code is sent — the user knows their PIN) |
require_2fa | bool | Must submit two_fa_code in Step 2 (no code is sent — generated by the authenticator app) |
email_sent | bool | A 6-digit code was emailed to the account's bound address |
sms_sent | bool | A 6-digit code was texted to the account's bound phone |
grace_active | bool | The grace window is live: nothing is sent, the cooldown is not consumed — skip Step 2's verification fields and call /card/info directly |
If the account only uses PIN and/or 2FA, nothing is sent (email_sent=false, sms_sent=false) and the cooldown is not consumed — proceed directly to Step 2 with the local factors.
Step 2: Call /card/info with the codes
Send with_sensitive=true together with every field the Step 1 response flagged as required. Codes are:
- Single-use — consumed on successful verification; request a new one for each verification (not needed while the grace window is active)
- Time-limited — valid for 10 minutes
- Brute-force protected — repeated wrong attempts temporarily lock verification (
429)
A successful verification opens the grace window — subsequent retrievals within it need no verification fields at all.
Path B — verification-exempt credential (approval-gated)
Off by default — explicit approval required
The exemption is disabled by default for every API credential and additionally guarded by a platform-wide switch. To request it, contact your account manager or submit a support ticket. You will need to:
- State the business reason (e.g. unattended automation that cannot involve a human second factor)
- Confirm your environment is PCI-DSS compliant for storing/displaying PAN
- Provide your
AppID(cp_xxxx...) so the exemption can be granted to the correct credential
When both the credential switch and the platform switch are ON, with_sensitive=true succeeds without any verification-code fields. Exempt credentials may still send the code fields; they are ignored.
Access preconditions
All four must be satisfied for sensitive fields to be returned:
- Request — caller explicitly sends
with_sensitive: true - Authorization — valid user second-factor codes (Path A) or a verification-exempt credential (Path B)
- Card state — the card must be
status=2 (active);pending/failed/closing/closed/frozenare rejected - No administrative ban — the platform administrator can force-disable sensitive info access per card or per account. A ban overrides both authorization paths (valid codes and exempt credentials alike) and returns
403 card_sensitive_access_banned; an account-level ban also blockssend_code. Contact support if you believe a ban is in error.
If any precondition fails, the response is 403 (or 429 when verification is locked) with the corresponding message_key. Fields are not partially returned — it is all-or-nothing per request.
Compliance & handling recommendations
Handle returned PAN as out-of-scope PCI data
- Do not log the full
card_number/cvvto your application logs or APM - Do not persist PAN / CVV to your own database or analytics warehouse
- Pass through to your front-end via TLS only; render and discard
- Mask before display when not actively in use; prefer
masked_card_no - Rotate your OpenAPI secret immediately if you suspect compromise
Operational notes
- The verification exemption (Path B) is revocable. If support revokes it, subsequent
with_sensitive=truecalls without codes fail within ~5 minutes (propagation window) — they then fall under Path A and return the corresponding*_requiredkey. - The
403error codes are distinct from the401returned by the auth middleware —403means the request was authenticated correctly but second-factor verification is missing/failed or the card state refuses sensitive access. - This endpoint never returns CVV via webhook payloads; sensitive fields are only available through this synchronous call.
Common Errors
| HTTP | message_key | Description |
|---|---|---|
| 400 | openapi_invalid_card_id | Bad card ID |
| 400 | openapi_card_type_not_supported | Sensitive fields requested for a non-virtual card type |
| 401 | openapi_invalid_credentials | Auth failure |
| 403 | email_code_required / sms_code_required / pin_required / 2fa_code_required | with_sensitive=true without the corresponding second-factor field (and the credential is not verification-exempt). Call send_code and resubmit. |
| 403 | invalid_email_code / invalid_sms_code / invalid_pin / invalid_2fa_code | The submitted second-factor value is wrong or expired |
| 403 | email_not_verified / phone_not_verified / no_usable_auth_method | The account has no usable verification channel for the configured method — fix the account's security settings on the web app |
| 403 | card_sensitive_access_banned | Sensitive info access force-disabled by the administrator for this card or account; no authorization path will succeed |
| 403 | openapi_sensitive_card_only_active | Card is not in status=2 (active); sensitive fields refused |
| 404 | card_not_found | Card not found / not owned |
| 429 | too_many_requests | Verification temporarily locked after repeated failures (or PIN locked); honor the Retry-After header |
| 500 | openapi_get_card_info_failed | Server error |
| 500 | openapi_internal_error | Failed to load credential context for sensitive branch |
/card/sensitive/send_code errors:
| HTTP | message_key | Description |
|---|---|---|
| 401 | openapi_invalid_credentials | Auth failure |
| 403 | card_sensitive_access_banned | Sensitive info access force-disabled for this account by the administrator; no code is sent |
| 429 | too_many_requests | Send cooldown (60 s per account); honor the Retry-After header |
| 500 | send_code_failed | Delivery failed — the cooldown is released so you may retry immediately |
Recommended Polling Strategy
import time
def wait_card_active(card_id, max_minutes=10):
"""Poll after async open until active or failure"""
deadline = time.time() + max_minutes * 60
backoff = 2
while time.time() < deadline:
info = call("/api/v1/openapi/card/info", {"card_id": card_id})
status = info['data']['status']
if status == 2:
return info['data'] # active
if status in (3, 5):
raise CardOpenFailed(info['data'])
time.sleep(backoff)
backoff = min(backoff * 2, 30) # exp backoff, max 30s
raise TimeoutError(f"Card {card_id} not active in {max_minutes} min")
def fetch_pan_for_display(card_id, email_code=None, sms_code=None, pin=None, two_fa_code=None):
"""Fetch sensitive card data for one-shot UI rendering. Never persist.
Path A (default): first POST /openapi/card/sensitive/send_code, collect the
required second factor(s) from the account owner, then pass them here.
Path B (verification-exempt credential): call with no codes at all.
"""
body = {"card_id": card_id, "with_sensitive": True}
for k, v in {"email_code": email_code, "sms_code": sms_code,
"pin": pin, "two_fa_code": two_fa_code}.items():
if v:
body[k] = v
resp = call("/api/v1/openapi/card/info", body)
if resp.get('code') == 403:
key = resp.get('message_key')
if key and key.endswith('_required'):
raise PermissionError(f"Second factor missing: {key} — "
"call /openapi/card/sensitive/send_code first")
if key and key.startswith('invalid_'):
raise PermissionError(f"Second factor rejected: {key}")
if key == 'openapi_sensitive_card_only_active':
raise RuntimeError("Card is not active; wait for activation")
if resp.get('code') == 429:
raise RuntimeError("Verification locked; retry after the Retry-After window")
if resp.get('code') != 200:
raise RuntimeError(f"Card info failed: {resp.get('message_key')}")
return resp['data'] # contains card_number / cvv / expiry_date / first_name / last_name
def request_sensitive_code():
"""Step 1 of Path A: trigger email/SMS code + learn which factors are needed."""
resp = call("/api/v1/openapi/card/sensitive/send_code", {})
if resp.get('code') != 200:
raise RuntimeError(f"send_code failed: {resp.get('message_key')}")
return resp['data'] # require_email / require_sms / require_pin / require_2fa / email_sent / sms_sentPrefer webhooks
Use webhooks where possible. card/info polling is a fallback for when your webhook receiver is temporarily down or for reconciliation.