Z Zise Developers 简体中文

Create an order: payment retries reuse the same idempotency key

POST /v1/remittances scope: remittances:write
On behalf of a member · x-on-behalf-of required Requires x-idempotency-key Moves funds · Hold the member’s available asset balance by locked_amount, including slippage reserve, and simultaneously hold your prepaid funds
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.

This is where money starts moving. A successful response means the member’s available balance has been moved into locked by locked_amount, including slippage reserve; your prepaid funds have been held simultaneously at the wholesale price; and limits have been reserved.

Prerequisites

  • Payee belongs to this member and is available
  • Member’s available asset balance ≥ locked_amount (= customer total × (1 + slippage))
  • Member satisfies the line’s KYC requirement and is neither banned nor blocklisted
  • Amount meets the minimum, per-transaction maximum, and daily limit
  • purpose_code belongs to this line’s permitted purpose-code list
  • The personal line, pobo, additionally requires an active upstream subaccount for the member
FieldTypeRequiredDescription
x-on-behalf-of string Required Member on whose behalf the call is made. funding_source=member debits their balance; merchant uses your prepaid funds.
x-idempotency-key string Required UUID v4. This key identifies one payment: reuse it for retries, generate another for a new payment. After 24 hours, the same key is treated as a new request.

Request Body

FieldTypeRequiredDescription
payee_id string Required Payee ID. Optional pye_ prefix; the remaining value must contain digits only.
payout_amount string Required Amount the recipient receives in the destination currency. Precision must match the currency exactly; an extra decimal place is rejected at ordering. This validation deliberately occurs before funds are held: later rejection would leave a dispatching order that cannot unlock funds or be cancelled. Maximum 32 characters.GBP uses 2 decimals: "380.00"; JPY uses 0: "50000"
slippage_bps integer Required Slippage ceiling in basis points. Required integer, 50 ~ 1000 (0.5% ~ 10%). If execution cost rises beyond this relative to the order snapshot, the order enters needs_reconfirm for the user to decide instead of silently charging more. It also determines the hold: locked_amount = customer_total × (1 + slippage_bps/10000).
funding_source "member" | "merchant" Optional Funding source. Defaults to member; merchant means your prepaid funds cover the order without locking member balance.
purpose_code string Required Remittance purpose. Required, selected from the purpose codes we provide. The upstream has 23 fixed enums, such as FAMILY_SUPPORT / GOODS_PURCHASED / EDUCATION_TRAINING. Missing or invalid values are rejected at ordering, because the upstream field is required and deferring validation until dispatch would reject after funds were held.
reference string Optional Remittance message forwarded to the recipient. Truncated beyond 200 characters.
line "express" | "pobo" Optional Defaults to express; unrecognized values silently fall back to express.
asset string Optional Asset to debit. Defaults to USDT.

Response

201Accepted. Idempotent hits also return 201, with duplicated: true and quote: null. This endpoint does not distinguish initial execution and replay using 200; use duplicated and the X-Idempotent-Replay header.
{
  "id": "rmt_9c1f0a7e-3b2d-4f81-9a55-1d2e3f4a5b6c",
  "status": "reviewing",
  "quote": {
    "source_asset": "USDT",
    "customer_total": "499.980000",
    "fee": "2.480000",
    "locked_amount": "504.979800",
    "indicative_rate": "1.2899",
    "applied_rate": "1.2743"
  }
}
400invalid_request: invalid JSON body or nonnumeric payee_id. resource_not_found: payee not associated with this member. corridor_not_supported: payee corridor no longer available, such as withdrawal or jurisdiction restrictions. product_not_available: line disabled or source asset unavailable. invalid_fields: payout_amount precision mismatches the currency, is too long, or cannot be parsed. request_rejected: risk-control rejection, without reason, rule name, or score; criteria are never exposed. Do not retry; contact us through the merchant console. service_unavailable: insufficient prepaid funds when the line does not queue. Remittance normally queues instead, returning 201 + status: pending.
409idempotency_key_reused: the body changed for the same key. We reject this because it gives two distinct payments the same identity. idempotency_in_progress: the first request is still running; retry later with the same key.
422Business rejections now have explicit code values, no longer 500, fixed on 2026-08-12: insufficient_balance: insufficient member available balance. · amount_out_of_range: below the minimum remittance amount; change the amount to proceed. · limit_exceeded: per-transaction or daily cumulative limit exceeded; do not retry that day. · kyc_required: insufficient KYC level. · invalid_fields: invalid purpose code, missing/out-of-range slippage_bps, or corridor/payee mismatch. · request_rejected: account banned or blocklisted. · product_not_available: personal line lacks an active subaccount, or asset disabled. · corridor_not_supported: unavailable corridor. HTTP status follows the public code’s category, usually 400. See the error code catalog for each code.

Additional Details

The corridor is not read from the request

