OI Payments Docs
Guides

Payable invoice page

The public, unauthenticated JSON behind a customer-facing branded invoice page — number, line items, totals, your branding, and the checkout URL to pay at — keyed by the opaque payment reference.

When you mint a payable link for an invoice, you get back a payableUrl like http://localhost:3000/pay/pay_7f3a9c2e. Your customer opens it and lands on a public, branded payable-invoice page — the bill, your branding, and a button to pay. This guide documents the single read-only endpoint that backs that page:

GET /api/v1/pay/{reference}

It returns a customer-safe PayableInvoiceViewDto — the invoice number, line items, totals, your per-app branding, and the hosted-checkout URL to proceed to. It deliberately carries no PII and no app-internal data — only what a paying customer may see.

All amounts are integer minor units (paisa): 150000 means 1,500.00 BDT — never floating point. Currency is BDT-only in v1.

Public and reference-keyed

This endpoint is public and unauthenticated — it carries no X-Api-Key, no X-Api-Secret, and no Authorization header. The opaque payment reference in the path is the bearer: minted by the payable-link endpoint, it is unguessable and the only credential needed to read the view. This is exactly how the hosted checkout page reads its context, so a customer's browser can render the invoice without ever holding your API key.

The mode (TEST or LIVE) is not a parameter here — it is fixed by the payment the reference resolves to, which carries its own app_id and mode. The invoice is then loaded scoped to that same app and mode, so the page can never cross app or mode isolation boundaries.

Flow

The page is a read-only projection. The customer pays by following checkoutUrl, which is the same hosted-checkout surface a standalone payment uses — the payable page never charges anything itself.

Read the view

curl http://localhost:8080/api/v1/pay/pay_7f3a9c2e

No headers are required. The response is wrapped in the standard envelope — { "data": …, "meta": { "success", "message", "errorCode", "timestamp" }, "pagination": null }. The rest of this page shows the data payload in context once and then refers to its fields by name.

{
  "data": {
    "invoiceNumber": "INV-000001",
    "status": "ISSUED",
    "currency": "BDT",
    "totalMinor": 100000,
    "amountPaidMinor": 0,
    "amountDueMinor": 100000,
    "amountDueDisplay": "1000.00",
    "dueDate": "2026-07-15",
    "lineItems": [
      { "id": 9001, "description": "Setup fee",   "qty": 1, "unitAmountMinor": 50000, "lineTotalMinor": 50000 },
      { "id": 9002, "description": "Monthly plan", "qty": 2, "unitAmountMinor": 25000, "lineTotalMinor": 50000 }
    ],
    "branding": {
      "logoUrl": "https://cdn.example.com/acme/logo.png",
      "companyName": "Acme Bangladesh Ltd.",
      "companyAddress": "House 12, Road 5, Banani, Dhaka 1213",
      "contactDetails": "[email protected] • +880 1700 000000",
      "footerNote": "Thank you for your business.",
      "accentColor": "#1A73E8",
      "createdAt": "2026-06-30T12:00:00",
      "updatedAt": "2026-06-30T12:10:00"
    },
    "paymentStatus": "PENDING",
    "checkoutUrl": "http://localhost:3000/checkout/pay_7f3a9c2e"
  },
  "meta": { "success": true, "message": null, "errorCode": null, "timestamp": "2026-06-30T12:06:00Z" },
  "pagination": null
}

PayableInvoiceViewDto

The DTO is serialized NON_NULL — any field that is null is omitted entirely (notably dueDate when the invoice has no due date, and the optional branding fields).

FieldTypeRequiredNotes
invoiceNumberstringYesThe invoice's per-app number, e.g. INV-000001.
statusstringYesThe invoice status: one of DRAFT, ISSUED, PARTIALLY_PAID, PAID, VOID, OVERDUE.
currencystringYesISO-4217 code; always BDT in v1.
totalMinorinteger (minor units)YesThe invoice total — sum of lineTotalMinor across the lines.
amountPaidMinorinteger (minor units)YesRunning total already settled by payments; 0 until the first payment lands.
amountDueMinorinteger (minor units)YesOutstanding balance = totalMinor − amountPaidMinor. This is exactly what following the link charges (see below).
amountDueDisplaystringYesamountDueMinor rendered in major units for display only, e.g. "1000.00" for 100000. No currency symbol, no grouping; computed with integer math, never floating point.
dueDatestring (ISO date)Noe.g. 2026-07-15. Omitted when the invoice has no due date.
lineItems[]arrayYesThe billed lines (see below).
brandingobjectYesYour per-app branding to apply to the page (see below). Always present, with sensible defaults.
paymentStatusstringYesThe lifecycle status of the payable payment backing this link: one of CREATED, PENDING, SUCCEEDED, FAILED, CANCELLED, EXPIRED, PARTIALLY_REFUNDED, REFUNDED. Distinct from the invoice status.
checkoutUrlstringYesThe service-hosted checkout URL the customer proceeds to in order to pay.

There is no customerReference, customer name, email, phone, applicationId, mode, metadata, or any internal id on this DTO. The view is a deliberate customer-safe subset of the full InvoiceDto — see Privacy below.

Line item — InvoiceLineItemDto

FieldTypeRequiredNotes
idnumberYesLine item id.
descriptionstringYesThe billed item.
qtyintegerYesQuantity.
unitAmountMinorinteger (minor units)YesPrice per unit.
lineTotalMinorinteger (minor units)YesunitAmountMinor × qty for this line.

