Customers
A customer is a lightweight reference — your own external id plus optional contact fields — find-or-created from the payments and invoices you raise. No standalone create endpoint, and no required PII.
A customer in OI Payments is a customer_ref — a thin pointer to one of your
app's customers. It is your identifier (externalCustomerId) plus a few
optional contact fields the service stores for display and to pre-fill the
gateway's hosted-checkout page. The service deliberately keeps references, not
required PII.
You never call a "create customer" endpoint. A customer reference is
find-or-created for you the moment you raise a payment
or an invoice that names a customerReference.
What a customer reference holds
A customer_ref row carries only these fields. Every contact field is optional —
there is no required personal data.
| Field | Type | Notes |
|---|---|---|
externalCustomerId | string | Your id for the customer (their reference, not our internal id). Becomes the lookup key. Max 255 chars. |
name | string | Last-seen display name, optional. Refreshed on each reuse. |
email | string | Last-seen email, optional. Used to pre-fill the hosted page. Max 320 chars. |
phone | string | Last-seen phone, optional. Used to pre-fill the hosted page. Max 32 chars. |
We store references, not required PII. The only mandatory field is
externalCustomerId — your own identifier. name, email, and phone are
optional conveniences; omit them and a customer reference is still perfectly
valid.
There is no create-customer endpoint
Customers are a side effect of charging money. When you create a payment (or an
invoice), the service looks for an existing reference under your app for that
externalCustomerId; if none exists it inserts one, otherwise it reuses the
existing row. The relevant fields on the create-payment body:
| Field | Type | Required | Notes |
|---|---|---|---|
customerReference | string | Yes | @NotBlank, max 255. Your own id for the customer; stored as externalCustomerId. |
customerName | string | No | Max 255. Stored as the reference's last-seen name and refreshed on reuse. |
customerEmail | string | No | @Email, max 320. Stored, refreshed on reuse, and used to pre-fill the hosted page. |
customerPhone | string | No | Max 32. Stored, refreshed on reuse, and used to pre-fill the hosted page. |
The mode (TEST or LIVE) is derived from the API credential that
authenticated the request — never from the request body. There is no mode
field to set when you create a payment, and none on the customer reference at
all (see Shared across modes below).
Uniqueness: one row per (app, externalCustomerId)
A unique index (uq_customer_ref_app_external on application_id +
external_customer_id) guarantees at most one customer reference per app per
external id. Repeat payments by the same customer reuse that single row, so all
of a customer's activity hangs off one reference. Two different apps can both use
cust_8842 — they are different rows, fully isolated. See
App isolation.
Contact details refresh on reuse
On reuse, each of name, email, and phone is overwritten only when you
supply a new, non-blank value that differs from what is stored. Omitting a
field (or sending a blank one) never erases a previously known value — so the
stored contact details track the latest payment without losing data when a later
call leaves a field out.
First payment
You send customerReference: "cust_8842", customerName: "Ayesha Rahman",
customerEmail: "[email protected]". A new reference is inserted with all three.
Second payment, partial contact
You send customerReference: "cust_8842", customerPhone: "+8801712345678", and
omit the name and email. The phone is added; the name and email are kept, not
wiped.
Third payment, updated email
You send customerReference: "cust_8842", customerEmail: "[email protected]".
Only the email is overwritten. The stored reference now reflects the latest of
each field the customer ever supplied.
Shared across modes
A customer reference has no TEST/LIVE column. The same customer_ref row
is shared across both modes: a test payment and a live payment for the same
externalCustomerId point at one identity record. Only the payments and
invoices attached to it are mode-scoped — those carry their own mode, derived
from the authenticating credential.
This is why the lookup endpoint returns identity (name/email/phone) regardless of which credential you use, while the payment-history endpoint is filtered to the mode of the credential that called it.
Contact identity is shared across test and live; activity is not. Read more in Test & live modes.
Looking up a customer
Two app-facing endpoints let you read your own customers. Both are authenticated
with your API key + secret and are scoped to the authenticating app — you can
never see another app's customers, and an unknown or cross-app id returns 404.
Every response uses the standard envelope:
{ "data": <payload|null>, "meta": { "success", "message", "errorCode", "timestamp" }, "pagination": null }.
The examples below show just the data payload after the first one.
GET /customers/{externalId}
Returns the customer reference's contact identity. Because a reference is shared across modes, this works with either a test or a live credential.
curl http://localhost:8080/api/v1/customers/cust_8842 \
-H "X-Api-Key: oi_test_3f9a2b71c0e44d6f" \
-H "X-Api-Secret: sk_test_2b9c7e1d4a6f8013"The response payload is a CustomerDto. Null fields are omitted from the JSON
(the DTO serializes non-null only).
| Field | Type | Required | Notes |
|---|---|---|---|
externalCustomerId | string | Always | The id you supplied as customerReference. |
name | string | Nullable | Last-seen display name; omitted from the JSON when never supplied. |
email | string | Nullable | Last-seen email; omitted when never supplied. |
phone | string | Nullable | Last-seen phone; omitted when never supplied. |
{
"data": {
"externalCustomerId": "cust_8842",
"name": "Ayesha Rahman",
"email": "[email protected]",
"phone": "+8801712345678"
},
"meta": {
"success": true,
"message": null,
"errorCode": null,
"timestamp": "2026-06-30T12:00:00Z"
},
"pagination": null
}An unknown or cross-app id returns 404 with a stable errorCode:
{
"data": null,
"meta": {
"success": false,
"message": "Customer not found with id: cust_9999",
"errorCode": "RESOURCE_NOT_FOUND",
"timestamp": "2026-06-30T12:00:00Z"
},
"pagination": null
}GET /customers/{externalId}/payments
Lists that customer's payments, scoped to the authenticating app and the mode of the credential. A test key sees only test payments; a live key sees only live payments — for the very same customer reference.
curl http://localhost:8080/api/v1/customers/cust_8842/payments \
-H "X-Api-Key: oi_live_91c4d7e0a2b53f68" \
-H "X-Api-Secret: sk_live_6d0f1a8c3e9b4275"The payload is a list of PaymentDto objects (the same shape returned by the
create and status-lookup endpoints — see Accept a payment
for the full field reference). Each entry's mode matches the calling
credential:
{
"data": [
{
"id": 50231,
"reference": "PAY-7K2M9QX4",
"status": "SUCCEEDED",
"mode": "LIVE",
"amountMinor": 150000,
"currency": "BDT",
"createdAt": "2026-06-29T09:14:03"
}
],
"meta": {
"success": true,
"message": null,
"errorCode": null,
"timestamp": "2026-06-30T12:00:00Z"
},
"pagination": null
}Amounts are integer minor units (paisa): 150000 is 1,500.00 BDT. See
Money & amounts.
Where customers show up
Look up customers
The app-facing lookup endpoints in practice — fetch a customer and their payment history by your own id.
Admin customer 360
The operator view: search customers and see aggregated payment, invoice, and refund activity for one customer.
Accept a payment
Where a customer reference is first find-or-created, via customerReference.
App isolation
Why one app never sees another app's customers.