API at 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 | Tingnan ang mga customer |
customers:write | Magdagdag, mag-edit at magbura ng mga customer |
invoices:read | Tingnan ang mga invoice, recurring invoice at ang buod |
invoices:write | Gumawa, mag-edit, maglabas at mag-void ng mga invoice; pamahalaan ang mga recurring invoice |
payments:write | Mag-record at magbura ng mga bayad sa invoice |
quotes:read | Tingnan ang mga quotation |
quotes:write | Gumawa, magpadala at mag-record ng sagot sa mga quotation; gawing invoice |
expenses:read | Tingnan ang mga gastos |
expenses:write | Mag-record, mag-edit at magbura ng mga gastos |
projects:read | Tingnan ang mga proyekto at ang buod nito |
projects:write | Gumawa, mag-edit at magbura ng mga proyekto |
products:read | Tingnan ang catalog ng produkto at serbisyo |
products:write | Magdagdag, mag-edit at mag-alis ng mga entry sa catalog |
deals:read | Tingnan ang mga deal |
deals:write | Gumawa, mag-edit, maglipat ng stage at magbura ng mga deal |
activities:read | Tingnan ang mga aktibidad ng customer |
activities:write | Mag-record ng aktibidad at tapusin ang mga follow-up |
reports:read | Tingnan ang mga financial report |
agents:read | Tingnan ang mga agent at ang kanilang mga run |
agents:run | Magsimula at magkansela ng mga agent (tao lang ang nag-aapruba, sa web o sa app) |
usage:read | Tingnan ang paggamit ng AI at ang gastos nito |
audit_log:read | Basahin ang activity log |
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 |
Listahan ng customer; ?q=, ?type=company|individual, ?per_page= (max. 100) |
GET |
/customers/{id} |
customers:read |
Isang customer |
POST |
/customers |
customers:write |
Magdagdag ng customer |
PUT |
/customers/{id} |
customers:write |
I-edit ang customer |
DELETE |
/customers/{id} |
customers:write |
Burahin ang customer na walang bukas na invoice |
GET |
/invoices |
invoices:read |
Listahan ng invoice; ?status=draft|open|overdue|paid|void, ?q=, ?customer_id= |
GET |
/invoices/{id} |
invoices:read |
Isang invoice kasama ang mga item at bayad nito |
GET |
/invoices/{id}/pdf |
invoices:read |
PDF ng invoice (application/pdf) |
GET |
/dashboard |
invoices:read |
Mga numero ng receivables at overdue na invoice |
POST |
/invoices |
invoices:write |
Gumawa ng draft na invoice; server ang nagkukuwenta ng total |
PUT |
/invoices/{id} |
invoices:write |
Mag-edit ng draft |
DELETE |
/invoices/{id} |
invoices:write |
Burahin ang draft |
POST |
/invoices/{id}/send |
invoices:write |
Ilabas; email_customer=true para ipadala ang email |
POST |
/invoices/{id}/void |
invoices:write |
Kanselahin ang invoice na walang bayad; opsyonal ang reason |
POST |
/invoices/{id}/payments |
payments:write |
Mag-record ng bayad: amount, paid_on, method, reference, notes |
DELETE |
/invoices/{id}/payments/{paymentId} |
payments:write |
Burahin ang record ng bayad |
GET |
/recurring-invoices |
invoices:read |
Mga schedule ng recurring invoice; ?status=active|paused|ended, ?customer_id= |
GET |
/recurring-invoices/{id} |
invoices:read |
Isang schedule kasama ang susunod na tatlong petsa nito |
POST |
/recurring-invoices |
invoices:write |
Gumawa ng schedule: title, customer_id, interval=month|year, interval_count, next_issue_on, due_days, send_automatically, items |
PUT |
/recurring-invoices/{id} |
invoices:write |
Mag-edit ng schedule; hindi nagbabago ang mga invoice na nagawa na |
POST |
/recurring-invoices/{id}/pause |
invoices:write |
I-pause; /resume para ituloy |
DELETE |
/recurring-invoices/{id} |
invoices:write |
Burahin ang schedule |
GET |
/quotes |
quotes:read |
Mga quotation; ?status=draft|sent|expired|accepted|declined, ?customer_id=, ?deal_id= |
GET |
/quotes/{id} |
quotes:read |
Isang quotation kasama ang mga item nito |
GET |
/quotes/{id}/pdf |
quotes:read |
PDF ng quotation (application/pdf) |
POST |
/quotes |
quotes:write |
Gumawa ng draft na quotation: customer_id, issue_date, valid_until, items |
PUT |
/quotes/{id} |
quotes:write |
Mag-edit ng draft na quotation |
POST |
/quotes/{id}/send |
quotes:write |
Ipadala; email_customer=true para i-email ito kasama ang PDF |
POST |
/quotes/{id}/accept |
quotes:write |
I-record ang pagtanggap; /decline na may reason para i-record ang pagtanggi |
POST |
/quotes/{id}/convert |
quotes:write |
Gawing draft na invoice nang isang beses; kailangan din ng invoices:write |
DELETE |
/quotes/{id} |
quotes:write |
Burahin ang draft na quotation |
GET |
/expenses |
expenses:read |
Listahan ng gastos; ?from=, ?to=, ?category=, ?q= |
GET |
/expenses/{id} |
expenses:read |
Isang gastos |
POST |
/expenses |
expenses:write |
Mag-record ng gastos: spent_on, category, description, amount, tax_amount, method |
PUT |
/expenses/{id} |
expenses:write |
I-edit ang gastos |
DELETE |
/expenses/{id} |
expenses:write |
Burahin ang gastos |
GET |
/projects |
projects:read |
Listahan ng proyekto; ?status=open|all|..., ?customer_id=, ?q= |
GET |
/projects/{id} |
projects:read |
Isang proyekto kasama ang buod ng na-invoice, gastos at margin |
POST |
/projects |
projects:write |
Gumawa ng proyekto: customer_id, name, status, contract_value |
PUT |
/projects/{id} |
projects:write |
I-edit ang proyekto |
DELETE |
/projects/{id} |
projects:write |
Burahin ang proyektong walang invoice o gastos |
GET |
/products |
products:read |
Catalog ng produkto at serbisyo; ?type=service|goods, ?q= |
GET |
/products/{id} |
products:read |
Isang entry sa catalog |
POST |
/products |
products:write |
Idagdag sa catalog: type, name, sku, unit, unit_price |
PUT |
/products/{id} |
products:write |
Mag-edit ng entry sa catalog; hindi nagbabago ang mga kasalukuyang invoice |
DELETE |
/products/{id} |
products:write |
Alisin sa catalog |
GET |
/deals |
deals:read |
Listahan ng deal; ?stage=open|won|..., ?customer_id=, ?owner_id= |
GET |
/deals/{id} |
deals:read |
Isang deal |
POST |
/deals |
deals:write |
Gumawa ng deal: customer_id, title, stage, value |
PUT |
/deals/{id} |
deals:write |
I-edit ang deal |
POST |
/deals/{id}/stage |
deals:write |
Palitan ang stage; lost_reason kapag talo |
DELETE |
/deals/{id} |
deals:write |
Burahin ang deal |
GET |
/activities |
activities:read |
Listahan ng aktibidad; ?customer_id=, ?deal_id=, ?follow_up=due |
POST |
/customers/{id}/activities |
activities:write |
Mag-record ng aktibidad: type, happened_on, body, follow_up_on |
POST |
/activities/{id}/complete |
activities:write |
Markahang tapos ang isang follow-up |
DELETE |
/activities/{id} |
activities:write |
Burahin ang aktibidad |
GET |
/reports/aging |
reports:read |
Edad ng receivables ayon sa customer; ?as_of= |
GET |
/reports/revenue |
reports:read |
Na-invoice at natanggap bawat buwan; ?year= |
GET |
/reports/cash-flow |
reports:read |
Pumasok at lumabas na pera bawat buwan; ?year= |
GET |
/reports/profit-and-loss |
reports:read |
Simpleng kita at lugi; ?year=, ?month= |
GET |
/reports/vat |
reports:read |
Output at input VAT bawat buwan; ?year= |
GET |
/reports/quotes |
reports:read |
Mga quotation na naipadala at tinanggap bawat buwan, acceptance rate; ?year= |
GET |
/reports/recurring-revenue |
reports:read |
Katumbas na recurring revenue bawat buwan at mga invoice sa susunod na 90 araw |
GET |
/agents |
agents:read |
Mga agent na puwede mong patakbuhin, ang input nila, at kung handa na ang AI |
POST |
/agents/{agent}/runs |
agents:run |
Magsimula ng agent; magpadala ng Idempotency-Key header para ligtas ang pag-ulit |
GET |
/agent-runs |
agents:read |
Mga agent run; ?status=, ?agent= |
GET |
/agent-runs/{id} |
agents:read |
Isang agent run kasama ang status history nito; nagli-link sa web app ang mga approval |
POST |
/agent-runs/{id}/cancel |
agents:run |
Kanselahin ang agent run |
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 | Naidagdag ang customer |
customer.updated | Na-update ang customer |
customer.deleted | Nabura ang customer |
invoice.created | Nagawa ang draft na invoice |
invoice.updated | Na-update ang draft na invoice |
invoice.sent | Inilabas ang invoice |
invoice.paid | Bayad na ang invoice |
invoice.voided | Kinansela ang invoice |
invoice.deleted | Nabura ang draft na invoice |
invoice.payment_recorded | Naitala ang bayad sa invoice |
invoice.payment_deleted | Nabura ang record ng bayad |
quote.created | Nagawa ang draft na quotation |
quote.updated | Na-edit ang draft na quotation |
quote.sent | Naipadala ang quotation |
quote.accepted | Tinanggap ng customer ang quotation |
quote.declined | Tinanggihan ng customer ang quotation |
quote.deleted | Nabura ang draft na quotation |
deal.created | Nagawa ang deal |
deal.updated | Na-edit ang deal |
deal.stage_changed | Nagbago ang stage ng deal (kasama ang panalo o talo) |
deal.deleted | Nabura ang deal |
project.created | Nagawa ang proyekto |
project.updated | Na-edit ang proyekto |
project.deleted | Nabura ang proyekto |
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.