Langkau ke kandungan
Inolab

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-Id header 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

ScopeAllows
customers:readLihat pelanggan
customers:writeTambah, ubah dan padam pelanggan
invoices:readLihat invois, invois berulang dan ringkasan
invoices:writeBuat, ubah, keluarkan dan batalkan invois; urus invois berulang
payments:writeRekod dan padam pembayaran invois
quotes:readLihat sebut harga
quotes:writeBuat, hantar dan rekod jawapan sebut harga; jadikannya invois
expenses:readLihat perbelanjaan
expenses:writeRekod, ubah dan padam perbelanjaan
projects:readLihat projek dan ringkasannya
projects:writeBuat, ubah dan padam projek
products:readLihat katalog produk dan perkhidmatan
products:writeTambah, ubah dan buang entri katalog
deals:readLihat peluang
deals:writeBuat, ubah, alihkan peringkat dan padam peluang
activities:readLihat aktiviti pelanggan
activities:writeRekod aktiviti dan selesaikan susulan
reports:readLihat laporan kewangan
agents:readLihat ejen dan larian mereka
agents:runMulakan dan batalkan ejen (kelulusan hanya oleh manusia, di web atau aplikasi)
usage:readLihat penggunaan AI dan kosnya
audit_log:readBaca log aktiviti

Account, subscription and notification endpoints are for the Inolab apps only and cannot be used with an API key.

Endpoints

MethodPathScopeDescription
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, 11 or 12 percent. 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"
}
StatusMeaning
401The key is missing, wrong, revoked or expired.
403The key's scopes are not enough (code: missing_scope), or the key creator's role does not allow it.
404The record does not exist or belongs to another business.
422Invalid data; details per field in errors.
429Too 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.

EventSent when
customer.createdPelanggan ditambah
customer.updatedPelanggan dikemas kini
customer.deletedPelanggan dipadam
invoice.createdDraf invois dibuat
invoice.updatedDraf invois dikemas kini
invoice.sentInvois dikeluarkan
invoice.paidInvois dijelaskan
invoice.voidedInvois dibatalkan
invoice.deletedDraf invois dipadam
invoice.payment_recordedPembayaran invois direkodkan
invoice.payment_deletedRekod pembayaran dipadam
quote.createdDraf sebut harga dibuat
quote.updatedDraf sebut harga diubah
quote.sentSebut harga dihantar
quote.acceptedSebut harga diterima oleh pelanggan
quote.declinedSebut harga ditolak oleh pelanggan
quote.deletedDraf sebut harga dipadam
deal.createdPeluang dibuat
deal.updatedPeluang diubah
deal.stage_changedPeringkat peluang berubah (termasuk menang atau kalah)
deal.deletedPeluang dipadam
project.createdProjek dibuat
project.updatedProjek diubah
project.deletedProjek 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"
  }
}
  • data holds an object shaped like the API response. Payment events contain data.payment and data.invoice.
  • Reply with a 2xx status 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 ping event.

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.

  1. 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.
  2. An owner or admin picks a business and approves the scopes. Inolab returns to redirect_uri with code and state, or error=access_denied.
  3. From your server, exchange the code (valid 10 minutes, single use) at POST https://app.inolab.id/oauth/token with grant_type=authorization_code, code, redirect_uri, code_verifier and the client credentials (HTTP Basic or client_id/client_secret in the body).
  4. 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.