Z Zise Developers 简体中文

Bind a physical card (three card details; no step-up authentication)

POST /v1/cards/bind scope: cards:write
On behalf of a member · x-on-behalf-of required Requires x-idempotency-key

Assigns a physical card that has reached the user but does not yet belong to anyone to this member. The input consists of three printed card details: card number / expiry date / CVV.

Omitting step-up authentication is deliberate: the CVV is printed only on the back, so these details themselves prove possession. Another verification would merely prevent the card's recipient from binding it. Activation is the truly irreversible step, and additional verification belongs there.

⚠ None of these three details is stored in our database or logs. CVV is compared only in request memory and immediately discarded. We store a keyed HMAC of the card number, not the number itself. Your servers should not retain them either; forwarding them alone already brings you within PCI scope.

⚠ Upstream reassignment is a one-time operation: assigning the card to the wrong person invalidates it with no second chance, and there is no upstream unbind API. A database constraint therefore allows only one reassignment in progress or successful reassignment per card. Do not implement retries as a separate check-then-submit sequence.

⚠ This interface could be used to brute-force card numbers, so it is rate-limited per member to 5 genuine failures in one hour. Network errors and rate-limit rejections themselves do not count. Once reached, the window must expire.

status is always binding: upstream reassignment is asynchronous, and callbacks establish the final state.

Prerequisites

  • The card exists in inventory and has been dispatched (shipped).
  • The member has a physical card application with status shipped or awaiting_bind.
  • The member has passed L1 KYC, which we use to create the cardholder upstream.
  • Fewer than 5 failed attempts in the last hour.
FieldTypeRequiredDescription
x-on-behalf-of string Required The member on whose behalf to call.

Request Body

FieldTypeRequiredDescription
pan string Required Full card number. Spaces and hyphens are removed; length 12~19 digits, depending on the network. Do not require exactly 16 digits.
expiry string Required Expiry date. Accepts MM/YY / MMYY / MM-YY / MMYYYY; unrecognized formats fail validation.
cvv string Required Security code on the back of the card, 3~4 digits. Compared in constant time and immediately discarded.
application_id string Optional The application's ID for this card, with or without the cap_ prefix. If omitted, we find the member's pending-binding application automatically.

Response

201Accepted; upstream reassignment is in progress.
{
  "application_id": "cap_1b7d90c4-5e2a-4f18-83b6-0c7a4d1e9f22",
  "masked_pan": "5240********7890",
  "status": "binding"
}
400product_not_available: the BIN or product does not support binding. invalid_request: body is not valid JSON. idempotency_key_required / idempotency_key_invalid. ⚠ Mismatched card details, invalid card-number format, rate limiting, a card already bound to someone else, no pending-binding application, and KYC not passed currently all return 500 api_error. Internal codes distinguish even an incorrect expiry from an incorrect CVV, but are not registered in the public catalog. Until fixed, the end-user message can only be the generic Incorrect card details. Track the attempt count yourself: 5 consecutive failures trigger a one-hour lock.
409idempotency_key_reused · idempotency_in_progress

Emitted Events

Green = successful terminal state · Red = terminal state requiring action · Purple = intermediate state. Open an event for its payload and signature verification details.

Request
curl -X POST 'https://api.zinfra.vip/v1/cards/bind' \
  -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 '{
    "pan": "5240121234567890",
    "expiry": "08/29",
    "cvv": "123"
  }'
const res = await fetch("https://api.zinfra.vip/v1/cards/bind", {
  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({
    "pan": "5240121234567890",
    "expiry": "08/29",
    "cvv": "123"
  }),
});
// Keep monetary amounts as strings, never numbers.
const data = await res.json();
import requests

res = requests.post(
    "https://api.zinfra.vip/v1/cards/bind",
    headers={
        "x-auth-token": "Bearer $TOKEN",
        "x-on-behalf-of": "$MEMBER_ID",
        "x-idempotency-key": "$IDEMPOTENCY_KEY",
        "content-type": "application/json",
    },
    json={
      "pan": "5240121234567890",
      "expiry": "08/29",
      "cvv": "123"
    },
)
# Use Decimal(str(...)) for amounts, not float.
data = res.json()
req, _ := http.NewRequest("POST", "https://api.zinfra.vip/v1/cards/bind",
    strings.NewReader(`{
  "pan": "5240121234567890",
  "expiry": "08/29",
  "cvv": "123"
}`))
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/bind"))
    .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("""
{
  "pan": "5240121234567890",
  "expiry": "08/29",
  "cvv": "123"
}
"""))
    .build();
// Use String / BigDecimal for amounts, not double.
$ch = curl_init('https://api.zinfra.vip/v1/cards/bind');
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'
{
  "pan": "5240121234567890",
  "expiry": "08/29",
  "cvv": "123"
}
JSON,
]);
$res = curl_exec($ch);
// Use bcmath / strings for amounts, not floatval.
201
{
  "application_id": "cap_1b7d90c4-5e2a-4f18-83b6-0c7a4d1e9f22",
  "masked_pan": "5240********7890",
  "status": "binding"
}