Skip to content

Apply Virtual Card

Async card-open endpoint. A successful response means "queued"; the final result is pushed via card.opened / card.open_failed webhook or polled via card/info.

Endpoint

ItemValue
MethodPOST
Path/api/v1/openapi/card/apply
AuthHMAC
Idempotency KeyRequired via Idempotency-Key header

Request Fields

Don't send card_type

This endpoint does not accept card_type. The server derives it from header_id and verifies it's within the virtual-card range.

FieldTypeRequiredDescription
header_idstringhdr_<id> (List Card Headers)
package_idstringpkg_<id> (List Card Configs)
first_namestring⚠️Cardholder first name (depends on header.require_phone/email)
last_namestring⚠️Last name
phone_codestring⚠️Country code (e.g. 86 / 1)
phonestring⚠️Phone number (without country code)
emailstring⚠️Email
use_bound_emailboolNoDefault false — server uses the email you sent. Set true to fall back to the account's bound email.
use_bound_phoneboolNoDefault false — server uses the phone + phone_code you sent. Set true to fall back to the account's bound phone.
first_deposit_amountstringNoCustom total first deposit, non-negative integer (pay/recharge asset unit, e.g. USDT). Empty = config base. Must be ≥ the package's min_first_deposit (List Card Configs); when that minimum is above base, this field is required (the empty default is rejected with first_recharge_below_min). The part above base is charged a recharge-style fee and frozen alongside the open fee. Dry-run the exact numbers with Preview First Deposit.

