AmmiKids Ops API102 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.

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"}'

Body

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

Response #86

{
  "ok": true,
  "id": 1,
  "name": "ABC Customer",
  "phone": "+919876543210",
  "area": "Ring Road",
  "city": "Surat",
  "state": "Gujarat",
  "pincode": "395002"
}
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 #87

{
  "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 {pcs, rate}. 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).

Request

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

Body

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

Response #88

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

Sales history — search by invoice number, customer name, phone, or exact date (&date=YYYY-MM-DD).

Request

curl -X GET 'http://localhost:3000/sales/invoices?q=ABC&limit=20&offset=0'

Response #89

{
  "ok": true,
  "items": [
    {
      "invoice_no": "INV-0001",
      "customer_name": "ABC Customer",
      "total_pcs": 12,
      "total_paise": 280000
    }
  ],
  "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 #90

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

Edit date, customer snapshot, item rows (pcs/rate plus per-line discountType percent/flat + discountValue), bill discount, GST, agent/commission. Totals recomputed.

Request

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

Body

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

Response #91

{
  "ok": true
}
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 #92

{
  "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. Balance = billed − returned − received.

Request

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

Response #93

{
  "ok": true,
  "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": {
    "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 #94

{
  "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 #95

{
  "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 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 #96

{
  "ok": true,
  "id": 1,
  "receiptNo": "RCPT-0001"
}
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 #97

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

Goods return (GR): auto-numbered (GR-), optional link to a bill, reduces what the customer owes.

Request

curl -X POST 'http://localhost:3000/sales/returns' \
  -H 'Content-Type: application/json' \
  -d '{"customerId":1,"invoiceId":3,"pcs":2,"amount":200}'

Body

{
  "customerId": 1,
  "invoiceId": 3,
  "pcs": 2,
  "amount": 200
}

Response #98

{
  "ok": true,
  "id": 1,
  "grNo": "GR-0001"
}
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 #99

{
  "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 #100

{
  "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 #101

{
  "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 #102

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