Beneficiaries
A beneficiary is the KYC-bearing record you create once for a counterparty and then reference by UUID on every payment. It replaces the block of customer details you would otherwise re-send on each request.
A beneficiary holds identity and KYC only. Provider routing — network_id,
provider_id, network codes — is never stored here; it stays on the payment
request. See How routing works.
POST /api/global/beneficiaries → uuidPOST /api/global/payments → "customer": { "customer_uuid": "<uuid>" }All endpoints on this page live under /api/global/beneficiaries and use the same
HTTP Basic API key as the rest of the global API.
The account is derived from your API key — you never send an account id, and a
UUID belonging to another merchant returns 404, never 403.
The KYC model
Section titled “The KYC model”Every beneficiary carries a kyc_level, recomputed from the metadata actually
present on it on every write. You never set it directly.
| Level | Required fields |
|---|---|
tier_0 |
name, country, email |
full |
tier_0 + phone, address, date_of_birth, id_number, id_type |
full in Nigeria |
the above, with id_type: NIN + additional_id_type: BVN and additional_id_number |
tier_0 is the floor: a record missing name, country or email is rejected on
create and cannot transact at all.
What a tier_0 beneficiary may transact
Section titled “What a tier_0 beneficiary may transact”A tier_0 beneficiary clears a payment only when all of these hold:
- the currency is not
ZAR,BWPorNGN - the amount is under 20 USD equivalent
- its lifetime tier-0 total is under 200 USD
Once that lifetime total reaches 200 USD, full KYC becomes mandatory for that beneficiary and further tier-0 payments are refused:
{ "error_code": "invalid_argument", "message": "full KYC information is required for the transaction" }A full beneficiary bypasses all three constraints, and its transactions are not
counted toward any tier-0 total.
Create a beneficiary
Section titled “Create a beneficiary”The smallest payload the API accepts is a name, a country and an email — that gets
you a tier_0 record you can transact with immediately, within the envelope above.
curl -X POST "$PH_BASE_URL/api/global/beneficiaries" \ -u "$PH_API_USERNAME:$PH_API_PASSWORD" \ -H "Content-Type: application/json" \ -d '{ "name": "John Doe", "country": "KE", "email": "john.doe@example.com" }'<?php$ch = curl_init("$baseUrl/api/global/beneficiaries");curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_USERPWD => "$username:$password", CURLOPT_HTTPHEADER => ["Content-Type: application/json"], CURLOPT_POSTFIELDS => json_encode([ "name" => "John Doe", "country" => "KE", "email" => "john.doe@example.com", ]),]);$beneficiary = json_decode(curl_exec($ch), true);curl_close($ch);echo $beneficiary["uuid"];import requests
beneficiary = requests.post( f"{base_url}/api/global/beneficiaries", auth=(username, password), json={ "name": "John Doe", "country": "KE", "email": "john.doe@example.com", },).json()
print(beneficiary["uuid"])const auth = Buffer.from(`${username}:${password}`).toString("base64");
const response = await fetch(`${baseUrl}/api/global/beneficiaries`, { method: "POST", headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json", }, body: JSON.stringify({ name: "John Doe", country: "KE", email: "john.doe@example.com", }),});
const beneficiary = await response.json();console.log(beneficiary.uuid);{ "id": 94, "uuid": "a2e0434e-17ae-41b1-a1cf-1694ad63ee99", "first_name": "John", "last_name": "Doe", "email": "john.doe@example.com", "country": "KE", "account_id": 63, "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 }}Full KYC
Section titled “Full KYC”Send the whole set up front and the record is created at full:
curl -X POST "$PH_BASE_URL/api/global/beneficiaries" \ -u "$PH_API_USERNAME:$PH_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" }'external_reference is your own id for the counterparty. It is unique per account
and makes create idempotent — repeat the call with the same reference and you
get the existing record back instead of a duplicate.
Nigeria needs two IDs
Section titled “Nigeria needs two IDs”For country: "NG", full additionally requires a second document — id_type: NIN
plus additional_id_type: "BVN" and additional_id_number:
{ "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"}Omit either additional field and the record stays tier_0 with
requires_additional_id: true.
Enrich a beneficiary (tier_0 → full)
Section titled “Enrich a beneficiary (tier_0 → full)”Send only the fields you have just collected — PATCH and PUT both merge.
curl -X PATCH "$PH_BASE_URL/api/global/beneficiaries/a2e0434e-17ae-41b1-a1cf-1694ad63ee99" \ -u "$PH_API_USERNAME:$PH_API_PASSWORD" \ -H "Content-Type: application/json" \ -d '{ "phone": "+254712345678", "address": "Moi Drive, Umoja" }'Still tier_0 — missing_full_kyc_fields is now
["date_of_birth", "id_number", "id_type"]. Send those and the level flips to
full on the response.
Deactivate without deleting:
curl -X PATCH "$PH_BASE_URL/api/global/beneficiaries/<uuid>" \ -u "$PH_API_USERNAME:$PH_API_PASSWORD" \ -H "Content-Type: application/json" \ -d '{"is_active": false}'An inactive beneficiary is refused at payment time with beneficiary is not active.
Fetch, list and delete
Section titled “Fetch, list and delete”# one recordcurl "$PH_BASE_URL/api/global/beneficiaries/<uuid>" \ -u "$PH_API_USERNAME:$PH_API_PASSWORD"
# a filtered pagecurl "$PH_BASE_URL/api/global/beneficiaries?page=1&per=20&kyc_level=tier_0&term=john" \ -u "$PH_API_USERNAME:$PH_API_PASSWORD"
# removecurl -X DELETE "$PH_BASE_URL/api/global/beneficiaries/<uuid>" \ -u "$PH_API_USERNAME:$PH_API_PASSWORD"List responses carry a pagination block (count, page, per, num_pages,
next_page, prev_page). Filters: term (matches first name, last name, email,
phone, external reference), kyc_level, is_active, uuid, and from/to over
created_at — the last two must be sent together.
Dry-run a payment against KYC
Section titled “Dry-run a payment against KYC”Answers “would this clear?” without moving money — useful before charging, or to decide whether to prompt the counterparty for more documents.
curl "$PH_BASE_URL/api/global/beneficiaries/<uuid>/kyc-eligibility?currency=KES&amount_usd=15" \ -u "$PH_API_USERNAME:$PH_API_PASSWORD"{ "beneficiary": { "uuid": "…", "kyc_level": "tier_0", "kyc": { "…": "…" } }, "currency": "KES", "amount_usd": 15, "eligible": true}A refusal is still a 200 — read eligible and reason:
{ "currency": "NGN", "amount_usd": 15, "eligible": false, "reason": "full KYC information is required for the transaction"}Use it on a payment
Section titled “Use it on a payment”This is the point of the resource. Send customer_uuid and nothing else in
customer; the stored record supplies the name, email, phone, country, IDs, date of
birth, address and destination account number.
curl -X POST "$PH_BASE_URL/api/global/payments" \ -u "$PH_API_USERNAME:$PH_API_PASSWORD" \ -H "Content-Type: application/json" \ -d '{ "request_type": "payment", "transaction_channel": "momo", "provider": "yc", "amount": 20000, "currency": "UGX", "country": "UG", "customer": { "customer_uuid": "a2e0434e-17ae-41b1-a1cf-1694ad63ee99" }, "vendor_config": { "vendor_id": 63 }, "provider_config": { "network_id": "04a3083a-567c-4a2b-aa39-fa8f19e64341", "network_name": "Airtel Mobile Money", "network_code": "AIRTEL_UGANDA", "channel_id": "e167def0-c4f0-46e2-aaa9-50046f13b0a7", "account_type": "momo" }, "payment_config": { "reference": "order_INV-2026-001", "account_number": "+256769759910", "remark": "order payment", "payment_category": "bill payment", "callback_url": "https://your-system.com/webhooks/payhero", "redirect_url": "https://your-system.com/payments/return" } }'Anything you do send alongside customer_uuid overrides the stored value for
that one transaction. customer_uid is accepted as a legacy alias for
customer_uuid.
Without a customer_uuid
Section titled “Without a customer_uuid”The inline customer object still works exactly as before, and first_name,
last_name, email and phone remain required in that case. Nothing is written
to your beneficiary book — to save a counterparty, call POST /api/global/beneficiaries yourself.
"customer": { "first_name": "John", "last_name": "Doe", "email": "john.doe@example.com", "phone": "+254712345678"}Payment-time errors
Section titled “Payment-time errors”| Condition | Status | Message |
|---|---|---|
| UUID not found on your account | 404 | beneficiary "<uuid>" does not exist |
| Beneficiary deactivated | 400 | beneficiary is not active |
| Tier-0 envelope exceeded | 400 | full KYC information is required for the transaction |
No customer_uuid and inline fields missing |
400 | invalid_arguments[] naming each missing field |
Field reference
Section titled “Field reference”For POST /api/global/beneficiaries and PUT/PATCH /api/global/beneficiaries/:uuid.
| Field | Notes |
|---|---|
name |
Alternative to first_name/last_name; split on the first space |
first_name, last_name |
Win over name when both are sent |
surname |
Optional |
email |
Required — part of the tier_0 floor |
country |
Required — ISO-2 code or country name |
phone |
Required for full. Returned as phone_number |
address |
Required for full |
date_of_birth |
Required for full. mm/dd/yyyy, yyyy-mm-dd or RFC 3339 |
id_number |
Required for full. Stored and returned as national_id |
id_type |
Required for full; must be NIN in Nigeria |
additional_id_type, additional_id_number |
Required for full in Nigeria (BVN) |
gender, marital_status |
Optional |
account_name, account_number, account_type |
Destination account; account_number backfills payment_config.account_number |
external_reference |
Your own id; unique per account, makes create idempotent |
customer_type |
retail (default) or institution |
is_active |
Update only |
Read-only, returned but never accepted: id, uuid, kyc_level,
tier_0_lifetime_usd, created_at, updated_at, and the whole kyc block.
- Create a collection (pay-in)
- Create a payout (pay-out)
- Full schemas in the API Reference

