Create or link a payee: local creation does not call the upstream
x-on-behalf-of required
Requires x-idempotency-key
A payee is a provider-independent entity. This call only writes our database and invokes no upstream endpoint. Choosing a provider at creation would pin a record that should be reusable across providers to whichever is enabled today. Upstream onboarding occurs at order dispatch.
Two consequences affect your product design: ① Successful local creation does not guarantee the payee can receive a remittance. We validate formats as far as possible, using pattern from GET /v1/remit/payee-form-schema, but the upstream has additional criteria. ② Local creation is fast, inexpensive, and can be offered whenever users need it.
⚠ The cost of ① reaches your user late. Upstream rejection can concern duplicates, sanctions matches, or a bank absent from its directory, not just formatting. It is discovered at order dispatch, after funds have moved from the member’s available balance into the locked bucket. The order remains processing pending manual intervention. Call POST /v1/remit/payees/{id}/verify after creating the payee to discover that failure before any money is involved. Verification moves no funds.
Prerequisites
- Corridor available, revalidated using the same criteria as
GET /v1/remit/payee-form-schema - At least one identifier—account number / IBAN / proxy—must be nonempty
Request Headers
| Field | Type | Required | Description |
|---|---|---|---|
x-on-behalf-of |
string | Required | Member on whose behalf the call is made. The payee association belongs to this member. |
x-idempotency-key |
string | Required | UUID v4. The same key and body replay the initial result, including errors, within 24 hours. |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
country_code |
string | Required | Country of the receiving bank, two uppercase letters. Alias: country. |
currency |
string | Required | Payout currency, three uppercase letters. |
payment_method |
string | Required | LOCAL or SWIFT. Alias: method. |
clearing_system |
string | Required | Clearing network, preserving case exactly. Alias: clearing. |
entity_type |
"INDIVIDUAL" | "COMPANY" | Required | Alias: entity. |
nickname |
string | Optional | The member’s nickname for this payee. Private to this association and invisible to other members. Truncated beyond 64 characters. |
fields |
object | Required | Values for the field contract’s key entries, in one flat level: dotted keys, not nested objects. Keys outside the contract are discarded.{"bank_details.iban": "GB33BUKB20201555555555", …} |
Response
outcome values return 201. linked / review are not errors; they mean we recognized an existing account.{
"id": "pye_1042",
"outcome": "created",
"status": "active"
}invalid_fields: per-field validation failed, with a fields array.
Each entry is {key, reason}, where reason is required / too_long /
pattern / not_in_enum / iban_length / iban_checksum /
account_identifier_required / unknown_clearing_system.
A generic “incorrect information” message is unhelpful on a form with many fields.
corridor_not_supported: incomplete four-part corridor, disabled corridor, restricted jurisdiction,
or no determinable routing-code type.idempotency_key_reused: the same key was reused with another request body or at another endpoint.Additional Details
Three outcomes: inspect outcome
created: a new payee.linked: the account already exists with the same holder name; linked rather than duplicated.review: the account already exists, but the holder name differs. Linking still succeeds with201, andname_mismatchis stored. Ask the user to review the holder name in your UI.
The criterion is the payee fingerprint, derived from the four corridor components plus account identifiers: account number / IBAN / routing code / proxy. The holder name uses a secondary fingerprint. Entering the same account twice does not create two entities; changing the name for that account is detected.
Three rules for fields
① Keys outside the contract are discarded without error and are not stored. Do not place your business fields here. ② Values are limited to 256 characters; longer values are truncated, not rejected. ③ We normalize before validating. Fields marked alnum_upper lose hyphens and spaces and become uppercase; accounts copied from bank Apps often include separators that upstream patterns disallow. We separately retain the original user input for dispute evidence.
⚠ Beyond pattern, IBAN validation includes country-specific length and mod-97 check digits. The error reason distinguishes iban_length / iban_checksum, so users know whether to count digits or check the original document. This is necessary: an IBAN one character short that passes locally may only be rejected at dispatch, when the upstream payee has already entered a terminal state.
curl -X POST 'https://api.zinfra.vip/v1/remit/payees' \
-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 '{
"country_code": "GB",
"currency": "GBP",
"payment_method": "LOCAL",
"clearing_system": "FASTER PAYMENTS",
"entity_type": "INDIVIDUAL",
"nickname": "房东",
"fields": {
"first_name": "MARY",
"last_name": "SMITH",
"bank_details.account_holder": "MARY SMITH",
"bank_details.bank_name": "Barclays",
"bank_details.bank_address": "1 Churchill Place, London",
"bank_details.swift_code": "BUKBGB22",
"bank_details.iban": "GB33BUKB20201555555555",
"bank_details.routing_code_value1": "202015",
"bank_details.routing_code_type1": "sort_code",
"address.country": "GB",
"address.state": "Greater London",
"address.city": "London",
"address.street_address": "221B Baker Street",
"address.postal_code": "NW1 6XE"
}
}'const res = await fetch("https://api.zinfra.vip/v1/remit/payees", {
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({
"country_code": "GB",
"currency": "GBP",
"payment_method": "LOCAL",
"clearing_system": "FASTER PAYMENTS",
"entity_type": "INDIVIDUAL",
"nickname": "房东",
"fields": {
"first_name": "MARY",
"last_name": "SMITH",
"bank_details.account_holder": "MARY SMITH",
"bank_details.bank_name": "Barclays",
"bank_details.bank_address": "1 Churchill Place, London",
"bank_details.swift_code": "BUKBGB22",
"bank_details.iban": "GB33BUKB20201555555555",
"bank_details.routing_code_value1": "202015",
"bank_details.routing_code_type1": "sort_code",
"address.country": "GB",
"address.state": "Greater London",
"address.city": "London",
"address.street_address": "221B Baker Street",
"address.postal_code": "NW1 6XE"
}
}),
});
// Keep monetary amounts as strings, never numbers.
const data = await res.json();import requests
res = requests.post(
"https://api.zinfra.vip/v1/remit/payees",
headers={
"x-auth-token": "Bearer $TOKEN",
"x-on-behalf-of": "$MEMBER_ID",
"x-idempotency-key": "$IDEMPOTENCY_KEY",
"content-type": "application/json",
},
json={
"country_code": "GB",
"currency": "GBP",
"payment_method": "LOCAL",
"clearing_system": "FASTER PAYMENTS",
"entity_type": "INDIVIDUAL",
"nickname": "房东",
"fields": {
"first_name": "MARY",
"last_name": "SMITH",
"bank_details.account_holder": "MARY SMITH",
"bank_details.bank_name": "Barclays",
"bank_details.bank_address": "1 Churchill Place, London",
"bank_details.swift_code": "BUKBGB22",
"bank_details.iban": "GB33BUKB20201555555555",
"bank_details.routing_code_value1": "202015",
"bank_details.routing_code_type1": "sort_code",
"address.country": "GB",
"address.state": "Greater London",
"address.city": "London",
"address.street_address": "221B Baker Street",
"address.postal_code": "NW1 6XE"
}
},
)
# Use Decimal(str(...)) for amounts, not float.
data = res.json()req, _ := http.NewRequest("POST", "https://api.zinfra.vip/v1/remit/payees",
strings.NewReader(`{
"country_code": "GB",
"currency": "GBP",
"payment_method": "LOCAL",
"clearing_system": "FASTER PAYMENTS",
"entity_type": "INDIVIDUAL",
"nickname": "房东",
"fields": {
"first_name": "MARY",
"last_name": "SMITH",
"bank_details.account_holder": "MARY SMITH",
"bank_details.bank_name": "Barclays",
"bank_details.bank_address": "1 Churchill Place, London",
"bank_details.swift_code": "BUKBGB22",
"bank_details.iban": "GB33BUKB20201555555555",
"bank_details.routing_code_value1": "202015",
"bank_details.routing_code_type1": "sort_code",
"address.country": "GB",
"address.state": "Greater London",
"address.city": "London",
"address.street_address": "221B Baker Street",
"address.postal_code": "NW1 6XE"
}
}`))
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/remit/payees"))
.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("""
{
"country_code": "GB",
"currency": "GBP",
"payment_method": "LOCAL",
"clearing_system": "FASTER PAYMENTS",
"entity_type": "INDIVIDUAL",
"nickname": "房东",
"fields": {
"first_name": "MARY",
"last_name": "SMITH",
"bank_details.account_holder": "MARY SMITH",
"bank_details.bank_name": "Barclays",
"bank_details.bank_address": "1 Churchill Place, London",
"bank_details.swift_code": "BUKBGB22",
"bank_details.iban": "GB33BUKB20201555555555",
"bank_details.routing_code_value1": "202015",
"bank_details.routing_code_type1": "sort_code",
"address.country": "GB",
"address.state": "Greater London",
"address.city": "London",
"address.street_address": "221B Baker Street",
"address.postal_code": "NW1 6XE"
}
}
"""))
.build();
// Use String / BigDecimal for amounts, not double.$ch = curl_init('https://api.zinfra.vip/v1/remit/payees');
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'
{
"country_code": "GB",
"currency": "GBP",
"payment_method": "LOCAL",
"clearing_system": "FASTER PAYMENTS",
"entity_type": "INDIVIDUAL",
"nickname": "房东",
"fields": {
"first_name": "MARY",
"last_name": "SMITH",
"bank_details.account_holder": "MARY SMITH",
"bank_details.bank_name": "Barclays",
"bank_details.bank_address": "1 Churchill Place, London",
"bank_details.swift_code": "BUKBGB22",
"bank_details.iban": "GB33BUKB20201555555555",
"bank_details.routing_code_value1": "202015",
"bank_details.routing_code_type1": "sort_code",
"address.country": "GB",
"address.state": "Greater London",
"address.city": "London",
"address.street_address": "221B Baker Street",
"address.postal_code": "NW1 6XE"
}
}
JSON,
]);
$res = curl_exec($ch);
// Use bcmath / strings for amounts, not floatval.
{
"id": "pye_1042",
"outcome": "created",
"status": "active"
}