AmmiKids Ops API126 endpoints openapi.jsonhealth
Auth: login via POST /auth/otp/verify, then send Authorization: Bearer <access-token>. Localhost OTP is always 1234. Lists use ?limit=10&offset=0 (max 100). Errors: {"error":"..."} with 401/403/422/429 codes. Supply status: 1 In review, 2 Approved, 3 Change Request, 4 Rejected.

System

GET /health
auth: none

Liveness check.

Request

curl -X GET 'http://localhost:3000/health'

Response #1

{
  "ok": true,
  "service": "ammikids-ops",
  "time": "2026-09-23T17:48:00.780Z"
}
GET /docs/openapi.json
auth: none

Machine-readable OpenAPI 3.0 spec generated from this catalog.

Request

curl -X GET 'http://localhost:3000/docs/openapi.json'

Response #2

{
  "openapi": "3.0.0",
  "info": {
    "title": "AmmiKids Ops API"
  }
}

Auth

POST /auth/signup
auth: none (rate-limited)

Public self signup, non-admin types only. Starts UNAPPROVED: login works, dashboards hidden until admin approves. Auto-creates a PENDING supplier/karigar master for those types.

Request

curl -X POST 'http://localhost:3000/auth/signup' \
  -H 'Content-Type: application/json' \
  -d '{"phone":"9876543210","roleCode":"supplier","displayName":"Sharma Fabrics"}'

Body

{
  "phone": "9876543210",
  "roleCode": "supplier",
  "displayName": "Sharma Fabrics"
}

Response #3

{
  "ok": true,
  "userId": 9,
  "message": "Registered. Login with OTP; admin approval is required for dashboard access."
}
GET /auth/signup/roles
auth: none

Public account-type picker for the signup page.

Request

curl -X GET 'http://localhost:3000/auth/signup/roles'

Response #4

{
  "ok": true,
  "roles": [
    {
      "code": "supplier",
      "name": "Supplier"
    }
  ]
}
POST /auth/bootstrap
auth: setup-token

Create the very first Admin (one-time). Fails 409 once any user exists.

Request

curl -X POST 'http://localhost:3000/auth/bootstrap' \
  -H 'Content-Type: application/json' \
  -d '{"phone":"+910000000000","setupToken":"change-me-one-time-token","displayName":"Owner"}'

Body

{
  "phone": "+910000000000",
  "setupToken": "change-me-one-time-token",
  "displayName": "Owner"
}

Response #5

{
  "ok": true,
  "userId": 1
}
POST /auth/otp/request
auth: none

Send WhatsApp OTP. Always returns the generic message (no account enumeration). Localhost/dev OTP is always 1234.

Request

curl -X POST 'http://localhost:3000/auth/otp/request' \
  -H 'Content-Type: application/json' \
  -d '{"phone":"9876543210"}'

Body

{
  "phone": "9876543210"
}

Response #6

{
  "ok": true,
  "message": "If registered and active, an OTP has been sent."
}
POST /auth/otp/verify
auth: none

Verify OTP → access (90d) + rotating refresh (90d, sliding). Tokens stored in app localStorage with silent refresh. Supplier first login includes isFirstLogin + profileNotice. Phones in config/superadmins.txt auto-provision as Admin, no activation needed.

Request

curl -X POST 'http://localhost:3000/auth/otp/verify' \
  -H 'Content-Type: application/json' \
  -d '{"phone":"9876543210","otp":"1234"}'

Body

{
  "phone": "9876543210",
  "otp": "1234"
}

Response #7

{
  "ok": true,
  "access": "eyJhbGciOi...",
  "refresh": "9f2c...",
  "isFirstLogin": true,
  "profileNotice": "your profile is under verification, please contact Ammikids",
  "user": {
    "id": 7,
    "phone": "+919876543210",
    "display": "Sharma Fabrics",
    "role": "supplier"
  }
}
POST /auth/refresh
auth: none

Rotate refresh token → new access + refresh pair.

Request

curl -X POST 'http://localhost:3000/auth/refresh' \
  -H 'Content-Type: application/json' \
  -d '{"refreshToken":"9f2c..."}'

Body

{
  "refreshToken": "9f2c..."
}

Response #8

{
  "ok": true,
  "access": "eyJhbGciOi...",
  "refresh": "b71d..."
}
POST /auth/logout
auth: Bearer

Revoke this refresh token, or everything with {all:true} (bumps token_version).

Request

curl -X POST 'http://localhost:3000/auth/logout' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"all":true}'

Body

{
  "all": true
}

Response #9

{
  "ok": true
}
GET /auth/me
auth: Bearer

Current user + server-resolved dashboard. Pending suppliers also get profileNotice.

Request

curl -X GET 'http://localhost:3000/auth/me' \
  -H 'Authorization: Bearer <access-token>'

Response #10

{
  "ok": true,
  "user": {
    "id": 7,
    "phone": "+919876543210",
    "display": "Sharma Fabrics",
    "role": "supplier",
    "perms": [
      "fabrics.submit"
    ],
    "dashboard": "Supplier Dashboard"
  }
}

Users

POST /users
auth: Bearer + users.create

Invite a user (Admin). Optionally link supplier/karigar master.

Request

curl -X POST 'http://localhost:3000/users' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"phone":"9811111111","roleCode":"supplier","supplierId":3}'

Body

{
  "phone": "9811111111",
  "roleCode": "supplier",
  "supplierId": 3
}

Response #11

{
  "ok": true,
  "userId": 8
}
GET /users?limit=10&offset=0
auth: Bearer + users.create

Paginated user list. Phones masked unless users.view_pii. Filters: ?role=<code>&active=1|0&q=<phone/name>.

Request

curl -X GET 'http://localhost:3000/users?limit=10&offset=0' \
  -H 'Authorization: Bearer <access-token>'

Response #12

{
  "ok": true,
  "items": [
    {
      "id": 8,
      "phone_e164": "XXXXXX1111",
      "display_name": "9811111111",
      "role": "supplier",
      "is_active": 1,
      "is_verified": 0,
      "last_login_at": null,
      "created_at": "2026-09-23T10:00:00.000Z"
    }
  ],
  "limit": 10,
  "offset": 0,
  "total": 42,
  "users": [
    "...same as items..."
  ]
}
GET /users/roles
auth: Bearer + users.create

All roles with their permission codes (drives admin create/role-change forms).

Request

curl -X GET 'http://localhost:3000/users/roles' \
  -H 'Authorization: Bearer <access-token>'

Response #13

{
  "ok": true,
  "roles": [
    {
      "id": 2,
      "code": "supplier",
      "name": "Supplier",
      "is_active": 1,
      "permissions": [
        "fabrics.submit"
      ]
    }
  ]
}
PATCH /users/:id/deactivate
auth: Bearer + users.deactivate

Instant block: sessions revoked via token_version.

Request

curl -X PATCH 'http://localhost:3000/users/:id/deactivate' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"reason":"Left the company"}'

Body

{
  "reason": "Left the company"
}

Response #14

{
  "ok": true
}
PATCH /users/:id/reactivate
auth: Bearer + users.deactivate

Re-activate. User must OTP-login again.

Request

curl -X PATCH 'http://localhost:3000/users/:id/reactivate' \
  -H 'Authorization: Bearer <access-token>'

Response #15

{
  "ok": true
}
PATCH /users/:id/verified
auth: Bearer + users.deactivate

Mark verified/unverified. Unverified accounts with a prior login cannot request or verify OTPs until re-verified; fresh invites still work.

Request

curl -X PATCH 'http://localhost:3000/users/:id/verified' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"is_verified":0,"reason":"Suspended pending KYC"}'

Body

{
  "is_verified": 0,
  "reason": "Suspended pending KYC"
}

Response #16

{
  "ok": true,
  "is_verified": 0
}
PATCH /users/:id/approve
auth: Bearer + users.deactivate

Approve/revoke a self-signed-up account (dashboard gate). Revoking kills sessions instantly.

Request

curl -X PATCH 'http://localhost:3000/users/:id/approve' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"approved":1}'

Body

{
  "approved": 1
}

Response #17

{
  "ok": true,
  "is_approved": 1
}
PATCH /users/:id/role
auth: Bearer + users.change_role

Change role (instant session revoke, audited old→new).

Request

curl -X PATCH 'http://localhost:3000/users/:id/role' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"roleCode":"accounts","reason":"Moved to accounts team"}'

Body

{
  "roleCode": "accounts",
  "reason": "Moved to accounts team"
}

Response #18

{
  "ok": true
}
POST /users/:id/phone/request
auth: Bearer + users.change_phone

Step 1 of verified phone change: OTP goes to the NEW number.

Request

curl -X POST 'http://localhost:3000/users/:id/phone/request' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"newPhone":"9822222222","reason":"Number ported"}'

Body

{
  "newPhone": "9822222222",
  "reason": "Number ported"
}

Response #19

{
  "ok": true,
  "message": "OTP sent to new number"
}
POST /users/:id/phone/confirm
auth: Bearer + users.change_phone

Step 2: confirm NEW-number OTP (1234 works on localhost).

Request

curl -X POST 'http://localhost:3000/users/:id/phone/confirm' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"newPhone":"9822222222","otp":"1234"}'

Body

{
  "newPhone": "9822222222",
  "otp": "1234"
}

Response #20

{
  "ok": true
}
PATCH /users/me/display-name
auth: Bearer

User edits own display name (2–100 chars).

Request

curl -X PATCH 'http://localhost:3000/users/me/display-name' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"displayName":"Sharma Fabrics"}'

Body

{
  "displayName": "Sharma Fabrics"
}

