Create a beneficiary
curl -X POST "https://api.payhero.africa/api/global/beneficiaries" \ -u "API_USERNAME:API_PASSWORD" \ -H "Content-Type: application/json" \ -d '{ "external_reference": "customer-8821", "name": "John Doe", "country": "KE", "phone": "+254712345678", "address": "Moi Drive, Umoja", "date_of_birth": "10/03/1997", "email": "john.doe@example.com", "id_number": "32460968", "id_type": "national_id", "account_name": "John Doe", "account_number": "+254712345678", "account_type": "momo"}'<?php$ch = curl_init("https://api.payhero.africa/api/global/beneficiaries");curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_USERPWD => "API_USERNAME:API_PASSWORD", CURLOPT_HTTPHEADER => ["Content-Type: application/json"], CURLOPT_POSTFIELDS => json_encode([ "external_reference" => "customer-8821", "name" => "John Doe", "country" => "KE", "phone" => "+254712345678", "address" => "Moi Drive, Umoja", "date_of_birth" => "10/03/1997", "email" => "john.doe@example.com", "id_number" => "32460968", "id_type" => "national_id", "account_name" => "John Doe", "account_number" => "+254712345678", "account_type" => "momo", ]),]);$response = curl_exec($ch);curl_close($ch);echo $response;import requests
response = requests.post( "https://api.payhero.africa/api/global/beneficiaries", auth=("API_USERNAME", "API_PASSWORD"), json={ "external_reference": "customer-8821", "name": "John Doe", "country": "KE", "phone": "+254712345678", "address": "Moi Drive, Umoja", "date_of_birth": "10/03/1997", "email": "john.doe@example.com", "id_number": "32460968", "id_type": "national_id", "account_name": "John Doe", "account_number": "+254712345678", "account_type": "momo", },)beneficiary = response.json()print(beneficiary["uuid"])const auth = Buffer.from("API_USERNAME:API_PASSWORD").toString("base64");
const response = await fetch("https://api.payhero.africa/api/global/beneficiaries", { method: "POST", headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json", }, body: JSON.stringify({ external_reference: "customer-8821", name: "John Doe", country: "KE", phone: "+254712345678", address: "Moi Drive, Umoja", date_of_birth: "10/03/1997", email: "john.doe@example.com", id_number: "32460968", id_type: "national_id", account_name: "John Doe", account_number: "+254712345678", account_type: "momo", }),});
const beneficiary = await response.json();console.log(beneficiary.uuid);Stores a counterparty’s identity and KYC once, and returns a uuid you then send as
customer.customer_uuid on every payment for that counterparty.
A beneficiary holds identity and KYC only — provider routing (provider_config)
is never stored here and always stays on the payment request.
name, country and email are the floor: without all three the record is rejected.
Supplying phone, address, date_of_birth, id_number and id_type as well
promotes it to kyc_level: full. kyc_level is recomputed on every write and is
never set directly.
Passing external_reference makes this call idempotent — repeating it with the same
reference returns the existing record instead of creating a duplicate.
Authorizations
Section titled “Authorizations”Request Bodyrequired
Section titled “Request Bodyrequired”object
Full name, split on the first space. Required on create unless you send
first_name + last_name instead, which win if both are present.
Required for tier_0, and therefore for every beneficiary.
Required for tier_0. ISO 3166-1 alpha-2 code (or country name).
Required for full. Returned as phone_number.
Required for full.
Required for full. Accepts mm/dd/yyyy (what the cross-border providers document), yyyy-mm-dd or RFC 3339. Returned as RFC 3339.
Required for full. Stored and returned as national_id. Unique per account — the same person can be a beneficiary of several merchants.
Required for full. Must be NIN in Nigeria.
Required for full in Nigeria, where it must be BVN.
Required for full in Nigeria.
Destination account holder name.
Destination account. Backfills payment_config.account_number on payments that reference this beneficiary.
Destination account type, e.g. momo or bank.
Your own identifier for this counterparty. Unique per account, and makes POST idempotent.
Examples
Reduced KYC — the smallest payload the API accepts
{ "name": "John Doe", "country": "KE", "email": "john.doe@example.com"}Full KYC — clears the tier-0 limits entirely
{ "external_reference": "customer-8821", "name": "John Doe", "country": "KE", "phone": "+254712345678", "address": "Moi Drive, Umoja", "date_of_birth": "10/03/1997", "email": "john.doe@example.com", "id_number": "32460968", "id_type": "national_id", "account_name": "John Doe", "account_number": "+254712345678", "account_type": "momo"}Nigeria — requires a second ID (BVN alongside NIN)
{ "name": "John Doe", "country": "NG", "phone": "+2348012345678", "address": "12 Awolowo Road, Ikoyi", "date_of_birth": "10/03/1997", "email": "john.doe@example.com", "id_number": "0123456789", "id_type": "NIN", "additional_id_type": "BVN", "additional_id_number": "0123456789"}Responses
Section titled “Responses”The created (or, with a repeated external_reference, the existing) beneficiary.
A stored counterparty. Note that a few fields are named differently on the way out than
on the way in: phone is returned as phone_number, and id_number as national_id.
object
Send this as customer.customer_uuid on payments.
The id_number you sent.
The phone you sent.
The PayHero account that owns this beneficiary, derived from your API key.
Recomputed on every write from the metadata actually present. Never settable.
The beneficiary’s KYC standing, recomputed on every write. Read-only.
object
Fields still needed to reach full. Send these to the update endpoint.
Fields still needed to reach tier_0. Non-empty only on a rejected create.
Lifetime USD total this beneficiary has transacted while at tier_0.
Headroom left before full KYC becomes mandatory.
Per-transaction ceiling while at tier_0.
Currencies a tier_0 beneficiary can never transact in.
True when the country needs a second ID (Nigeria) and it has not been supplied.
Examples
{ "id": 94, "uuid": "a2e0434e-17ae-41b1-a1cf-1694ad63ee99", "first_name": "John", "last_name": "Doe", "surname": null, "national_id": null, "date_of_birth": null, "gender": null, "phone_number": null, "email": "john.doe@example.com", "address": null, "country": "KE", "account_id": 63, "account_name": null, "account_number": null, "account_type": null, "customer_type": "retail", "kyc_level": "tier_0", "tier_0_lifetime_usd": 0, "is_active": true, "created_at": "2026-09-05T09:41:12.113306Z", "updated_at": "2026-09-05T09:41:12.113306Z", "kyc": { "level": "tier_0", "full_kyc": false, "missing_full_kyc_fields": [ "address", "date_of_birth", "id_number", "id_type", "phone" ], "missing_reduced_kyc_fields": [], "tier_0_lifetime_usd": 0, "tier_0_remaining_usd": 200, "tier_0_limit_reached": false, "tier_0_max_transaction_usd": 20, "tier_0_excluded_currencies": [ "BWP", "NGN", "ZAR" ], "requires_additional_id": false }}{ "id": 94, "uuid": "a2e0434e-17ae-41b1-a1cf-1694ad63ee99", "first_name": "John", "last_name": "Doe", "surname": null, "national_id": "32460968", "date_of_birth": "1997-10-03T00:00:00Z", "gender": null, "phone_number": "+254712345678", "email": "john.doe@example.com", "address": "Moi Drive, Umoja", "country": "KE", "account_id": 63, "account_name": "John Doe", "account_number": "+254712345678", "account_type": "momo", "customer_type": "retail", "kyc_level": "full", "tier_0_lifetime_usd": 0, "is_active": true, "created_at": "2026-09-05T09:41:12.113306Z", "updated_at": "2026-09-05T09:41:12.113306Z", "kyc": { "level": "full", "full_kyc": true, "missing_full_kyc_fields": [], "missing_reduced_kyc_fields": [], "tier_0_lifetime_usd": 0, "tier_0_remaining_usd": 200, "tier_0_limit_reached": false, "tier_0_max_transaction_usd": 20, "tier_0_excluded_currencies": [ "BWP", "NGN", "ZAR" ], "requires_additional_id": false }}The request was rejected (validation or business-rule failure).
object
Examples
{ "error_code": "invalid_argument", "error_message": "the transaction amount is insufficient as it wont cater for cost: (4.62)", "status_code": 400}{ "error_code": "invalid_argument", "error_message": "The passed channel is not active", "status_code": 400}Missing or invalid Basic auth credentials.
object
Example
{ "error_code": "invalid_argument", "error_message": "the transaction amount is insufficient as it wont cater for cost: (4.62)", "status_code": 400}