In the example above: 50000 + (2 × 25000) = 100000 — i.e. 1,000.00 BDT.

Branding — InvoiceTemplateDto

Your per-app branding template (shared across TEST and LIVE — branding does not differ by mode), projected onto the page. Serialized NON_NULL, so optional fields you never set are simply absent.

FieldTypeRequiredNotes
logoUrlstringNoMerchant logo URL; omitted when unset.
companyNamestringYesHeader company name. Defaulted to your app's name when your template leaves it blank, so it is always present.
companyAddressstringNoPostal address; omitted when unset.
contactDetailsstringNoContact line (email/phone/web); omitted when unset.
footerNotestringNoFree-text footer note; omitted when unset.
accentColorstringYesAccent colour as a #RRGGBB hex string. Defaulted to #111827 (a neutral dark) when your template has no colour, so it is always present.
createdAt / updatedAtstring (date-time)NoTemplate audit timestamps; omitted when you have no saved template.

Branding defaults

The page never fails for lack of branding. If your app has no saved template (or a blank field), the server fills sensible defaults so the customer still sees a coherent invoice:

  • companyName falls back to your app name.
  • accentColor falls back to #111827 — the shared neutral default used by both the web view and the PDF renderer.
  • Every other branding field is simply omitted when unset.

So an app that has never called PUT /invoices/template still gets a branded view:

{
  "data": {
    "invoiceNumber": "INV-000007",
    "status": "ISSUED",
    "currency": "BDT",
    "totalMinor": 25000,
    "amountPaidMinor": 0,
    "amountDueMinor": 25000,
    "amountDueDisplay": "250.00",
    "lineItems": [
      { "id": 9100, "description": "API credits", "qty": 1, "unitAmountMinor": 25000, "lineTotalMinor": 25000 }
    ],
    "branding": {
      "companyName": "Acme Sandbox App",
      "accentColor": "#111827"
    },
    "paymentStatus": "PENDING",
    "checkoutUrl": "http://localhost:3000/checkout/pay_55de0a18"
  },
  "meta": { "success": true, "message": null, "errorCode": null, "timestamp": "2026-06-30T12:06:00Z" },
  "pagination": null
}

Note dueDate is absent (this invoice has none), and the branding object carries only the two defaulted fields.

amountDueMinor is the invoice's live outstanding balancetotalMinor − amountPaidMinor — and it is exactly the amount the customer is charged by following checkoutUrl. The payable link was minted for the outstanding balance at link time, so the page and the charge agree.

This matters for installment invoices. After a partial payment, the same view reflects the reduced balance:

{
  "data": {
    "invoiceNumber": "INV-000001",
    "status": "PARTIALLY_PAID",
    "currency": "BDT",
    "totalMinor": 100000,
    "amountPaidMinor": 40000,
    "amountDueMinor": 60000,
    "amountDueDisplay": "600.00",
    "paymentStatus": "PENDING",
    "checkoutUrl": "http://localhost:3000/checkout/pay_91b4c7f0"
  },
  "meta": { "success": true, "message": null, "errorCode": null, "timestamp": "2026-06-30T13:00:00Z" },
  "pagination": null
}

amountDueMinor is the invoice's outstanding total, while checkoutUrl settles the specific payment this reference points to. Each installment is a separate payable link minted from the balance at that moment — always send the customer the freshest link rather than reusing an old reference.

Relationship to hosted checkout

The payable page is the branded shell; the hosted checkout is where money moves. They share the same opaque reference:

Branded view — /pay/{reference}

The customer reads this endpoint's JSON and sees the itemized, branded invoice. Read-only.

Hosted checkout — checkoutUrl

Clicking "Pay" sends them to checkoutUrl (…/checkout/{reference}), which forwards to the SSLCOMMERZ gateway page. The settlement callback applies the payment to the invoice and advances amountPaidMinor / status.

Because both surfaces resolve the same reference, re-reading /pay/{reference} after a payment shows the updated status, amountPaidMinor, and amountDueMinor.

Privacy: no PII

This view is the customer-safe projection by design. Compared with the full InvoiceDto, it omits:

  • Customer identity — no customerReference, name, email, or phone.
  • App internals — no applicationId, mode, internal invoice id, metadata, or the settling payments[] array.

Only the invoice number, line items, totals, branding, the two status strings, and the checkout URL are exposed. Nothing that identifies the customer or your account leaves the boundary, which is what makes it safe to serve over an unauthenticated, reference-keyed URL.

Errors

A single failure mode: 404.

curl -i http://localhost:8080/api/v1/pay/pay_unknown
{
  "data": null,
  "meta": {
    "success": false,
    "message": "Payable invoice not found with id: pay_unknown",
    "errorCode": "RESOURCE_NOT_FOUND",
    "timestamp": "2026-06-30T12:06:00Z"
  },
  "pagination": null
}

The same 404 RESOURCE_NOT_FOUND is returned in both of these cases — the endpoint never distinguishes them, so an unguessable reference can't be probed for existence:

ConditionStatuserrorCode
The reference resolves to no payment (unknown / mistyped)404RESOURCE_NOT_FOUND
The reference resolves to a payment that settles no invoice (a standalone payment with invoiceId = null)404RESOURCE_NOT_FOUND

In other words, the view only exists for a payment that was minted as an invoice payable link. See responses & errors for the envelope shape.

On this page