Response #21

{
  "ok": true
}
PATCH /users/me/profile
auth: Bearer + assignment_manager/karigar role

Assignment manager or karigar edits own name and contact address (address ≤ 500 chars).

Request

curl -X PATCH 'http://localhost:3000/users/me/profile' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"displayName":"Night Shift Manager","address":"Surat depot"}'

Body

{
  "displayName": "Night Shift Manager",
  "address": "Surat depot"
}

Response #22

{
  "ok": true,
  "name": "Night Shift Manager",
  "address": "Surat depot"
}
POST /users/me/phone/request
auth: Bearer + assignment_manager/karigar role

Self phone change, step 1: OTP goes to the NEW number (rate-limited). Karigar master phone syncs on confirm.

Request

curl -X POST 'http://localhost:3000/users/me/phone/request' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"newPhone":"9822222222"}'

Body

{
  "newPhone": "9822222222"
}

Response #23

{
  "ok": true,
  "message": "OTP sent to new number"
}
POST /users/me/phone/confirm
auth: Bearer + assignment_manager/karigar role

Self phone change, step 2: confirm NEW-number OTP. Verifying the new number revokes all sessions, including the current one.

Request

curl -X POST 'http://localhost:3000/users/me/phone/confirm' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"newPhone":"9822222222","otp":"1234"}'

Body

{
  "newPhone": "9822222222",
  "otp": "1234"
}

Response #24

{
  "ok": true
}

Fabric

GET /assignments/mine/:id
auth: Bearer (own karigar, or staff)

One job's details for its karigar: source, product/fabric image, pattern + pattern image, per-metre source cost, instructions, sizes with cloth and row totals, plus the karigar-saved pattern cost list. Never includes vendor cost fields.

Request

curl -X GET 'http://localhost:3000/assignments/mine/:id' \
  -H 'Authorization: Bearer <access-token>'

Response #25

{
  "ok": true,
  "job": {
    "id": 31,
    "assign_no": "ASG-2026-00031",
    "qty_base_m": 25,
    "status": "ASSIGNED",
    "pattern_number": "PAT-101"
  },
  "sizes": [
    {
      "size": 24,
      "qty": 40,
      "cloth": 2.6,
      "total_m": 104
    }
  ],
  "costs": [
    {
      "id": 1,
      "label": "Stitching",
      "value_paise": 4500,
      "unit": "Rs",
      "qty": 2,
      "total_paise": 9000
    }
  ]
}
POST /assignments/pattern-numbers/:number/costs
auth: Bearer (karigar with this pattern on own job, or assignments.create)

Karigar saves one cost row (free-text label + numeric value + unit Meter/Rs + whole-piece qty) on a pattern. Row total is value × qty. The same list reappears on every job with that pattern.

Request

curl -X POST 'http://localhost:3000/assignments/pattern-numbers/:number/costs' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"label":"Stitching","value":45,"unit":"Rs","qty":2}'

Body

{
  "label": "Stitching",
  "value": 45,
  "unit": "Rs",
  "qty": 2
}

Response #26

{
  "ok": true,
  "id": 1
}
DELETE /assignments/pattern-costs/:id
auth: Bearer (cost owner, or assignments.create)

Remove a saved pattern cost row.

Request

curl -X DELETE 'http://localhost:3000/assignments/pattern-costs/:id' \
  -H 'Authorization: Bearer <access-token>'

Response #27

{
  "ok": true
}
GET /assignments/patterns?limit=10&offset=0
auth: Bearer (karigar, or assignments.create)

Pattern master list: code + image + cloth per piece + full dynamic costing list each.

Request

curl -X GET 'http://localhost:3000/assignments/patterns?limit=10&offset=0' \
  -H 'Authorization: Bearer <access-token>'

Response #28

{
  "ok": true,
  "limit": 10,
  "offset": 0,
  "total": 1,
  "patterns": [
    {
      "pattern_number": "PAT-101",
      "image_url": "/uploads/patterns/1727....jpg",
      "cloth_per_piece_m": 2.5,
      "costs": [
        {
          "id": 1,
          "label": "Stitching",
          "value_paise": 4500,
          "unit": "Rs",
          "qty": 2,
          "total_paise": 9000
        }
      ]
    }
  ]
}
POST /assignments/patterns
auth: Bearer (karigar, or assignments.create)

Create a pattern: code + image required (image via /assignments/pattern-image), optional clothPerPiece fallback (metres, ≤1000), dynamic costs list + per-size cloth list optional. Cost rows are {label, value, unit, qty} (total = value × qty); size rows are {size, cloth} for even sizes 12–40.

Request

curl -X POST 'http://localhost:3000/assignments/patterns' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"code":"PAT-101","imageUrl":"/uploads/patterns/1727....jpg","clothPerPiece":2.5,"costs":[{"label":"Stitching","value":45,"unit":"Rs","qty":2}],"sizes":[{"size":24,"cloth":2.6},{"size":26,"cloth":2.7}]}'

Body

{
  "code": "PAT-101",
  "imageUrl": "/uploads/patterns/1727....jpg",
  "clothPerPiece": 2.5,
  "costs": [
    {
      "label": "Stitching",
      "value": 45,
      "unit": "Rs",
      "qty": 2
    }
  ],
  "sizes": [
    {
      "size": 24,
      "cloth": 2.6
    },
    {
      "size": 26,
      "cloth": 2.7
    }
  ]
}

Response #29

{
  "ok": true,
  "code": "PAT-101"
}
PATCH /assignments/patterns/:code
auth: Bearer (karigar, or assignments.create)

Update a pattern image, fallback cloth, and/or replace its whole costing list and per-size cloth list. Code is immutable.

Request

curl -X PATCH 'http://localhost:3000/assignments/patterns/:code' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"imageUrl":"/uploads/patterns/1727....jpg","costs":[{"label":"Stitching","value":45,"unit":"Rs","qty":2}],"sizes":[{"size":24,"cloth":2.6}]}'

Body

{
  "imageUrl": "/uploads/patterns/1727....jpg",
  "costs": [
    {
      "label": "Stitching",
      "value": 45,
      "unit": "Rs",
      "qty": 2
    }
  ],
  "sizes": [
    {
      "size": 24,
      "cloth": 2.6
    }
  ]
}

Response #30

{
  "ok": true,
  "code": "PAT-101"
}
DELETE /assignments/patterns/:code
auth: Bearer (karigar, or assignments.create)

Delete a pattern + its costs. Blocked (409) while any assignment uses the pattern.

Request

curl -X DELETE 'http://localhost:3000/assignments/patterns/:code' \
  -H 'Authorization: Bearer <access-token>'

Response #31

{
  "ok": true
}

Suppliers

POST /fabrics/suppliers
auth: Bearer + suppliers.manage

Create supplier master (starts PENDING verification).

Request

curl -X POST 'http://localhost:3000/fabrics/suppliers' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"firmName":"Sharma Fabrics","contact":"R. Sharma","phone":"+919811111111","gstin":"07ABCDE1234F1Z5","address":"Surat","userId":8}'

Body

{
  "firmName": "Sharma Fabrics",
  "contact": "R. Sharma",
  "phone": "+919811111111",
  "gstin": "07ABCDE1234F1Z5",
  "address": "Surat",
  "userId": 8
}

Response #32

{
  "ok": true,
  "id": 3
}
GET /fabrics/suppliers?limit=10&offset=0
auth: Bearer

Paginated supplier masters.

Request

curl -X GET 'http://localhost:3000/fabrics/suppliers?limit=10&offset=0' \
  -H 'Authorization: Bearer <access-token>'

Response #33

{
  "ok": true,
  "items": [
    {
      "id": 3,
      "firm_name": "Sharma Fabrics",
      "verification_status": "PENDING",
      "is_active": 1
    }
  ],
  "limit": 10,
  "offset": 0,
  "total": 12,
  "suppliers": [
    "...same as items..."
  ]
}
GET /fabrics/suppliers/me
auth: Bearer

Own supplier profile (phone + ID included, read-only).

Request

curl -X GET 'http://localhost:3000/fabrics/suppliers/me' \
  -H 'Authorization: Bearer <access-token>'

Response #34

{
  "ok": true,
  "profile": {
    "id": 3,
    "firm_name": "Sharma Fabrics",
    "contact_person": "R. Sharma",
    "phone_e164": "+919811111111",
    "email": "a@b.com",
    "gstin": "07ABCDE1234F1Z5",
    "address": "Surat",
    "verification_status": "PENDING"
  }
}
PATCH /fabrics/suppliers/me
auth: Bearer

Update name/contact/address/email/GST. phone + ID rejected (422).

Request

curl -X PATCH 'http://localhost:3000/fabrics/suppliers/me' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"firmName":"Sharma Fabrics Pvt Ltd","email":"ops@sharma.com","gstin":"07ABCDE1234F1Z5","address":"Surat, Gujarat"}'

Body

{
  "firmName": "Sharma Fabrics Pvt Ltd",
  "email": "ops@sharma.com",
  "gstin": "07ABCDE1234F1Z5",
  "address": "Surat, Gujarat"
}

Response #35

{
  "ok": true,
  "profile": {
    "id": 3,
    "firm_name": "Sharma Fabrics Pvt Ltd",
    "email": "ops@sharma.com"
  }
}
PATCH /fabrics/suppliers/:id/verify
auth: Bearer + suppliers.manage

Admin clears supplier profile (clears the first-login notice).

Request

curl -X PATCH 'http://localhost:3000/fabrics/suppliers/:id/verify' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"decision":"VERIFIED"}'

Body

{
  "decision": "VERIFIED"
}

