Z Zise Developers 简体中文

Activate a physical card (irreversible; call directly after your own user verification)

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

The second step after binding. It remains irreversible, but the Open API no longer sends end users to our hosted screen for email-code verification.

This endpoint serves merchant_hosted merchants whose end users are already in your App / H5. Inserting hosted_url would split the flow into you collecting the PIN → our page collecting a verification code → you resending the request. External-link opening and email delivery would reduce completion rates and noticeably worsen the experience.

The responsibility boundary is therefore:

  • Verify the end user yourself, using a logged-in session, additional confirmation, SMS / biometrics, etc.
  • Then call this endpoint directly.
  • We handle card ownership and status checks, failed-attempt locking, audit records, and actual upstream activation.

⚠ 3 failures lock the card for 24 hours, counted per card. Network errors and upstream 5xx do not count: counting transient network errors as incorrect credentials would lock users out for a full day after three glitches. Once locked, there is no self-service unlock, which would give attackers a way to resume their attempts. Manual unlocking through our administration system is required. A locked response includes locked_until.

For cards whose PIN is submitted in the same upstream activation request, calling again after the card is already active still succeeds and does not increment the failure count. That is usually a double tap. For cards whose PIN is a separate request, an already active card still sets the PIN from this call. If the PIN rules reject it, the response is card_pin_rejected and the card stays active.

Branch on code. Do not treat these results as a generic field-validation failure:

  • card_pin_invalid: the length is not valid for this card. Use the required length and resend with a new idempotency key. This attempt does not count.
  • card_pin_rejected: the length is already valid, but the PIN does not meet this card's rules. Choose another PIN and resend with a new idempotency key. This attempt counts.
  • card_activate_failed: the expiry or PIN was not accepted. Check the card and resend with a new idempotency key. This attempt counts.

After 3 failures the card locks. The response becomes state_invalid and includes locked_until.

Prerequisites

  • The card is physical and its status is unactivated.
  • The card's inventory row has left inventory (shipped / binding / bound).
  • Not currently within a 24-hour lock window.

Path Parameters

FieldTypeRequiredDescription
id string Required Card ID
FieldTypeRequiredDescription
x-on-behalf-of string Required The member on whose behalf to call.

Request Body

FieldTypeRequiredDescription
expiry string Required Printed expiry date, MM/YY.
pin string Required The PIN chosen by the user or supplied in the envelope. Never stored in our database or logs; do not retain it on your side either.

Response

200Activated (irreversible)
{
  "id": "crd_9f2c1b7a-3d51-4a2e-9c08-6b1f0d4e77aa",
  "status": "active"
}
400card_pin_invalid: the PIN length is not valid for this card. Use the required length and resend with a new idempotency key. This attempt does not count. card_pin_rejected: this PIN does not meet the card's rules. Choose another PIN and resend with a new idempotency key. This attempt counts. card_activate_failed: the expiry or PIN was not accepted. Check the card and resend with a new idempotency key. This attempt counts. state_invalid: the card is not awaiting activation, has not shipped, or it is locked. A locked response also includes locked_until, an ISO timestamp. Use it to show when to retry rather than letting users keep tapping. product_not_available: issuer unavailable. idempotency_key_required / idempotency_key_invalid.
404not_found: the card does not exist or does not belong to this member.

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/{id}/activate' \
  -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 '{
    "expiry": "08/29",
    "pin": "1234"
  }'
const res = await fetch("https://api.zinfra.vip/v1/cards/{id}/activate", {
  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({
    "expiry": "08/29",
    "pin": "1234"
  }),
});
// Keep monetary amounts as strings, never numbers.
const data = await res.json();
import requests

res = requests.post(
    "https://api.zinfra.vip/v1/cards/{id}/activate",
    headers={
        "x-auth-token": "Bearer $TOKEN",
        "x-on-behalf-of": "$MEMBER_ID",
        "x-idempotency-key": "$IDEMPOTENCY_KEY",
        "content-type": "application/json",
    },
    json={
      "expiry": "08/29",
      "pin": "1234"
    },
)
# Use Decimal(str(...)) for amounts, not float.
data = res.json()
req, _ := http.NewRequest("POST", "https://api.zinfra.vip/v1/cards/{id}/activate",
    strings.NewReader(`{
  "expiry": "08/29",
  "pin": "1234"
}`))
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/{id}/activate"))
    .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("""
{
  "expiry": "08/29",
  "pin": "1234"
}
"""))
    .build();
// Use String / BigDecimal for amounts, not double.
$ch = curl_init('https://api.zinfra.vip/v1/cards/{id}/activate');
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'
{
  "expiry": "08/29",
  "pin": "1234"
}
JSON,
]);
$res = curl_exec($ch);
// Use bcmath / strings for amounts, not floatval.
200
{
  "id": "crd_9f2c1b7a-3d51-4a2e-9c08-6b1f0d4e77aa",
  "status": "active"
}