OI Payments Docs
Core concepts

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.

FieldTypeNotes
externalCustomerIdstringYour id for the customer (their reference, not our internal id). Becomes the lookup key. Max 255 chars.
namestringLast-seen display name, optional. Refreshed on each reuse.
emailstringLast-seen email, optional. Used to pre-fill the hosted page. Max 320 chars.
phonestringLast-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:

FieldTypeRequiredNotes
customerReferencestringYes@NotBlank, max 255. Your own id for the customer; stored as externalCustomerId.
customerNamestringNoMax 255. Stored as the reference's last-seen name and refreshed on reuse.
customerEmailstringNo@Email, max 320. Stored, refreshed on reuse, and used to pre-fill the hosted page.
customerPhonestringNoMax 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).

FieldTypeRequiredNotes
externalCustomerIdstringAlwaysThe id you supplied as customerReference.
namestringNullableLast-seen display name; omitted from the JSON when never supplied.
emailstringNullableLast-seen email; omitted when never supplied.
phonestringNullableLast-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

On this page