Skip to content

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 → uuid
POST /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.

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.

A tier_0 beneficiary clears a payment only when all of these hold:

  • the currency is not ZAR, BWP or NGN
  • 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.

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.

Terminal window
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"
}'
{
"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
}
}

Send the whole set up front and the record is created at full:

Terminal window
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.

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.

Send only the fields you have just collected — PATCH and PUT both merge.

Terminal window
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:

Terminal window
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.

Terminal window
# one record
curl "$PH_BASE_URL/api/global/beneficiaries/<uuid>" \
-u "$PH_API_USERNAME:$PH_API_PASSWORD"
# a filtered page
curl "$PH_BASE_URL/api/global/beneficiaries?page=1&per=20&kyc_level=tier_0&term=john" \
-u "$PH_API_USERNAME:$PH_API_PASSWORD"
# remove
curl -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.

Answers “would this clear?” without moving money — useful before charging, or to decide whether to prompt the counterparty for more documents.

Terminal window
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"
}

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.

Terminal window
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.

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"
}
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

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.