Response #36

{
  "ok": true
}

Supplier ledger

POST /fabrics/suppliers/payments
auth: Bearer + suppliers.manage | fabrics.price_review

Record a payment made TO a supplier (posts to the ledger's Paid column — cash out clears the payable). Finance roles only — suppliers get 403. Writes an audit row (critical).

Request

curl -X POST 'http://localhost:3000/fabrics/suppliers/payments' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"supplierId":3,"amount":5000,"mode":"Bank","reference":"TXN99","paidOn":"2026-10-01","remarks":"advance"}'

Body

{
  "supplierId": 3,
  "amount": 5000,
  "mode": "Bank",
  "reference": "TXN99",
  "paidOn": "2026-10-01",
  "remarks": "advance"
}

Response #37

{
  "ok": true,
  "id": 1
}
GET /fabrics/suppliers/:id/ledger?limit=10&offset=0
auth: Bearer (own supplier, or finance/report roles)

Statement of account with running balance. bills = APPROVED supplies (goods received); paid = payments made. balance = bills − paid. `:id` may be `me`. Cross-supplier reads by a supplier return 403.

Request

curl -X GET 'http://localhost:3000/fabrics/suppliers/:id/ledger?limit=10&offset=0' \
  -H 'Authorization: Bearer <access-token>'

Response #38

{
  "ok": true,
  "supplier": {
    "id": 3,
    "firm_name": "Sharma Fabrics"
  },
  "totals": {
    "supplied_paise": 1055000,
    "paid_paise": 500000,
    "balance_paise": 555000
  },
  "total": 4,
  "limit": 10,
  "offset": 0,
  "entries": [
    {
      "dt": "2026-10-01",
      "particulars": "Supply SUP-2026-00001",
      "bill_paise": 370000,
      "paid_paise": 0,
      "balance_paise": 370000
    },
    {
      "dt": "2026-10-02",
      "particulars": "Payment Bank TXN99",
      "bill_paise": 0,
      "paid_paise": 500000,
      "balance_paise": -130000
    }
  ]
}
GET /fabrics/suppliers/ledger-summary
auth: Bearer + finance/report roles

All-supplier outstanding snapshot (balance sheet of payables): supplied vs paid vs owed per supplier, plus grand totals.

Request

curl -X GET 'http://localhost:3000/fabrics/suppliers/ledger-summary' \
  -H 'Authorization: Bearer <access-token>'

Response #39

{
  "ok": true,
  "summary": [
    {
      "id": 3,
      "firm_name": "Sharma Fabrics",
      "supplied_paise": 1055000,
      "paid_paise": 500000,
      "balance_paise": 555000
    }
  ],
  "totals": {
    "supplied": 6033500,
    "paid": 500000,
    "balance": 5533500
  }
}

Suppliers

POST /fabrics/karigars
auth: Bearer + karigars.manage

Create karigar master.

Request

curl -X POST 'http://localhost:3000/fabrics/karigars' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Mohan","phone":"+919833333333","skillTags":"stitching","capacityM":1000}'

Body

{
  "name": "Mohan",
  "phone": "+919833333333",
  "skillTags": "stitching",
  "capacityM": 1000
}

Response #40

{
  "ok": true,
  "id": 5
}
GET /fabrics/karigars?limit=10&offset=0
auth: Bearer

Paginated karigar masters.

Request

curl -X GET 'http://localhost:3000/fabrics/karigars?limit=10&offset=0' \
  -H 'Authorization: Bearer <access-token>'

Response #41

{
  "ok": true,
  "items": [
    {
      "id": 5,
      "name": "Mohan",
      "is_active": 1,
      "capacity_m": 1000
    }
  ],
  "limit": 10,
  "offset": 0,
  "total": 30,
  "karigars": [
    "...same as items..."
  ]
}
PATCH /fabrics/karigars/:id
auth: Bearer + karigars.manage

Rename a karigar master (2–150 chars). Names set here appear in assignment karigar dropdowns.

Request

curl -X PATCH 'http://localhost:3000/fabrics/karigars/:id' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Mohan"}'

Body

{
  "name": "Mohan"
}

Response #42

{
  "ok": true,
  "id": 5,
  "name": "Mohan"
}

Vendors

POST /vendors
auth: Bearer + vendors.manage

Create an external processing vendor with one or more services. processing accepts comma-separated names; unknown names become active catalogue entries and are linked automatically.

Request

curl -X POST 'http://localhost:3000/vendors' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Fine Thread Works","phone":"+919811234567","address":"Surat","processing":"Embroidery, Lace"}'

Body

{
  "name": "Fine Thread Works",
  "phone": "+919811234567",
  "address": "Surat",
  "processing": "Embroidery, Lace"
}

Response #43

{
  "ok": true,
  "id": 1,
  "name": "Fine Thread Works",
  "processing": [
    {
      "id": 1,
      "name": "Embroidery"
    },
    {
      "id": 2,
      "name": "Lace"
    }
  ]
}
GET /vendors?limit=10&offset=0
auth: Bearer + vendors.manage | assignments.create | reports.view | audit.view

Paginated external vendors with their offered processing services.

Request

curl -X GET 'http://localhost:3000/vendors?limit=10&offset=0' \
  -H 'Authorization: Bearer <access-token>'

Response #44

{
  "ok": true,
  "items": [
    {
      "id": 1,
      "name": "Fine Thread Works",
      "services": "Embroidery, Lace"
    }
  ],
  "limit": 10,
  "offset": 0,
  "total": 1,
  "vendors": [
    "...same as items..."
  ]
}
GET /vendors/processing-types?limit=10&offset=0
auth: Bearer + vendors.manage | assignments.create | reports.view | audit.view

Dynamic processing catalogue used by vendor and cost forms.

Request

curl -X GET 'http://localhost:3000/vendors/processing-types?limit=10&offset=0' \
  -H 'Authorization: Bearer <access-token>'

Response #45

{
  "ok": true,
  "items": [
    {
      "id": 1,
      "name": "Embroidery"
    },
    {
      "id": 2,
      "name": "Lace"
    }
  ],
  "limit": 10,
  "offset": 0,
  "total": 2,
  "processingTypes": [
    "...same as items..."
  ]
}
POST /vendors/processing-types
auth: Bearer + vendors.manage

Explicitly add a processing catalogue entry. Existing names return the existing ID.

Request

curl -X POST 'http://localhost:3000/vendors/processing-types' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Printing"}'

Body

{
  "name": "Printing"
}

Response #46

{
  "ok": true,
  "id": 3,
  "name": "Printing"
}

Supplies

GET /supplies/fabric-types
auth: Bearer

Active fabric names for the form dropdown (managed master).

Request

curl -X GET 'http://localhost:3000/supplies/fabric-types' \
  -H 'Authorization: Bearer <access-token>'

Response #47

{
  "ok": true,
  "fabrics": [
    "Cotton",
    "Rayon",
    "Viscose",
    "...31 total..."
  ]
}
POST /supplies
auth: Bearer + fabrics.submit

Add a single-product supply. images_0..N gallery (up to 100 images, 720p-optimized) + items JSON with one line per image, each with its own quantity [{quantity, unit, price, taxPct?, tax?, extraCharges?, fabric?}]. Legacy image/images[] mapping still accepted. Fabric validated against master (optional). Status auto 1 In review; header total = sum of lines.

Request

curl -X POST 'http://localhost:3000/supplies' \
  -H 'Authorization: Bearer <access-token>' \
  -F 'images_0=@a.jpg' \
  -F 'images_1=@b.jpg' \
  -F 'items=[{"quantity":10' \
  -F '"unit":"M"=' \
  -F '"price":100=' \
  -F '"taxPct":12}=' \
  -F '{"quantity":20=' \
  -F '"unit":"M"=' \
  -F '"price":100}]='

Response #48

{
  "ok": true,
  "id": 11,
  "supplyNo": "SUP-2026-00011",
  "status": 1,
  "status_label": "In review",
  "total_paise": 112000,
  "image_url": "/uploads/supplies/1727....jpg",
  "items_count": 1,
  "images_count": 2
}
GET /supplies?limit=10&offset=0&status=
auth: Bearer

Paginated supplies (own only for suppliers; staff may add supplierId/status). Status: 1 In review, 2 Approved, 3 Change Request, 4 Rejected — single or comma list (?status=2,3,4) for decided views.

Request

curl -X GET 'http://localhost:3000/supplies?limit=10&offset=0&status=' \
  -H 'Authorization: Bearer <access-token>'

Response #49

{
  "ok": true,
  "items": [
    {
      "id": 11,
      "supply_no": "SUP-2026-00011",
      "quantity": 50,
      "unit": "M",
      "total_paise": 606000,
      "status": 1,
      "status_label": "In review"
    }
  ],
  "limit": 10,
  "offset": 0,
  "total": 4,
  "supplies": [
    "...same as items..."
  ]
}
GET /supplies/:id
auth: Bearer

Supply detail + per-product gallery images + full supplier block + Ammikids company block + status history (ownership-checked for suppliers). Powers the printable invoice popup.

Request

curl -X GET 'http://localhost:3000/supplies/:id' \
  -H 'Authorization: Bearer <access-token>'

Response #50

{
  "ok": true,
  "supply": {
    "id": 11,
    "supply_no": "SUP-2026-00011",
    "status": 1,
    "status_label": "In review"
  },
  "items": [
    {
      "line_no": 1,
      "quantity": 10,
      "unit": "M",
      "total_paise": 112000,
      "images": [
        "/uploads/supplies/a.jpg",
        "/uploads/supplies/b.jpg"
      ]
    }
  ],
  "history": []
}
PATCH /supplies/:id
auth: Bearer + fabrics.submit (owner)

