-
Notifications
You must be signed in to change notification settings - Fork 0
API Reference
This page mirrors backend/API.md in the repository — that file is the source of truth; update it first on any API change and resync this page. See also Architecture for the auth model and Development-Guide for local setup.
Base URL: http://<host>:<port>/api (default port 8080).
-
All request/response bodies are JSON (
Content-Type: application/json). -
All routes except
GET /api/healthandPOST /api/auth/loginrequire a valid JWT access token in theAuthorization: Bearer <token>header. -
Most routes additionally require the authenticated employee to hold a specific permission (a
TabTypestring from the frontend's permission model) or haverole: "admin"(admins implicitly pass every permission check). This is enforced server-side; there is no client-side-only authorization anywhere in this API. -
Every non-2xx response uses this envelope:
{ "type": "validation_error", "title": "Human-readable summary", "status": 400, "errors": [{ "field": "name", "message": "..." }], "request_id": "..." }errorsis only present for validation failures.typeis one of:validation_error(400),unauthorized(401),forbidden(403),not_found(404),conflict(409),internal_error(500), or an endpoint-specific type such asocr_extraction_failed/ai_request_failed. -
IDs: entities accept an optional client-supplied
idstring on create; if omitted, the server generates a UUID. IDs are immutable after creation. -
Timestamps (
createdAt/updatedAt) are RFC3339 UTC, server-managed — never accepted from the client. -
List endpoints currently return the full collection as a JSON array (no pagination) — pharmacy-scale data volumes (thousands, not millions, of rows per table) don't yet warrant cursor pagination; add it first if this API is ever exposed beyond a single store's internal traffic.
No auth required.
Request:
{ "identifier": "EMP-ADMIN-1", "password": "plaintext-password" }identifier matches an employee's id OR their name (case-insensitive).
Response 200:
{
"accessToken": "eyJ...",
"expiresAt": "2026-08-21T08:23:25Z",
"user": {
"id": "EMP-ADMIN-1",
"name": "Master Admin",
"desig": "Director & Admin",
"role": "admin",
"permissions": ["dashboard", "pos", "..."]
}
}401 unauthorized for any wrong identifier or password (identical error for both, to prevent account enumeration).
Auth required (any authenticated employee). Re-reads the employee record from the database (not just JWT claims), so permission edits or a deleted account take effect immediately rather than waiting for token expiry.
Response 200: same user shape as login's user field.
401 unauthorized if the employee has since been deleted.
Access tokens expire after 12 hours. There is no refresh-token endpoint in this version — the client must re-login after expiry.
Permission: inventory (or admin role).
| Method | Path | Purpose |
|---|---|---|
| GET | /api/medicines |
List all medicines/lab-tests, ordered by name |
| GET | /api/medicines/:id |
Get one medicine |
| POST | /api/medicines |
Create |
| PUT | /api/medicines/:id |
Full update |
| DELETE | /api/medicines/:id |
Delete |
Object shape (request body for POST/PUT, response body for all):
{
"id": "string (optional on create)",
"name": "string (required)",
"company": "string",
"dist": "string",
"distributor": "string | null",
"hsn": "string",
"batch": "string",
"pack": "string",
"salt": "string",
"generic": "string | null",
"group": "string",
"rack": "string",
"stock": 0,
"rate": 0,
"omrp": 0,
"mrp": 0,
"scheme": "string",
"gst": 0,
"disc": 0,
"tabsPerStrip": 1,
"expiry": "YYYY-MM",
"isLabTest": false,
"trackStock": true,
"itemType": "medicine | lab_test",
"createdAt": "2026-08-21T00:00:00Z",
"updatedAt": "2026-08-21T00:00:00Z"
}Permission: patients.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/patients |
List all patients |
| GET | /api/patients/:id |
Get one patient |
| POST | /api/patients |
Create |
| PUT | /api/patients/:id |
Full update |
| DELETE | /api/patients/:id |
Delete |
{
"id": "string",
"name": "string (required)",
"phone": "string",
"age": "string | null",
"gender": "string | null",
"ageGender": "string | null",
"addr": "string | null",
"address": "string | null",
"doc": "string | null",
"doctor": "string | null",
"reason": "string | null",
"totalDue": 0,
"dueAmount": 0,
"lastDate": "string | null",
"lastVisitDate": "string | null",
"totalVisits": 0,
"purchaseHistory": [{ "date": "string", "items": "string", "amount": 0 }],
"bloodTests": ["string"],
"createdAt": "...", "updatedAt": "..."
}Permission: due-khata.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/due-khata |
List all due-khata entries |
| GET | /api/due-khata/:id |
Get one |
| POST | /api/due-khata |
Create |
| PUT | /api/due-khata/:id |
Full update |
| DELETE | /api/due-khata/:id |
Delete |
{
"id": "string",
"patientId": "string | null (FK -> patients.id, ON DELETE SET NULL)",
"name": "string (required)",
"phone": "string",
"addr": "string",
"doc": "string",
"reason": "string",
"due": 0,
"lastDate": "string",
"createdAt": "...", "updatedAt": "..."
}Append-mostly: no PUT/DELETE. Corrections should be recorded as new sales records (e.g. a return), preserving an audit trail.
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET | /api/sales?date=YYYY-MM-DD&patientId=... |
daily-sales |
List, optionally filtered |
| GET | /api/sales/:id |
daily-sales |
Get one sale |
| POST | /api/sales |
pos |
Create a sale |
{
"id": "string",
"inv": "string | null",
"invoiceNo": "string | null",
"date": "YYYY-MM-DD (required)",
"cust": "string | null",
"name": "string | null",
"patient": "string | null",
"patientId": "string | null (FK -> patients.id, ON DELETE SET NULL)",
"phone": "string | null",
"items": "string | null",
"qty": "string | null",
"amt": 0,
"total": 0,
"mode": "string (required, e.g. cash/upi/card)",
"itemsDetail": [{ "name": "string", "qty": 0, "price": 0, "total": 0 }],
"subtotal": 0,
"discountPercent": 0,
"doctor": "string | null",
"address": "string | null",
"ageGender": "string | null",
"paidAmount": 0,
"dueAmount": 0,
"createdAt": "...", "updatedAt": "..."
}If patientId is supplied but doesn't reference an existing patient, the
insert fails with 409 conflict (foreign key violation).
Permission: expenses.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/expenses?date=YYYY-MM-DD |
List, optionally filtered by date |
| GET | /api/expenses/:id |
Get one |
| POST | /api/expenses |
Create |
| PUT | /api/expenses/:id |
Full update |
| DELETE | /api/expenses/:id |
Delete |
{ "id": "string", "date": "YYYY-MM-DD (required)", "cat": "string", "desc": "string", "amt": 0, "createdAt": "...", "updatedAt": "..." }Permission: medicine-orders.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/needed-meds |
List all |
| GET | /api/needed-meds/:id |
Get one |
| POST | /api/needed-meds |
Create |
| PUT | /api/needed-meds/:id |
Full update |
| DELETE | /api/needed-meds/:id |
Delete |
{
"id": "string",
"patientId": "string | null (FK -> patients.id, ON DELETE SET NULL)",
"med": "string (required)",
"name": "string",
"phone": "string",
"dist": "string",
"time": "string",
"qty": 0,
"status": "Distributor Ordered | Processing | Pending | Delivered | Cancelled",
"createdAt": "...", "updatedAt": "..."
}status defaults to "Pending" if omitted on create; invalid values return 400.
Permission: opd.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/opd-visits |
List all, newest visit date first |
| GET | /api/opd-visits/:id |
Get one |
| POST | /api/opd-visits |
Create |
| PUT | /api/opd-visits/:id |
Full update |
| DELETE | /api/opd-visits/:id |
Delete |
{
"id": "string", "name": "string (required)", "phone": "string", "ageSex": "string",
"doc": "string", "vdate": "string", "rvdate": "string", "btest": "string", "reminder": "string",
"createdAt": "...", "updatedAt": "..."
}Permission: inventory.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/distributors |
List all |
| GET | /api/distributors/:id |
Get one |
| POST | /api/distributors |
Create |
| PUT | /api/distributors/:id |
Full update |
| DELETE | /api/distributors/:id |
Delete |
{
"id": "string", "name": "string (required)", "gstin": "string", "phone": "string", "addr": "string",
"dlNo": "string | null", "email": "string | null", "contactPerson": "string | null",
"registeredDate": "string | null", "source": "OCR Purchase Bill | Manual Registration | null",
"createdAt": "...", "updatedAt": "..."
}Referenced by medicines.distributor_id (ON DELETE SET NULL — deleting a
distributor never fails or cascades into medicine records).
Permission: business-dev.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/marketing-campaigns |
List all |
| GET | /api/marketing-campaigns/:id |
Get one |
| POST | /api/marketing-campaigns |
Create |
| PUT | /api/marketing-campaigns/:id |
Full update |
| DELETE | /api/marketing-campaigns/:id |
Delete |
{
"id": "string", "doc": "string", "date": "string", "action": "string (required)",
"status": "7-Day Alert Active | Upcoming | Planned | Completed",
"createdAt": "...", "updatedAt": "..."
}status defaults to "Planned" if omitted.
Permission: business-dev.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/worksheet-tasks |
List all |
| GET | /api/worksheet-tasks/:id |
Get one |
| POST | /api/worksheet-tasks |
Create |
| PUT | /api/worksheet-tasks/:id |
Full update |
| DELETE | /api/worksheet-tasks/:id |
Delete |
{
"id": "string", "cat": "string", "desc": "string (required)", "date": "string",
"status": "Pending | In Progress | Planned | Completed",
"createdAt": "...", "updatedAt": "..."
}status defaults to "Pending" if omitted.
GET requires employee-mgmt permission (or admin). POST/PUT/DELETE
require role: admin specifically — permission arrays cannot grant employee
mutation rights, only a real admin role can.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/employees |
employee-mgmt |
List all (no password/PIN hashes ever included) |
| GET | /api/employees/:id |
employee-mgmt |
Get one |
| POST | /api/employees |
admin | Create |
| PUT | /api/employees/:id |
admin | Update |
| DELETE | /api/employees/:id |
admin | Delete |
Create request:
{
"id": "string (optional)",
"name": "string (required)",
"desig": "string",
"password": "string (required, min 8 chars)",
"phone": "string | null",
"role": "admin | staff | ... (default: staff)",
"pin": "string | null (optional quick-unlock PIN)",
"permissions": ["pos", "inventory", "..."]
}Update request: same shape, but password/pin are only changed if
non-empty — omit them to leave the existing credentials untouched.
Response (both, and GET):
{
"id": "string", "name": "string", "desig": "string", "phone": "string | null",
"role": "string", "permissions": ["string"], "createdAt": "...", "updatedAt": "..."
}password/pin are never returned — the underlying bcrypt hashes are
marked json:"-" in the Go model and cannot leak through this or any other endpoint.
Safety rails (all return 409 conflict):
- Cannot delete or demote (
role!=admin) the last remaining admin account. - Cannot delete your own account while authenticated as it.
Valid permissions values (any of): dashboard, daily-calc, daily-sales,
pos, due-khata, medicine-orders, inventory, inward, inward-ocr,
opd, patients, expenses, business-dev, employee-mgmt,
invoice-settings, system-reset. Unknown values return 400.
Singleton-per-org record (one row, always). Permission: daily-calc.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/daily-register |
Get the current register (zeroed defaults if never set) |
| PUT | /api/daily-register |
Upsert (full replace) the register |
{
"date": "YYYY-MM-DD | null",
"prevBD": 0, "todaySell": 0, "phonePe": 0, "expenses": 0, "bankShift": 0,
"isLocked": false, "openingCash": 0, "totalSales": 0, "cashSales": 0,
"upiSales": 0, "cardSales": 0, "totalExpenses": 0,
"denominations": { "2000": 0, "500": 0, "100": 0 },
"closingPhysicalCash": 0, "cashDifference": 0, "isDrawerClosed": false,
"updatedAt": "..."
}Singleton-per-org record. GET requires only authentication (any employee
needs this to print invoices); PUT requires invoice-settings permission.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/invoice-config |
any authenticated employee | Get current config (defaults if never set) |
| PUT | /api/invoice-config |
invoice-settings |
Upsert (full replace) |
{
"name": "string (required)", "storeName": "string | null", "subtitle": "string | null",
"dl": "string", "gst": "string", "phone": "string", "waGroup": "string", "addr": "string", "terms": "string",
"logoUrl": "string | null", "retentionMonths": 6, "retentionPolicyNotice": "string | null",
"autoPurgeOldInvoices": true, "lastPurgeDate": "string | null",
"director": "string | null", "pharmacist": "string | null", "currency": "string | null",
"printerType": "string | null", "headerTheme": "string | null", "updatedAt": "..."
}Both call the Gemini REST API (gemini-3.7-flash, generateContent)
directly via net/http using GEMINI_API_KEY from the environment. When
that variable is unset, both return a fallback response (success: false, fallback: true) instead of erroring, so the frontend can degrade gracefully.
Permission: inward-ocr.
Request (send either imageBase64 or textContent, not both):
{
"imageBase64": "data:image/jpeg;base64,... (optional; data: prefix stripped automatically)",
"mimeType": "image/jpeg (default)",
"textContent": "raw OCR/plain text of the invoice (optional, used if imageBase64 absent)",
"distributorHint": "string (optional, currently informational only — not sent to Gemini)"
}400 if neither imageBase64 nor textContent is supplied.
Success response (Gemini configured, valid JSON extracted):
{
"success": true,
"data": {
"distributor": "string", "gstin": "string", "phone": "string", "address": "string",
"invNo": "string", "invDate": "YYYY-MM-DD", "totalCost": 0.0,
"items": [{ "name": "string", "company": "string", "salt": "string", "pack": "string",
"hsn": "string", "batch": "string", "exp": "YYYY-MM", "qty": 0, "rate": 0.0,
"dmrp": 0.0, "mrp": 0.0, "scheme": "string", "disc": 0.0, "gst": 0.0 }]
}
}If Gemini's response isn't valid JSON: {"success": true, "rawText": "...", "data": null}.
Fallback (no GEMINI_API_KEY):
{ "success": false, "fallback": true, "message": "GEMINI_API_KEY not configured on server. Using built-in pharmaceutical OCR engine." }Gemini call failure: 500 with type: "ocr_extraction_failed".
No specific permission beyond authentication (any logged-in employee may consult the assistant).
Request:
{ "prompt": "string (required)", "medicineContext": "string (optional)" }Success response:
{ "success": true, "response": "Gemini's clinical-pharmacist-styled answer text" }Fallback (no GEMINI_API_KEY):
{ "success": false, "fallback": true, "message": "AI assistant is operating in offline mode.", "response": "Clinical Pharmacist Advisory: For <prompt>, ..." }Gemini call failure: 500 with type: "ai_request_failed".
No auth required.
{ "status": "ok", "timestamp": "2026-08-21T00:00:00Z" }The old prototype exposed /api/state/sync and /api/state/reset — a
single flat JSON blob synced/replaced wholesale with no auth at all. That
model is replaced entirely by the per-entity REST resources above (each with
its own table, real FKs, and real authorization), so there is no equivalent
"sync the whole blob" or "wipe everything" endpoint in this backend. If a
bulk "factory reset" / "new store onboarding" flow is still needed, it
should be a new, explicitly admin-gated endpoint built on top of these
tables — intentionally left out of this pass rather than resurrected as an
unauthenticated bulk-write foot-gun.