When holder fields are required

  • If header.require_phone == true: send phone_code + phone (or set use_bound_phone == true to fall back to the account's bound phone)
  • If header.require_email == true: send email (or set use_bound_email == true to fall back to the account's bound email)
  • first_name / last_name are usually required (always required for virtual_g / virtual_a, see below)
  • OpenAPI default differs from the H5 app: use_bound_phone and use_bound_email default to false here — the server uses what you sent. Merchants normally don't have bound phones/emails on our side, so falling back to bound values would fail. Send true only if you have a verified reason to read the bound value.

Custom first deposit

By default a card opens with the config's base initial deposit. To top it up, send first_deposit_amount (a non-negative integer ≥ base). The base is credited 1:1 with no fee; the excess above base is charged a recharge-style fee, the remainder is credited 1:1 (USDT/USD, no FX), and the first deposit sent to the provider is rounded to an integer. The open fee + the full computed freeze are held from your wallet at apply time.

A package may also enforce a minimum first deposit above the base (min_first_deposit in List Card Configs, min_first_deposit_amount in the preview). In that case first_deposit_amount is required and must be ≥ that minimum; only the base part stays fee-free (e.g. base 10, minimum 500 → deposit ≥ 500, 10 fee-free, 490 charged).

Preview before applying

The validation (format / >= base / max recharge / recharge limit / balance) is shared with Preview First Deposit. Call the preview first to show the customer the exact total_freeze / first_recharge_card — the numbers match this endpoint 1:1. Invalid amounts are rejected here with the same keys the preview reports as invalid_reason (see Common Errors).

Virtual-G specific constraints

virtual_g (G card) has extra constraints enforced server-side beyond what header.require_* may say:

ConstraintRuleFailure message_key
EmailAlways required (server overrides header)bank_card_email_required
Phonephone_code + phone always required (server overrides header)bank_card_phone_required
Cardholder namefirst_name + last_name combined must match ^[A-Za-z]+(?: [A-Za-z]+)*$ and total length ≤ 40 chars (ASCII letters only; single-space separators; no digits, symbols, or non-ASCII)virtual_g_name_invalid
Birthday & billing addressDo not send — generated server-side automaticallyvirtual_g_required_fields_missing (only if server-side fill fails)

Trim and validate the name before calling apply

Many real-world cardholder names contain accents, hyphens, or apostrophes — those are rejected by the provider. Inform end users that G-card applications require an ASCII-letter-only name with single-space separators.

Virtual-A specific constraints

virtual_a (A card) is issued through the same provider family as V / R / G and follows the provider's own application protocol. The server enforces these rules regardless of what header.require_* says — and the header list already reports the effective values (require_email=true, require_phone=false) for A-card headers:

ConstraintRuleFailure message_key
EmailAlways required — send email (or use_bound_email=true if the account has a bound email)bank_card_email_required
PhoneNot collectedphone_code / phone / use_bound_phone are ignored; the provider protocol has no phone field
Cardholder namefirst_name and last_name are both required and must be non-blank (whitespace-only values are rejected); checked before any funds are frozenvirtual_a_required_fields_missing
Birthday & billing addressDo not send — generated server-side automaticallyvirtual_a_required_fields_missing (only if server-side fill fails)

Everything else (custom first deposit, idempotency, async result via card.opened / card.open_failed, recharge, freeze / unfreeze, close, transactions, sensitive card info) works exactly as for the other virtual card types.

Example Request

json
{
  "header_id": "hdr_5",
  "package_id": "pkg_12",
  "first_name": "John",
  "last_name": "Doe"
}

Required header:

http
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

Response Fields

FieldTypeDescription
card_idstringcard_<id> (used for subsequent queries)
statusint1=pending 2=active 3=failed 4=closing 5=closed 6=frozen
status_descstringEnglish description
created_atstringRFC3339 timestamp

Example Response (Queued)

json
{
  "code": 200,
  "message": "OK",
  "data": {
    "card_id": "card_12345",
    "status": 1,
    "status_desc": "pending",
    "created_at": "2026-04-29T11:00:12Z"
  }
}

status=1 doesn't mean ready

status=1 (pending) only means "queued". Wait for the card.opened webhook, or poll card/info until you see status=2 (active).

Subsequent Flow

diagram

Common Errors

HTTPmessage_keyDescription
400insufficient_balanceInsufficient balance (open fee + initial deposit)
400invalid_first_deposit_amountfirst_deposit_amount is not a plain non-negative integer (decimals / signs / scientific notation / too many digits)
400first_recharge_below_basefirst_deposit_amount is below the config base
400first_recharge_below_minfirst_deposit_amount (or the empty default = base) is below the package's minimum first deposit min_first_deposit
400first_recharge_exceeds_maxExcess above base exceeds max_recharge_amount
400first_recharge_limit_exceededExcess exceeds the account's recharge limit
400first_recharge_asset_mismatchOpen-fee asset ≠ recharge asset; custom excess unsupported for this config
400first_recharge_excess_too_smallAfter fee + integer rounding nothing reaches the card; increase the amount
400kyc_requiredPlease complete identity verification first
400kyc_not_approvedAccount KYC not approved
400bank_card_email_requiredCard type requires email; send email or set use_bound_email=true (only if the account has one bound)
400bank_card_phone_requiredCard type requires phone; send phone_code+phone or set use_bound_phone=true (only if the account has one bound)
400openapi_card_type_not_supportedHeader's card type not in virtual range
400account_manager_bind_requiredAccount-manager-controlled header and your account has no account manager bound — only use header_id values returned by /card_headers/list
400account_manager_unavailableThe account manager bound to your account is no longer active; contact platform support
400account_manager_open_disabledYour account manager has not opened this header / package for your account; use only headers and packages returned by the list endpoints (they already omit closed ones)
400virtual_g_name_invalid(G card) Cardholder name fails ASCII-letters / single-space / ≤40 chars rule
400virtual_g_required_fields_missing(G card) Server could not assemble a complete apply payload (rare; usually a missing config or address-pool issue)
400virtual_a_required_fields_missing(A card) first_name / last_name missing or blank, or the server could not assemble a complete apply payload (header / address-pool issue)
400openapi_invalid_header_idheader_id missing/wrong prefix or not found
400openapi_invalid_package_idpackage_id missing/wrong prefix or not under header
400openapi_idempotency_key_requiredMissing idempotency key
400openapi_idempotency_key_too_longKey over 128 chars
400openapi_idempotency_key_invalid_charsBad characters in key
401openapi_invalid_credentialsAuth failure
409openapi_idempotency_key_conflictSame key, different body
500openapi_apply_card_failedServer error

Notes

  • Open fee + initial deposit are charged / held immediately. With a custom first_deposit_amount, the excess fee is held too (see Custom first deposit). Failed opens auto-refund.
  • Async tasks can take a few minutes. Don't retry apply because no webhook arrived in 30 seconds (retries must use the same Idempotency-Key).
  • A returned card_id permanently identifies the open attempt (even if it ultimately fails).

Released under MIT-equivalent terms.