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.

Activating an already active card returns 200 without incrementing failures, as repeated user taps are common.

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"
}
400state_invalid: the card is not awaiting activation, 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. ⚠ Mismatched card details and a card not yet shipped currently return 500 api_error (internal codes exist but are not registered in the public catalog). The former counts toward the 3 failures.
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"
}