End-to-end tutorial
A practical, copy-pasteable walkthrough that takes you from nothing → first paid invoice in 8 API calls. Use this when you're wiring up a new integration. About 15 minutes if your token is already minted.
What you'll build
Imagine an e-commerce backend pushing fulfilled orders into FinzBooks:
- Customer signs up → push them as a Contact.
- Order placed → push an item to the catalog if it's new.
- Order fulfilled → push an invoice tagged with the order id.
- Payment cleared → push a receipt that closes the invoice.
- (Bonus) Goods returned → push a credit note that knocks it off.
Prerequisites
- A Personal Access Token. Mint one at Console → API Tokens. Use the “Full access” preset for the tutorial — narrow it later.
- Save it as an env var so the curl examples work:
export PAT="aibk_pat_…"
export BASE="https://api.finzbooks.com/api/public/v1"1. Verify the token (sanity check)
curl -H "Authorization: Bearer $PAT" "$BASE/whoami"
# Response — confirms your org and scopes
{
"code": 0, "message": "success",
"token": {
"token_id": "43cb7580…",
"org_id": "0c2c3781-5c1f-49b2-941c-996587fff65a",
"user_id": "d65edfea-…",
"scopes": ["AIBooks.fullaccess.all"],
"tier": "DEFAULT"
}
}The org_id in the response is the org your token is bound to. All ids you pass on subsequent requests must live in this same org. A cross-org id returns 404 not_found.resource with a field hint — see the Multi-org guide.
2. Create the customer
curl -X POST "$BASE/contacts" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ext-customer-abc123" \
-d '{
"contact_name": "Acme Corp",
"contact_type": "CUSTOMER",
"email": "ap@acme.com",
"phone": "+91 99xxxxxxxx",
"billing_address": {
"address": "1 MG Road", "city": "Bengaluru",
"state_code": "IN-KA", "zip": "560001"
}
}'
# Response → grab "contact_id"
{ "code": 0, "contact": { "contact_id": "ct_8a4f…", ... } }Why Idempotency-Key?If the network flakes and you retry, you'll get the same Contact back instead of a duplicate. See Idempotency.
3. Create (or look up) the item
Items are the catalog of things you sell. Pass account_id to route the line credit to a specific revenue ledger:
# Find an existing INCOME account first
curl -H "Authorization: Bearer $PAT" "$BASE/accounts?per_page=200" \
| jq '.accounts[] | select(.account_type == "INCOME") | {account_id, name}'
# Create the item
curl -X POST "$BASE/items" \
-H "Authorization: Bearer $PAT" -H "Content-Type: application/json" \
-H "Idempotency-Key: ext-item-widget-pro" \
-d '{
"name": "Widget Pro (annual licence)",
"sku": "WIDGET-PRO-001",
"type": "SERVICE",
"rate": 12000,
"tax_id": "<gst_18pct_tax_id>",
"hsn_sac": "997331",
"account_id": "<consulting_revenue_account_id>"
}'4. Create + auto-approve the invoice
Pass auto_approve: true so the journal entry gets posted immediately (status flips DRAFT → SENT). Pass reference_number with your external order id so duplicate retries get caught by the dedupe guard.
curl -X POST "$BASE/invoices" \
-H "Authorization: Bearer $PAT" -H "Content-Type: application/json" \
-H "Idempotency-Key: ext-order-99031" \
-d '{
"customer_id": "<contact_id from step 2>",
"date": "2026-05-19",
"place_of_supply": "IN-KA",
"reference_number": "ORDER-99031",
"salesperson_id": "<optional salesperson uuid>",
"auto_approve": true,
"line_items": [
{
"item_id": "<item_id from step 3>",
"quantity": 1,
"rate": 12000
}
]
}'
# Response — invoice posted, AR booked
{
"code": 0,
"message": "The invoice has been created and approved.",
"invoice": {
"invoice_id": "inv_…",
"invoice_number": "INV-2627-001",
"status": "SENT",
"total": 14160.00, // 12000 + 18% GST
"balance": 14160.00,
"round_off": 0,
"line_items": [ ... ]
}
}Want whole-rupee totals? Turn on roundInvoiceTotals in your org preferences (UI: Settings → Invoice Profile → Round-off). Every new invoice picks up the org default, but you can override per-call with explicit round_off. See Round-off behaviour.
5. Record the receipt
Customer paid. One call creates the payment row + posts Dr Bank / Cr AR + allocates against the invoice:
curl -X POST "$BASE/payments" \
-H "Authorization: Bearer $PAT" -H "Content-Type: application/json" \
-H "Idempotency-Key: ext-payment-utr-1234567" \
-d '{
"flow": "INCOMING",
"contact_id": "<contact_id>",
"date": "2026-05-19",
"amount": 14160,
"mode": "BANK_TRANSFER",
"bank_account_id": "<HDFC_account_id>",
"reference_number": "UTR-1234567",
"allocations": [
{ "invoice_id": "<invoice_id from step 4>", "amount": 14160 }
]
}'
# Invoice is now PAID, AR cleared, INVOICE_PAID webhook firesWhy pass bank_account_id? The auto- fallback picks the first BANK or CASH account — fine for single- bank orgs but multi-bank setups need the explicit id. Get it from GET /accounts?per_page=500 filtered to account_type IN (BANK, CASH).
6. (Optional) Refund / credit note
Goods returned partial — issue a CN, then apply it:
# Create
CN_ID=$(curl -s -X POST "$BASE/credit_notes" \
-H "Authorization: Bearer $PAT" -H "Content-Type: application/json" \
-d '{
"customer_id": "<contact_id>",
"date": "2026-05-25",
"line_items": [{ "name": "Returned 1 unit", "quantity": 1, "rate": 14160 }]
}' | jq -r '.credit_note.credit_note_id')
# Apply to the invoice (could be a different invoice for the same customer)
curl -X POST "$BASE/credit_notes/$CN_ID/apply-to-invoice" \
-H "Authorization: Bearer $PAT" -H "Content-Type: application/json" \
-d '{ "invoice_id": "<other_invoice_id>", "amount": 14160 }'7. (Optional) Pull a customer statement
Useful for showing the customer their account history:
curl -H "Authorization: Bearer $PAT" \
"$BASE/reports/customer_ledger?contact_id=<contact_id>&from_date=2026-04-01&to_date=2026-05-31"
# Returns invoices + receipts + CNs + DNs + JVs in date order
# with a running balance.8. (Optional) Subscribe to webhooks
Instead of polling, register a webhook endpoint and get INVOICE_PAID, PAYMENT_RECEIVED, etc. delivered to your URL. See Webhooks reference.
Done. Now harden it.
Before you ship this to production:
- Use
Idempotency-Keyon every mutating call — a stable id tied to your upstream event. We caught duplicates server-side too (see Idempotency), but the header is the contract. - Handle 4xx vs 5xx differently. 4xx (validation, dedupe, not-found) is deterministic — fix the input. 5xx is server-side — retry with exponential backoff using the same idempotency key.
- Narrow the token scopes.
AIBooks.fullaccess.allis great for the tutorial but your prod token should only have the scopes it actually needs. See Authentication. - Monitor your rate-limit headers. Every response carries
X-RateLimit-Remaining. Don't wait for the 429 — back off proactively under burst loads. See Rate limits. - Subscribe to
PAYMENT_*webhooks if you handle refunds — easier than polling.
Next reads
- Invoice reference — full field list including TDS, multi-currency, custom series.
- Payment reference — one-shot vs two-step apply, split receipts, vendor payments, FX.
- Credit / Debit Notes — full lifecycle including refunds and TDS reversal.
- Multi-org guide — what to do when your integrator app handles multiple FinzBooks tenants.