Submit the payee for upstream onboarding early: moves no funds
x-on-behalf-of required
What this solves
Our payees are provider-independent entities: local creation invokes no upstream endpoint. Choosing a provider early would pin the entity to whichever is enabled today. Upstream onboarding normally occurs at order dispatch.
The cost is that failure occurs after funds are held. Upstream payee rejection is terminal, while the order has already moved member funds from available balance into the locked bucket, requiring manual intervention. Rejection reasons include duplicates, sanctions matches, and banks missing from the upstream directory, not only format errors. We can catch formatting locally; other failures otherwise require a fund hold before their first discovery.
This endpoint moves that check earlier, before money is involved. Call it immediately after successful POST /v1/remit/payees and do not allow ordering until upstream_status becomes active.
Path Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Required | Payee ID. Optional pye_ prefix; the remaining value must contain digits only. |
Query Parameters
| Field | Type | Required | Description |
|---|---|---|---|
line |
"express" | "pobo" | Optional | Line for onboarding. The same payee has independent upstream records for the two lines, with the personal line using the member’s own subaccount. If both lines are needed, call once for each. |
Request Headers
| Field | Type | Required | Description |
|---|---|---|---|
x-on-behalf-of |
string | Required | Member on whose behalf the call is made; must be the member associated with this payee. |
Response
{
"id": "pye_1042",
"upstream_status": "pending",
"reason": ""
}product_not_available: no usable provider for this line, or no usable upstream subaccount for this member on the personal line. This is not a payee issue. · service_unavailable: upstream configuration is missing.not_found: payee not associated with this member, or id is nonnumeric.rate_limited: a separate limit of 30 requests/minute per merchant. Each cache miss makes a real upstream write that creates an irreversible upstream entity.upstream_error: the upstream explicitly rejected this call, for example credentials, parameters, or rate limiting.upstream_timeout: outcome unknown.
⚠ Retrying is safe here: we generate and persist the upstream idempotency key. Reusing it retrieves the original result rather than creating a second upstream payee.Additional Details
What it does and does not do
- Moves no funds: no order, hold, or debit to your prepaid balance.
- Does not accept a provider or upstream account selected by you. We derive both from
line. A mistyped value could create a second upstream record for the same bank account that no subsequent business path ever uses; the upstream has no delete endpoint. - Does not retry
failed. It is terminal; unchanged resubmission will still be rejected.
Idempotency
No x-idempotency-key is required; polling is safe. The idempotency layer would replay the first response for 24 hours, making same-key polling repeatedly return an old pending even after the payee becomes active. The actual, stronger idempotency guarantee is on our side: the upstream key is stored in our database and reused for retries. However often you call, a payee produces only one upstream record.
Response
upstream_status uses the same values as GET /v1/remit/payees/{id}; only pending / active / failed occur here. For failed, reason comes from our vocabulary:
upstream_rejected: the upstream rejected the data. A person must inspect the detailed reason; the upstream has no error-code catalog, only free-form English. Forwarding it verbatim would encourage branching on mutable prose. Ask the user to correct and recreate the payee, or contact us.upstream_gone: a previously created upstream record has disappeared or been deleted.payee_not_found/no_beneficiary_id: an internal issue on our side; open a ticket.
⚠ Sandbox also calls the real upstream, because this line has no upstream sandbox. A beneficiary verified from sandbox is real, and there is no upstream delete endpoint. Use real, usable recipient accounts for integration testing rather than filling the system with dummy data.
curl -X POST 'https://api.zinfra.vip/v1/remit/payees/{id}/verify' \
-H 'x-auth-token: Bearer $TOKEN' \
-H 'x-on-behalf-of: $MEMBER_ID'const res = await fetch("https://api.zinfra.vip/v1/remit/payees/{id}/verify", {
method: "POST",
headers: {
"x-auth-token": "Bearer $TOKEN",
"x-on-behalf-of": "$MEMBER_ID",
},
});
// Keep monetary amounts as strings, never numbers.
const data = await res.json();import requests
res = requests.post(
"https://api.zinfra.vip/v1/remit/payees/{id}/verify",
headers={
"x-auth-token": "Bearer $TOKEN",
"x-on-behalf-of": "$MEMBER_ID",
},
)
# Use Decimal(str(...)) for amounts, not float.
data = res.json()req, _ := http.NewRequest("POST", "https://api.zinfra.vip/v1/remit/payees/{id}/verify",
nil)
req.Header.Set("x-auth-token", "Bearer $TOKEN")
req.Header.Set("x-on-behalf-of", "$MEMBER_ID")
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/remit/payees/{id}/verify"))
.header("x-auth-token", "Bearer $TOKEN")
.header("x-on-behalf-of", "$MEMBER_ID")
.method("POST", HttpRequest.BodyPublishers.noBody())
.build();
// Use String / BigDecimal for amounts, not double.$ch = curl_init('https://api.zinfra.vip/v1/remit/payees/{id}/verify');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-auth-token: Bearer $TOKEN',
'x-on-behalf-of: $MEMBER_ID',
],
]);
$res = curl_exec($ch);
// Use bcmath / strings for amounts, not floatval.
{
"id": "pye_1042",
"upstream_status": "pending",
"reason": ""
}