API dan 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://api.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://api.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 | Lihat pelanggan |
customers:write | Tambah, ubah dan padam pelanggan |
invoices:read | Lihat invois, invois berulang dan ringkasan |
invoices:write | Buat, ubah, keluarkan dan batalkan invois; urus invois berulang |
payments:write | Rekod dan padam pembayaran invois |
quotes:read | Lihat sebut harga |
quotes:write | Buat, hantar dan rekod jawapan sebut harga; jadikannya invois |
expenses:read | Lihat perbelanjaan |
expenses:write | Rekod, ubah dan padam perbelanjaan |
projects:read | Lihat projek dan ringkasannya |
projects:write | Buat, ubah dan padam projek |
products:read | Lihat katalog produk dan perkhidmatan |
products:write | Tambah, ubah dan buang entri katalog |
deals:read | Lihat peluang |
deals:write | Buat, ubah, alihkan peringkat dan padam peluang |
activities:read | Lihat aktiviti pelanggan |
activities:write | Rekod aktiviti dan selesaikan susulan |
reports:read | Lihat laporan kewangan |
agents:read | Lihat ejen dan larian mereka |
agents:run | Mulakan dan batalkan ejen (kelulusan hanya oleh manusia, di web atau aplikasi) |
usage:read | Lihat penggunaan AI dan kosnya |
audit_log:read | Baca log aktiviti |
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 |
Senarai pelanggan; ?q=, ?type=company|individual, ?per_page= (maks. 100) |
GET |
/customers/{id} |
customers:read |
Satu pelanggan |
POST |
/customers |
customers:write |
Tambah pelanggan |
PUT |
/customers/{id} |
customers:write |
Ubah pelanggan |
DELETE |
/customers/{id} |
customers:write |
Padam pelanggan tanpa invois terbuka |
GET |
/invoices |
invoices:read |
Senarai invois; ?status=draft|open|overdue|paid|void, ?q=, ?customer_id= |
GET |
/invoices/{id} |
invoices:read |
Satu invois beserta item dan pembayarannya |
GET |
/invoices/{id}/pdf |
invoices:read |
PDF invois (application/pdf) |
GET |
/dashboard |
invoices:read |
Angka belum terima dan invois lewat tarikh akhir |
POST |
/invoices |
invoices:write |
Buat draf invois; jumlah dikira oleh pelayan |
PUT |
/invoices/{id} |
invoices:write |
Ubah draf |
DELETE |
/invoices/{id} |
invoices:write |
Padam draf |
POST |
/invoices/{id}/send |
invoices:write |
Keluarkan; email_customer=true untuk menghantar e-mel |
POST |
/invoices/{id}/void |
invoices:write |
Batalkan invois tanpa pembayaran; reason pilihan |
POST |
/invoices/{id}/payments |
payments:write |
Rekod pembayaran: amount, paid_on, method, reference, notes |
DELETE |
/invoices/{id}/payments/{paymentId} |
payments:write |
Padam rekod pembayaran |
GET |
/recurring-invoices |
invoices:read |
Jadual invois berulang; ?status=active|paused|ended, ?customer_id= |
GET |
/recurring-invoices/{id} |
invoices:read |
Satu jadual beserta tiga tarikh seterusnya |
POST |
/recurring-invoices |
invoices:write |
Buat jadual: title, customer_id, interval=month|year, interval_count, next_issue_on, due_days, send_automatically, items |
PUT |
/recurring-invoices/{id} |
invoices:write |
Ubah jadual; invois yang sudah dibuat tidak berubah |
POST |
/recurring-invoices/{id}/pause |
invoices:write |
Jeda; /resume untuk menyambung |
DELETE |
/recurring-invoices/{id} |
invoices:write |
Padam jadual |
GET |
/quotes |
quotes:read |
Sebut harga; ?status=draft|sent|expired|accepted|declined, ?customer_id=, ?deal_id= |
GET |
/quotes/{id} |
quotes:read |
Satu sebut harga beserta itemnya |
GET |
/quotes/{id}/pdf |
quotes:read |
PDF sebut harga (application/pdf) |
POST |
/quotes |
quotes:write |
Buat draf sebut harga: customer_id, issue_date, valid_until, items |
PUT |
/quotes/{id} |
quotes:write |
Ubah draf sebut harga |
POST |
/quotes/{id}/send |
quotes:write |
Hantar; email_customer=true untuk menghantar e-mel bersama PDF |
POST |
/quotes/{id}/accept |
quotes:write |
Rekod penerimaan; /decline dengan reason untuk merekod penolakan |
POST |
/quotes/{id}/convert |
quotes:write |
Jadikan draf invois sekali; juga memerlukan invoices:write |
DELETE |
/quotes/{id} |
quotes:write |
Padam draf sebut harga |
GET |
/expenses |
expenses:read |
Senarai perbelanjaan; ?from=, ?to=, ?category=, ?q= |
GET |
/expenses/{id} |
expenses:read |
Satu perbelanjaan |
POST |
/expenses |
expenses:write |
Rekod perbelanjaan: spent_on, category, description, amount, tax_amount, method |
PUT |
/expenses/{id} |
expenses:write |
Ubah perbelanjaan |
DELETE |
/expenses/{id} |
expenses:write |
Padam perbelanjaan |
GET |
/projects |
projects:read |
Senarai projek; ?status=open|all|..., ?customer_id=, ?q= |
GET |
/projects/{id} |
projects:read |
Satu projek beserta ringkasan diinvoiskan, perbelanjaan dan margin |
POST |
/projects |
projects:write |
Buat projek: customer_id, name, status, contract_value |
PUT |
/projects/{id} |
projects:write |
Ubah projek |
DELETE |
/projects/{id} |
projects:write |
Padam projek tanpa invois atau perbelanjaan |
GET |
/products |
products:read |
Katalog produk dan perkhidmatan; ?type=service|goods, ?q= |
GET |
/products/{id} |
products:read |
Satu entri katalog |
POST |
/products |
products:write |
Tambah ke katalog: type, name, sku, unit, unit_price |
PUT |
/products/{id} |
products:write |
Ubah entri katalog; invois sedia ada tidak berubah |
DELETE |
/products/{id} |
products:write |
Buang daripada katalog |
GET |
/deals |
deals:read |
Senarai peluang; ?stage=open|won|..., ?customer_id=, ?owner_id= |
GET |
/deals/{id} |
deals:read |
Satu peluang |
POST |
/deals |
deals:write |
Buat peluang: customer_id, title, stage, value |
PUT |
/deals/{id} |
deals:write |
Ubah peluang |
POST |
/deals/{id}/stage |
deals:write |
Tukar peringkat; lost_reason jika kalah |
DELETE |
/deals/{id} |
deals:write |
Padam peluang |
GET |
/activities |
activities:read |
Senarai aktiviti; ?customer_id=, ?deal_id=, ?follow_up=due |
POST |
/customers/{id}/activities |
activities:write |
Rekod aktiviti: type, happened_on, body, follow_up_on |
POST |
/activities/{id}/complete |
activities:write |
Tandakan susulan sebagai selesai |
DELETE |
/activities/{id} |
activities:write |
Padam aktiviti |
GET |
/reports/aging |
reports:read |
Umur belum terima mengikut pelanggan; ?as_of= |
GET |
/reports/revenue |
reports:read |
Diinvoiskan dan diterima setiap bulan; ?year= |
GET |
/reports/cash-flow |
reports:read |
Wang masuk dan keluar setiap bulan; ?year= |
GET |
/reports/profit-and-loss |
reports:read |
Untung rugi ringkas; ?year=, ?month= |
GET |
/reports/vat |
reports:read |
Cukai output dan input setiap bulan; ?year= |
GET |
/reports/quotes |
reports:read |
Sebut harga dihantar dan diterima setiap bulan, kadar penerimaan; ?year= |
GET |
/reports/recurring-revenue |
reports:read |
Hasil berulang setara sebulan dan invois 90 hari akan datang |
GET |
/agents |
agents:read |
Ejen yang boleh dijalankan, inputnya, dan sama ada AI sedia |
POST |
/agents/{agent}/runs |
agents:run |
Mulakan ejen; hantar pengepala Idempotency-Key supaya cubaan semula selamat |
GET |
/agent-runs |
agents:read |
Larian ejen; ?status=, ?agent= |
GET |
/agent-runs/{id} |
agents:read |
Satu larian ejen beserta sejarah status; kelulusan memaut ke aplikasi web |
POST |
/agent-runs/{id}/cancel |
agents:run |
Batalkan larian ejen |
Adding a customer:
curl -X POST "https://api.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 | Pelanggan ditambah |
customer.updated | Pelanggan dikemas kini |
customer.deleted | Pelanggan dipadam |
invoice.created | Draf invois dibuat |
invoice.updated | Draf invois dikemas kini |
invoice.sent | Invois dikeluarkan |
invoice.paid | Invois dijelaskan |
invoice.voided | Invois dibatalkan |
invoice.deleted | Draf invois dipadam |
invoice.payment_recorded | Pembayaran invois direkodkan |
invoice.payment_deleted | Rekod pembayaran dipadam |
quote.created | Draf sebut harga dibuat |
quote.updated | Draf sebut harga diubah |
quote.sent | Sebut harga dihantar |
quote.accepted | Sebut harga diterima oleh pelanggan |
quote.declined | Sebut harga ditolak oleh pelanggan |
quote.deleted | Draf sebut harga dipadam |
deal.created | Peluang dibuat |
deal.updated | Peluang diubah |
deal.stage_changed | Peringkat peluang berubah (termasuk menang atau kalah) |
deal.deleted | Peluang dipadam |
project.created | Projek dibuat |
project.updated | Projek diubah |
project.deleted | Projek dipadam |
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.