Partner Portal - Invoice API Contract
Base URL: /api/partner/v1
Authentication: Bearer token (JWT via Knock or Doorkeeper OAuth)
Required Header: Authorization: Bearer <token>
Authorization: All endpoints require the authenticated staff to have owner role for the restaurant. Staff with admin, contributor, viewer, or staff roles will receive 403 Forbidden.
Common Error Responses
| Status | Description |
|---|---|
401 Unauthorized | Missing or invalid Bearer token |
403 Forbidden | Authenticated staff does not have owner role for the restaurant |
1. List Invoices
GET /invoices
Returns a paginated list of invoices for the restaurant, ordered by latest billing period. Includes total outstanding balance.
Query Parameters
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| page | integer | No | 1 | Page number |
| per_page | integer | No | 15 | Items per page |
| status | string | No | — | Filter by status: pending, partial, paid, cancelled |
| restaurant_id | integer | No | — | Filter by specific restaurant (for multi-branch staff) |
| sort_by | string | No | billing_period | Column to sort by. Allowed values: billing_period, external_invoice_code, issue_date, due_date, total_amount, status |
| sort_direction | string | No | desc | Sort direction. Allowed values: asc, desc |
Note: Invalid
sort_byorsort_directionvalues are silently ignored and fall back to the defaults (billing_period desc).
Response 200 OK
{
"data": [
{
"id": "123",
"type": "accounting_invoice",
"attributes": {
"external_invoice_id": "INV-500",
"external_invoice_code": "INV001",
"billing_period": "2026-05-01",
"issue_date": "2026-05-01",
"due_date": "2026-06-01",
"status": "pending",
"total_amount": "3000.0",
"remain_amount": "3000.0",
"currency": "THB",
"document_url": "https://doc.peaksandbox.com/invoice?emi=MjkxNQ%3D%3D&eti=MTc5NTU0",
"created_at": "2026-05-01T10:00:00.000Z",
"restaurant_id": 456,
"restaurant_name": "My Restaurant",
"is_overdue": false,
"total_amount_format": "฿3,000",
"remain_amount_format": "฿3,000"
}
}
],
"meta": {
"total": 25,
"page": 1
},
"links": {
"self": "/api/partner/v1/invoices?page=1",
"first": "/api/partner/v1/invoices?page=1",
"next": "/api/partner/v1/invoices?page=2",
"prev": null,
"last": "/api/partner/v1/invoices?page=3"
},
"outstanding": {
"total_balance": 4500.0,
"total_balance_format": "฿4,500",
"currency": "THB"
}
}
2. Invoice Detail
GET /invoices/:id
Returns detail of a single invoice.
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Invoice ID |
Response 200 OK
{
"data": {
"id": "123",
"type": "accounting_invoice",
"attributes": {
"external_invoice_id": "INV-500",
"external_invoice_code": "INV001",
"billing_period": "2026-05-01",
"issue_date": "2026-05-01",
"due_date": "2026-06-01",
"status": "pending",
"total_amount": "3000.0",
"remain_amount": "3000.0",
"currency": "THB",
"document_url": "https://doc.peaksandbox.com/invoice?emi=MjkxNQ%3D%3D&eti=MTc5NTU0",
"created_at": "2026-05-01T10:00:00.000Z",
"restaurant_id": 456,
"restaurant_name": "My Restaurant",
"is_overdue": false,
"total_amount_format": "฿3,000",
"remain_amount_format": "฿3,000"
}
}
}
Response 404 Not Found
{
"data": null,
"success": false,
"message": "Invoice not found"
}
3. Invoice by Period
GET /invoices/period_detail
Returns the invoice for a specific billing period (year & month).
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
| year | integer | Yes | Billing year (e.g. 2026) |
| month | integer | Yes | Billing month (1-12) |
| restaurant_id | integer | No | Filter by specific restaurant |
Response 200 OK (invoice found)
{
"data": {
"id": "123",
"type": "accounting_invoice",
"attributes": {
"external_invoice_id": "INV-500",
"external_invoice_code": "INV001",
"billing_period": "2026-05-01",
"issue_date": "2026-05-01",
"due_date": "2026-06-01",
"status": "paid",
"total_amount": "15000.0",
"remain_amount": "0.0",
"currency": "THB",
"document_url": "https://doc.peaksandbox.com/invoice?emi=MjkxNQ%3D%3D&eti=MTc5NTU0",
"created_at": "2026-05-01T10:00:00.000Z",
"restaurant_id": 456,
"restaurant_name": "My Restaurant",
"is_overdue": false,
"total_amount_format": "฿15,000",
"remain_amount_format": "฿0"
}
}
}
Response 200 OK (no invoice for period)
{
"data": null
}
Response 422 Unprocessable Entity
{
"success": false,
"message": "Invalid year or month"
}
4. Overdue Alert
GET /invoices/overdue_alert
Returns overdue payment alert information. Use this on the dashboard to show a warning banner.
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
| restaurant_id | integer | No | Filter by specific restaurant |
Response 200 OK (has overdue)
{
"data": {
"has_overdue": true,
"type": "warning",
"message": "Payment Overdue: You have an outstanding invoice of ฿4,500 due on May 14, 2026. Please settle this balance to avoid service interruption.",
"invoice_id": 123,
"document_url": "https://doc.peaksandbox.com/invoice?emi=MjkxNQ%3D%3D&eti=MTc5NTU0"
}
}
Response 200 OK (no overdue)
{
"data": {
"has_overdue": false,
"type": null,
"message": null,
"invoice_id": null,
"document_url": null
}
}
5. Refresh Payment Status
POST /invoices/:id/refresh_payment_status
Manually re-fetches the payment status for a specific invoice from the upstream accounting provider (PEAK, etc.) and enqueues a background job to update it.
When to use: A restaurant partner pays their invoice in the PEAK dashboard and wants to see the updated status immediately, without waiting for the nightly sync that runs at 09:00 ICT.
The update is asynchronous — call GET /invoices/:id a few seconds after this request to see the refreshed status and remain_amount.
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Invoice ID |
Request
No request body required.
Response 200 OK
{
"success": true,
"message": "Payment status refresh has been queued. Please check back shortly."
}
Response 404 Not Found
Returned when the :id does not exist or belongs to a different restaurant.
{
"success": false,
"message": "Invoice not found"
}
Response 422 Unprocessable Entity
Returned when the invoice has not been submitted to PEAK yet (no external_invoice_id).
{
"success": false,
"message": "This invoice has not been synced to the accounting provider yet."
}
Response 500 Internal Server Error
{
"success": false,
"message": "Failed to queue payment status refresh. Please try again."
}
Data Types Reference
Invoice Status
| Value | Description |
|---|---|
pending | Invoice created, no payment received |
partial | Partial payment received |
paid | Fully paid |
cancelled | Invoice cancelled |
Invoice Attributes
| Attribute | Type | Description |
|---|---|---|
id | string | Invoice ID |
external_invoice_id | string | Peak invoice ID |
external_invoice_code | string | Peak invoice code (e.g. “INV001”) |
billing_period | string | First day of billing month (YYYY-MM-DD) |
issue_date | string | Date invoice was issued (YYYY-MM-DD) |
due_date | string | Payment due date (YYYY-MM-DD) |
status | string | One of: pending, partial, paid, cancelled |
total_amount | string | Total invoice amount (decimal as string) |
remain_amount | string | Remaining unpaid amount (decimal as string) |
currency | string | Currency code (e.g. “THB”) |
document_url | string | PEAK invoice view URL (null if not yet synced) |
created_at | string | ISO 8601 timestamp |
restaurant_id | integer | Restaurant ID |
restaurant_name | string | Restaurant name |
is_overdue | boolean | Whether invoice is past due and unpaid |
total_amount_format | string | Formatted total amount (e.g. “฿15,000”) |
remain_amount_format | string | Formatted remaining amount (e.g. “฿3,000”) |
Usage Notes
- Outstanding balance (
total_balance) is calculated fromremain_amountof allpending+partialinvoices. - Overdue means
due_date < todayAND status ispendingorpartial. invoice_idin overdue alert points to the oldest overdue invoice — use it to navigate to invoice detail viaGET /invoices/:id.document_urlin overdue alert is the PEAK invoice view URL. Open it directly in a browser tab.nullwhen there is no overdue invoice or invoice is not yet synced to PEAK.document_urlon invoice attributes is the PEAK-hosted invoice page. Open it directly — no additional API call needed.- Refresh Payment Status (
POST /invoices/:id/refresh_payment_status) is asynchronous — pollGET /invoices/:idafter a few seconds to confirm the updated status. - All endpoints are scoped to the authenticated staff’s restaurants.
- Owner-only access: All endpoints require the staff to have
ownerrole (viastaff_rolestable) for the restaurant. Other roles (admin,contributor,viewer,staff) receive403 Forbidden. Admin panel OAuth (partner-portal-adminDoorkeeper token) bypasses this check. - Sorting by status: Use
sort_by=statusto group invoices by their payment status alphabetically.