FinzBooksDevelopers

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:

  1. Customer signs up → push them as a Contact.
  2. Order placed → push an item to the catalog if it's new.
  3. Order fulfilled → push an invoice tagged with the order id.
  4. Payment cleared → push a receipt that closes the invoice.
  5. (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 fires

Why 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-Key on 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.all is 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