# 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`](../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 1. Open **Dashboard → API Key** (requires `api` permission) and copy your key. 2. Every request must include the key in one of: - Header `Authorization: Bearer ` - Header `X-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: ```json { "ok": true, "data": { } } ``` Error: ```json { "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`](../../.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`](../.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`](openapi.yaml). Human-friendly reference: [`reference.php`](reference.php) (examples + Try console). ## Cursor / AI coding assistants Download the maintained **Agent Skill** for this API (implementation notes, auth, endpoints, error codes): - **[SKILL.md download](cursor-skill-download.php)** save as `.cursor/skills/onestop-api/SKILL.md` (or your tool’s equivalent skill folder). - **[reference appendix](cursor-skill-download.php?part=reference)** 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 ```bash 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. ```bash 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.skipped` is `false`, and `data.client.client_id` is 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 `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:** ```bash 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`. ```bash 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: ```bash 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). ```bash 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). ```json "items": [ { "description": "Rate difference adjustment", "expense_date": "2026-03-21", "amount": 500 } ] ``` ### 7) Typical automation flow 1. `GET /v1/me` confirm key and currency. 2. `POST /v1/clients` upsert client; read `client_id` (handle `skipped`). 3. `POST /v1/quotations` or `POST /v1/invoices` include full `items` arrays. 4. `GET /v1/.../{id}` verify stored lines and totals. 5. `POST /v1/sendouts` email PDF when ready (`document_type`: `invoice`, `quotation`, or `debit-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_suspended` or `subscription_expired`. - **`404`** wrong id or resource not in your account. Full error table: [reference appendix](cursor-skill-download.php?part=reference).