Edit while In review only: per-line quantities + shared unit/price/taxPct/extra/fabric + remarks. Totals recomputed. Photos lock at submit (delete + resubmit to change photos).

Request

curl -X PATCH 'http://localhost:3000/supplies/:id' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"lines":[{"id":40,"quantity":20}],"price":200}'

Body

{
  "lines": [
    {
      "id": 40,
      "quantity": 20
    }
  ],
  "price": 200
}

Response #51

{
  "ok": true,
  "total_paise": 525000
}
DELETE /supplies/:id
auth: Bearer + fabrics.submit (owner)

Withdraw an In-review supply (cascades lines + gallery). Decided supplies cannot be deleted.

Request

curl -X DELETE 'http://localhost:3000/supplies/:id' \
  -H 'Authorization: Bearer <access-token>'

Response #52

{
  "ok": true
}
PATCH /supplies/:id/status
auth: Bearer + fabrics.verify|suppliers.manage

Review supply: 2 Approved, 3/4 need reason. Only from 1/3.

Request

curl -X PATCH 'http://localhost:3000/supplies/:id/status' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"status":2}'

Body

{
  "status": 2
}

Response #53

{
  "ok": true,
  "status": 2,
  "status_label": "Approved"
}

Fabric

POST /fabrics
auth: Bearer + fabrics.submit

Create draft fabric entry with lots. Charges apportioned pro-rata; totals in paise.

Request

curl -X POST 'http://localhost:3000/fabrics' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"supplierId":3,"invoiceNo":"INV-101","entryDate":"2026-09-20","receivedDate":"2026-09-21","taxMode":"EXCLUSIVE","taxRate":5,"freight":200,"lots":[{"fabricName":"Cotton","fabricCode":"CT-40","colour":"Red","lotNo":"L1","qty":500,"unit":"M","rate":120}]}'

Body

{
  "supplierId": 3,
  "invoiceNo": "INV-101",
  "entryDate": "2026-09-20",
  "receivedDate": "2026-09-21",
  "taxMode": "EXCLUSIVE",
  "taxRate": 5,
  "freight": 200,
  "lots": [
    {
      "fabricName": "Cotton",
      "fabricCode": "CT-40",
      "colour": "Red",
      "lotNo": "L1",
      "qty": 500,
      "unit": "M",
      "rate": 120
    }
  ]
}

Response #54

{
  "ok": true,
  "id": 21,
  "entryNo": "RF-2026-00021"
}
POST /fabrics/:id/submit
auth: Bearer + fabrics.submit

Submit draft (needs invoice attachment). → SUBMITTED.

Request

curl -X POST 'http://localhost:3000/fabrics/:id/submit' \
  -H 'Authorization: Bearer <access-token>'

Response #55

{
  "ok": true
}
GET /fabrics?limit=10&offset=0
auth: Bearer

Paginated entries (own firm for suppliers; cost hidden for karigars).

Request

curl -X GET 'http://localhost:3000/fabrics?limit=10&offset=0' \
  -H 'Authorization: Bearer <access-token>'

Response #56

{
  "ok": true,
  "items": [
    {
      "id": 21,
      "entry_no": "RF-2026-00021",
      "status": "SUBMITTED"
    }
  ],
  "limit": 10,
  "offset": 0,
  "total": 9,
  "entries": [
    "...same as items..."
  ]
}
GET /fabrics/:id
auth: Bearer

Entry + lots + status history + stock ledger.

Request

curl -X GET 'http://localhost:3000/fabrics/:id' \
  -H 'Authorization: Bearer <access-token>'

Response #57

{
  "ok": true,
  "entry": {
    "id": 21,
    "status": "VERIFIED"
  },
  "lots": [],
  "history": [],
  "ledger": []
}
POST /verifications
auth: Bearer + fabrics.verify

Verify a lot (item-level). verifiedQtyM ≤ received; reason mandatory on shortage. Cannot verify own supply (SoD).

Request

curl -X POST 'http://localhost:3000/verifications' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"lotId":55,"verifiedQtyM":485,"defectCode":"DAMAGED","qcGrade":"B","reason":"15m water damage","priceFlag":"ok"}'

Body

{
  "lotId": 55,
  "verifiedQtyM": 485,
  "defectCode": "DAMAGED",
  "qcGrade": "B",
  "reason": "15m water damage",
  "priceFlag": "ok"
}

Response #58

{
  "ok": true
}
POST /verifications/:lotId/correction
auth: Bearer + fabrics.verify

Send lot back to supplier (max 3 cycles, then escalated).

Request

curl -X POST 'http://localhost:3000/verifications/:lotId/correction' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"reason":"Invoice qty mismatch, recheck roll L1"}'

Body

{
  "reason": "Invoice qty mismatch, recheck roll L1"
}

Response #59

{
  "ok": true
}
POST /samples
auth: Bearer + samples.create

Cut sample from VERIFIED lot (deducts stock in-txn).

Request

curl -X POST 'http://localhost:3000/samples' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"lotId":55,"qtyM":2,"purpose":"Colour approval"}'

Body

{
  "lotId": 55,
  "qtyM": 2,
  "purpose": "Colour approval"
}

Response #60

{
  "ok": true,
  "id": 9,
  "sampleNo": "S-2026-00009"
}
POST /samples/approve
auth: Bearer + samples.approve

COO approve/reject (no self-approval; delegates allowed). Rejection needs reason. Unlocks the lot for assignment.

Request

curl -X POST 'http://localhost:3000/samples/approve' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"sampleId":9,"decision":"APPROVED"}'

Body

{
  "sampleId": 9,
  "decision": "APPROVED"
}

Response #61

{
  "ok": true
}
GET /samples/pending?limit=10&offset=0
auth: Bearer

Paginated COO approval queue.

Request

curl -X GET 'http://localhost:3000/samples/pending?limit=10&offset=0' \
  -H 'Authorization: Bearer <access-token>'

Response #62

{
  "ok": true,
  "items": [
    {
      "id": 9,
      "sample_no": "S-2026-00009",
      "lot_no": "L1"
    }
  ],
  "limit": 10,
  "offset": 0,
  "total": 2,
  "samples": [
    "...same as items..."
  ]
}
GET /assignments/approved-supplies?limit=10&offset=0
auth: Bearer + assignments.create

All approved supplies with remaining allocation balances and AVAILABLE/PARTIALLY_ALLOCATED/FULLY_ALLOCATED status.

Request

curl -X GET 'http://localhost:3000/assignments/approved-supplies?limit=10&offset=0' \
  -H 'Authorization: Bearer <access-token>'

Response #63

{
  "ok": true,
  "items": [
    {
      "id": 2,
      "supply_no": "SUP-2026-00002",
      "received_qty_m": 10,
      "available_qty_m": 9,
      "allocation_status": "PARTIALLY_ALLOCATED"
    }
  ],
  "limit": 10,
  "offset": 0,
  "total": 4,
  "supplies": [
    "...same as items..."
  ]
}
POST /assignments/pattern-image
auth: Bearer + assignments.create

Upload one pattern reference image (multipart field `image`, jpg/png/webp). Returns the URL to send as patternImageUrl.

Request

curl -X POST 'http://localhost:3000/assignments/pattern-image' \
  -H 'Authorization: Bearer <access-token>' \
  -F 'image=@pattern.jpg'

Response #64

{
  "ok": true,
  "imageUrl": "/uploads/patterns/1727....jpg"
}
GET /assignments/pattern-numbers?q=&limit=10&offset=0
auth: Bearer + assignments.create

Saved pattern numbers with their images, for autocomplete while typing a pattern number.

Request

curl -X GET 'http://localhost:3000/assignments/pattern-numbers?q=&limit=10&offset=0' \
  -H 'Authorization: Bearer <access-token>'

Response #65

{
  "ok": true,
  "items": [
    {
      "pattern_number": "PAT-101",
      "image_url": "/uploads/patterns/1727....jpg"
    }
  ],
  "limit": 10,
  "offset": 0,
  "total": 1,
  "patterns": [
    "...same as items..."
  ]
}
POST /assignments
auth: Bearer + assignments.create

Assign approved-supply qty to karigar (row-locked; over-allocation 422; capacity overload warns via overload:true). Supply allocations decrement the same supply for later use. supplyItemId allocates from one product line and decrements both line and header. Optional sizes is a piece breakdown using even sizes 12–40; when sizes and a pattern with cloth-per-piece are given, pieces × cloth must fit the assigned metres (else 422 Assigned Cloth is Less); patternNumber replaces the dashboard job reference; patternImageUrl (from /assignments/pattern-image) saves/refreshes the pattern library image.

Request

curl -X POST 'http://localhost:3000/assignments' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"supplyId":2,"supplyItemId":2,"karigarId":5,"qty":150,"unit":"M","expectedReturnDate":"2026-10-05","patternNumber":"PAT-101","patternImageUrl":"/uploads/patterns/1727....jpg","sizes":[{"size":24,"qty":40},{"size":26,"qty":35}]}'

Body

{
  "supplyId": 2,
  "supplyItemId": 2,
  "karigarId": 5,
  "qty": 150,
  "unit": "M",
  "expectedReturnDate": "2026-10-05",
  "patternNumber": "PAT-101",
  "patternImageUrl": "/uploads/patterns/1727....jpg",
  "sizes": [
    {
      "size": 24,
      "qty": 40
    },
    {
      "size": 26,
      "qty": 35
    }
  ]
}

Response #66

