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_7f3a9c2eNo 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).
| Field | Type | Required | Notes |
|---|---|---|---|
invoiceNumber | string | Yes | The invoice's per-app number, e.g. INV-000001. |
status | string | Yes | The invoice status: one of DRAFT, ISSUED, PARTIALLY_PAID, PAID, VOID, OVERDUE. |
currency | string | Yes | ISO-4217 code; always BDT in v1. |
totalMinor | integer (minor units) | Yes | The invoice total — sum of lineTotalMinor across the lines. |
amountPaidMinor | integer (minor units) | Yes | Running total already settled by payments; 0 until the first payment lands. |
amountDueMinor | integer (minor units) | Yes | Outstanding balance = totalMinor − amountPaidMinor. This is exactly what following the link charges (see below). |
amountDueDisplay | string | Yes | amountDueMinor 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. |
dueDate | string (ISO date) | No | e.g. 2026-07-15. Omitted when the invoice has no due date. |
lineItems[] | array | Yes | The billed lines (see below). |
branding | object | Yes | Your per-app branding to apply to the page (see below). Always present, with sensible defaults. |
paymentStatus | string | Yes | The 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. |
checkoutUrl | string | Yes | The 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
| Field | Type | Required | Notes |
|---|---|---|---|
id | number | Yes | Line item id. |
description | string | Yes | The billed item. |
qty | integer | Yes | Quantity. |
unitAmountMinor | integer (minor units) | Yes | Price per unit. |
lineTotalMinor | integer (minor units) | Yes | unitAmountMinor × 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.
| Field | Type | Required | Notes |
|---|---|---|---|
logoUrl | string | No | Merchant logo URL; omitted when unset. |
companyName | string | Yes | Header company name. Defaulted to your app's name when your template leaves it blank, so it is always present. |
companyAddress | string | No | Postal address; omitted when unset. |
contactDetails | string | No | Contact line (email/phone/web); omitted when unset. |
footerNote | string | No | Free-text footer note; omitted when unset. |
accentColor | string | Yes | Accent colour as a #RRGGBB hex string. Defaulted to #111827 (a neutral dark) when your template has no colour, so it is always present. |
createdAt / updatedAt | string (date-time) | No | Template 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:
companyNamefalls back to your app name.accentColorfalls 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 what the link charges
amountDueMinor is the invoice's live outstanding balance — totalMinor − 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 invoiceid,metadata, or the settlingpayments[]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:
| Condition | Status | errorCode |
|---|---|---|
The reference resolves to no payment (unknown / mistyped) | 404 | RESOURCE_NOT_FOUND |
The reference resolves to a payment that settles no invoice (a standalone payment with invoiceId = null) | 404 | RESOURCE_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.
Related
Invoicing
Create invoices and mint the payable link this page renders.
Accept a payment
The hosted checkout the customer is sent to via checkoutUrl.
Money & amounts
Why every amount is integer minor units.
App & mode isolation
How the reference scopes the view to one app and mode.
Responses & errors
The standard envelope and error codes.
Admin · Invoices
The cross-app invoice console.
Invoicing
Create itemized invoices, issue them with a per-app number, mint a hosted payable link, brand the PDF, and track installments through to paid.
Refunds (app API)
Issue full or partial refunds against a settled payment, understand the over-refund guard, and track the async outcome through webhooks.