The payee’s own corridor is authoritative. This removes a manipulable input; any corridor supplied by you is ignored.

Only the recipient-amount direction is supported

Orders do not accept source_amount. Upstream payout_amount is the fixed side and must match the quote’s buy amount exactly. To order by a sender budget, first reverse-calculate payout_amount with POST /v1/remit/quotes, then order with that value.

One idempotency key per confirmation

Payment retries reuse the same key, retrieving the first transaction with duplicated: true. A new key == a second real payment. After 504 or network errors, you must retry with the same key, not generate a new one: we may already have created the order and held funds.

slippage_bps is mandatory

Read the line’s permitted range through GET /v1/remit/products first, then send the exact value confirmed by the end user. Missing, string-valued, or out-of-range input returns invalid_fields with fields[0].key = "slippage_bps".

funding_source=merchant: merchant-funded payment

The default, member, debits the member balance. merchant makes your prepaid funds pay for the order without holding the member’s balance. x-on-behalf-of remains mandatory because order ownership, payee ownership, KYC, and notifications still refer to that member.

Three successful status categories require different handling

  • dispatching: released to the dispatch executor, which is running.
  • reviewing / platform_reviewing: awaiting our manual review. Merchant orders always pass platform-admin review; this is a procedural gate, not a problem.
  • pending, internally pending_merchant_funds: your prepaid balance is insufficient. No member funds have moved, no limits are reserved, and no upstream order exists; only a queued order row exists. Fund your prepaid account and we advance it automatically. Inspect queue depth through GET /v1/merchant/pending.

⚠ The member never sees the reason for pending. End-user interfaces may say only “processing”, not “merchant balance insufficient”.

⚠ With duplicated: true, there is no quote, since no new quote was obtained. status is the existing order’s current status and may already be completed. Do not treat the response as a newly placed order.

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/remittances' \
  -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 '{
    "payee_id": "pye_1042",
    "payout_amount": "380.00",
    "slippage_bps": 100,
    "funding_source": "member",
    "purpose_code": "FAMILY_SUPPORT",
    "reference": "Rent Aug",
    "line": "express",
    "asset": "USDT"
  }'
const res = await fetch("https://api.zinfra.vip/v1/remittances", {
  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({
    "payee_id": "pye_1042",
    "payout_amount": "380.00",
    "slippage_bps": 100,
    "funding_source": "member",
    "purpose_code": "FAMILY_SUPPORT",
    "reference": "Rent Aug",
    "line": "express",
    "asset": "USDT"
  }),
});
// Keep monetary amounts as strings, never numbers.
const data = await res.json();
import requests

res = requests.post(
    "https://api.zinfra.vip/v1/remittances",
    headers={
        "x-auth-token": "Bearer $TOKEN",
        "x-on-behalf-of": "$MEMBER_ID",
        "x-idempotency-key": "$IDEMPOTENCY_KEY",
        "content-type": "application/json",
    },
    json={
      "payee_id": "pye_1042",
      "payout_amount": "380.00",
      "slippage_bps": 100,
      "funding_source": "member",
      "purpose_code": "FAMILY_SUPPORT",
      "reference": "Rent Aug",
      "line": "express",
      "asset": "USDT"
    },
)
# Use Decimal(str(...)) for amounts, not float.
data = res.json()
req, _ := http.NewRequest("POST", "https://api.zinfra.vip/v1/remittances",
    strings.NewReader(`{
  "payee_id": "pye_1042",
  "payout_amount": "380.00",
  "slippage_bps": 100,
  "funding_source": "member",
  "purpose_code": "FAMILY_SUPPORT",
  "reference": "Rent Aug",
  "line": "express",
  "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/remittances"))
    .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("""
{
  "payee_id": "pye_1042",
  "payout_amount": "380.00",
  "slippage_bps": 100,
  "funding_source": "member",
  "purpose_code": "FAMILY_SUPPORT",
  "reference": "Rent Aug",
  "line": "express",
  "asset": "USDT"
}
"""))
    .build();
// Use String / BigDecimal for amounts, not double.
$ch = curl_init('https://api.zinfra.vip/v1/remittances');
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'
{
  "payee_id": "pye_1042",
  "payout_amount": "380.00",
  "slippage_bps": 100,
  "funding_source": "member",
  "purpose_code": "FAMILY_SUPPORT",
  "reference": "Rent Aug",
  "line": "express",
  "asset": "USDT"
}
JSON,
]);
$res = curl_exec($ch);
// Use bcmath / strings for amounts, not floatval.
201
{
  "id": "rmt_9c1f0a7e-3b2d-4f81-9a55-1d2e3f4a5b6c",
  "status": "reviewing",
  "quote": {
    "source_asset": "USDT",
    "customer_total": "499.980000",
    "fee": "2.480000",
    "locked_amount": "504.979800",
    "indicative_rate": "1.2899",
    "applied_rate": "1.2743"
  }
}