{
  "ok": true,
  "id": 31,
  "assignNo": "ASG-2026-00031",
  "overload": false
}
GET /assignments?limit=10&offset=0&status=&karigarId=
auth: Bearer + assignments.create

Paginated assignment list for managers (status/karigar filters; source=supply|lot filter; includes lot- and supply-sourced rows with source labels).

Request

curl -X GET 'http://localhost:3000/assignments?limit=10&offset=0&status=&karigarId=' \
  -H 'Authorization: Bearer <access-token>'

Response #67

{
  "ok": true,
  "items": [
    {
      "id": 31,
      "assign_no": "ASG-2026-00031",
      "karigar_name": "Mohan",
      "status": "ASSIGNED"
    }
  ],
  "limit": 10,
  "offset": 0,
  "total": 1,
  "assignments": [
    "...same as items..."
  ]
}
POST /assignments/:id/ack
auth: Bearer + assignments.ack

Ack receipt (handover photo proof; managers proxy-ack flagged). Or refuse with refusalReason (qty restored).

Request

curl -X POST 'http://localhost:3000/assignments/:id/ack' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"handoverPhotoUrl":"/uploads/handover/1.jpg"}'

Body

{
  "handoverPhotoUrl": "/uploads/handover/1.jpg"
}

Response #68

{
  "ok": true
}
POST /assignments/:id/cancel
auth: Bearer + assignments.create

Cancel assignment (qty restored from ASSIGNED/ACKED/IN_PROCESS/CHALLAN_PENDING, audited).

Request

curl -X POST 'http://localhost:3000/assignments/:id/cancel' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"reason":"Karigar unavailable"}'

Body

{
  "reason": "Karigar unavailable"
}

Response #69

{
  "ok": true
}
POST /assignments/:id/challan
auth: Bearer (own karigar, or assignments.create)

Submit challan with final lines. All cloth rows and pattern cost lines stay on the challan; paidSizes/paidCostIds mark per-line Paid flags (unchecked by default). ASSIGNED/ACKED/IN_PROCESS → CHALLAN_PENDING.

Request

curl -X POST 'http://localhost:3000/assignments/:id/challan' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"paidSizes":[24],"paidCostIds":[3],"note":"Advance received"}'

Body

{
  "paidSizes": [
    24
  ],
  "paidCostIds": [
    3
  ],
  "note": "Advance received"
}

Response #70

{
  "ok": true
}
GET /assignments/challan-pending
auth: Bearer + finance/report roles

Challan approval queue: jobs with sizes+cloth, pattern costs, exclusions, and per-metre rate.

Request

curl -X GET 'http://localhost:3000/assignments/challan-pending' \
  -H 'Authorization: Bearer <access-token>'

Response #71

{
  "ok": true,
  "limit": 10,
  "offset": 0,
  "total": 1,
  "challans": [
    {
      "id": 31,
      "assign_no": "ASG-2026-00031",
      "status": "CHALLAN_PENDING"
    }
  ]
}
POST /assignments/:id/challan-decide
auth: Bearer + assignments.create | suppliers.manage

Decide a pending challan: APPROVED → CHALLAN_APPROVED (terminal), REJECTED → IN_PROCESS (reason mandatory).

Request

curl -X POST 'http://localhost:3000/assignments/:id/challan-decide' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"decision":"APPROVED"}'

Body

{
  "decision": "APPROVED"
}

Response #72

{
  "ok": true
}
POST /assignments/:id/process
auth: Bearer (own karigar, or assignments.create)

Karigar starts production: ASSIGNED/ACKED → IN_PROCESS (qty stays reserved, audited). Powers the Challan button in the job popup.

Request

curl -X POST 'http://localhost:3000/assignments/:id/process' \
  -H 'Authorization: Bearer <access-token>'

Response #73

{
  "ok": true
}
POST /assignments/:id/processing-costs
auth: Bearer + assignments.create

Save an external-vendor processing charge on an active assignment. processingTypeId must be offered by the vendor; custom processing names are created and linked automatically. Qty is whole pieces in NOs/PCs, rate is Rs per piece, patternNumber is required, and amount is calculated server-side as qty x rate.

Request

curl -X POST 'http://localhost:3000/assignments/:id/processing-costs' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"vendorId":1,"processing":"Embroidery","qty":25,"unit":"PCs","rate":50,"patternNumber":"PAT-101"}'

Body

{
  "vendorId": 1,
  "processing": "Embroidery",
  "qty": 25,
  "unit": "PCs",
  "rate": 50,
  "patternNumber": "PAT-101"
}

Response #74

{
  "ok": true,
  "id": 1,
  "totals": {
    "entries": 1,
    "material_paise": 150000,
    "processing_paise": 125000,
    "total_paise": 275000
  }
}
GET /assignments/:id/processing-costs
auth: Bearer + assignments.create | vendors.manage | reports.view | audit.view

Saved processing-cost entries and charge totals for one assignment.

Request

curl -X GET 'http://localhost:3000/assignments/:id/processing-costs' \
  -H 'Authorization: Bearer <access-token>'

Response #75

{
  "ok": true,
  "entries": [
    {
      "id": 1,
      "vendor_name": "Fine Thread Works",
      "processing_name": "Embroidery",
      "qty": 25,
      "unit": "PCs",
      "rate_paise": 5000,
      "amount_paise": 125000,
      "pattern_number": "PAT-101"
    }
  ],
  "totals": {
    "entries": 1,
    "processing_paise": 125000,
    "vendor_payable_paise": 125000
  }
}
GET /assignments/:id/cost-summary
auth: Bearer + assignments.create | vendors.manage | reports.view | audit.view

Material plus processing rollup for future end-product costing. pro-rates the source material by assigned metres and includes the assigned entry image when available. Closed assignments are excluded from product cost while vendor charges remain payable.

Request

curl -X GET 'http://localhost:3000/assignments/:id/cost-summary' \
  -H 'Authorization: Bearer <access-token>'

Response #76

{
  "ok": true,
  "assignment": {
    "id": 31,
    "assign_no": "ASG-2026-00031",
    "qty_base_m": 25,
    "pattern_number": "PAT-101",
    "source": {
      "kind": "supply",
      "ref": "SUP-2026-00011",
      "image": "/uploads/supplies/a.jpg"
    }
  },
  "sizes": [
    {
      "size": 24,
      "qty": 40
    },
    {
      "size": 26,
      "qty": 35
    }
  ],
  "totals": {
    "material_paise": 150000,
    "processing_paise": 125000,
    "total_paise": 275000,
    "vendor_payable_paise": 125000,
    "included_in_product_cost": true
  }
}
GET /assignments/mine/list?limit=10&offset=0
auth: Bearer

Karigar's own jobs, paginated.

Request

curl -X GET 'http://localhost:3000/assignments/mine/list?limit=10&offset=0' \
  -H 'Authorization: Bearer <access-token>'

Response #77

{
  "ok": true,
  "items": [
    {
      "id": 31,
      "assign_no": "ASG-2026-00031",
      "status": "ASSIGNED"
    }
  ],
  "limit": 10,
  "offset": 0,
  "total": 6,
  "assignments": [
    "...same as items..."
  ]
}
POST /delegates
auth: Bearer + samples.approve

Grant time-boxed COO approval cover.

Request

curl -X POST 'http://localhost:3000/delegates' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"delegateUserId":12,"days":7}'

Body

{
  "delegateUserId": 12,
  "days": 7
}

Response #78

{
  "ok": true
}

Reports

GET /reports/stock?limit=10&offset=0
auth: Bearer + reports.view

Per-lot received/verified/available.

Request

curl -X GET 'http://localhost:3000/reports/stock?limit=10&offset=0' \
  -H 'Authorization: Bearer <access-token>'

Response #79

{
  "ok": true,
  "items": [
    {
      "lot_id": 55,
      "received": 500,
      "verified": 485,
      "available": 333
    }
  ],
  "limit": 10,
  "offset": 0,
  "total": 40,
  "stock": [
    "...same as items..."
  ]
}
GET /reports/available?limit=10&offset=0
auth: Bearer + reports.view

COO-approved lots with qty left to assign.

Request

curl -X GET 'http://localhost:3000/reports/available?limit=10&offset=0' \
  -H 'Authorization: Bearer <access-token>'

Response #80

{
  "ok": true,
  "items": [],
  "limit": 10,
  "offset": 0,
  "total": 0,
  "lots": []
}
GET /reports/overdue?limit=10&offset=0
auth: Bearer + reports.view

Unverified >48h + unacked >72h.

Request

curl -X GET 'http://localhost:3000/reports/overdue?limit=10&offset=0' \
  -H 'Authorization: Bearer <access-token>'

Response #81

{
  "ok": true,
  "limit": 10,
  "offset": 0,
  "overdueVerification": [],
  "unackedAssignments": []
}
GET /reports/cost-summary?limit=10&offset=0
auth: Bearer + reports.view

Landed cost per entry (recomputable from components).

Request

curl -X GET 'http://localhost:3000/reports/cost-summary?limit=10&offset=0' \
  -H 'Authorization: Bearer <access-token>'

Response #82

{
  "ok": true,
  "items": [
    {
      "entry_no": "RF-2026-00021",
      "total_m": 500,
      "total_rs": 63000,
      "per_m_rs": 126
    }
  ],
  "limit": 10,
  "offset": 0,
  "total": 9,
  "summary": [
    "...same as items..."
  ]
}
GET /reports/lot/:id/timeline
auth: Bearer + reports.view

Full lot timeline: ledger + history + audit.

Request

curl -X GET 'http://localhost:3000/reports/lot/:id/timeline' \
  -H 'Authorization: Bearer <access-token>'

