Clients & documents
Client CRUD with dedupe, plus invoices, quotations, and debit notes.
Interactive reference
Copy-ready
curl
commands use the production base.
Send request
hits same-origin
/v1.
Stored in
localStorage
only · sent as
Authorization: Bearer …
https://api.onestopinvoice.com/v1
Use this URL in apps and integrations.
Overview
Versioned JSON REST for clients, invoices, quotations, debit notes, inventory, expenses, sendouts, and analytics — the same data as your dashboard.
Client CRUD with dedupe, plus invoices, quotations, and debit notes.
Keep stock and costs aligned with your books and PDFs.
Email document PDFs and pull summary analytics.
Per-account keys with the same subscription rules as the web app.
Send your key as
Authorization: Bearer <api_key>
or
X-API-Key: <api_key>.
Create or rotate it under
Dashboard → API access
.
?api_key=
is for quick tests only, not production.
Success:
{"ok": true, "data": …}
Error:
{"ok": false, "error": {"code", "message", "details"}}
curl -sS -H "Authorization: Bearer YOUR_API_KEY" "https://api.onestopinvoice.com/v1/me"
Endpoints at a glance
| Methods | Path | Notes |
|---|---|---|
| GET |
/v1/me
|
Account summary |
| GETPOST |
/v1/clients
|
List & create · dedupe on phone / email / GSTIN |
| GETPATCHDELETE |
/v1/clients/{id}
|
Read, update, delete |
| GETPOST |
/v1/invoices
|
Invoices with line items |
| GETPATCHDELETE |
/v1/invoices/{id}
|
|
| GETPOST |
/v1/quotations
|
|
| GETPATCHDELETE |
/v1/quotations/{id}
|
|
| GETPOST |
/v1/debit-notes
|
|
| GETPATCHDELETE |
/v1/debit-notes/{id}
|
|
| GETPOST |
/v1/inventory
|
|
| GETPATCHDELETE |
/v1/inventory/{id}
|
|
| GETPOST |
/v1/expenses
|
|
| GETPATCHDELETE |
/v1/expenses/{id}
|
|
| POST |
/v1/sendouts
|
Email document PDF |
| GET |
/v1/analytics/summary
|
Dashboard analytics |
Developer guide
Authentication, envelopes, pagination, dedupe rules, and integration recipes.
JSON API under /v1/… on the same host as the web app (or on a subdomain whose document root is this project). Implementation entrypoint: `api/v1/index.php`.
The API hub and reference show the public base `https://api.onestopinvoice.com/v1` even on localhost, so copy-paste targets production. To override (e.g. staging), set `ONESTOP_API_V1_BASE`.
api permission) and copy your key. - Header Authorization: Bearer <your_api_key>
- Header X-API-Key: <your_api_key>
- Query ?api_key= (discouraged; logs and referrer leakage)
The key is stored on accounts.api_key. Invalid or missing key → 401 with error.code = unauthorized.
Same rules as the logged-in app:
403 (account_suspended)403 (subscription_expired)Success:
{ "ok": true, "data": { } }
Error:
{
"ok": false,
"error": {
"code": "validation_error",
"message": "…",
"details": { }
}
}
If Origin is sent, the API reflects it and allows GET, POST, PATCH, DELETE, OPTIONS with Authorization, Content-Type, and X-API-Key. Preflight OPTIONS returns 204.
List endpoints accept limit (default 50, max 200) and offset (default 0).
POST /v1/clients checks existing rows in your account. If any of the following match a non-empty value you send, the API returns 200 with data.skipped: true and the existing data.client (no new row):
DELETE /v1/clients/{id} without related invoices/quotations/debit notes succeeds directly. If related documents exist, you get 409 unless you call DELETE /v1/clients/{id}?cascade=1, which removes related invoice/quotation/debit data, nulls expense.client_id for that client, then deletes the client.
Same document root as the main site (recommended): `.htaccess` maps /v1/... to api/v1/index.php.
Subdomain `api.example.com` with document root = project root: no extra rules; use https://api.example.com/v1/me.
Subdomain with document root = `/api` folder only: ship the repo’s `../.htaccess` in that folder. It maps /v1/… to v1/index.php and rewrites extensionless paths (e.g. /docs/cursor-skill-download) to *.php so public links match api_docs_url(). Older /docs/guide and /docs/reference URLs redirect to the unified docs hub.
Machine-readable contract: `openapi.yaml`. Human-friendly reference (examples + Try console) lives on the unified docs hub at `../index.php` (#ref-meta and section anchors).
Download the maintained Agent Skill for this API (implementation notes, auth, endpoints, error codes):
.cursor/skills/onestop-api/SKILL.md (or your tool’s equivalent skill folder).If these links 404, the server checkout is missing .cursor/skills/onestop-api/ (deploy with that directory or copy the files from the repo).
Replace YOUR_API_KEY and the base URL with yours. Production base is typically https://api.onestopinvoice.com/v1 (or your own host + /v1).
curl -sS -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.onestopinvoice.com/v1/me"
You should see {"ok":true,"data":{...}} with account_id, currency, etc. If you see 401 / unauthorized, the key is wrong or missing.
Minimum JSON: name, phone, email are required. address and gstin are optional. Use gstin only when you have a valid 15-character GSTIN; otherwise send "" or omit.
curl -sS -X POST "https://api.onestopinvoice.com/v1/clients" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Retail Pvt Ltd",
"phone": "9876543210",
"email": "billing@acme.example",
"address": "Shop 12, MG Road, Bengaluru 560001",
"gstin": ""
}'
Response patterns:
data.skipped is false, and data.client.client_id is the id you need for invoices and quotations.200 with data.skipped: true and data.client set to the existing row. Always read client_id from this response before creating documents.List or find clients:
curl -sS -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.onestopinvoice.com/v1/clients?limit=50&offset=0"
Both invoices and quotations accept an items array. Each element is one line. The server ignores any array entry where item_description is missing or empty after trim—so an “empty” {} or { "quantity": 1 } does nothing. Always set item_description.
| Field | Meaning |
|--------|---------|
| item_description | Required for the line to be saved (human-readable line text). |
| hsn_code | HSN/SAC string (can be ""). |
| quantity | Number (decimals allowed). |
| rate | Unit rate before tax (as you use in the app). |
| tax_rate | Percentage (e.g. 18 for 18%). |
| cgst_amount, sgst_amount, igst_amount | Tax amounts your integration calculated (intra-state often CGST+SGST; inter-state often IGST). |
| total_amount | Line total including tax (must match how your app/PDF logic works; the API stores what you send). |
| discount_type, discount_value | Optional; only if your database has discount columns (same as dashboard). discount_type: percentage or fixed. |
Inter-state example (IGST only, CGST/SGST zero): set igst_amount to your computed IGST and keep cgst_amount / sgst_amount at 0.
Required top-level fields: client_id, invoice_date, payment_due_date (dates as Y-m-d).
Optional: payment_status, status, currency, invoice_type, lut_number, declaration_text, items.
curl -sS -X POST "https://api.onestopinvoice.com/v1/invoices" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"client_id": 10,
"invoice_date": "2026-03-21",
"payment_due_date": "2026-04-05",
"payment_status": "pending",
"status": "draft",
"currency": "INR",
"invoice_type": "local_sale",
"items": [
{
"item_description": "Professional services March",
"hsn_code": "998314",
"quantity": 1,
"rate": 10000,
"tax_rate": 18,
"cgst_amount": 900,
"sgst_amount": 900,
"igst_amount": 0,
"total_amount": 11800
}
]
}'
Then fetch the full document with lines:
curl -sS -H "Authorization: Bearer YOUR_API_KEY" \
"https://api.onestopinvoice.com/v1/invoices/101"
GET /v1/invoices returns list rows only (no items). Use GET /v1/invoices/{id} for items[].
Required: client_id, quotation_date.
If you omit validity_date, the API defaults it to quotation_date.
items uses the same keys as invoice lines (see table above).
curl -sS -X POST "https://api.onestopinvoice.com/v1/quotations" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"client_id": 10,
"quotation_date": "2026-03-21",
"validity_date": "2026-04-21",
"approval_status": "pending",
"currency": "INR",
"quotation_type": "bill_of_supply",
"items": [
{
"item_description": "Annual maintenance (quoted)",
"hsn_code": "9987",
"quantity": 1,
"rate": 50000,
"tax_rate": 18,
"cgst_amount": 4500,
"sgst_amount": 4500,
"igst_amount": 0,
"total_amount": 59000
}
]
}'
Why it looked like “empty parentheses” before: sample docs used "items": []. That creates a valid quotation/invoice with no saved lines, because there are no rows with a non-empty item_description. Populate items as in the example above.
Debit notes use different item fields: description, expense_date, amount (not item_description / HSN).
"items": [
{ "description": "Rate difference adjustment", "expense_date": "2026-03-21", "amount": 500 }
]
GET /v1/me confirm key and currency. POST /v1/clients upsert client; read client_id (handle skipped). POST /v1/quotations or POST /v1/invoices include full items arrays. GET /v1/.../{id} verify stored lines and totals. POST /v1/sendouts email PDF when ready (document_type: invoice, quotation, or debit-note).client_id, missing required dates, or wrong field names (e.g. debit note lines vs invoice lines). account_suspended or subscription_expired. Full error table: reference appendix.
/v1/me
Returns the account tied to your API key (id, business name, email, currency, GSTIN, PAN, subscription). PAN is the account business PAN (same for all documents).
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"account_id": 1,
"business_name": "Demo Traders",
"business_email": "hello@example.com",
"currency": "INR",
"business_gstin": "27AAAAA0000A1Z5",
"business_pan": "AAAAA9999A",
"subscription_expires_at": "2026-12-31 23:59:59"
}
}
/v1/clients
Paginated clients. Query: limit (max 200, default 50), offset.
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"clients": [
{
"client_id": 10,
"name": "Ravi Kumar",
"phone": "9876543210",
"email": "ravi@client.test",
"gstin": null
}
],
"total": 1
}
}
/v1/clients
Create client (name, phone, email required). Optional: address, gstin (15-char format when used). Duplicate phone (10 digits), email, or GSTIN → 200 with data.skipped true and data.client = existing row.
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"skipped": false,
"client": {
"client_id": 11,
"name": "Acme Retail Pvt Ltd",
"phone": "9876543210",
"email": "billing@acme.example",
"address": "Shop 12, MG Road, Bengaluru 560001",
"gstin": null,
"state_code": null
}
}
}
/v1/clients/{id}
Single client by id.
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"client_id": 10,
"name": "Ravi Kumar",
"phone": "9876543210",
"email": "ravi@client.test"
}
}
/v1/clients/{id}
Partial update (JSON merged).
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"client_id": 10,
"name": "Ravi K. (updated)"
}
}
/v1/clients/{id}
Delete client. Query cascade=1 to remove related invoices/quotations/debit notes.
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"deleted": true
}
}
/v1/invoices
List invoices. Query: limit, offset, client_id. Rows are DB columns (no line items on list).
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"invoices": [
{
"invoice_id": 101,
"client_id": 10,
"invoice_number": "INV-2026-0042",
"invoice_date": "2026-03-21",
"payment_due_date": "2026-04-05",
"payment_status": "pending",
"status": "draft",
"currency": "INR"
}
],
"total": 1
}
}
/v1/invoices
Create invoice. Required: client_id, invoice_date, payment_due_date (Y-m-d). Each items[] row needs item_description (non-empty) or the row is skipped. Send tax splits your app already calculated (CGST+SGST for intra-state, or IGST). Optional: transport_details, lr_number (per document). Business PAN is not on the invoice; use GET /me business_pan. Optional: discount_type percentage|fixed + discount_value when DB supports it.
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"invoice_id": 101,
"message": "created"
}
}
/v1/invoices/{id}
Single invoice: header fields from invoices row plus items[] (invoice_items columns, ordered by item_id).
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"invoice_id": 101,
"client_id": 10,
"invoice_number": "INV-2026-0042",
"invoice_date": "2026-03-21",
"payment_due_date": "2026-04-05",
"currency": "INR",
"items": [
{
"item_id": 501,
"invoice_id": 101,
"item_description": "Professional services March",
"hsn_code": "998314",
"quantity": 1,
"rate": 10000,
"tax_rate": 18,
"cgst_amount": 900,
"sgst_amount": 900,
"igst_amount": 0,
"total_amount": 11800
}
]
}
}
/v1/invoices/{id}
Partial update.
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"invoice_id": 101
}
}
/v1/invoices/{id}
Delete invoice.
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"deleted": true
}
}
/v1/quotations
List quotations. Query: limit, offset, client_id. Rows omit line items (use GET /quotations/{id} for items[]).
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"quotations": [
{
"quotation_id": 55,
"client_id": 10,
"quotation_number": "QT-2026-0007",
"quotation_date": "2026-03-21",
"validity_date": "2026-04-21",
"approval_status": "pending",
"currency": "INR"
}
],
"total": 1
}
}
/v1/quotations
Create quotation. Required: client_id, quotation_date. validity_date defaults to quotation_date if omitted. items[] uses the same keys as invoice line items; each row must have item_description or it is ignored. Optional: transport_details, lr_number (per document). Business PAN from GET /me.
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"quotation_id": 55
}
}
/v1/quotations/{id}
Single quotation header plus items[] (quotation_items columns).
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"quotation_id": 55,
"client_id": 10,
"quotation_number": "QT-2026-0007",
"quotation_date": "2026-03-21",
"validity_date": "2026-04-21",
"currency": "INR",
"items": [
{
"item_id": 8801,
"quotation_id": 55,
"item_description": "Annual maintenance (quoted)",
"hsn_code": "9987",
"quantity": 1,
"rate": 50000,
"tax_rate": 18,
"cgst_amount": 4500,
"sgst_amount": 4500,
"igst_amount": 0,
"total_amount": 59000
}
]
}
}
/v1/quotations/{id}
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"quotation_id": 55
}
}
/v1/quotations/{id}
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"deleted": true
}
}
/v1/debit-notes
List debit notes. Query: limit, offset, client_id.
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"debit_notes": [
{
"debit_note_id": 7,
"client_id": 10,
"debit_note_number": "DN-3",
"debit_note_date": "2026-03-21",
"currency": "INR"
}
],
"total": 1
}
}
/v1/debit-notes
Create debit note with line items. Optional: transport_details, lr_number (per document). Business PAN from GET /me.
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"debit_note_id": 7
}
}
/v1/debit-notes/{id}
Debit note with items[] (description, expense_date, amount per line).
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"debit_note_id": 7,
"client_id": 10,
"debit_note_date": "2026-03-21",
"currency": "INR",
"items": [
{
"item_id": 120,
"debit_note_id": 7,
"description": "Rate difference adjustment",
"expense_date": "2026-03-21",
"amount": 500
}
]
}
}
/v1/debit-notes/{id}
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"debit_note_id": 7
}
}
/v1/debit-notes/{id}
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"deleted": true
}
}
/v1/inventory
List inventory rows (products/services).
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"items": [
{
"inventory_id": 3,
"name": "Widget A",
"type": "product",
"rate": 250,
"quantity": 100,
"tax_rate": 18,
"hsn_code": "1234"
}
],
"total": 1
}
}
/v1/inventory
Create product/service line.
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"inventory_id": 3
}
}
/v1/inventory/{id}
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"inventory_id": 3,
"name": "Widget A"
}
}
/v1/inventory/{id}
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"inventory_id": 3
}
}
/v1/inventory/{id}
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"deleted": true
}
}
/v1/expenses
List expenses.
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"expenses": [
{
"expense_id": 9,
"expense_description": "Office supplies",
"expense_amount": 1200,
"expense_date": "2026-03-20",
"expense_category": "General"
}
],
"total": 1
}
}
/v1/expenses
Create expense.
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"expense_id": 9
}
}
/v1/expenses/{id}
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"expense_id": 9
}
}
/v1/expenses/{id}
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"expense_id": 9
}
}
/v1/expenses/{id}
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"deleted": true
}
}
/v1/sendouts
Queue email with PDF for invoice, quotation, or debit-note.
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"success": true,
"message": "Email queued",
"queue_id": 42
}
}
/v1/analytics/summary
Dashboard-style aggregates (same idea as legacy get-analytics).
Production base · uses your pasted API key when set
/v1
{
"ok": true,
"data": {
"total_invoices": 12,
"total_revenue": 450000,
"note": "Shape matches your live deployment"
}
}