API và webhook
The Inolab API connects the customers, invoices and payments in Inolab with other systems, such as an online store, a point-of-sale app or accounting software. All requests and responses are JSON over HTTPS.
Base URL: https://app.inolab.id/api/v1 · OpenAPI 3.1 specification: openapi.yaml
Authentication
Create an API key in the app under Settings › Integrations. Only business owners and admins can create keys. A key is shown once; store it somewhere safe and never put it in code that runs in a browser or mobile app.
Send the key in the Authorization header of every request:
curl "https://app.inolab.id/api/v1/invoices?status=overdue" \
-H "Authorization: Bearer 12|inolab_xxxxxxxxxxxxxxxxxxxx" \
-H "Accept: application/json"
- A key works for one business, the one it was created in. The
X-Tenant-Idheader is not needed. - A key acts on behalf of the person who created it. It is limited to the scopes chosen and never exceeds that person's role. If they leave the business, the key stops working.
- Keys can be revoked at any time from the same page.
API key scopes
| Scope | Allows |
|---|---|
customers:read | Xem khách hàng |
customers:write | Thêm, sửa và xóa khách hàng |
invoices:read | Xem hóa đơn, hóa đơn định kỳ và tóm tắt |
invoices:write | Tạo, sửa, phát hành và hủy hóa đơn; quản lý hóa đơn định kỳ |
payments:write | Ghi nhận và xóa thanh toán hóa đơn |
quotes:read | Xem báo giá |
quotes:write | Tạo, gửi và ghi nhận phản hồi báo giá; chuyển thành hóa đơn |
expenses:read | Xem chi phí |
expenses:write | Ghi, sửa và xóa chi phí |
projects:read | Xem dự án và tóm tắt |
projects:write | Tạo, sửa và xóa dự án |
products:read | Xem danh mục sản phẩm và dịch vụ |
products:write | Thêm, sửa và gỡ các mục danh mục |
deals:read | Xem cơ hội |
deals:write | Tạo, sửa, chuyển giai đoạn và xóa cơ hội |
activities:read | Xem hoạt động khách hàng |
activities:write | Ghi nhận hoạt động và hoàn tất việc theo dõi |
reports:read | Xem báo cáo tài chính |
agents:read | Xem tác tử và các lượt chạy |
agents:run | Chạy và hủy tác tử (chỉ con người mới phê duyệt, trên web hoặc ứng dụng) |
usage:read | Xem mức dùng AI và chi phí |
audit_log:read | Xem nhật ký hoạt động |
Account, subscription and notification endpoints are for the Inolab apps only and cannot be used with an API key.
Endpoints
| Method | Path | Scope | Description |
|---|---|---|---|
GET |
/customers |
customers:read |
Danh sách khách hàng; ?q=, ?type=company|individual, ?per_page= (tối đa 100) |
GET |
/customers/{id} |
customers:read |
Một khách hàng |
POST |
/customers |
customers:write |
Thêm khách hàng |
PUT |
/customers/{id} |
customers:write |
Sửa khách hàng |
DELETE |
/customers/{id} |
customers:write |
Xóa khách hàng không có hóa đơn đang mở |
GET |
/invoices |
invoices:read |
Danh sách hóa đơn; ?status=draft|open|overdue|paid|void, ?q=, ?customer_id= |
GET |
/invoices/{id} |
invoices:read |
Một hóa đơn kèm các mặt hàng và thanh toán |
GET |
/invoices/{id}/pdf |
invoices:read |
PDF hóa đơn (application/pdf) |
GET |
/dashboard |
invoices:read |
Số liệu công nợ và hóa đơn quá hạn |
POST |
/invoices |
invoices:write |
Tạo bản nháp hóa đơn; tổng tiền do máy chủ tính |
PUT |
/invoices/{id} |
invoices:write |
Sửa một bản nháp |
DELETE |
/invoices/{id} |
invoices:write |
Xóa bản nháp |
POST |
/invoices/{id}/send |
invoices:write |
Phát hành; email_customer=true để gửi email |
POST |
/invoices/{id}/void |
invoices:write |
Hủy hóa đơn chưa có thanh toán; reason không bắt buộc |
POST |
/invoices/{id}/payments |
payments:write |
Ghi nhận thanh toán: amount, paid_on, method, reference, notes |
DELETE |
/invoices/{id}/payments/{paymentId} |
payments:write |
Xóa bản ghi thanh toán |
GET |
/recurring-invoices |
invoices:read |
Lịch hóa đơn định kỳ; ?status=active|paused|ended, ?customer_id= |
GET |
/recurring-invoices/{id} |
invoices:read |
Một lịch kèm ba ngày tiếp theo |
POST |
/recurring-invoices |
invoices:write |
Tạo lịch: title, customer_id, interval=month|year, interval_count, next_issue_on, due_days, send_automatically, items |
PUT |
/recurring-invoices/{id} |
invoices:write |
Sửa một lịch; các hóa đơn đã tạo không thay đổi |
POST |
/recurring-invoices/{id}/pause |
invoices:write |
Tạm dừng; /resume để tiếp tục |
DELETE |
/recurring-invoices/{id} |
invoices:write |
Xóa lịch |
GET |
/quotes |
quotes:read |
Báo giá; ?status=draft|sent|expired|accepted|declined, ?customer_id=, ?deal_id= |
GET |
/quotes/{id} |
quotes:read |
Một báo giá kèm các mặt hàng |
GET |
/quotes/{id}/pdf |
quotes:read |
PDF báo giá (application/pdf) |
POST |
/quotes |
quotes:write |
Tạo bản nháp báo giá: customer_id, issue_date, valid_until, items |
PUT |
/quotes/{id} |
quotes:write |
Sửa một bản nháp báo giá |
POST |
/quotes/{id}/send |
quotes:write |
Gửi; email_customer=true để gửi email kèm PDF |
POST |
/quotes/{id}/accept |
quotes:write |
Ghi nhận chấp nhận; /decline kèm reason để ghi nhận từ chối |
POST |
/quotes/{id}/convert |
quotes:write |
Chuyển thành bản nháp hóa đơn một lần; cũng cần invoices:write |
DELETE |
/quotes/{id} |
quotes:write |
Xóa bản nháp báo giá |
GET |
/expenses |
expenses:read |
Danh sách chi phí; ?from=, ?to=, ?category=, ?q= |
GET |
/expenses/{id} |
expenses:read |
Một chi phí |
POST |
/expenses |
expenses:write |
Ghi chi phí: spent_on, category, description, amount, tax_amount, method |
PUT |
/expenses/{id} |
expenses:write |
Sửa chi phí |
DELETE |
/expenses/{id} |
expenses:write |
Xóa chi phí |
GET |
/projects |
projects:read |
Danh sách dự án; ?status=open|all|..., ?customer_id=, ?q= |
GET |
/projects/{id} |
projects:read |
Một dự án kèm tóm tắt số đã lập hóa đơn, chi phí và biên lợi nhuận |
POST |
/projects |
projects:write |
Tạo dự án: customer_id, name, status, contract_value |
PUT |
/projects/{id} |
projects:write |
Sửa dự án |
DELETE |
/projects/{id} |
projects:write |
Xóa dự án không có hóa đơn hoặc chi phí |
GET |
/products |
products:read |
Danh mục sản phẩm và dịch vụ; ?type=service|goods, ?q= |
GET |
/products/{id} |
products:read |
Một mục danh mục |
POST |
/products |
products:write |
Thêm vào danh mục: type, name, sku, unit, unit_price |
PUT |
/products/{id} |
products:write |
Sửa một mục danh mục; các hóa đơn hiện có không thay đổi |
DELETE |
/products/{id} |
products:write |
Gỡ khỏi danh mục |
GET |
/deals |
deals:read |
Danh sách cơ hội; ?stage=open|won|..., ?customer_id=, ?owner_id= |
GET |
/deals/{id} |
deals:read |
Một cơ hội |
POST |
/deals |
deals:write |
Tạo cơ hội: customer_id, title, stage, value |
PUT |
/deals/{id} |
deals:write |
Sửa cơ hội |
POST |
/deals/{id}/stage |
deals:write |
Chuyển giai đoạn; lost_reason khi thua |
DELETE |
/deals/{id} |
deals:write |
Xóa cơ hội |
GET |
/activities |
activities:read |
Danh sách hoạt động; ?customer_id=, ?deal_id=, ?follow_up=due |
POST |
/customers/{id}/activities |
activities:write |
Ghi nhận hoạt động: type, happened_on, body, follow_up_on |
POST |
/activities/{id}/complete |
activities:write |
Đánh dấu việc theo dõi là hoàn tất |
DELETE |
/activities/{id} |
activities:write |
Xóa hoạt động |
GET |
/reports/aging |
reports:read |
Tuổi nợ phải thu theo khách hàng; ?as_of= |
GET |
/reports/revenue |
reports:read |
Đã lập hóa đơn và đã thu theo tháng; ?year= |
GET |
/reports/cash-flow |
reports:read |
Tiền vào và ra theo tháng; ?year= |
GET |
/reports/profit-and-loss |
reports:read |
Lãi lỗ đơn giản; ?year=, ?month= |
GET |
/reports/vat |
reports:read |
VAT đầu ra và đầu vào theo tháng; ?year= |
GET |
/reports/quotes |
reports:read |
Báo giá đã gửi và được chấp nhận theo tháng, tỷ lệ chấp nhận; ?year= |
GET |
/reports/recurring-revenue |
reports:read |
Doanh thu định kỳ quy đổi theo tháng và hóa đơn trong 90 ngày tới |
GET |
/agents |
agents:read |
Các tác tử bạn được chạy, đầu vào của chúng và AI đã sẵn sàng hay chưa |
POST |
/agents/{agent}/runs |
agents:run |
Chạy tác tử; gửi header Idempotency-Key để thử lại an toàn |
GET |
/agent-runs |
agents:read |
Lượt chạy tác tử; ?status=, ?agent= |
GET |
/agent-runs/{id} |
agents:read |
Một lượt chạy tác tử kèm lịch sử trạng thái; phê duyệt liên kết tới ứng dụng web |
POST |
/agent-runs/{id}/cancel |
agents:run |
Hủy một lượt chạy tác tử |
Adding a customer:
curl -X POST "https://app.inolab.id/api/v1/customers" \
-H "Authorization: Bearer 12|inolab_xxxxxxxxxxxxxxxxxxxx" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"type": "company", "name": "PT Maju Bersama", "email": "keuangan@majubersama.co.id"}'
- Money is whole Rupiah without decimals, for example
5550000. - Dates use
YYYY-MM-DD; times use ISO 8601 with a time zone. - Invoice tax rates:
0,11or12percent. Numbers, subtotal, tax and total are calculated by the server. - Payment methods:
bank_transfer,qris,ewallet,cash,card,other.
Responses and errors
Every response uses the same envelope. Paginated lists include meta.pagination.
{
"success": true,
"message": "Permintaan berhasil.",
"data": [ ... ],
"meta": { "pagination": { "current_page": 1, "per_page": 25, "total": 42, "last_page": 2 } }
}
{
"success": false,
"message": "Kunci API ini tidak memiliki izin untuk tindakan ini.",
"code": "missing_scope"
}
| Status | Meaning |
|---|---|
401 | The key is missing, wrong, revoked or expired. |
403 | The key's scopes are not enough (code: missing_scope), or the key creator's role does not allow it. |
404 | The record does not exist or belongs to another business. |
422 | Invalid data; details per field in errors. |
429 | Too many requests. The limit is 120 requests a minute per user; wait as long as the Retry-After header says. |
Send Accept-Language: en for messages in English. The default is Indonesian.
Webhooks
Webhooks tell your system when data changes, so you do not have to poll the API. Add a receiving URL under Settings › Integrations and choose the events to receive. The URL must be public HTTPS on port 443; internal network addresses are refused.
| Event | Sent when |
|---|---|
customer.created | Đã thêm khách hàng |
customer.updated | Đã cập nhật khách hàng |
customer.deleted | Đã xóa khách hàng |
invoice.created | Đã tạo bản nháp hóa đơn |
invoice.updated | Đã cập nhật bản nháp hóa đơn |
invoice.sent | Đã phát hành hóa đơn |
invoice.paid | Hóa đơn đã thanh toán |
invoice.voided | Đã hủy hóa đơn |
invoice.deleted | Đã xóa bản nháp hóa đơn |
invoice.payment_recorded | Đã ghi nhận thanh toán hóa đơn |
invoice.payment_deleted | Đã xóa bản ghi thanh toán |
quote.created | Đã tạo bản nháp báo giá |
quote.updated | Đã sửa bản nháp báo giá |
quote.sent | Đã gửi báo giá |
quote.accepted | Khách hàng đã chấp nhận báo giá |
quote.declined | Khách hàng đã từ chối báo giá |
quote.deleted | Đã xóa bản nháp báo giá |
deal.created | Đã tạo cơ hội |
deal.updated | Đã sửa cơ hội |
deal.stage_changed | Giai đoạn cơ hội thay đổi (kể cả thắng hoặc thua) |
deal.deleted | Đã xóa cơ hội |
project.created | Đã tạo dự án |
project.updated | Đã sửa dự án |
project.deleted | Đã xóa dự án |
Each event is sent as a JSON POST:
POST /webhooks/inolab HTTP/1.1
Content-Type: application/json
User-Agent: Inolab-Webhooks/1.0
Inolab-Event: invoice.paid
Inolab-Event-Id: evt_01k7c3m0q4f9w2v8t6r5y1x3za
Inolab-Delivery: 01k7c3m0r8a2b4c6d8e0f2g4h6
Inolab-Signature: t=1791520200,v1=5f2b9c…
{
"id": "evt_01k7c3m0q4f9w2v8t6r5y1x3za",
"type": "invoice.paid",
"created_at": "2026-10-09T14:30:00+07:00",
"tenant_id": "01k6xq5a9d3m7p2r4t6v8x0z2b",
"data": {
"id": "01k7a0b2c4d6e8f0g2h4j6k8m0",
"number": "INV/2026/0042",
"status": "paid",
"customer": { "id": "01k6…", "name": "PT Maju Bersama", "email": "keuangan@majubersama.co.id" },
"issue_date": "2026-10-01",
"due_date": "2026-10-15",
"currency": "IDR",
"subtotal": 5000000,
"tax_rate": 11,
"tax_amount": 550000,
"total": 5550000,
"amount_paid": 5550000,
"balance_due": 0,
"items": [ { "product_id": null, "description": "Jasa desain kemasan", "quantity": 1, "unit": "paket", "unit_price": 5000000, "amount": 5000000 } ],
"payments": [ ... ],
"paid_at": "2026-10-09T14:30:00+07:00"
}
}
dataholds an object shaped like the API response. Payment events containdata.paymentanddata.invoice.- Reply with a
2xxstatus within 10 seconds. Any other status, a timeout or a redirect counts as a failure. - Failed deliveries are retried up to 5 times over about 9 hours, then marked failed. You can see and resend them in the app.
- An event may arrive more than once and order is not guaranteed. Use the event
id(Inolab-Event-Id) to ignore duplicates. - The Send test button sends a
pingevent.
Verifying signatures
The Inolab-Signature header holds the send time (t, Unix seconds) and v1, an HMAC-SHA256 of t, a dot and the raw body, keyed with the webhook's signing secret. Reject requests whose signature does not match or whose time is more than 5 minutes away from now.
// $secret: the signing secret shown when the webhook was created.
$body = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_INOLAB_SIGNATURE'] ?? ''), $signature);
$expected = hash_hmac('sha256', ($signature['t'] ?? '').'.'.$body, $secret);
$fresh = abs(time() - (int) ($signature['t'] ?? 0)) <= 300;
if (! $fresh || ! hash_equals($expected, $signature['v1'] ?? '')) {
http_response_code(400);
exit;
}
$event = json_decode($body, true);
// Use $event['id'] to ignore an event you have already handled.
http_response_code(200);
import crypto from 'node:crypto';
// rawBody: the request body exactly as received (a string, not parsed JSON).
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((part) => part.split('=')));
const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
return fresh
&& typeof parts.v1 === 'string'
&& parts.v1.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}
The signing secret is shown once when the webhook is created. If it leaks, create a new secret from the webhook page; the old one stops working immediately.
OAuth for third-party apps
If your app is used by many businesses, ask for access through OAuth 2.0 (authorization code; PKCE S256 recommended) instead of asking them to copy an API key. Register the app under Settings › Integrations › OAuth apps to get a client ID and client secret.
- Send the person to
https://app.inolab.id/oauth/authorize?response_type=code&client_id=…&redirect_uri=…&scope=customers:read invoices:read&state=…&code_challenge=…&code_challenge_method=S256. - An owner or admin picks a business and approves the scopes. Inolab returns to
redirect_uriwithcodeandstate, orerror=access_denied. - From your server, exchange the code (valid 10 minutes, single use) at
POST https://app.inolab.id/oauth/tokenwithgrant_type=authorization_code,code,redirect_uri,code_verifierand the client credentials (HTTP Basic orclient_id/client_secretin the body). - The response holds an
access_token(Bearer) valid for 365 days for one business with the approved scopes, used exactly like an API key. The business can revoke it at any time.
Not available yet
- Official SDKs and OAuth refresh tokens. Use openapi.yaml to generate a client.
Technical questions: support@inolab.id.