Response #83

{
  "ok": true,
  "lot": {},
  "ledger": [],
  "history": [],
  "audit": []
}
GET /audit?limit=10&offset=0&entity=&id=
auth: Bearer + audit.view

Audit trail, optionally filtered by entity (e.g. ?entity=supply&id=11).

Request

curl -X GET 'http://localhost:3000/audit?limit=10&offset=0&entity=&id=' \
  -H 'Authorization: Bearer <access-token>'

Response #84

{
  "ok": true,
  "items": [
    {
      "action": "supply_created",
      "entity_type": "supply",
      "entity_id": "11"
    }
  ],
  "limit": 10,
  "offset": 0,
  "total": 120,
  "audit": [
    "...same as items..."
  ]
}

Sales

GET /sales/customers?q=9876&limit=8&offset=0
auth: none

Search customers by name or phone (autocomplete for the sales form).

Request

curl -X GET 'http://localhost:3000/sales/customers?q=9876&limit=8&offset=0'

Response #85

{
  "ok": true,
  "items": [
    {
      "id": 1,
      "name": "ABC Customer",
      "phone_e164": "+919876543210"
    }
  ],
  "limit": 8,
  "offset": 0,
  "total": 1,
  "customers": [
    "...same as items..."
  ]
}
POST /sales/customers
auth: none (rate-limited)

Add a customer. Contact number required and unique — 409 when it already exists. Address: area/city text, state from the all-India list, 6-digit pincode. Optional openingBalance (Rs, may be negative) carries forward into the customer ledger.

Request

curl -X POST 'http://localhost:3000/sales/customers' \
  -H 'Content-Type: application/json' \
  -d '{"name":"ABC Customer","phone":"9876543210","area":"Ring Road","city":"Surat","state":"Gujarat","pincode":"395002","openingBalance":0}'

Body

{
  "name": "ABC Customer",
  "phone": "9876543210",
  "area": "Ring Road",
  "city": "Surat",
  "state": "Gujarat",
  "pincode": "395002",
  "openingBalance": 0
}

Response #86

{
  "ok": true,
  "id": 1,
  "name": "ABC Customer",
  "phone": "+919876543210",
  "area": "Ring Road",
  "city": "Surat",
  "state": "Gujarat",
  "pincode": "395002",
  "openingBalancePaise": 0
}
PATCH /sales/customers/:id
auth: none (rate-limited)

Edit customer details (10-digit phone stays unique). openingBalance (Rs, may be negative) updates the ledger carry-forward. Invoices keep their snapshots.

Request

curl -X PATCH 'http://localhost:3000/sales/customers/:id' \
  -H 'Content-Type: application/json' \
  -d '{"city":"Navsari","pincode":"396445","openingBalance":2500}'

Body

{
  "city": "Navsari",
  "pincode": "396445",
  "openingBalance": 2500
}

Response #87

{
  "ok": true
}
GET /sales/next-invoice
auth: none

Advisory next invoice number for the form header (authoritative number is assigned on save).

Request

curl -X GET 'http://localhost:3000/sales/next-invoice'

Response #88

{
  "ok": true,
  "invoiceNo": "INV-0001"
}
POST /sales/invoices
auth: none (rate-limited)

Save a sale: customerId or name/phone (+ optional details) plus 1–100 item rows {type, pcs, rate}. type is one of Frock, Hanger Set, Kurti Set, Cord Set, Leggings, Night Suit and prints as a column on the invoice. Amounts auto-computed; unknown phones create the customer, known phones reuse it. Optional: gstType IGST (5%) or CGST_SGST (2.5+2.5, default for Maharashtra), discountType percent/flat/per_piece + discountValue (discount on subtotal, GST on the net), agentId (type Agent) + commType percent (of net) / flat (per pc x pcs). When agentId is omitted and the customer has exactly one active agent in the agent-customer commission book, that agent and its commission percent are applied automatically on create (invoice updates keep the saved snapshot).

Request

curl -X POST 'http://localhost:3000/sales/invoices' \
  -H 'Content-Type: application/json' \
  -d '{"date":"2026-10-15","customerId":1,"items":[{"type":"Frock","pcs":2,"rate":100},{"type":"Leggings","pcs":4,"rate":200}],"gstType":"IGST","discountType":"percent","discountValue":10,"agentId":2,"commType":"percent","commValue":2}'

Body

{
  "date": "2026-10-15",
  "customerId": 1,
  "items": [
    {
      "type": "Frock",
      "pcs": 2,
      "rate": 100
    },
    {
      "type": "Leggings",
      "pcs": 4,
      "rate": 200
    }
  ],
  "gstType": "IGST",
  "discountType": "percent",
  "discountValue": 10,
  "agentId": 2,
  "commType": "percent",
  "commValue": 2
}

Response #89

{
  "ok": true,
  "id": 1,
  "invoiceNo": "INV-0001",
  "totalPcs": 6,
  "totalPaise": 100000
}
GET /sales/invoices?q=ABC&from=2026-10-01&to=2026-10-31&limit=20&offset=0
auth: none

Sales history — search by invoice number, customer name, phone, exact date (&date=YYYY-MM-DD) or a from/to date range (&from=YYYY-MM-DD&to=YYYY-MM-DD). Each row carries the bill item types (comma-joined), linked GR, Bank/Cash received, round off and balance due.

Request

curl -X GET 'http://localhost:3000/sales/invoices?q=ABC&from=2026-10-01&to=2026-10-31&limit=20&offset=0'

Response #90

{
  "ok": true,
  "items": [
    {
      "invoice_no": "INV-0001",
      "customer_name": "ABC Customer",
      "types": "Frock, Leggings",
      "total_pcs": 12,
      "total_paise": 280000,
      "grPaise": 0,
      "bankPaise": 0,
      "cashPaise": 0,
      "roundOffPaise": 0,
      "balPaise": 280000,
      "acctSplits": [
        {
          "name": "AXIS BANK",
          "mop": "Bank",
          "paise": 0
        }
      ],
      "bankEntries": [
        {
          "id": 1,
          "account_name": "AXIS BANK",
          "amount_paise": 0,
          "recv_date": "2026-10-20"
        }
      ]
    }
  ],
  "limit": 20,
  "offset": 0,
  "total": 1,
  "sales": [
    "...same as items..."
  ]
}
GET /sales/invoices/:id
auth: none

One sale with its item lines (used for View / Edit / Print).

Request

curl -X GET 'http://localhost:3000/sales/invoices/:id'

Response #91

{
  "ok": true,
  "invoice": {
    "invoice_no": "INV-0001",
    "total_pcs": 12,
    "total_paise": 280000
  },
  "items": [
    {
      "lineNo": 1,
      "itemType": "Frock",
      "pcs": 2,
      "ratePaise": 10000,
      "amountPaise": 20000
    }
  ]
}
PATCH /sales/invoices/:id
auth: none (rate-limited)

Edit date, customer snapshot, item rows (type is required when items are sent; pcs/rate plus per-line discountType percent/flat + discountValue), bill discount, GST, agent/commission. Send commAmount (Rs) to override the stored agent commission for this bill (requires an agent on the sale); sending items without type returns 422. Totals recomputed.

Request

curl -X PATCH 'http://localhost:3000/sales/invoices/:id' \
  -H 'Content-Type: application/json' \
  -d '{"items":[{"type":"Frock","pcs":3,"rate":150,"discountType":"percent","discountValue":10}],"gstType":"CGST_SGST","commAmount":45}'

Body

{
  "items": [
    {
      "type": "Frock",
      "pcs": 3,
      "rate": 150,
      "discountType": "percent",
      "discountValue": 10
    }
  ],
  "gstType": "CGST_SGST",
  "commAmount": 45
}

Response #92

{
  "ok": true
}
PATCH /sales/invoices/:id/payments
auth: none (rate-limited)

Reconcile the received amount on one invoice. cash / bank are absolute totals (rupees): a higher value adds a receipt for the difference, a lower value reduces or deletes the newest receipts of that MOP — so reducing a receipt no longer needs a Customer Ledger delete. New receipts are dated to the invoice sale date (so Bank/Cash Ledger dates match Sales History). Optional accountId / payableUserId apply to newly created receipts.

Request

curl -X PATCH 'http://localhost:3000/sales/invoices/:id/payments' \
  -H 'Content-Type: application/json' \
  -d '{"cash":900,"bank":100,"accountId":5,"note":"Corrected"}'

Body

{
  "cash": 900,
  "bank": 100,
  "accountId": 5,
  "note": "Corrected"
}

Response #93

{
  "ok": true,
  "created": 1,
  "updated": 0,
  "deleted": 1
}
PATCH /sales/invoices/:id/round-off
auth: none (rate-limited)

Write off part of the bill so it settles (round off). total_paise is stored net of round_off_paise; cannot exceed the outstanding balance (bill − GR − received). Send 0 to remove the write-off.

Request

curl -X PATCH 'http://localhost:3000/sales/invoices/:id/round-off' \
  -H 'Content-Type: application/json' \
  -d '{"amount":2.5}'

Body

{
  "amount": 2.5
}

Response #94

{
  "ok": true,
  "roundOffPaise": 250,
  "totalPaise": 279750
}
DELETE /sales/invoices/:id
auth: none (rate-limited)

Delete a sale and its lines (client confirms first).

Request

curl -X DELETE 'http://localhost:3000/sales/invoices/:id'

Response #95

{
  "ok": true,
  "deleted": 1
}
GET /sales/customers/:id/ledger
auth: none

