Freeze a card (transitional state: freezing)
x-on-behalf-of required
Requires x-idempotency-key
Freezing and unfreezing have separate endpoints and separate transitional states, not two values of one boolean. Merging them caused the production incident where unfreezing was followed by invalid card status and top-up failures: freezing alone cannot indicate whether the target is frozen or active.
We perform two actions: send a freeze instruction to the upstream asynchronously, and reduce its available spending limit to 0 synchronously. With only the former, the card could continue spending until the instruction took effect.
The returned status is always freezing, because we have persisted the local transitional state. A webhook reports the terminal state, or an immediate upstream readback may establish it within seconds. Do not interpret 200 as frozen.
An empty request object ({}) is sufficient.
Prerequisites
- The card status is one of
active/pending/unactivated; all others are rejected.
Path Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Required | Card ID |
Request Headers
| Field | Type | Required | Description |
|---|---|---|---|
x-on-behalf-of |
string | Required | The member on whose behalf to call. |
Response
{
"id": "crd_9f2c1b7a-3d51-4a2e-9c08-6b1f0d4e77aa",
"status": "freezing"
}state_invalid: the current status cannot be frozen. product_not_available: issuer unavailable.
idempotency_key_required / idempotency_key_invalid.
⚠ Upstream rejection caused by differing local and upstream state currently returns 500 api_error.
This requires entirely different handling from state_invalid: refresh and retry the latter;
refreshing cannot resolve the former, which requires manual intervention. Do not retry them as if they were equivalent.not_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.
curl -X POST 'https://api.zinfra.vip/v1/cards/{id}/freeze' \
-H 'x-auth-token: Bearer $TOKEN' \
-H 'x-on-behalf-of: $MEMBER_ID' \
-H 'x-idempotency-key: $IDEMPOTENCY_KEY'const res = await fetch("https://api.zinfra.vip/v1/cards/{id}/freeze", {
method: "POST",
headers: {
"x-auth-token": "Bearer $TOKEN",
"x-on-behalf-of": "$MEMBER_ID",
"x-idempotency-key": "$IDEMPOTENCY_KEY",
},
});
// Keep monetary amounts as strings, never numbers.
const data = await res.json();import requests
res = requests.post(
"https://api.zinfra.vip/v1/cards/{id}/freeze",
headers={
"x-auth-token": "Bearer $TOKEN",
"x-on-behalf-of": "$MEMBER_ID",
"x-idempotency-key": "$IDEMPOTENCY_KEY",
},
)
# Use Decimal(str(...)) for amounts, not float.
data = res.json()req, _ := http.NewRequest("POST", "https://api.zinfra.vip/v1/cards/{id}/freeze",
nil)
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")
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}/freeze"))
.header("x-auth-token", "Bearer $TOKEN")
.header("x-on-behalf-of", "$MEMBER_ID")
.header("x-idempotency-key", "$IDEMPOTENCY_KEY")
.method("POST", HttpRequest.BodyPublishers.noBody())
.build();
// Use String / BigDecimal for amounts, not double.$ch = curl_init('https://api.zinfra.vip/v1/cards/{id}/freeze');
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',
],
]);
$res = curl_exec($ch);
// Use bcmath / strings for amounts, not floatval.
{
"id": "crd_9f2c1b7a-3d51-4a2e-9c08-6b1f0d4e77aa",
"status": "freezing"
}