Developer guide

Rendered from README.md for reading in the browser.

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

  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 <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.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:

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

  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.

Chat with us on WhatsApp