Customer statement — sales (bill), GR returns and payments received in one date-wise list with running balance. Opening balance (openingBalancePaise) is the carry-forward starting point. Balance = opening + billed − returned − received. Only Completed GRs are counted.

Request

curl -X GET 'http://localhost:3000/sales/customers/:id/ledger'

Response #96

{
  "ok": true,
  "openingBalancePaise": 0,
  "entries": [
    {
      "kind": "sale",
      "date": "2026-10-15",
      "ref": "INV-0001",
      "pcs": 12,
      "billPaise": 280000,
      "type": "sale",
      "receivedPaise": 0,
      "mop": null,
      "account": null,
      "balancePaise": 280000
    }
  ],
  "totals": {
    "openingBalancePaise": 0,
    "billedPaise": 280000,
    "returnedPaise": 0,
    "receivedPaise": 0,
    "balancePaise": 280000
  }
}
GET /sales/accounts
auth: none

Shop cash/bank accounts (payment destinations).

Request

curl -X GET 'http://localhost:3000/sales/accounts'

Response #97

{
  "ok": true,
  "items": [
    {
      "id": 1,
      "name": "Main Cash"
    }
  ],
  "accounts": [
    "...same as items..."
  ]
}
POST /sales/accounts
auth: none (rate-limited)

Add an account — same fields as the customer form (name + phone/address/note). type General/Cash/Bank/Agent (agents earn commission).

Request

curl -X POST 'http://localhost:3000/sales/accounts' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Agent Raju","type":"Agent","phone":"9811111111"}'

Body

{
  "name": "Agent Raju",
  "type": "Agent",
  "phone": "9811111111"
}

Response #98

{
  "ok": true,
  "id": 2,
  "name": "Agent Raju",
  "type": "Agent"
}
POST /sales/payments
auth: none (rate-limited)

Record money received: receipt auto-numbered (RCPT-), MOP Cash/Bank, optional To-account (accountId for shop accounts, or payableUserId for app/Payable Accounts users) and optional against-bill link (invoiceId).

Request

curl -X POST 'http://localhost:3000/sales/payments' \
  -H 'Content-Type: application/json' \
  -d '{"customerId":1,"amount":500,"mop":"Cash","accountId":1,"invoiceId":3}'

Body

{
  "customerId": 1,
  "amount": 500,
  "mop": "Cash",
  "accountId": 1,
  "invoiceId": 3
}

Response #99

{
  "ok": true,
  "id": 1,
  "receiptNo": "RCPT-0001"
}
PATCH /sales/payments/:id
auth: none (rate-limited)

Adjust one received entry amount (used by the per-account bank inputs in Sales History; balances update).

Request

curl -X PATCH 'http://localhost:3000/sales/payments/:id' \
  -H 'Content-Type: application/json' \
  -d '{"amount":450}'

Body

{
  "amount": 450
}

Response #100

{
  "ok": true,
  "id": 1,
  "amountPaise": 45000
}
DELETE /sales/payments/:id
auth: none (rate-limited)

Delete a received entry (balances update).

Request

curl -X DELETE 'http://localhost:3000/sales/payments/:id'

Response #101

{
  "ok": true,
  "deleted": 1
}
POST /sales/return-image
auth: none (rate-limited)

Upload one GR line image (multipart field `image`, jpg/png/webp). Resized to 480p (max 854x480, no upscale) and stored under /uploads/returns/. Send the returned imageUrl on the GR line.

Request

curl -X POST 'http://localhost:3000/sales/return-image' \
  -F 'image=@gr-item.jpg'

Response #102

{
  "ok": true,
  "imageUrl": "/uploads/returns/gr-1727....jpg"
}
GET /sales/returns?from=2026-10-01&to=2026-10-19&limit=100&offset=0
auth: none

GR (goods return) entries — standalone or linked to an invoice. Search by GR no/customer/phone/note, exact date, date range (from/to) or customerId. Each row carries sales_person and status (Processing/Completed). totals covers all rows; completedTotals and processingTotal are the ledger view.

Request

curl -X GET 'http://localhost:3000/sales/returns?from=2026-10-01&to=2026-10-19&limit=100&offset=0'

Response #103

{
  "ok": true,
  "items": [
    {
      "id": 1,
      "gr_no": "GR-0001",
      "return_date": "2026-10-19",
      "customer_name": "Abdul Samad",
      "customer_phone": "+919653322781",
      "sales_person": "Zakir",
      "invoice_id": null,
      "invoice_no": null,
      "pcs": 2,
      "amount_paise": 20000,
      "status": "Processing",
      "note": null
    }
  ],
  "returns": [
    "...same as items..."
  ],
  "totals": {
    "amountPaise": 20000,
    "pcs": 2
  },
  "completedTotals": {
    "amountPaise": 0,
    "pcs": 0
  },
  "processingTotal": 1
}
GET /sales/returns/:id
auth: none

One GR entry with its line items (type, pcs, rate, amount, imageUrl).

Request

curl -X GET 'http://localhost:3000/sales/returns/:id'

Response #104

{
  "ok": true,
  "gr": {
    "id": 1,
    "gr_no": "GR-0001",
    "pcs": 2,
    "amount_paise": 20000,
    "status": "Processing"
  },
  "items": [
    {
      "lineNo": 1,
      "itemType": "Frock",
      "pcs": 2,
      "ratePaise": 10000,
      "amountPaise": 20000,
      "imageUrl": "/uploads/returns/gr-1727....jpg"
    }
  ]
}
POST /sales/returns
auth: none (rate-limited)

Goods return (GR): auto-numbered (GR-). Standalone — not linked to an invoice. Send items[] (type/pcs/rate plus optional imageUrl) and salesPerson; new GRs start as Processing and are excluded from ledgers/calculations until PATCHed to Completed. Legacy amount/pcs without items still works. invoiceId links the GR to that invoice (inheriting its customer/sales person when omitted) and defaults to Completed.

Request

curl -X POST 'http://localhost:3000/sales/returns' \
  -H 'Content-Type: application/json' \
  -d '{"customerId":1,"date":"2026-10-19","salesPerson":"Zakir","note":"Damaged","items":[{"type":"Frock","pcs":2,"rate":100,"imageUrl":"/uploads/returns/gr-1727....jpg"}]}'

Body

{
  "customerId": 1,
  "date": "2026-10-19",
  "salesPerson": "Zakir",
  "note": "Damaged",
  "items": [
    {
      "type": "Frock",
      "pcs": 2,
      "rate": 100,
      "imageUrl": "/uploads/returns/gr-1727....jpg"
    }
  ]
}

Response #105

{
  "ok": true,
  "id": 1,
  "grNo": "GR-0001",
  "status": "Processing"
}
PATCH /sales/returns/:id
auth: none (rate-limited)

Edit a GR entry (date, customer, salesPerson, status, note). Send items[] to replace the line list (pcs/amount are recalculated). Set status=Completed to make it count in the customer ledger and all totals.

Request

curl -X PATCH 'http://localhost:3000/sales/returns/:id' \
  -H 'Content-Type: application/json' \
  -d '{"status":"Completed","items":[{"type":"Frock","pcs":2,"rate":125}]}'

Body

{
  "status": "Completed",
  "items": [
    {
      "type": "Frock",
      "pcs": 2,
      "rate": 125
    }
  ]
}

Response #106

{
  "ok": true,
  "id": 1
}
DELETE /sales/returns/:id
auth: none (rate-limited)

Delete a GR entry (balances update).

Request

curl -X DELETE 'http://localhost:3000/sales/returns/:id'

Response #107

{
  "ok": true,
  "deleted": 1
}
GET /sales/accounts/:id/ledger
auth: none

Agent statement — commission earned per bill minus payouts, oldest first with running balance payable.

Request

curl -X GET 'http://localhost:3000/sales/accounts/:id/ledger'

Response #108

{
  "ok": true,
  "entries": [
    {
      "kind": "commission",
      "date": "2026-10-15",
      "ref": "INV-0001",
      "commPaise": 1800,
      "paidPaise": 0,
      "balancePaise": 1800
    }
  ],
  "totals": {
    "earnedPaise": 1800,
    "paidPaise": 0,
    "balancePaise": 1800
  }
}
POST /sales/agent-payouts
auth: none (rate-limited)

Payout to an Agent account: auto-numbered (AGP-), MOP Cash/Bank.

Request

curl -X POST 'http://localhost:3000/sales/agent-payouts' \
  -H 'Content-Type: application/json' \
  -d '{"accountId":2,"amount":500,"mop":"Cash"}'

Body

{
  "accountId": 2,
  "amount": 500,
  "mop": "Cash"
}

Response #109

{
  "ok": true,
  "id": 1,
  "payoutNo": "AGP-0001"
}
DELETE /sales/agent-payouts/:id
auth: none (rate-limited)

Delete a payout (balances update).

Request

curl -X DELETE 'http://localhost:3000/sales/agent-payouts/:id'

Response #110

{
  "ok": true,
  "deleted": 1
}
PATCH /sales/accounts/:id
auth: none (rate-limited)

Edit an account/user (name, type, contact, address).

Request

curl -X PATCH 'http://localhost:3000/sales/accounts/:id' \
  -H 'Content-Type: application/json' \
  -d '{"city":"Navsari","type":"Staff"}'

Body

{
  "city": "Navsari",
  "type": "Staff"
}

Response #111

{
  "ok": true
}
GET /sales/agents
auth: none

Agent accounts (type=Agent) with their customer commission book — each linked customer and the commission percent applied to that customer's new sales.

Request

curl -X GET 'http://localhost:3000/sales/agents'

Response #112

