Z Zise Developers 简体中文

Top up a card (two asynchronous stages; the webhook confirms credit)

POST /v1/cards/{id}/topups scope: cards:write
On behalf of a member · x-on-behalf-of required Requires x-idempotency-key Moves funds · Debits the member's available source-asset balance and freezes merchant prepayment; card credit is confirmed by webhook.
This endpoint moves funds

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.

── Success from this endpoint does not mean the card has been credited ── An upstream recharge result of succeed means only that the order was received. Actual card credit is confirmed by the card.topup.credited webhook. The status returned here is therefore usually processing; do not use it to authorize any downstream action for the user. Card limits must likewise be applied only after credit is confirmed; applying them earlier opens spending capacity before funds arrive.

── Failure and an unknown outcome are different ── An explicit upstream rejection causes immediate release of the hold. You receive a business error and no funds are lost. A network failure or timeout leaves the order in processing for reconciliation; we never release the hold in this case. Do not retry as a second payment. Resend with the same idempotency key to retrieve the same order.

── Idempotency replays the original outcome; it does not guarantee success ── With the same x-idempotency-key, an order that failed initially still returns failure on the second request. Use a new key to initiate a new attempt. Previously this endpoint always replayed success, causing the second click to show success even though the card was not credited and the transaction record showed failure.

⚠ Debits the member's available balance and also freezes your prepaid reserve funds. If your prepayment is insufficient, the member sees only Temporarily unavailable. Do not disclose the underlying reason to the end user.

Prerequisites

  • The card is in a top-up-eligible status (active / frozen / pending; all transitional states are excluded).
  • The member is not in funds-protection mode; we block this path first when card limits and upstream state diverge.
  • The member's available balance in this asset ≥ customer_total.
  • Your prepaid balance covers this order's wholesale cost.

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
amount string Required Base amount, a decimal string in the source asset, using the same convention as the estimate. The actual debit is the estimate's customer_total.USDT allows up to 6 decimal places: "100.00".
source_asset string Optional Debit asset. If omitted or unavailable, selected automatically by payment priority; as with the estimate, this is a preference rather than an instruction.

Response

201Accepted. status: processing (order accepted, outcome awaiting reconciliation, or queued) · completed (only when a shared-limit card settles immediately). 201 does not mean the card has been credited.
{
  "id": "ctp_4a8e5d21-90bc-4f37-b1e2-8d0c6a3f5719",
  "status": "processing"
}
400state_invalid: the card status does not allow top-ups. product_not_available: product or issuer unavailable. service_unavailable: card issuing is not enabled or restrictions have been applied to your funding account. idempotency_key_required / idempotency_key_invalid. ⚠ Insufficient balance, below-minimum amounts, amounts exceeding the per-transaction limit, upstream rejection, and unavailable exchange rates currently return 500 api_error; their internal codes are not registered in the public catalog. For 500, retry once with the same key to confirm the outcome. If it is still 500, report it as a business failure rather than retrying in a loop.
409idempotency_key_reused · idempotency_in_progress
504upstream_timeout: outcome unknown. Retry with the same idempotency key; we may already have processed it.

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}/topups' \
  -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 '{
    "amount": "100.00",
    "source_asset": "USDT"
  }'
const res = await fetch("https://api.zinfra.vip/v1/cards/{id}/topups", {
  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({
    "amount": "100.00",
    "source_asset": "USDT"
  }),
});
// Keep monetary amounts as strings, never numbers.
const data = await res.json();
import requests

res = requests.post(
    "https://api.zinfra.vip/v1/cards/{id}/topups",
    headers={
        "x-auth-token": "Bearer $TOKEN",
        "x-on-behalf-of": "$MEMBER_ID",
        "x-idempotency-key": "$IDEMPOTENCY_KEY",
        "content-type": "application/json",
    },
    json={
      "amount": "100.00",
      "source_asset": "USDT"
    },
)
# Use Decimal(str(...)) for amounts, not float.
data = res.json()
req, _ := http.NewRequest("POST", "https://api.zinfra.vip/v1/cards/{id}/topups",
    strings.NewReader(`{
  "amount": "100.00",
  "source_asset": "USDT"
}`))
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}/topups"))
    .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("""
{
  "amount": "100.00",
  "source_asset": "USDT"
}
"""))
    .build();
// Use String / BigDecimal for amounts, not double.
$ch = curl_init('https://api.zinfra.vip/v1/cards/{id}/topups');
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'
{
  "amount": "100.00",
  "source_asset": "USDT"
}
JSON,
]);
$res = curl_exec($ch);
// Use bcmath / strings for amounts, not floatval.
201
{
  "id": "ctp_4a8e5d21-90bc-4f37-b1e2-8d0c6a3f5719",
  "status": "processing"
}