# One Stop API reference appendix

## File map

| Role | Path |
|------|------|
| Entry (JSON) | `api/v1/index.php` |
| DB + auth | `api/bootstrap.php` (requires `includes/db-connect.php`, `includes/utils.php`) |
| JSON response + body read | `includes/api/json.php` |
| Exception type | `includes/api/ApiException.php` |
| Clients | `includes/api-services/clients.php` |
| Invoices | `includes/api-services/invoices.php` |
| Quotations | `includes/api-services/quotations.php` |
| Debit notes | `includes/api-services/debit_notes.php` |
| Inventory | `includes/api-services/inventory.php` |
| Expenses | `includes/api-services/expenses.php` |
| Email PDF | `includes/api-services/send_document.php` |
| Analytics | `includes/api-services/analytics.php` |
| Marketing / docs hub (HTML) | `api/index.php` |
| Dashboard API key UI | `app/api/api.php` |

## Apache routing

From project `.htaccess` (docroot = repo root):

```apache
RewriteRule ^v1(?:/(.*))?$ api/v1/index.php?__api_route=$1 [QSA,L]
```

**Subdomain** with docroot = project: URLs like `https://api.example.com/v1/me` hit `api/v1/index.php` if the server maps `/v1` accordingly (see `api/docs/README.md` for docroot = `/api` only separate rewrite snippet).

## `ApiException` codes in api-services

| code | Typical HTTP | Where |
|------|----------------|------|
| `unauthorized` | 401 | `api/bootstrap.php` |
| `account_suspended` | 403 | bootstrap |
| `subscription_expired` | 403 | bootstrap |
| `validation_error` | 400 | all services; `details` often field → message |
| `invalid_json` | 400 | `api_read_json_body()` |
| `not_found` | 404 | get/update/delete when row missing |
| `conflict` | 409 | client delete with related docs, no cascade |
| `forbidden` | 403 | sendouts: wrong client/doc |
| `signature_required` | 400 | sendouts: no signature |
| `send_failed` | 500 | sendouts mail failure |
| `not_available` | 503 | inventory/expenses table missing |
| `server_error` | 500 | DB / unexpected pipeline failure |

Router unknown route: `not_found` **404**. Outer catch: `server_error` **500** with empty `details`.

## Public URL helpers (`includes/utils.php`)

- `api_docs_public_v1_base()` production-style base for docs (env `ONESTOP_API_V1_BASE` overrides).
- `api_try_request_base_url()` same-origin `/v1` on dev/www for reference page `fetch`.
- `app_public_url('api/docs/...')` links to guide, reference, OpenAPI.

## Smoke test configuration

- Config: `tests/config.local.php` (from `tests/config.example.php`).
- Env: `ONESTOP_BASE_URL`, `ONESTOP_API_KEY`.
- Read-only: `php tests/api-smoke-test.php`
- Writes: `php tests/api-smoke-test.php --write` or `RUN_API_WRITE_TESTS=1`.

## OpenAPI + reference catalog

- `api/docs/reference-catalog.php` states it is kept conceptually in sync with `openapi.yaml`.
- When you add fields or paths, update **both** plus any examples in `reference.php` if the catalog drives them.

## GSTIN validation (clients)

Pattern in `api_clients_validate_gstin`: Indian GSTIN 15-character structure (numeric state code, PAN core, entity, Z, checksum). `state_code` on create is derived from first two characters when GSTIN length is 15.
