Customer accounts
One permanent account number per customer, in their own name. A transfer into it arrives already carrying your own customer id.
#Issue an account
The endpoint the whole product is for.
curl https://api.paymonetra.com/v1/customer_accounts \
-H "Authorization: Bearer sk_live_..." \
-d customer_reference=cus_9f21 \
-d customer_name="Ngozi Nwosu" \
-d customer_email=ngozi@example.com \
-d customer_phone=08031234567JSON or form encoding, both accepted.
{
"customer_reference": "cus_9f21",
"customer_name": "Ngozi Nwosu",
"customer_email": "ngozi@example.com",
"customer_phone": "08031234567",
"account_number": "8021234567",
"account_name": "MONETRA - Samifunds (Ngozi Nwosu)",
"bank_name": "Payrep MFB",
"status": "active",
"mode": "live",
"credits": 0,
"collected_total": 0,
"last_credit_at": null,
"created_at": "2026-08-28T19:18:33+01:00"
}201 on a new account, 200 when one already exists for that reference. Same body either way, so no branching. Calling it twice is safe and is the intended way to be idempotent.
customer_reference is the merchant's own key. Letters, numbers and . _ : - only, up to 80 characters, because it comes back in every webhook and appears in URLs.
customer_email is required for a new account from 21 October 2026. Until then an account without one still opens, and the response carries a warning:
"warnings": [{ "code": "customer_email_missing",
"message": "Send customer_email for this customer. From 21 October 2026 a new customer account without one is refused. Sending this customer_reference again with customer_email adds it." }]From 21 October a new customer_reference without customer_email answers 422 invalid_request with field: "customer_email". Accounts that already exist keep working without one.
customer_phone is optional. 7 to 15 digits with an optional leading +; spaces, dashes and brackets are stripped before it is stored.
Adding them to an existing account: send the same customer_reference again with customer_email and/or customer_phone. The account and number do not change; only the fields you send are filled in, and an empty field never clears a stored one. An integration that already calls this endpoint each time a customer opens their funding page picks the emails up as customers return.
A bad email or phone is refused with 422 invalid_request and field naming it. Every customer account shape, here and on GET, carries both fields (empty strings when not given).
Account limit. Your business can hold 500 active customer accounts, plus 2 more for every account that has received a payment. The limit grows on its own as your customers pay. At the limit a new customer_reference answers 403 account_limit, and so does POST /payments naming a new customer_reference. Customers who already have an account are unaffected. Create an account when a customer is ready to pay, not at signup, and reuse it every time after that. Test keys are not limited.
Refused names. A customer_name that impersonates a bank or Paymonetra (for example "Paymonetra Support") answers 422 bad_name with field: "customer_name", here and on POST /payments.
#Why an account each
A Nigerian bank transfer carries no memo you can rely on, so twenty customers paying into one collection account is twenty credits matched by timing. Give each their own number and the account the money arrived in is the answer.
The account is permanent, so the payer’s bank statement reads the same every time too.
#Idempotency
No separate header. customer_reference is the key: ask again with the same one and you get 200 and the same account. Never track whether you have issued one. Ask, and store what comes back.
#Fetch one
One account, by your customer_reference.
curl https://api.paymonetra.com/v1/customer_accounts/cus_9f21 \
-H "Authorization: Bearer sk_live_..."{
"customer_reference": "cus_9f21",
"customer_name": "Ngozi Nwosu",
"account_number": "8021234567",
"account_name": "MONETRA - Samifunds (Ngozi Nwosu)",
"bank_name": "Payrep MFB",
"status": "active",
"mode": "live",
"credits": 3,
"collected_total": 45000,
"last_credit_at": "2026-09-27T14:02:10+01:00",
"created_at": "2026-08-28T19:18:33+01:00"
}404 not_found if you have no account with that reference.
There is no per-account balance. Every payment goes straight into your business balance (GET /balance). credits and collected_total are what this account has received in total, the amounts payers sent, before our fee; each payment's fee and net are in its webhook. What each of your users may spend is yours to track.
#List them
curl "https://api.paymonetra.com/v1/customer_accounts?status=active&limit=50&offset=0" \
-H "Authorization: Bearer sk_live_..."| query | default | notes |
|---|---|---|
status | active | active, retired or all |
limit | 50 | up to 100 |
offset | 0 |
{
"data": [
{
"customer_reference": "cus_9f21",
"customer_name": "Ngozi Nwosu",
"account_number": "8021234567",
"account_name": "MONETRA - Samifunds (Ngozi Nwosu)",
"bank_name": "Payrep MFB",
"status": "active",
"mode": "live",
"credits": 3,
"collected_total": 45000,
"last_credit_at": "2026-09-27T14:02:10+01:00",
"created_at": "2026-08-28T19:18:33+01:00"
}
],
"limit": 50,
"offset": 0
}Each item has the same fields as the single fetch.
#Rate limits
The app's API throttles on the IP because there the thing being abused is a password guess. Here the caller is a server that legitimately makes every request from one address, so an IP limit would be a limit on the merchant's whole business.
POST /customer_accounts | 300 an hour |
GET anything | 600 an hour |
Account creation is tightest because it reaches our bank partner, who rate limit by IP across all of us, so one merchant's loop is everybody's outage. 300 an hour is a merchant onboarding five users a minute all day.
#Then wait for the webhook
When money reaches that account we post a collection.success event carrying the same customer_reference. See webhooks.