{
  "ok": true,
  "agents": [
    {
      "id": 8,
      "name": "Agent Raju",
      "phone_e164": "+919876500001",
      "customers": [
        {
          "customerId": 1,
          "customerName": "Cool Kids",
          "customerPhone": "+913223232312",
          "commissionPercent": 5
        }
      ]
    }
  ]
}
POST /sales/agents
auth: none (rate-limited)

Create an Agent account and replace-free initial customer book in one save. customers[] rows are {customerId, percent} (0–100); a customer may appear only once per agent.

Request

curl -X POST 'http://localhost:3000/sales/agents' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Agent Raju","phone":"9876500001","details":"West zone","customers":[{"customerId":1,"percent":5},{"customerId":2,"percent":3.5}]}'

Body

{
  "name": "Agent Raju",
  "phone": "9876500001",
  "details": "West zone",
  "customers": [
    {
      "customerId": 1,
      "percent": 5
    },
    {
      "customerId": 2,
      "percent": 3.5
    }
  ]
}

Response #113

{
  "ok": true,
  "id": 8,
  "name": "Agent Raju"
}
PATCH /sales/agents/:id
auth: none (rate-limited)

Edit an Agent account. Sending customers[] replaces the whole commission book for that agent; omit it to leave the book untouched.

Request

curl -X PATCH 'http://localhost:3000/sales/agents/:id' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Agent Raju","customers":[{"customerId":1,"percent":4.25}]}'

Body

{
  "name": "Agent Raju",
  "customers": [
    {
      "customerId": 1,
      "percent": 4.25
    }
  ]
}

Response #114

{
  "ok": true,
  "id": 8
}
GET /sales/account-users
auth: Bearer (admin or accounts)

App login users (admin role excluded everywhere here) + assignable types, for the Payable Accounts tab. Each user carries opening_balance_paise for the ledger.

Request

curl -X GET 'http://localhost:3000/sales/account-users' \
  -H 'Authorization: Bearer <access-token>'

Response #115

{
  "ok": true,
  "users": [
    {
      "id": 9,
      "phone_e164": "+919876543210",
      "role": "karigar",
      "opening_balance_paise": 0
    }
  ],
  "roles": [
    {
      "code": "karigar",
      "name": "Karigar"
    }
  ]
}
POST /sales/account-users
auth: Bearer (admin or accounts)

Invite a login user (any non-admin type; admin is invite-only). Optional openingBalance (Rs, may be negative) seeds the payable ledger.

Request

curl -X POST 'http://localhost:3000/sales/account-users' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"phone":"9876543210","roleCode":"karigar","displayName":"K1","openingBalance":0}'

Body

{
  "phone": "9876543210",
  "roleCode": "karigar",
  "displayName": "K1",
  "openingBalance": 0
}

Response #116

{
  "ok": true,
  "userId": 9,
  "openingBalancePaise": 0
}
PATCH /sales/account-users/:id
auth: Bearer (admin or accounts)

Edit a Payable Accounts user: displayName, roleCode, isActive and/or openingBalance (Rs, may be negative). Role/active changes revoke sessions.

Request

curl -X PATCH 'http://localhost:3000/sales/account-users/:id' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"displayName":"K1","openingBalance":2500}'

Body

{
  "displayName": "K1",
  "openingBalance": 2500
}

Response #117

{
  "ok": true,
  "id": 9,
  "openingBalancePaise": 250000
}
GET /sales/payable-accounts/:id/ledger
auth: Bearer (admin or accounts)

Ledger for one Payable Accounts app user: money received (payments with payableUserId) vs expenses paid out, oldest first with running balance. Starts from the account opening balance when set.

Request

curl -X GET 'http://localhost:3000/sales/payable-accounts/:id/ledger' \
  -H 'Authorization: Bearer <access-token>'

Response #118

{
  "ok": true,
  "openingBalancePaise": 0,
  "account": {
    "id": 9,
    "display_name": "K1",
    "role_name": "Karigar"
  },
  "entries": [
    {
      "kind": "received",
      "date": "2026-10-19",
      "ref": "RCPT-0013",
      "particulars": "Received from Abdul Samad",
      "mop": "Cash",
      "inPaise": 20000,
      "outPaise": 0,
      "balancePaise": 20000
    }
  ],
  "totals": {
    "receipts": 1,
    "receivedPaise": 20000,
    "expenses": 0,
    "spentPaise": 0,
    "openingBalancePaise": 0,
    "balancePaise": 20000
  }
}
GET /sales/cash-ledger
auth: Bearer (admin or accounts)

Day book Cash / Bank ledger: receipts with the given MOP as money in, plus the Cash Paid / Bank Paid portion of expenses as money out, oldest first with a running balance. Always shown month-wise when from/to are omitted; opening balance is carried from before the from date. mop=Cash|Bank (default Cash); optional q / from / to filters.

Request

curl -X GET 'http://localhost:3000/sales/cash-ledger' \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"mop":"Cash","from":"2026-10-01","to":"2026-10-31"}'

Body

{
  "mop": "Cash",
  "from": "2026-10-01",
  "to": "2026-10-31"
}

Response #119

{
  "ok": true,
  "mop": "Cash",
  "openingBalancePaise": 5000,
  "entries": [
    {
      "kind": "received",
      "id": 1,
      "date": "2026-10-19",
      "ref": "RCPT-0013",
      "particulars": "Received from Abdul Samad",
      "inPaise": 20000,
      "outPaise": 0,
      "balancePaise": 25000
    }
  ],
  "totals": {
    "mop": "Cash",
    "receipts": 1,
    "receivedPaise": 20000,
    "expenses": 0,
    "spentPaise": 0,
    "openingBalancePaise": 5000,
    "balancePaise": 25000
  }
}
GET /sales/sales-persons
auth: none

Sales Person picker names: Payable Accounts users of type Sales Person (unverified/inactive included) plus legacy free-text names saved on bills, de-duplicated.

Request

curl -X GET 'http://localhost:3000/sales/sales-persons'

Response #120

{
  "ok": true,
  "names": [
    "Abdul Basit",
    "Zakwan"
  ]
}
PATCH /sales/sales-persons
auth: none (rate-limited)

Rename a salesperson across all bills.

Request

curl -X PATCH 'http://localhost:3000/sales/sales-persons' \
  -H 'Content-Type: application/json' \
  -d '{"oldName":"Ravi","newName":"Ravi Kumar"}'

Body

{
  "oldName": "Ravi",
  "newName": "Ravi Kumar"
}

Response #121

{
  "ok": true,
  "updated": 3
}
DELETE /sales/sales-persons
auth: none (rate-limited)

Remove a salesperson name from all bills (bills kept).

Request

curl -X DELETE 'http://localhost:3000/sales/sales-persons' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Ravi Kumar"}'

Body

{
  "name": "Ravi Kumar"
}

Response #122

{
  "ok": true,
  "cleared": 3
}
GET /sales/expenses?date=2026-10-19&limit=100&offset=0
auth: none

Day book expenses — search by category/particulars/account/reference, filter by exact category (Accounts Head), exact date or from/to range. Returns filtered totals (cash/bank split) and the distinct category list for suggestions.

Request

curl -X GET 'http://localhost:3000/sales/expenses?date=2026-10-19&limit=100&offset=0'

Response #123

{
  "ok": true,
  "items": [
    {
      "id": 1,
      "expense_no": "EXP-0001",
      "expense_date": "2026-10-19",
      "category": "Packing",
      "description": "Packing tape",
      "amount_paise": 12550,
      "cash_paid_paise": 12550,
      "bank_paid_paise": 0
    }
  ],
  "expenses": [
    "...same as items..."
  ],
  "categories": [
    "Packing"
  ],
  "totals": {
    "amountPaise": 12550,
    "cashPaise": 12550,
    "bankPaise": 0
  }
}
POST /sales/expenses
auth: none (rate-limited)

Create an expense: auto-numbered (EXP-). Cash Paid + Bank Paid are split amounts (total stored in amount_paise); From Account is an optional Payable Accounts user (payableUserId). Legacy amount is treated as cash when no split is sent.

Request

curl -X POST 'http://localhost:3000/sales/expenses' \
  -H 'Content-Type: application/json' \
  -d '{"date":"2026-10-19","category":"Packing","description":"Packing tape","cashPaid":100,"bankPaid":25.5,"payableUserId":9}'

Body

{
  "date": "2026-10-19",
  "category": "Packing",
  "description": "Packing tape",
  "cashPaid": 100,
  "bankPaid": 25.5,
  "payableUserId": 9
}

Response #124

{
  "ok": true,
  "id": 1,
  "expenseNo": "EXP-0001"
}
PATCH /sales/expenses/:id
auth: none (rate-limited)

Edit an expense (date, category/Accounts Head, particulars, cashPaid/bankPaid split, From Account payableUserId, note). Sending only one side keeps the stored value for the other.

Request

curl -X PATCH 'http://localhost:3000/sales/expenses/:id' \
  -H 'Content-Type: application/json' \
  -d '{"cashPaid":100,"bankPaid":50,"note":"Revised"}'

Body

{
  "cashPaid": 100,
  "bankPaid": 50,
  "note": "Revised"
}

Response #125

{
  "ok": true,
  "expense": {
    "id": 1,
    "expense_no": "EXP-0001",
    "amount_paise": 15000,
    "cash_paid_paise": 10000,
    "bank_paid_paise": 5000
  }
}
DELETE /sales/expenses/:id
auth: none (rate-limited)

Delete an expense (client confirms first).

Request

curl -X DELETE 'http://localhost:3000/sales/expenses/:id'

Response #126

{
  "ok": true,
  "deleted": 1
}