One Stop Invoice - HTTP API (v1)
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`.
Authentication
- Open Dashboard → API Key (requires
apipermission) and copy your key. - Every request must include the key in one of:
- 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.
Account gates
Same rules as the logged-in app:
- Suspended account →
403(account_suspended) - Subscription expired →
403(subscription_expired)
Response shape
Success:
{ "ok": true, "data": { } }
Error:
{
"ok": false,
"error": {
"code": "validation_error",
"message": "…",
"details": { }
}
}
CORS
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.
Pagination
List endpoints accept limit (default 50, max 200) and offset (default 0).
Client deduplication
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):
- Phone (normalized to 10 digits, compared to stored digits)
- Email (case-insensitive)
- GSTIN (exact)
Delete client
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.
Apache / subdomain
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/guide, /docs/reference, /docs/cursor-skill-download) to *.php so public links match api_docs_url().
OpenAPI
Machine-readable contract: `openapi.yaml`. Human-friendly reference: `reference.php` (examples + Try console).
Cursor / AI coding assistants
Download the maintained Agent Skill for this API (implementation notes, auth, endpoints, error codes):
- !MD0! save as
.cursor/skills/onestop-api/SKILL.md(or your tool’s equivalent skill folder). - !MD0! extra file map and error-code table (optional; link from the skill).
If these links 404, the server checkout is missing .cursor/skills/onestop-api/ (deploy with that directory or copy the files from the repo).
---
Integration cookbook (step-by-step)
Replace YOUR_API_KEY and the base URL with yours. Production base is typically https://api.onestopinvoice.com/v1 (or your own host + /v1).
1) Verify the key
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.
2) Create a client (or reuse an existing one)
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:
- New client:
data.skippedisfalse, anddata.client.client_idis the id you need for invoices and quotations. - Duplicate: if phone (10 digits), email, or GSTIN matches an existing client in your account, you get
200withdata.skipped: trueanddata.clientset to the existing row. Always readclient_idfrom 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"
3) Invoice and quotation line items (same shape)
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.
4) Create an invoice with lines
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[].
5) Create a quotation with lines
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.
6) Debit note lines (different shape)
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 }
]
7) Typical automation flow
GET /v1/meconfirm key and currency.POST /v1/clientsupsert client; readclient_id(handleskipped).POST /v1/quotationsorPOST /v1/invoicesinclude fullitemsarrays.GET /v1/.../{id}verify stored lines and totals.POST /v1/sendoutsemail PDF when ready (document_type:invoice,quotation, ordebit-note).
8) Debugging bad requests
- `validation_error` often invalid
client_id, missing required dates, or wrong field names (e.g. debit note lines vs invoice lines). - `401` key missing/invalid.
- `403`
account_suspendedorsubscription_expired. - `404` wrong id or resource not in your account.
Full error table: reference appendix.