Submit a card application (virtual or physical, selected by form_factor)
x-on-behalf-of required
Requires x-idempotency-key
Moves funds · Debits the member's available balance by default; funding_source=merchant debits only the merchant's main prepaid account.
See the response table below for failure handling. Retry timeouts (504) with the same idempotency key — we may already have processed the request; use a new key after a business failure; the same key replays that failure.
Creates the application and debits the issuance fee (plus shipping for physical cards). Returns synchronously without calling the upstream. Actual issuance is asynchronous; webhooks report the final outcome.
If merchant prepayment is insufficient, new applications return service_unavailable without creating a pending-funding application. 201 means accepted: products requiring manual review initially return pending_review; automatic-review products initially return submitted. Query application details using the returned id; only issued means the card has been issued.
⚠ There are two idempotency keys. Do not confuse them: the x-idempotency-key header belongs to the Open API layer (a 24-hour window replaying the original result); the request body's client_key belongs to the card issuance business layer (persisted permanently). If you omit client_key, we generate a random one. Resending the same body after 24 hours can therefore issue a second card. Provide it yourself when you need idempotency.
Prerequisites
- The member's own KYC meets the product requirements, or a valid quick KYC binding is used (also available to L0; quick profiles cannot bypass L2 requirements). When selected explicitly, use the same
quick_kyc_binding_idfor the invoice and application; this does not change the member's own verification level. - The member has not reached the card limit for this issuer/form factor; applications in progress also occupy a slot.
- The member has no outstanding penalty debts.
- Physical cards: a shipping address belonging to the member must exist. Country restrictions check only the KYC used for this application; the shipping country affects shipping fees only.
- Default mode: member available balance ≥ issuance fee + shipping fee + initial top-up.
- Merchant-funded mode: available only to merchant_hosted merchants; issuance fees and initial top-ups are both debited from prepayment.
- Your prepaid balance covers this order's wholesale cost; otherwise returns
service_unavailablewithout creating an order.
Request Headers
| Field | Type | Required | Description |
|---|---|---|---|
x-on-behalf-of |
string | Required | The member on whose behalf to call. |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
quick_kyc_binding_id |
string | Optional | The binding.id of a quick KYC profile already bound to the current merchant and member. L0, L1, and L2 members may explicitly select it to apply for eligible card products using that profile. It does not replace their own identity verification, raise their verification level, or bypass a product's L2 requirement. Use the same ID when requesting the invoice. |
quick_kyc_email |
string | Optional | Required when the member has no genuine L1 verification and a new quick KYC profile must be allocated automatically; it must never have been used for any KYC. Not required when reusing an existing binding. |
form_factor |
string | Optional | physical means a physical card; all other values, including omission, are treated as virtual cards.
There is no spelling validation: Physical silently creates a virtual card.
When product_id is provided, the product's form factor takes precedence; an explicitly conflicting form factor returns product_not_available. |
product_id |
string | Optional | Product identifier from the id of GET /v1/card/products (cpd_…).
Recommended for placing orders: it uniquely determines the BIN and issuing arrangement,
including the form factor.
An unrecognized product, or one not available to you, always returns product_not_available. |
bin |
string | Optional | Card BIN from bin in GET /v1/card/products.
Used only when product_id is omitted; the BIN must support the requested form factor and funding mode. |
source_asset |
string | Required | Debit asset code, such as USDT. Must be in the product's allowed list; otherwise returns product_not_available. |
first_topup |
string | Optional | Initial top-up amount, a decimal string in the source asset. Omit to skip the initial top-up.
Amounts below a configured initial top-up minimum are rejected. The issuance fee and initial top-up are separate amounts:
if issuance fails, the issuance fee is refunded; the initial top-up is actually credited to the card only after issuance succeeds.
Credit is reported separately by card.topup.credited, the same event used for subsequent top-ups.USDT allows up to 6 decimal places: "50.00". |
funding_source |
"member" | "merchant" | Optional | Who pays the card issuance costs. member preserves the existing member-funded behavior; merchant
debits wholesale costs, including the issuance fee and initial top-up, from the merchant's main prepaid account without debiting the member wallet.
Merchant funding is available only to merchant_hosted merchants. |
address_id |
string | Optional | Shipping address ID for a physical card. If omitted, uses the member's default address. The address is snapshotted into the order; later address changes do not affect existing orders. |
client_key |
string | Optional | Business-level idempotency key; see the distinction between the two keys above. Strongly recommended. |
Response
{
"id": "cap_1b7d90c4-5e2a-4f18-83b6-0c7a4d1e9f22",
"status": "submitted",
"form_factor": "virtual"
}product_not_available: issuer/product/BIN unavailable, or source_asset not in the allowed list.
service_unavailable: card issuing is not enabled for you, or restrictions have been applied to your funding account.
member_context_required: missing x-on-behalf-of.
idempotency_key_required / idempotency_key_invalid: the idempotency key is missing or is not a UUID v4.
⚠ Some business rejections currently return 500 api_error: insufficient member balance,
KYC not passed, card limit reached, initial top-up below the minimum, outstanding penalties, or unsupported shipping country.
They have specific internal codes that are not yet registered in the public code catalog. Do not retry 500 indefinitely:
retrying with the same idempotency key only returns the same 500. This is our defect and is being fixed;
once fixed, these cases will return 400 with their respective code.idempotency_key_reused: the same key was used with a different body or endpoint. idempotency_in_progress: the original request is still being processed.Emitted Events
Green = successful terminal state · Red = terminal state requiring action · Purple = intermediate state. Open an event for its payload and signature verification details.
curl -X POST 'https://api.zinfra.vip/v1/cards/applications' \
-H 'x-auth-token: Bearer $TOKEN' \
-H 'x-on-behalf-of: $MEMBER_ID' \
-H 'x-idempotency-key: $IDEMPOTENCY_KEY' \
-H 'content-type: application/json' \
-d '{
"form_factor": "virtual",
"product_id": "cpd_1042",
"source_asset": "USDT",
"first_topup": "50.00",
"client_key": "6d0f3a2e-8b41-4c77-9a10-2f5c7e91b3d4"
}'const res = await fetch("https://api.zinfra.vip/v1/cards/applications", {
method: "POST",
headers: {
"x-auth-token": "Bearer $TOKEN",
"x-on-behalf-of": "$MEMBER_ID",
"x-idempotency-key": "$IDEMPOTENCY_KEY",
"content-type": "application/json",
},
body: JSON.stringify({
"form_factor": "virtual",
"product_id": "cpd_1042",
"source_asset": "USDT",
"first_topup": "50.00",
"client_key": "6d0f3a2e-8b41-4c77-9a10-2f5c7e91b3d4"
}),
});
// Keep monetary amounts as strings, never numbers.
const data = await res.json();import requests
res = requests.post(
"https://api.zinfra.vip/v1/cards/applications",
headers={
"x-auth-token": "Bearer $TOKEN",
"x-on-behalf-of": "$MEMBER_ID",
"x-idempotency-key": "$IDEMPOTENCY_KEY",
"content-type": "application/json",
},
json={
"form_factor": "virtual",
"product_id": "cpd_1042",
"source_asset": "USDT",
"first_topup": "50.00",
"client_key": "6d0f3a2e-8b41-4c77-9a10-2f5c7e91b3d4"
},
)
# Use Decimal(str(...)) for amounts, not float.
data = res.json()req, _ := http.NewRequest("POST", "https://api.zinfra.vip/v1/cards/applications",
strings.NewReader(`{
"form_factor": "virtual",
"product_id": "cpd_1042",
"source_asset": "USDT",
"first_topup": "50.00",
"client_key": "6d0f3a2e-8b41-4c77-9a10-2f5c7e91b3d4"
}`))
req.Header.Set("x-auth-token", "Bearer $TOKEN")
req.Header.Set("x-on-behalf-of", "$MEMBER_ID")
req.Header.Set("x-idempotency-key", "$IDEMPOTENCY_KEY")
req.Header.Set("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
// Decode amount fields as string, not float64.HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.zinfra.vip/v1/cards/applications"))
.header("x-auth-token", "Bearer $TOKEN")
.header("x-on-behalf-of", "$MEMBER_ID")
.header("x-idempotency-key", "$IDEMPOTENCY_KEY")
.header("content-type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{
"form_factor": "virtual",
"product_id": "cpd_1042",
"source_asset": "USDT",
"first_topup": "50.00",
"client_key": "6d0f3a2e-8b41-4c77-9a10-2f5c7e91b3d4"
}
"""))
.build();
// Use String / BigDecimal for amounts, not double.$ch = curl_init('https://api.zinfra.vip/v1/cards/applications');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-auth-token: Bearer $TOKEN',
'x-on-behalf-of: $MEMBER_ID',
'x-idempotency-key: $IDEMPOTENCY_KEY',
'content-type: application/json',
],
CURLOPT_POSTFIELDS => <<<'JSON'
{
"form_factor": "virtual",
"product_id": "cpd_1042",
"source_asset": "USDT",
"first_topup": "50.00",
"client_key": "6d0f3a2e-8b41-4c77-9a10-2f5c7e91b3d4"
}
JSON,
]);
$res = curl_exec($ch);
// Use bcmath / strings for amounts, not floatval.
{
"id": "cap_1b7d90c4-5e2a-4f18-83b6-0c7a4d1e9f22",
"status": "submitted",
"form_factor": "virtual"
}