{"openapi":"3.0.0","info":{"title":"Sample API","version":"1.0.0","description":"Express + MongoDB boilerplate — JWT sessions, RBAC, audit trail."},"servers":[{"url":"http://localhost:5100","description":"Local dev"}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"Error":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string"},"code":{"type":"string","description":"token_expired | session_revoked"}}},"ValidationError":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string","example":"Validation failed"},"errors":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}}}}}},"Pagination":{"type":"object","properties":{"total":{"type":"integer"},"page":{"type":"integer"},"limit":{"type":"integer"},"pages":{"type":"integer"}}},"User":{"type":"object","properties":{"_id":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"username":{"type":"string"},"email":{"type":"string","format":"email"},"role":{"type":"string","enum":["owner","member"]},"customRole":{"type":"string","nullable":true},"isActive":{"type":"boolean"}}},"Settings":{"type":"object","properties":{"_id":{"type":"string"},"appName":{"type":"string"},"email":{"type":"string","format":"email"},"phone":{"type":"string"},"website":{"type":"string"},"logo":{"type":"string"},"timezone":{"type":"string","example":"Europe/London"},"currency":{"type":"string","example":"GBP"},"locale":{"type":"string"},"values":{"type":"object","description":"Free-form, merged on update"}}},"AuditEntry":{"type":"object","properties":{"_id":{"type":"string"},"user":{"type":"string","nullable":true},"userName":{"type":"string"},"action":{"type":"string","example":"user.created"},"entity":{"type":"string","example":"User"},"entityId":{"type":"string","nullable":true},"entityLabel":{"type":"string"},"changes":{"type":"object","nullable":true,"description":"Only the fields that moved"},"reason":{"type":"string"},"ip":{"type":"string"},"at":{"type":"string","format":"date-time"}}},"Role":{"type":"object","properties":{"_id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"permissions":{"type":"array","items":{"type":"string"}},"isSystem":{"type":"boolean"}}},"AuthTokens":{"type":"object","properties":{"success":{"type":"boolean"},"token":{"type":"string","description":"Access token (JWT, short-lived)"},"refreshToken":{"type":"string","description":"Opaque, rotated on every use"},"expiresIn":{"type":"string","example":"15m"},"sessionId":{"type":"string"},"data":{"$ref":"#/components/schemas/User"}}}}},"security":[{"BearerAuth":[]}],"tags":[{"name":"Auth","description":"First-run setup, sign in, sessions, passwords"},{"name":"Users","description":"Team members"},{"name":"Roles","description":"Roles & permissions"},{"name":"Settings","description":"Installation settings"},{"name":"Audit log","description":"Who did what"},{"name":"Dashboard","description":"The overview numbers"},{"name":"Plans","description":"What is for sale: prices, limits, modules"},{"name":"Clients","description":"Clients: the shops that run the POS"},{"name":"Subscriptions","description":"Who is on which plan, and their state"},{"name":"Requests","description":"Plan requests from shops, approved or rejected by staff"},{"name":"Invoices","description":"Invoices, and the payments recorded against them"},{"name":"Payments","description":"Reversals"}],"paths":{"/api/v1/users":{"get":{"tags":["Users"],"summary":"List the team","parameters":[{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"isActive","schema":{"type":"boolean"}},{"in":"query","name":"page","schema":{"type":"integer","default":1}}],"responses":{"200":{"description":"Paginated list","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"pagination":{"$ref":"#/components/schemas/Pagination"},"data":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}}}}}}},"post":{"tags":["Users"],"summary":"Add a team member","description":"The new account is always a member with the role you give it. Creating a second owner is not something a permission grants.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["firstName","lastName","username","email","password","customRole"],"properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"username":{"type":"string"},"email":{"type":"string","format":"email"},"password":{"type":"string","format":"password"},"customRole":{"type":"string","description":"Role id"}}}}}},"responses":{"201":{"description":"Created — the password is emailed to them"},"400":{"description":"That role does not exist"},"422":{"description":"Validation failed","or the password was refused":null}}}},"/api/v1/users/{id}":{"get":{"tags":["Users"],"summary":"One team member","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The user"},"404":{"description":"Not found"}}},"put":{"tags":["Users"],"summary":"Update a team member","description":"You cannot change your own role or deactivate yourself, and only the owner can edit the owner.\n","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"responses":{"200":{"description":"Updated"},"403":{"description":"Refused — see the message"}}},"delete":{"tags":["Users"],"summary":"Deactivate a team member","description":"Deactivation, not deletion — a deleted user takes their name off every audit entry that referenced them. Their sessions end immediately.\n","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deactivated"},"400":{"description":"The owner","or yourself":null}}}},"/api/v1/users/{id}/password":{"put":{"tags":["Users"],"summary":"Set someone else's password","description":"Ends every session that user has.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["password"],"properties":{"password":{"type":"string","format":"password"}}}}}},"responses":{"200":{"description":"Password set"},"422":{"description":"The password was refused"}}}},"/api/v1/users/{id}/avatar":{"post":{"tags":["Users"],"summary":"Upload an avatar","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"image":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"The new avatar URL"},"400":{"description":"Not an image","or larger than 5 MB":null}}}},"/api/v1/subscriptions":{"get":{"tags":["Subscriptions"],"summary":"List subscriptions","parameters":[{"in":"query","name":"state","schema":{"type":"string","enum":["trialing","active","past_due","restricted","suspended","cancelled"]}},{"in":"query","name":"client","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated subscriptions"}}}},"/api/v1/subscriptions/{id}":{"get":{"tags":["Subscriptions"],"summary":"One subscription with its live derived state, price, limits and modules","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The subscription"},"404":{"description":"Not found"}}}},"/api/v1/subscriptions/{id}/extend":{"post":{"tags":["Subscriptions"],"summary":"Give time without an invoice (a goodwill extension). A reason is required.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["until","reason"],"properties":{"until":{"type":"string","format":"date-time"},"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Extended"}}}},"/api/v1/subscriptions/{id}/suspend":{"post":{"tags":["Subscriptions"],"summary":"Suspend by hand, overriding payment. Needs subscriptions.suspend.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Suspended"},"409":{"description":"ALREADY_SUSPENDED"}}}},"/api/v1/subscriptions/{id}/resume":{"post":{"tags":["Subscriptions"],"summary":"Lift a manual suspension","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Resumed"}}}},"/api/v1/subscriptions/{id}/cancel":{"post":{"tags":["Subscriptions"],"summary":"Cancel at period end (default) or now. Voids an unpaid renewal for later time.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cancellation scheduled or effective"},"409":{"description":"ALREADY_CANCELLED","or RENEWAL_PARTIALLY_PAID when a renewal for later time has money on it":null}}}},"/api/v1/subscriptions/{id}/uncancel":{"post":{"tags":["Subscriptions"],"summary":"Take back a cancellation that has not happened yet","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cancellation withdrawn"}}}},"/api/v1/subscriptions/{id}/change-plan":{"post":{"tags":["Subscriptions"],"summary":"Upgrade now (prorated invoice) or downgrade / change cycle at renewal. In a trial, always now.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["plan"],"properties":{"plan":{"type":"string"},"cycle":{"type":"string","enum":["monthly","yearly"]}}}}}},"responses":{"200":{"description":"Changed; data.effective says 'now' or 'renewal'"},"409":{"description":"NO_CHANGE","PLAN_ARCHIVED":null,"ADDONS_BLOCK_CYCLE_CHANGE":null,"RENEWAL_PARTIALLY_PAID":null,"CANCELLATION_PENDING":null}}}},"/api/v1/subscriptions/{id}/add-addon":{"post":{"tags":["Subscriptions"],"summary":"Add extra branches, users, registers or a module. Price is paisa per unit per cycle.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["key","qty","price"],"properties":{"key":{"type":"string","example":"registers"},"qty":{"type":"integer","example":1},"price":{"type":"integer","example":25000}}}}}},"responses":{"200":{"description":"Added; returns the new limits and modules"},"409":{"description":"MODULE_ALREADY_INCLUDED","LIMIT_UNLIMITED":null,"RENEWAL_PARTIALLY_PAID or CANCELLATION_PENDING":null}}}},"/api/v1/subscription-requests":{"get":{"tags":["Requests"],"summary":"Plan requests from shops — waiting ones oldest first","parameters":[{"in":"query","name":"status","schema":{"type":"string","enum":["pending","approved","rejected","cancelled"]}}],"responses":{"200":{"description":"Paginated requests with their client"}}}},"/api/v1/subscription-requests/{id}":{"get":{"tags":["Requests"],"summary":"One request, with its quote recomputed now","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ request, liveQuote, stale, staleReason }"}}}},"/api/v1/subscription-requests/{id}/approve":{"post":{"tags":["Requests"],"summary":"Approve — paid (records the shop's payment) or unpaid (extends cover, invoice stays owed)","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["paid"],"properties":{"paid":{"type":"boolean"},"note":{"type":"string"}}}}}},"responses":{"200":{"description":"Approved"},"409":{"description":"REQUEST_NOT_PENDING","QUOTE_STALE":null,"DUPLICATE_REFERENCE":null,"PLAN_ARCHIVED":null},"422":{"description":"PAYMENT_REQUIRED"}}}},"/api/v1/subscription-requests/{id}/reject":{"post":{"tags":["Requests"],"summary":"Reject with a reason the shop will see","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Rejected"}}}},"/api/v1/settings":{"get":{"tags":["Settings"],"summary":"This installation's settings","description":"Created on first read, so this never 404s.","responses":{"200":{"description":"The settings","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Settings"}}}}}}}},"put":{"tags":["Settings"],"summary":"Change settings","description":"`values` is merged, not replaced.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"appName":{"type":"string"},"email":{"type":"string","format":"email"},"phone":{"type":"string"},"website":{"type":"string"},"timezone":{"type":"string","example":"Europe/London"},"currency":{"type":"string","example":"GBP"},"values":{"type":"object"}}}}}},"responses":{"200":{"description":"Saved"},"422":{"description":"Validation failed"}}}},"/api/v1/settings/logo":{"post":{"tags":["Settings"],"summary":"Upload a logo","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"image":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"The new logo URL"},"400":{"description":"Not an image","or larger than 5 MB":null}}}},"/api/v1/roles/permissions":{"get":{"tags":["Roles"],"summary":"Every permission key, grouped and labelled for the picker","responses":{"200":{"description":"The catalogue","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"permissions":{"type":"array","items":{"type":"string"}},"groups":{"type":"array","items":{"type":"object"}}}}}}}}}}}},"/api/v1/roles/seed":{"post":{"tags":["Roles"],"summary":"Re-create any missing built-in role","description":"Idempotent, and it never overwrites a role someone has edited.","responses":{"200":{"description":"What was created","if anything":null}}}},"/api/v1/roles/assign":{"post":{"tags":["Roles"],"summary":"Give a user a role","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["user","role"],"properties":{"user":{"type":"string"},"role":{"type":"string"}}}}}},"responses":{"200":{"description":"Assigned"},"403":{"description":"You cannot change your own role"}}}},"/api/v1/roles":{"get":{"tags":["Roles"],"summary":"List roles, with how many users hold each","responses":{"200":{"description":"The roles","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Role"}}}}}}}}},"post":{"tags":["Roles"],"summary":"Create a role","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","example":"Support"},"description":{"type":"string"},"permissions":{"type":"array","items":{"type":"string"},"example":["clients.view","billing.view"]}}}}}},"responses":{"201":{"description":"Created"},"422":{"description":"An unknown permission key was sent"}}}},"/api/v1/roles/{id}":{"get":{"tags":["Roles"],"summary":"One role","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The role","with its user count":null},"404":{"description":"Not found"}}},"put":{"tags":["Roles"],"summary":"Update a role","description":"You cannot edit the role you hold yourself — that is one request away from granting yourself everything. Added and removed permissions are written to the audit trail.\n","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Role"}}}},"responses":{"200":{"description":"Updated"},"403":{"description":"That is your own role"}}},"delete":{"tags":["Roles"],"summary":"Delete a role","description":"Refused while anyone still holds it, and for built-in roles.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"409":{"description":"Users still hold this role"}}}},"/api/v1/roles/{id}/users":{"get":{"tags":["Roles"],"summary":"Who holds this role","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The users"}}}},"/api/v1/plans":{"get":{"tags":["Plans"],"summary":"List plans (archived ones only with ?archived=true)","parameters":[{"in":"query","name":"archived","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Plans","cheapest first":null}}},"post":{"tags":["Plans"],"summary":"Create a plan. Prices are in paisa; a null limit means unlimited.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code","name","prices","limits"],"properties":{"code":{"type":"string","example":"growth"},"name":{"type":"string","example":"Growth"},"prices":{"type":"object","example":{"monthly":300000,"yearly":3000000}},"limits":{"type":"object","example":{"branches":5,"users":10,"registers":5}},"modules":{"type":"array","items":{"type":"string","enum":["sales","inventory","purchases","collections","accounting"]}},"isPublic":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Created"},"422":{"description":"Validation failed"}}}},"/api/v1/plans/{id}/archive":{"post":{"tags":["Plans"],"summary":"Stop selling a plan. Existing subscriptions keep their terms.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Archived"},"409":{"description":"Already archived (PLAN_ARCHIVED)"}}}},"/api/v1/plans/{id}/apply-to-existing":{"post":{"tags":["Plans"],"summary":"Preview (default) or schedule moving current subscribers to this plan's current terms at their renewal","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"dryRun":{"type":"boolean","default":true}}}}}},"responses":{"200":{"description":"Who changes","from what":null,"to what":null,"and when":null}}}},"/api/v1/plans/{id}":{"get":{"tags":["Plans"],"summary":"One plan","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The plan"},"404":{"description":"Not found"}}},"patch":{"tags":["Plans"],"summary":"Change a plan. Affects new subscriptions and renewals only.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated"},"409":{"description":"Archived (PLAN_ARCHIVED)"}}}},"/api/v1/payments/{id}/reverse":{"post":{"tags":["Payments"],"summary":"Record a reversal of a payment (append-only; the original stays). A reason is required.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"Reversed — the invoice re-opens and the subscription is re-derived"},"409":{"description":"ALREADY_REVERSED or CANNOT_REVERSE_REVERSAL"}}}},"/api/v1/licence/public-keys":{"get":{"tags":["Licence"],"summary":"The public key licences are signed with (no sign-in)","security":[],"responses":{"200":{"description":"[{ kid, alg: EdDSA, pem }] — empty when no signing key is configured"}}}},"/api/v1/licence/heartbeat":{"post":{"tags":["Licence"],"summary":"A POS installation checking in; answers with its signed licence","security":[],"parameters":[{"in":"header","name":"x-instance-key","required":true,"schema":{"type":"string","example":"pos_live_1a2b3c4d_…"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["version","usage"],"properties":{"version":{"type":"string","example":"2.0.0"},"usage":{"type":"object","properties":{"branches":{"type":"integer"},"users":{"type":"integer"},"registers":{"type":"integer"}}}}}}}},"responses":{"200":{"description":"{ token, srv }"},"401":{"description":"Bad key — no detail","by design":null},"409":{"description":"NO_SUBSCRIPTION"},"429":{"description":"More than 12 an hour for this key","or more than 30 failed key checks an hour from this address":null},"503":{"description":"LICENCE_NOT_CONFIGURED"}}}},"/api/v1/licence/plans":{"get":{"tags":["Licence"],"summary":"The plans on sale (no sign-in) — what a POS shows its owner","security":[],"responses":{"200":{"description":"data: [{ _id, code, name, prices, limits, modules }], payTo: the vendor's payment instructions"}}}},"/api/v1/licence/register":{"post":{"tags":["Licence"],"summary":"A POS's first request — creates the client, its installation, a trial and a pending request","security":[],"responses":{"201":{"description":"{ clientCode, request }"},"409":{"description":"PREFIX_TAKEN or PLAN_NOT_AVAILABLE"},"429":{"description":"More than 5 an hour from this address"}}}},"/api/v1/licence/quote":{"get":{"tags":["Licence"],"summary":"What a request for this plan and cycle would cost (instance key)","security":[],"parameters":[{"in":"header","name":"x-instance-key","required":true,"schema":{"type":"string"}},{"in":"query","name":"plan","schema":{"type":"string"}},{"in":"query","name":"cycle","schema":{"type":"string","enum":["monthly","yearly"]}}],"responses":{"200":{"description":"{ kind, planCode, cycle, amount, effective, coversFrom, coversUntil, … }"},"409":{"description":"ALREADY_PAID_AHEAD","PLAN_NOT_AVAILABLE":null,"…":null}}}},"/api/v1/licence/requests":{"post":{"tags":["Licence"],"summary":"Ask for a plan — renew, change or restart, decided by the console (instance key)","security":[],"responses":{"201":{"description":"The request summary"},"409":{"description":"REQUEST_PENDING","PLAN_NOT_AVAILABLE":null,"ALREADY_PAID_AHEAD":null,"…":null}}}},"/api/v1/licence/requests/{id}/cancel":{"post":{"tags":["Licence"],"summary":"Withdraw this POS's pending request (instance key)","security":[],"responses":{"200":{"description":"The request summary"},"409":{"description":"REQUEST_NOT_PENDING"}}}},"/api/v1/invoices":{"get":{"tags":["Invoices"],"summary":"List invoices, newest first","parameters":[{"in":"query","name":"status","schema":{"type":"string","enum":["open","paid","void"]}},{"in":"query","name":"client","schema":{"type":"string"}},{"in":"query","name":"kind","schema":{"type":"string","enum":["initial","renewal","proration","manual"]}},{"in":"query","name":"search","schema":{"type":"string"},"description":"Invoice number"}],"responses":{"200":{"description":"Paginated invoices"}}},"post":{"tags":["Invoices"],"summary":"Issue a manual invoice (amounts in paisa). It is emailed to the client.","responses":{"201":{"description":"Issued"},"409":{"description":"CLIENT_CLOSED"}}}},"/api/v1/invoices/{id}/void":{"post":{"tags":["Invoices"],"summary":"Void an invoice nobody has paid anything against. A reason is required.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Voided"},"409":{"description":"INVOICE_HAS_PAYMENTS or INVOICE_VOID"}}}},"/api/v1/invoices/{id}/payments":{"post":{"tags":["Invoices"],"summary":"Record money received against an invoice. Paying in full re-derives the subscription.","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["amount","method"],"properties":{"amount":{"type":"integer","description":"Paisa","example":3300000},"method":{"type":"string","enum":["bkash","nagad","bank","cash"]},"reference":{"type":"string","description":"Required unless cash; stored upper-case","example":"8N7A2KX91Q"},"receivedAt":{"type":"string","format":"date-time"},"note":{"type":"string"},"idempotencyKey":{"type":"string","description":"8–80 chars","one per form submission; a repeat is DUPLICATE_PAYMENT":null}}}}}},"responses":{"201":{"description":"Recorded — returns the payment","invoice":null,"subscription and derived state":null},"409":{"description":"INVOICE_ALREADY_PAID","INVOICE_VOID":null,"DUPLICATE_REFERENCE or DUPLICATE_PAYMENT":null},"422":{"description":"AMOUNT_EXCEEDS_BALANCE or validation"}}}},"/api/v1/invoices/{id}":{"get":{"tags":["Invoices"],"summary":"One invoice with its balance (and, from Task 9, its payments)","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The invoice"},"404":{"description":"Not found"}}}},"/api/v1/instances":{"get":{"tags":["Instances"],"summary":"Every POS installation, silent ones first","responses":{"200":{"description":"Paginated instances with their client"}}}},"/api/v1/instances/{id}":{"patch":{"tags":["Instances"],"summary":"Rename an installation or change its URL","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated"}}}},"/api/v1/instances/{id}/revoke":{"post":{"tags":["Instances"],"summary":"Refuse this installation's key from now on (decommissioned or stolen)","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Revoked"},"409":{"description":"INSTANCE_REVOKED"}}}},"/api/v1/instances/{id}/reinstate":{"post":{"tags":["Instances"],"summary":"Accept this installation's key again","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Reinstated"}}}},"/api/v1/instances/{id}/offline-code":{"post":{"tags":["Instances"],"summary":"A 16-character code that keeps an offline POS licensed for up to 14 days","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["days"],"properties":{"days":{"type":"integer","minimum":1,"maximum":14}}}}}},"responses":{"200":{"description":"{ code, expiresAt, state, graceEndsAt }"},"409":{"description":"OFFLINE_CODE_NOT_ALLOWED or NO_SUBSCRIPTION"}}}},"/api/v1/dashboard/summary":{"get":{"tags":["Dashboard"],"summary":"MRR, state counts, amount owed, renewals in the next 14 days, recent payments. Amounts in paisa.","responses":{"200":{"description":"The overview numbers; silentInstances is null until instances exist"}}}},"/api/v1/clients":{"get":{"tags":["Clients"],"summary":"List clients","parameters":[{"in":"query","name":"search","schema":{"type":"string"},"description":"Name","code or phone":null},{"in":"query","name":"status","schema":{"type":"string","enum":["onboarding","live","closed"]}},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"limit","schema":{"type":"integer"}}],"responses":{"200":{"description":"Paginated clients"}}}},"/api/v1/clients/{id}":{"get":{"tags":["Clients"],"summary":"One client","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The client"},"404":{"description":"Not found"}}},"patch":{"tags":["Clients"],"summary":"Update a client","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated"},"409":{"description":"SUBSCRIPTION_ACTIVE when closing a client still being billed"}}}},"/api/v1/clients/{id}/instances":{"get":{"tags":["Instances"],"summary":"The client's POS installations","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Instances","without secrets":null}}},"post":{"tags":["Instances"],"summary":"Add an installation to an existing client","description":"The console makes the key and offline secret and returns them ONCE. Set them on that POS as LICENCE_INSTANCE_KEY and LICENCE_OFFLINE_SECRET. The installation shares the client's subscription.\n","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["label"],"properties":{"label":{"type":"string","maxLength":80,"example":"Staging"},"baseUrl":{"type":"string","example":"https://pos.shop.example"}}}}}},"responses":{"201":{"description":"{ data, key, offlineSecret, message } — key and secret are never shown again"},"404":{"description":"Client not found"},"409":{"description":"CLIENT_CLOSED"}}}},"/api/v1/auth/setup":{"get":{"tags":["Auth"],"summary":"Does this installation still need its first account?","description":"So a frontend can send a visitor to the setup screen instead of a sign-in form nobody can pass. Public by definition — there is no account to authenticate with yet.\n","security":[],"responses":{"200":{"description":"Whether setup is still open","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"required":{"type":"boolean"}}}}}}}}}},"post":{"tags":["Auth"],"summary":"Create the first administrator","description":"One-shot. Creates the owner and the starter roles together, then refuses forever after — this replaces open registration, which on a single-tenant installation would let anyone who finds the URL create themselves an account. Everybody after this one is added by an administrator through POST /api/v1/users.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["firstName","lastName","username","email","password"],"properties":{"firstName":{"type":"string","example":"Ada"},"lastName":{"type":"string","example":"Lovelace"},"username":{"type":"string","example":"ada"},"email":{"type":"string","format":"email"},"password":{"type":"string","format":"password"}}}}}},"responses":{"201":{"description":"Signed in, with the starter roles created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokens"}}}},"409":{"description":"Already set up"},"422":{"description":"Validation failed, or the password was refused","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}}}}},"/api/v1/auth/login":{"post":{"tags":["Auth"],"summary":"Sign in","description":"Eight wrong passwords locks the account for fifteen minutes. The answer is deliberately identical for an unknown address and a wrong password.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email"},"password":{"type":"string","format":"password"}}}}}},"responses":{"200":{"description":"Signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokens"}}}},"401":{"description":"Invalid credentials"},"423":{"description":"Account temporarily locked"}}}},"/api/v1/auth/refresh":{"post":{"tags":["Auth"],"summary":"Trade a refresh token for a new pair","description":"The refresh token is rotated on every use. Presenting one that has already been rotated revokes every session in that chain — it means two parties hold the same token.\n","security":[],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"refreshToken":{"type":"string","description":"Optional — the httpOnly cookie is used when absent"}}}}}},"responses":{"200":{"description":"A new pair","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokens"}}}},"401":{"description":"Sign in again (code=session_revoked)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/auth/forgotpassword":{"post":{"tags":["Auth"],"summary":"Send a password reset link","description":"Answers the same way whether or not the address has an account.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}}}}},"responses":{"200":{"description":"Accepted"}}}},"/api/v1/auth/resetpassword/{resetToken}":{"put":{"tags":["Auth"],"summary":"Set a new password from a reset link","description":"Ends every existing session for that account.","security":[],"parameters":[{"in":"path","name":"resetToken","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["password"],"properties":{"password":{"type":"string","format":"password"}}}}}},"responses":{"200":{"description":"Password set and signed in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokens"}}}},"400":{"description":"Invalid or expired token"}}}},"/api/v1/auth/password-rules":{"get":{"tags":["Auth"],"summary":"What a password must satisfy, for the form to show up front","security":[],"responses":{"200":{"description":"The rules"}}}},"/api/v1/auth/me":{"get":{"tags":["Auth"],"summary":"The signed-in user","responses":{"200":{"description":"The account, with its role","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/User"}}}}}}}},"put":{"tags":["Auth"],"summary":"Update your own profile","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"language":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated"}}}},"/api/v1/auth/updatepassword":{"put":{"tags":["Auth"],"summary":"Change your own password","description":"Ends every other session; this device stays signed in.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["currentPassword","newPassword"],"properties":{"currentPassword":{"type":"string","format":"password"},"newPassword":{"type":"string","format":"password"}}}}}},"responses":{"200":{"description":"Changed"},"401":{"description":"Current password is incorrect"},"422":{"description":"The new password was refused"}}}},"/api/v1/auth/logout":{"post":{"tags":["Auth"],"summary":"End this session","responses":{"200":{"description":"Logged out"}}}},"/api/v1/auth/logout-all":{"post":{"tags":["Auth"],"summary":"End every session, including this one","responses":{"200":{"description":"All sessions ended"}}}},"/api/v1/auth/sessions":{"get":{"tags":["Auth"],"summary":"Devices this account is signed in on","responses":{"200":{"description":"One row per sign-in, not per token","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"current":{"type":"boolean"},"userAgent":{"type":"string","nullable":true},"ip":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"lastUsedAt":{"type":"string","format":"date-time"}}}}}}}}}}}},"/api/v1/auth/sessions/{id}":{"delete":{"tags":["Auth"],"summary":"Sign out one device","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"That device has been signed out"},"404":{"description":"No such session"}}}},"/api/v1/audit-log":{"get":{"tags":["Audit log"],"summary":"Who did what","description":"Read-only — there is deliberately no write endpoint. The trail is written by the actions it describes.\n","parameters":[{"in":"query","name":"action","schema":{"type":"string","example":"user.created"}},{"in":"query","name":"user","schema":{"type":"string"}},{"in":"query","name":"entity","schema":{"type":"string","example":"Resource"}},{"in":"query","name":"at[gte]","schema":{"type":"string","format":"date-time"},"description":"Range filters use bracket operators — gte, lt, in, ne"},{"in":"query","name":"page","schema":{"type":"integer","default":1}}],"responses":{"200":{"description":"Paginated entries, newest first","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"pagination":{"$ref":"#/components/schemas/Pagination"},"data":{"type":"array","items":{"$ref":"#/components/schemas/AuditEntry"}}}}}}},"403":{"description":"Your role does not permit this"}}}},"/api/v1/audit-log/{entity}/{id}":{"get":{"tags":["Audit log"],"summary":"One record's whole history, oldest first","parameters":[{"in":"path","name":"entity","required":true,"schema":{"type":"string"},"example":"Resource"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Every entry touching that record"}}}}}}