openapi: 3.0.3 info: title: 'Dukanam Mobile API' description: 'Interactive Dukanam API reference for authentication, billing, POS, inventory, GST, compliance, exports, team access, testimonials and private feedback.' version: 1.0.0 servers: - url: 'https://dukanam.com' tags: - name: Authentication description: "\nPrimary WhatsApp OTP authentication for mobile clients." - name: Businesses description: '' - name: 'Business onboarding' description: "\nComplete the resumable setup required after registering a new business. Until onboarding finishes,\nclients may resubmit a completed required step to correct saved data without moving the current step backwards." - name: 'GST verification' description: "\nVerify an Indian GSTIN through the configured Masters India taxpayer service.\nThe endpoint is available during onboarding so a merchant can use the verified\nlegal name and registered address before saving their business tax profile." - name: Contacts description: "\nLoading customers and suppliers from a spreadsheet, which is how a shop moves its party list\noff whatever it used before. A file is uploaded, checked, and reviewed before anything is\nwritten; parties appear only when the import is committed, and then all of them at once." - name: 'Khata ledger' description: '' - name: Inventory description: "\nSuppliers of an item: the terms agreed with each (rate, lead time, minimum order, their own\nitem code, preferred flag) beside what purchase invoices show was actually billed." - name: 'Sales invoices' description: "\nSuccessful sales create purchase-date warranties from each covered item's policy, including\nnon-serialised goods. Invoice responses expose warranties and warranty_delivery. Automatic\nprimary-customer email includes invoice and warranty PDFs when mail and the recipient are available.\nIssued policies are snapshots; item edits do not change existing coverage. Returns reduce\ncovered quantities, voiding cancels cover, and coverage corrections supersede old slips." - name: 'Business documents' description: '' - name: Payments description: '' - name: Expenses description: "\nExpense records are a core Purchases capability on every plan. Workspace purchase permissions still apply." - name: POS description: "\nPOS and cash-register endpoints remain available while onboarding is on the `first_work` step so a shop can complete its first sale. Other business endpoints continue to return `onboarding_incomplete` until setup finishes." - name: 'Cash register' description: "\nCash-register and POS endpoints remain available while onboarding is on the `first_work` step so a shop can complete its first POS sale. Other business endpoints continue to return `onboarding_incomplete` until setup finishes." - name: Reports description: '' - name: 'Business compliance guidance' description: '' - name: 'GST compliance' description: '' - name: Billing description: '' - name: Referrals description: "\nRefer other shops and earn free subscription months. Referrals belong to the signed-in\naccount, not to one workspace, so these routes are not business-scoped. The server owns\neligibility, reward calculation and the free-period schedule; clients display what it returns." - name: 'Referral program administration' description: '' - name: 'Data exports' description: "\nAn export too large to finish inside a request is generated on the queue. The export endpoint\nanswers **202** with the record below; poll it until `status` is `completed`, then fetch\n`download_url`. Generated files are a convenience copy of data the workspace already owns and\nare deleted once they expire." - name: 'Team access' description: "\nOwners and administrators can manage workspace members. Smart Books supports one accountant seat in addition to the owner. Business supports all documented roles, with five total seats including the owner and pending invitations." - name: 'Team invitations' description: '' - name: Testimonials description: '' - name: 'Testimonial moderation' description: '' - name: Feedback description: '' - name: 'Feedback moderation' description: '' - name: 'Apple subscriptions' description: "\nApple App Store subscription verification and authoritative Dukanam entitlement state.\nOwner endpoints require the business's `owner_user_id` to match the signed-in user,\nas with web billing and API onboarding. A pivot role alone does not confer ownership." - name: 'Account deletion' description: "\nRequest, inspect and cancel deletion of the signed-in account and its data, as\nrequired by the Google Play user data policy. The public web equivalent of these\nendpoints is published at /delete-account." - name: 'App link analytics' description: "\nOpens of the promoted pages: the https://dukanam.com/app download link and the store-type\npages (the /billing-software-for-retail-stores index and one page per store type, in every\nlanguage). /app redirects phones to their store, so its opens are read from the request;\nstore pages may be served from the CDN cache and report their own opens with a beacon,\nadding reading time, scroll depth, screen size and the first call to action used.\nLocation is Cloudflare's IP-based estimate (city level, approximate). An open is one human\nvisit; link previews and crawlers are counted separately.\nDates are calendar days in Asia/Kolkata. Requires a super-admin token." - name: 'Billing webhooks' description: '' - name: 'Business details' description: "\nThe card a shop shows a customer so they can pay it by direct bank transfer:\nthe registered name and GSTIN, plus one bank account in full.\n\nThe full account number, IFSC and branch are stored encrypted on the payment\naccount. The payment-accounts listing carries only the last four digits.\nFull details are available here and in receiving_bank_account on business\nresources for accounting members. This card endpoint is served no-store." - name: 'ChatGPT plugin' description: '' - name: 'Invoice-linked expenses' description: "\nAdvanced Plan feature `invoice_costs`. Owner/admin writes; accountant reads. Every sales\nor purchase cost is an operating expense, recorded once in Expenses with an immutable\ninvoice reference and description. Purchase costs do not increase inventory/COGS. Sales\ncosts reduce party/worker contribution. Unpaid costs accrue a payable; settlement never\ncreates a second expense. Costs remain after invoice returns/voids until explicitly voided." - name: 'Invoice branding' description: "\nHow this workspace's bills look: one of three ready-made templates or a custom\nlayout, any accent colour, a logo, a signature, closing lines, and an optional UPI QR.\n\nBranding is frozen onto every invoice and business document at record time. Changing\nit here changes what future documents print as and leaves every saved document\nexactly as it was issued, which is what a reprint two years from now has to show." - name: 'Loans and EMI' description: "\nBorrowings the shop carries — a vehicle loan, a working-capital loan, a credit card — and the\nrepayments made against them. Each loan owns a liability account in the existing chart of\naccounts, so it appears on the balance sheet as a liability rather than a negative asset, and\nevery EMI records a split journal: the principal reduces what is owed, the interest is an expense.\n\nPlan feature `loans_and_income`; permission `accounting`. Online-only — nothing here belongs in\nthe offline cache or the write queue." - name: Lookups description: "\nSearch tenant-scoped values for lookup controls. Results are limited to the active business and the caller's workspace permissions and plan features." - name: 'Master partner teams' description: "\nAuthenticated partner-portal session cookies are required (not a workspace bearer\ntoken). Sign in at /partners/login with WhatsApp OTP. Mutating requests also require\nthe session CSRF token in X-CSRF-TOKEN or X-XSRF-TOKEN. Only active masters may use\nthese routes. Team data is scoped to that master; foreign IDs return 404. Amounts\nare integer paise in responses; reward requests use rupees and percent." - name: 'Partner portal' description: "\nOwn-account access for individual partners, masters and team members. Requires a\npartner session from /partners/login (WhatsApp OTP), not a workspace bearer token.\nWrites require the session CSRF token. Responses are private and not cached.\nTeam members see their master's eligible programs and their own rewards/payments." - name: 'Partner program administration' description: "\nRequires a super-admin token. Named programs supplement the existing default shop\nreferral program; global enablement, payout minimum and TDS still apply." - name: 'Party and referrer profitability' description: "\nAdvanced Plan exports require the corresponding profitability feature, accounting and\nexports permissions, and data_export. PDF, Excel and CSV contain every matching row,\nindependently of screen pagination. Dates, calculations and tenant checks match the\non-screen reports. Excel/CSV money is numeric rupees; PDF displays INR." - name: 'Party pricing' description: "\nRate cards that decide what a party pays. A contact carries one card and, optionally, a\nblanket discount; an invoice line for an item then takes its rate from the card unless the\nrequest quotes a rate of its own. See the Contacts endpoints for attaching a card to a party." - name: 'Payment accounts' description: '' - name: 'Platform broadcasts' description: "\nSuper admins push an announcement, optionally with a banner image, to the shops that match\na set of workspace filters: where they trade, what they sell, and what they pay for." - name: 'Platform business insights' description: '' - name: 'Platform overview' description: "\nThe numbers behind the admin console's overview, one section per call. Requires a\nsuper-admin token. Money is in paise and excludes GST: plan prices include 18% GST, so\nthe taxable part is price × 10000 / 11800. Days are calendar days in Asia/Kolkata.\nAn active business had someone use the web app, open the mobile app, or create records\nthat day; request-level tracking started on 27 September 2026 and earlier days are\nreconstructed from created records. Invoices are sales invoices except voided ones." - name: 'Push testing' description: "\nUnauthenticated bench for firing a push at a single device token, so mobile work can be\nverified without a signed-in user or a scheduled reminder behind it.\n\nEnabled by default in all environments, including production, for temporary testing.\nSet `FIREBASE_TEST_ENDPOINT_ENABLED=false` to disable after testing. This endpoint is public." - name: 'Referrers and referral commissions' description: "\nReferrers recommend the shop and bring customers; link them to sales to track performance and commissions.\nThe UI calls these people Referrers. Existing /workers URLs and worker_* request/response keys are retained for compatibility.\n\nAdvanced Plan feature `referral_workers`. Owners/administrators manage referrers, assign\ncommissions and make payments. Accountants can read profiles and statements. All amounts\nare integer paise; percentage rates use basis points (1000 means 10%). Commission is earned\non a saved invoice's discounted, tax-exclusive value. Customer dues remain separate." - name: 'Usage reports' description: '' components: securitySchemes: default: type: http scheme: bearer description: 'WhatsApp OTP is the primary auth flow. The documentation helper above uses the legacy password fallback and automatically copies the returned `data.token` into every authenticated endpoint.' partnerSession: type: apiKey in: cookie name: dukanam-session description: 'Partner portal session obtained through WhatsApp OTP at /partners/login. Writes also require the session CSRF token.' security: - default: [] paths: /api/v1/auth/otp/request: post: summary: 'Request a WhatsApp verification code.' operationId: requestAWhatsAppVerificationCode description: "The response is intentionally identical for an existing account and a\nnew phone number, so callers cannot use this endpoint to discover users.\nDuring the resend cooldown, supply your existing challenge_id to resume\nthat same pending code without sending another message. Keep it private." parameters: [] responses: 202: description: '' content: application/json: schema: type: object example: message: 'If this number can receive WhatsApp, a verification code has been sent.' challenge_id: 00000000-0000-4000-8000-000000000001 expires_in_seconds: 600 resend_in_seconds: 60 properties: message: type: string example: 'If this number can receive WhatsApp, a verification code has been sent.' challenge_id: type: string example: 00000000-0000-4000-8000-000000000001 description: 'Opaque identifier required to verify the code.' expires_in_seconds: type: integer example: 600 description: 'Remaining whole seconds before this code expires, rounded down.' resend_in_seconds: type: integer example: 60 description: 'Remaining whole seconds in the resend cooldown; decreases when resuming an existing challenge.' 422: description: '' content: application/json: schema: type: object example: message: 'The given data was invalid.' errors: phone: - 'Enter a valid 10-digit Indian mobile number.' properties: message: type: string example: 'The given data was invalid.' errors: type: object properties: phone: type: array example: - 'Enter a valid 10-digit Indian mobile number.' items: type: string 429: description: '' content: application/json: schema: type: object example: message: 'Please wait before requesting another verification code.' properties: message: type: string example: 'Please wait before requesting another verification code.' 503: description: '' content: application/json: schema: type: object example: message: 'WhatsApp verification is not available right now. Please use email and password instead.' properties: message: type: string example: 'WhatsApp verification is not available right now. Please use email and password instead.' tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: phone: type: string description: 'An Indian mobile number in local or E.164 form.' example: '9876543210' challenge_id: type: string description: 'Optional opaque ID from an earlier request for the same phone. A valid pending challenge is reused during cooldown; other callers remain rate limited.' example: 00000000-0000-4000-8000-000000000001 nullable: true required: - phone security: [] /api/v1/auth/otp/verify: post: summary: 'Verify a WhatsApp code and sign in to the matching account.' operationId: verifyAWhatsAppCodeAndSignInToTheMatchingAccount description: "After a valid OTP, an existing verified identity takes precedence. Otherwise,\na unique normalized user.phone match signs in immediately without email,\npassword, or account linking. Its verified identity is saved automatically\nif it has none; an existing different verified identity is preserved.\nBusiness and customer contact numbers are never used to select an account.\nMultiple matching user accounts return 422 with errors.code and no token\nor registration proof; offer password sign-in instead of choosing an account.\nOnly an unknown phone receives a short-lived, one-time registration proof.\nOpen account setup directly; do not show an account-linking step. Keep the\nproof secret and never put it in a URL." parameters: [] responses: 200: description: '' content: application/json: schema: oneOf: - description: '' type: object example: data: user: id: 1 name: 'Shop Owner' email: owner@example.com phone: null token: 1|example-mobile-token default_business_id: 1 properties: data: type: object properties: user: type: object properties: id: type: integer example: 1 name: type: string example: 'Shop Owner' email: type: string example: owner@example.com phone: type: string example: null nullable: true description: 'Present for an existing verified identity or unique normalized user.phone match.' token: type: string example: 1|example-mobile-token description: 'Sanctum bearer token, present for an existing account.' default_business_id: type: integer example: 1 description: 'Last selected eligible business, otherwise the first eligible business by name; null if none. Present after sign-in.' - description: '' type: object example: data: requires_registration: true registration_proof: one-time-registration-proof properties: data: type: object properties: requires_registration: type: boolean example: true description: 'Present and true only when the verified phone matches no account. Open account setup directly.' registration_proof: type: string example: one-time-registration-proof description: 'One-time secret for account setup; also accepted by the legacy enrollment endpoint.' 422: description: '' content: application/json: schema: oneOf: - description: '' type: object example: message: 'The given data was invalid.' errors: code: - 'The verification code is invalid or has expired.' properties: message: type: string example: 'The given data was invalid.' errors: type: object properties: code: type: array example: - 'The verification code is invalid or has expired.' items: type: string - description: 'Ambiguous saved phone' type: object example: message: 'The given data was invalid.' errors: code: - 'This number is saved on more than one account. Sign in with email and password to continue.' properties: message: type: string example: 'The given data was invalid.' errors: type: object properties: code: type: array example: - 'This number is saved on more than one account. Sign in with email and password to continue.' items: type: string 503: description: '' content: application/json: schema: type: object example: message: 'WhatsApp verification is not available right now. Please use email and password instead.' properties: message: type: string example: 'WhatsApp verification is not available right now. Please use email and password instead.' tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: challenge_id: type: string description: 'The opaque challenge ID from the request response.' example: 00000000-0000-4000-8000-000000000001 code: type: string description: 'The six-digit WhatsApp code.' example: '123456' device_name: type: string description: 'A name for the device token.' example: "Priya's phone" fcm_token: type: string description: 'Optional Firebase Cloud Messaging registration token to save after successful sign-in.' example: eY2x9_example_fcm_registration_token nullable: true required: - challenge_id - code - device_name security: [] /api/v1/auth/otp/register: post: summary: 'Create an account after WhatsApp verification.' operationId: createAnAccountAfterWhatsAppVerification description: "The proof is single-use and expires quickly. A recovery password is\nrequired for sensitive actions, while WhatsApp remains the primary sign-in\nroute." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: user: id: 1 name: 'Priya Rao' email: priya@example.com phone: null business: id: 1 name: 'Priya Textiles' role: owner is_owner: true token: 1|example-mobile-token properties: data: type: object properties: user: type: object properties: id: type: integer example: 1 name: type: string example: 'Priya Rao' email: type: string example: priya@example.com phone: type: string example: null nullable: true business: type: object properties: id: type: integer example: 1 name: type: string example: 'Priya Textiles' role: type: string example: owner is_owner: type: boolean example: true description: "Always true for the newly registered account's workspace." token: type: string example: 1|example-mobile-token 422: description: '' content: application/json: schema: type: object example: message: 'The given data was invalid.' errors: registration_proof: - 'Your verified phone session has expired. Request a new WhatsApp code.' properties: message: type: string example: 'The given data was invalid.' errors: type: object properties: registration_proof: type: array example: - 'Your verified phone session has expired. Request a new WhatsApp code.' items: type: string 503: description: '' content: application/json: schema: type: object example: message: 'WhatsApp verification is not available right now. Please use email and password instead.' properties: message: type: string example: 'WhatsApp verification is not available right now. Please use email and password instead.' tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: challenge_id: type: string description: 'The verified challenge ID.' example: 00000000-0000-4000-8000-000000000001 registration_proof: type: string description: 'One-time secret returned by OTP verification.' example: one-time-registration-proof name: type: string description: "The account owner's name." example: 'Priya Rao' business_name: type: string description: 'The first workspace name.' example: 'Priya Textiles' email: type: string description: 'Account and recovery email address.' example: priya@example.com password: type: string description: 'A recovery password, at least eight characters.' example: SecurePassword123! device_name: type: string description: 'A name for the device token.' example: "Priya's phone" fcm_token: type: string description: 'Optional Firebase Cloud Messaging registration token to save for the new user.' example: eY2x9_example_fcm_registration_token nullable: true referral_code: type: string description: "Optional referral code from a shared link's `ref` parameter: a customer's referral code or an influencer partner's link code. An unknown code is rejected with 422 while the referral program is on, and ignored while it is paused." example: K7M2QX9A password_confirmation: type: string description: 'Must match password.' example: SecurePassword123! acquisition: type: object description: 'Optional first-touch attribution captured by the mobile client. Stored only at signup; referral records take precedence.' example: utm_source: google utm_medium: organic utm_campaign: launch referrer_host: www.google.com properties: utm_source: type: string description: 'Optional campaign source, max 255 characters.' example: google utm_medium: type: string description: 'Optional campaign medium, max 255 characters.' example: organic utm_campaign: type: string description: 'Optional campaign name, max 255 characters.' example: launch referrer_host: type: string description: 'Optional hostname only, max 253 characters; no URL path or query.' example: www.google.com required: - challenge_id - registration_proof - name - business_name - email - password - device_name - password_confirmation security: [] /api/v1/auth/otp/link: post: summary: 'Link a newly verified WhatsApp number to an existing password account (legacy compatibility).' operationId: linkANewlyVerifiedWhatsAppNumberToAnExistingPasswordAccountlegacyCompatibility description: "Clients must first obtain the same short-lived registration proof used\nfor new account creation. The email/password check makes enrollment an\nexplicit migration path instead of trusting mutable historical contact\nphone fields." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: user: id: 1 name: 'Shop Owner' email: owner@example.com phone: null token: 1|example-mobile-token default_business_id: 1 properties: data: type: object properties: user: type: object properties: id: type: integer example: 1 name: type: string example: 'Shop Owner' email: type: string example: owner@example.com phone: type: string example: null nullable: true token: type: string example: 1|example-mobile-token default_business_id: type: integer example: 1 description: 'Last selected eligible business, otherwise the first eligible business by name; null if none.' 422: description: '' content: application/json: schema: type: object example: message: 'The given data was invalid.' errors: registration_proof: - 'Your verified phone session has expired. Request a new WhatsApp code.' properties: message: type: string example: 'The given data was invalid.' errors: type: object properties: registration_proof: type: array example: - 'Your verified phone session has expired. Request a new WhatsApp code.' items: type: string 503: description: '' content: application/json: schema: type: object example: message: 'WhatsApp verification is not available right now. Please use email and password instead.' properties: message: type: string example: 'WhatsApp verification is not available right now. Please use email and password instead.' tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: challenge_id: type: string description: 'The verified challenge ID.' example: 00000000-0000-4000-8000-000000000001 registration_proof: type: string description: 'One-time secret returned by OTP verification.' example: one-time-registration-proof email: type: string description: 'Existing Dukanam account email address.' example: owner@example.com password: type: string description: 'Existing account password.' example: SecurePassword123! device_name: type: string description: 'A name for the device token.' example: "Priya's phone" fcm_token: type: string description: 'Optional Firebase Cloud Messaging registration token to save after linking.' example: eY2x9_example_fcm_registration_token nullable: true required: - challenge_id - registration_proof - email - password - device_name security: [] /api/v1/auth/register: post: summary: 'Register an account and its first workspace.' operationId: registerAnAccountAndItsFirstWorkspace description: "The returned business includes `role: owner` and `is_owner: true` immediately,\nwithout a follow-up workspace request. Ownership is determined by `owner_user_id`." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: user: id: 1 name: 'Shop Owner' email: owner@example.com business: id: 1 name: 'Owner Shop' role: owner is_owner: true token: 1|example-mobile-token properties: data: type: object properties: user: type: object properties: id: type: integer example: 1 name: type: string example: 'Shop Owner' email: type: string example: owner@example.com business: type: object properties: id: type: integer example: 1 name: type: string example: 'Owner Shop' role: type: string example: owner is_owner: type: boolean example: true description: "Always true for the newly registered account's workspace." token: type: string example: 1|example-mobile-token tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Must not be greater than 128 characters.' example: b email: type: string description: 'Must be a valid email address. Must not be greater than 255 characters.' example: zbailey@example.net phone: type: string description: 'Must not be greater than 32 characters.' example: i nullable: true business_name: type: string description: 'Must not be greater than 128 characters.' example: 'y' password: type: string description: 'Must be at least 8 characters.' example: pBNvYg referral_code: type: string description: "Optional referral code from a shared link's `ref` parameter: a customer's referral code or an influencer partner's link code. An unknown code is rejected with 422 while the referral program is on, and ignored while it is paused." example: K7M2QX9A nullable: true acquisition: type: object description: 'Optional first-touch attribution captured by the mobile client. Stored only at signup; referral records take precedence.' example: utm_source: google utm_medium: organic utm_campaign: launch referrer_host: www.google.com properties: utm_source: type: string description: 'Optional campaign source, max 255 characters.' example: google utm_medium: type: string description: 'Optional campaign medium, max 255 characters.' example: organic utm_campaign: type: string description: 'Optional campaign name, max 255 characters.' example: launch referrer_host: type: string description: 'Optional hostname only, max 253 characters; no URL path or query.' example: www.google.com device_name: type: string description: 'A name for the device token.' example: 'Scribe API Docs' fcm_token: type: string description: 'An optional Firebase Cloud Messaging registration token to save for the user.' example: eY2x9_example_fcm_registration_token required: - name - email - business_name - password - device_name security: [] /api/v1/auth/login: post: summary: 'Sign in with the password fallback and return the default business.' operationId: signInWithThePasswordFallbackAndReturnTheDefaultBusiness description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: user: id: 1 name: 'Shop Owner' email: owner@example.com phone: null token: 1|example-mobile-token default_business_id: 1 properties: data: type: object properties: user: type: object properties: id: type: integer example: 1 name: type: string example: 'Shop Owner' email: type: string example: owner@example.com phone: type: string example: null nullable: true token: type: string example: 1|example-mobile-token default_business_id: type: integer example: 1 description: 'Last selected eligible business, otherwise the first eligible business by name; null if none.' tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: 'Must be a valid email address.' example: gbailey@example.net password: type: string description: '' example: '|]|{+-' remember: type: boolean description: '' example: false fcm_token: type: string description: 'Must not be greater than 4096 characters.' example: v nullable: true device_name: type: string description: 'A name for the device token.' example: 'Scribe API Docs' required: - email - password - device_name security: [] /api/v1/auth/forgot-password: post: summary: 'Email a password reset link.' operationId: emailAPasswordResetLink description: 'Always returns the same response so callers cannot discover registered email addresses.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'If an account exists for that email address, a password reset link has been sent.' properties: message: type: string example: 'If an account exists for that email address, a password reset link has been sent.' tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: 'The account email address.' example: owner@example.com required: - email security: [] /api/v1/auth/reset-password: post: summary: 'Reset an account password.' operationId: resetAnAccountPassword description: 'A successful reset revokes every mobile bearer token and sends a security notification.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Password reset successfully. Sign in with your new password.' properties: message: type: string example: 'Password reset successfully. Sign in with your new password.' 422: description: '' content: application/json: schema: type: object example: message: 'The given data was invalid.' errors: email: - 'This password reset token is invalid.' properties: message: type: string example: 'The given data was invalid.' errors: type: object properties: email: type: array example: - 'This password reset token is invalid.' items: type: string tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: token: type: string description: 'The token delivered in the password-reset email.' example: reset-token email: type: string description: 'The account email address.' example: owner@example.com password: type: string description: 'The new password, at least eight characters.' example: new-secure-password password_confirmation: type: string description: 'Must match password.' example: new-secure-password required: - token - email - password - password_confirmation security: [] /api/v1/auth/me: get: summary: 'Return the current user and active workspaces available under each plan and seat assignment.' operationId: returnTheCurrentUserAndActiveWorkspacesAvailableUnderEachPlanAndSeatAssignment description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: user: id: 1 name: 'Shop Owner' email: owner@example.com phone: null businesses: - id: 1 name: 'My Shop' role: owner is_owner: true default_business_id: 1 workspaces: limit: 1 used: 1 remaining: 0 can_create: false properties: data: type: object properties: user: type: object properties: id: type: integer example: 1 name: type: string example: 'Shop Owner' email: type: string example: owner@example.com phone: type: string example: null nullable: true businesses: type: array example: - id: 1 name: 'My Shop' role: owner is_owner: true items: type: object properties: id: type: integer example: 1 name: type: string example: 'My Shop' role: type: string example: owner is_owner: type: boolean example: true default_business_id: type: integer example: 1 description: 'Last selected business if still accessible, otherwise the first eligible business by name; null when none is available.' workspaces: type: object properties: limit: type: integer example: 1 description: 'Workspaces the current plan covers. 0 means unlimited.' used: type: integer example: 1 description: 'Active workspaces the account already owns. Workspaces it merely belongs to are not counted.' remaining: type: integer example: 0 description: 'Workspaces still available, or null when the limit is unlimited.' can_create: type: boolean example: false description: 'Whether POST /api/v1/businesses would succeed rather than return workspace_limit_reached.' description: "How many workspaces the account's plan covers and how many it already owns. A plan covers every workspace its owner holds, so this allowance is account-wide rather than per workspace." tags: - Authentication /api/v1/auth/fcm-token: patch: summary: "Update or clear the current user's Firebase Cloud Messaging registration token." operationId: updateOrClearTheCurrentUsersFirebaseCloudMessagingRegistrationToken description: 'Send `null` to remove the currently stored token.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'FCM token updated.' properties: message: type: string example: 'FCM token updated.' tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: fcm_token: type: string description: 'The Firebase Cloud Messaging registration token, or null to clear it.' example: eY2x9_example_fcm_registration_token nullable: true required: - fcm_token /api/v1/auth/token: delete: summary: '' operationId: deleteApiV1AuthToken description: '' parameters: [] responses: {} tags: - Authentication /api/v1/auth/tokens: delete: summary: '' operationId: deleteApiV1AuthTokens description: '' parameters: [] responses: {} tags: - Authentication /api/v1/businesses: get: summary: "List the active workspaces available under the caller's current plan and seat assignment." operationId: listTheActiveWorkspacesAvailableUnderTheCallersCurrentPlanAndSeatAssignment description: "Ended paid subscriptions automatically fall back to Free Essentials. The ended subscription remains in history.\nBusiness resources include `invoice_allowance` (limit, used, remaining, can_create, resets_at) and\nowner-only `free_transition` (subscription_id, reason, requires_acknowledgement; null otherwise).\nWhen acknowledgement is required, offer paid plans and Continue on Free via POST billing/continue-free.\nKeep viewing, downloading, and exporting existing invoices available throughout this flow.\n`is_active` describes workspace access, independently of paid subscription status." parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Businesses post: summary: 'Create a business for the signed-in account.' operationId: createABusinessForTheSignedInAccount description: "Any authenticated account can create its first or an additional business,\neven without an accessible workspace or while another workspace is in setup.\nThe caller becomes its owner. Creation uses the same setup service as web\nregistration: an active Free subscription, Walk-in Customer, and default\npayment accounts are created atomically. Existing memberships and the saved\ndefault business are unchanged; use the selection endpoint to change it.\nContinue guided onboarding from the language step using the returned ID.\nSupply contact phone, store type, and tax details through onboarding.\nDuplicate names are allowed and receive distinct slugs. Each successful\nrequest creates a new business; this endpoint is not idempotent.\n\nSend `skip_setup` to create a workspace that is immediately usable, with\nonboarding already marked complete and every step recorded as completed.\nNothing is guessed: only the name is set, and GST, address, and store\ndetails are entered later through the settings endpoint. This is what the\nweb account menu does, because an account adding a second workspace has\nalready been through guided setup once. Omit it to keep the guided flow." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: id: 2 name: 'Priya Textiles' slug: priya-textiles role: owner is_owner: true onboarding: current_step: language completed_steps: [] skipped_steps: [] completed_at: null subscription: status: active billing_interval: monthly provider: manual plan: id: 1 name: Free slug: free properties: data: type: object properties: id: type: integer example: 2 name: type: string example: 'Priya Textiles' slug: type: string example: priya-textiles role: type: string example: owner is_owner: type: boolean example: true description: 'Always true; ownership is assigned to the authenticated account.' onboarding: type: object properties: current_step: type: string example: language description: 'language for a guided business, or complete when skip_setup was sent.' completed_steps: type: array example: [] skipped_steps: type: array example: [] completed_at: type: string example: null nullable: true subscription: type: object properties: status: type: string example: active billing_interval: type: string example: monthly provider: type: string example: manual plan: type: object properties: id: type: integer example: 1 name: type: string example: Free slug: type: string example: free description: 'The standard business resource, including the new business ID, ownership, subscription, and onboarding state.' 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 409: description: '' content: application/json: schema: type: object example: message: 'Your plan covers 1 workspace.' code: workspace_limit_reached limit: 1 used: 1 properties: message: type: string example: 'Your plan covers 1 workspace.' code: type: string example: workspace_limit_reached limit: type: integer example: 1 used: type: integer example: 1 422: description: '' content: application/json: schema: type: object example: message: 'The name field is required.' errors: name: - 'The name field is required.' properties: message: type: string example: 'The name field is required.' errors: type: object properties: name: type: array example: - 'The name field is required.' items: type: string tags: - Businesses requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Business name, up to 128 characters.' example: 'Priya Textiles' skip_setup: type: boolean description: 'Create the workspace ready to use instead of at the start of guided onboarding. Transactional endpoints work immediately. Defaults to false.' example: true required: - name '/api/v1/businesses/{business}/select': post: summary: 'Select the business to open by default on the next sign-in.' operationId: selectTheBusinessToOpenByDefaultOnTheNextSignIn description: "Only an active, plan-eligible membership can be selected. The preference\nis shared with web sign-in; existing browser sessions retain their current\nworkspace. API requests always use the explicit business ID in their URL.\nSelection is allowed before onboarding completes so clients can switch\nout of an unfinished workspace. Follow data.business.onboarding afterward." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: default_business_id: 1 business: id: 1 name: 'My Shop' role: owner is_owner: true onboarding: current_step: complete properties: data: type: object properties: default_business_id: type: integer example: 1 description: 'The selected business to open on the next sign-in.' business: type: object properties: id: type: integer example: 1 name: type: string example: 'My Shop' role: type: string example: owner is_owner: type: boolean example: true onboarding: type: object properties: current_step: type: string example: complete description: "The selected business, including the caller's role and onboarding state." 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 403: description: '' content: application/json: schema: type: object example: message: 'Your role requires an eligible team or accountant plan.' properties: message: type: string example: 'Your role requires an eligible team or accountant plan.' 404: description: '' content: application/json: schema: type: object example: message: 'Not Found' properties: message: type: string example: 'Not Found' tags: - Businesses parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}': get: summary: 'Show the workspace and its current subscription.' operationId: showTheWorkspaceAndItsCurrentSubscription description: "Ended paid subscriptions automatically fall back to Free Essentials. The ended subscription remains in history.\nFree features and limits apply on both web and API; `is_active` describes workspace access.\nScheduled cancellations retain paid access until `ends_at`. A renewal due date alone does not end access." parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Businesses parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/settings': patch: summary: 'Update business settings.' operationId: updateBusinessSettings description: "The `default_locale` and manual-sharing template fields may only be changed by workspace owners\nand admins. Manual sharing itself is available on every plan.\n\nGSTIN is required for a regular or composition registration, and its first two digits must match `state_code`. An unregistered business has its GSTIN cleared. `state_code` must be an Indian state or union territory (96, 97 and 99 are refused), and `default_place_of_supply` refuses 99." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 1 name: 'Anika Stores' slug: anika-stores role: owner phone: '9876543210' email: anika@example.com legal_name: 'Anika Stores Private Limited' gstin: 27AAPFU0939F1ZV gst_registration_type: regular currency: INR timezone: Asia/Kolkata address: line_1: '12 Market Road' line_2: null city: Pune state_code: '27' pincode: '411001' default_place_of_supply: '27' invoice_prefix: INV prices_include_tax: true upi_id: anikastores@bank theme: '#2563EB' default_locale: hi is_active: true manual_sharing: enabled: true invoice_share_message_template: 'Hello {customer_name}, invoice {invoice_number} for {invoice_total} is ready.' subscription: status: active billing_interval: monthly trial_ends_at: null renews_at: '2026-09-19T00:00:00.000000Z' ends_at: null cancelled_at: null plan: id: 2 name: Smart slug: smart features: - inventory - pos - expenses limits: [] receiving_bank_account: id: 2 institution: 'HDFC Bank' account_name: 'Anika Stores' account_number: '001234567890' ifsc: HDFC0000123 branch: Pune show_on_invoice: true theme_colours: label: 'Custom colour' brand: '#2159D4' brand_dark: '#19439F' soft: '#E5ECFD' properties: data: type: object properties: id: type: integer example: 1 name: type: string example: 'Anika Stores' slug: type: string example: anika-stores role: type: string example: owner phone: type: string example: '9876543210' email: type: string example: anika@example.com legal_name: type: string example: 'Anika Stores Private Limited' gstin: type: string example: 27AAPFU0939F1ZV gst_registration_type: type: string example: regular currency: type: string example: INR timezone: type: string example: Asia/Kolkata address: type: object properties: line_1: type: string example: '12 Market Road' line_2: type: string example: null nullable: true city: type: string example: Pune state_code: type: string example: '27' pincode: type: string example: '411001' default_place_of_supply: type: string example: '27' invoice_prefix: type: string example: INV prices_include_tax: type: boolean example: true upi_id: type: string example: anikastores@bank theme: type: string example: '#2563EB' description: 'Saved preset key or canonical custom #RRGGBB, independently per workspace.' default_locale: type: string example: hi is_active: type: boolean example: true manual_sharing: type: object properties: enabled: type: boolean example: true invoice_share_message_template: type: string example: 'Hello {customer_name}, invoice {invoice_number} for {invoice_total} is ready.' subscription: type: object properties: status: type: string example: active billing_interval: type: string example: monthly trial_ends_at: type: string example: null nullable: true renews_at: type: string example: '2026-09-19T00:00:00.000000Z' ends_at: type: string example: null nullable: true cancelled_at: type: string example: null nullable: true plan: type: object properties: id: type: integer example: 2 name: type: string example: Smart slug: type: string example: smart features: type: array example: - inventory - pos - expenses items: type: string limits: type: array example: [] receiving_bank_account: type: object properties: id: type: integer example: 2 institution: type: string example: 'HDFC Bank' account_name: type: string example: 'Anika Stores' account_number: type: string example: '001234567890' ifsc: type: string example: HDFC0000123 branch: type: string example: Pune show_on_invoice: type: boolean example: true description: 'Current receiving bank, including full account details. Only returned to members with accounting permission. Details are encrypted at rest and omitted from audit snapshots.' theme_colours: type: object properties: label: type: string example: 'Custom colour' brand: type: string example: '#2159D4' brand_dark: type: string example: '#19439F' soft: type: string example: '#E5ECFD' description: 'Effective label, brand, brand_dark and soft tokens. Custom colours generate readable shades for white and tinted backgrounds; use these tokens instead of raw theme hex for interface text and buttons.' tags: - Businesses requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Must not be greater than 128 characters.' example: b legal_name: type: string description: 'Must not be greater than 191 characters.' example: 'n' nullable: true phone: type: string description: 'Must not be greater than 32 characters.' example: g nullable: true email: type: string description: 'Must be a valid email address. Must not be greater than 255 characters.' example: rowan.gulgowski@example.com nullable: true gst_registration_type: type: string description: '' example: unregistered enum: - unregistered - regular - composition gstin: type: string description: '15-character GSTIN. Required when gst_registration_type is regular or composition; ignored and cleared when unregistered.' example: 36ABCDE1234F1Z5 nullable: true address_line_1: type: string description: 'Must not be greater than 191 characters.' example: w nullable: true address_line_2: type: string description: 'Must not be greater than 191 characters.' example: p nullable: true city: type: string description: 'Must not be greater than 96 characters.' example: w nullable: true state_code: type: string description: 'Two-digit Indian state or union territory code of the business.' example: '36' nullable: true pincode: type: string description: 'Must be 6 digits.' example: '569775' nullable: true default_place_of_supply: type: string description: '' example: 1 enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 26 - 27 - 29 - 30 - 31 - 32 - 33 - 34 - 35 - 36 - 37 - 38 - 96 - 97 nullable: true invoice_prefix: type: string description: 'Invoice-series prefix, up to 15 characters. GST permits letters, numbers, hyphens, and slashes; the generated invoice number is limited to 16 characters.' example: HAWIOT/26-27/ prices_include_tax: type: boolean description: '' example: false authorized_signatory: type: string description: 'Must not be greater than 128 characters.' example: g nullable: true bank_details: type: string description: 'Must not be greater than 1000 characters.' example: z nullable: true upi_id: type: string description: 'Must match the regex /^[a-zA-Z0-9._-]{2,191}@[a-zA-Z0-9.-]{2,63}$/. Must not be greater than 255 characters.' example: m nullable: true receiving_bank_account: type: object description: 'Shop bank account for customer payments. Omit to retain the current account; send all text fields empty to clear instructions. Uses the active default receiving bank without creating a duplicate ledger.' example: institution: 'HDFC Bank' account_name: 'Anika Stores' account_number: '001234567890' ifsc: HDFC0000123 branch: Pune show_on_invoice: true properties: institution: type: string description: 'Bank name, required when any bank text field is filled.' example: 'HDFC Bank' nullable: true account_name: type: string description: 'Account holder, required when any bank text field is filled.' example: 'Anika Stores' nullable: true account_number: type: string description: 'Full account number as 6–34 digits; preserve leading zeroes. Required when any bank text field is filled.' example: '001234567890' nullable: true ifsc: type: string description: 'Valid 11-character IFSC; normalised to uppercase. Required when any bank text field is filled.' example: HDFC0000123 nullable: true branch: type: string description: 'Optional branch, up to 128 characters.' example: Pune nullable: true show_on_invoice: type: boolean description: 'Include the bank on invoice PDFs, buyer links and customer sharing/reminder messages. Defaults to true when bank text fields are supplied; a visibility-only object preserves saved details and empty accounts remain hidden.' example: true theme: type: string description: 'Workspace preset key (blue, emerald, teal, violet, rose, maroon, graphite) or a custom hex colour. Accepts #RGB/#RRGGBB or bare hex, normalised to uppercase #RRGGBB. Null resets to the default; omission preserves it.' example: '#2563EB' nullable: true custom_theme_colour: type: string description: 'Optional web-form mirror for a hex theme. Used only when theme itself is a hex value; API clients normally send theme directly.' example: '#2563EB' nullable: true default_locale: type: string description: '' example: en enum: - en - hi - ta - te - ml - kn - mr - gu - bn invoice_share_message_template: type: string description: 'Must not be greater than 2000 characters.' example: w nullable: true notification_preferences: type: array description: '' example: null items: type: object properties: enabled: type: boolean description: '' example: false days: type: array description: 'Must be between -365 and 365.' example: - -364 items: type: integer threshold: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 6 min_shelf_life_days: type: integer description: 'Must be at least 0. Must not be greater than 730.' example: 25 short_expiry_action: type: string description: '' example: warn enum: - warn - block required: - name - gst_registration_type - invoice_prefix parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/dashboard': get: summary: '' operationId: getApiV1BusinessesBusinessDashboard description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Businesses parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/onboarding': get: summary: '' operationId: getApiV1BusinessesBusinessOnboarding description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Business onboarding' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/onboarding/language': patch: summary: '' operationId: patchApiV1BusinessesBusinessOnboardingLanguage description: '' parameters: [] responses: {} tags: - 'Business onboarding' requestBody: required: true content: application/json: schema: type: object properties: locale: type: string description: 'Supported locale code.' example: hi required: - locale parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/onboarding/business': post: summary: '' operationId: postApiV1BusinessesBusinessOnboardingBusiness description: '' parameters: [] responses: {} tags: - 'Business onboarding' requestBody: required: true content: multipart/form-data: schema: type: object properties: name: type: string description: 'Public business name.' example: 'Veera Stores' store_type: type: string description: 'Store type key.' example: kirana-store supply_type: type: string description: 'goods, services, or both.' example: goods phone: type: string description: 'Public phone in E.164 or a 10-digit Indian local format.' example: '9876543210' logo: type: string format: binary description: 'Optional PNG, JPG, or WebP logo up to 2 MB.' nullable: true required: - name - store_type - supply_type - phone parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/onboarding/tax': patch: summary: '' operationId: patchApiV1BusinessesBusinessOnboardingTax description: '' parameters: [] responses: {} tags: - 'Business onboarding' requestBody: required: true content: application/json: schema: type: object properties: gst_status: type: string description: 'registered or not_registered.' example: registered gst_registration_type: type: string description: 'Required when registered: normal or composition. Ignored for not_registered.' example: normal nullable: true gstin: type: string description: 'Required when registered. Ignored for not_registered. May match another business workspace.' example: 29ABCDE1234F1Z5 nullable: true legal_name: type: string description: 'Must not be greater than 191 characters.' example: a nullable: true address_line_1: type: string description: '' example: '12 Market Road' address_line_2: type: string description: 'Must not be greater than 191 characters.' example: k nullable: true city: type: string description: '' example: Bengaluru state_code: type: string description: 'Indian GST state code.' example: '29' pincode: type: string description: 'Six digit PIN code.' example: '560001' required: - gst_status - address_line_1 - city - state_code - pincode parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/onboarding/steps/{step}': post: summary: '' operationId: postApiV1BusinessesBusinessOnboardingStepsStep description: '' parameters: [] responses: {} tags: - 'Business onboarding' requestBody: required: false content: application/json: schema: type: object properties: skip: type: boolean description: 'Set true to finish this optional step later.' example: true parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: step description: 'The step.' example: architecto required: true schema: type: string '/api/v1/businesses/{business}/onboarding/product-imports': post: summary: 'Check the setup product workbook against existing masters only; no masters are created.' operationId: checkTheSetupProductWorkbookAgainstExistingMastersOnlyNoMastersAreCreated description: "Nonblank category/subcategory/brand/manufacturer names must exist in this business.\nA supplied taxable GST percentage for a Regular GST business must match an existing master.\nBlank organisation columns may be assigned later. Preview rows include nullable resolved master IDs.\nReuploading an unconfirmed file refreshes its validation and returns the same record with HTTP 200.\nAlready imported files remain unchanged; fresh uploads return HTTP 201." parameters: [] responses: {} tags: - 'Business onboarding' requestBody: required: true content: multipart/form-data: schema: type: object properties: file: type: string format: binary description: 'Completed Dukanam XLSX template, maximum 5 MB.' required: - file parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/onboarding/product-imports/{itemImport_id}/confirm': post: summary: "Import every validated product row without exceeding the current plan's total item allowance." operationId: importEveryValidatedProductRowWithoutExceedingTheCurrentPlansTotalItemAllowance description: "Rechecks the workbook before writing; new SKU/barcode conflicts return 422 under file.\nRenamed/deleted classification or GST masters also return 422 under file; no masters are created." parameters: [] responses: 409: description: '' content: application/json: schema: type: object example: message: 'This file has already been imported.' properties: message: type: string example: 'This file has already been imported.' 422: description: '' content: application/json: schema: type: object example: message: 'Your current plan includes up to 5 items. Upgrade to Smart Books to add more.' errors: plan: - 'Your current plan includes up to 5 items. Upgrade to Smart Books to add more.' properties: message: type: string example: 'Your current plan includes up to 5 items. Upgrade to Smart Books to add more.' errors: type: object properties: plan: type: array example: - 'Your current plan includes up to 5 items. Upgrade to Smart Books to add more.' items: type: string tags: - 'Business onboarding' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: itemImport_id description: 'The ID of the itemImport.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/gstin/lookup': get: summary: 'Look up and normalize an Indian GSTIN.' operationId: lookUpAndNormalizeAnIndianGSTIN description: '' parameters: - in: query name: gstin description: 'The 15-character GSTIN to verify.' example: 03AAFCE1234J1Z0 required: true schema: type: string description: 'The 15-character GSTIN to verify.' example: 03AAFCE1234J1Z0 responses: 200: description: '' content: application/json: schema: type: object example: data: gstin: 03AAFCE1234J1Z0 legal_name: 'Edsafe Logistics Private Limited' trade_name: 'EdLogistics Private Limited' status: Active registration_type: Regular constitution: 'Private Limited Company' einvoice_status: 'Yes' registration_date: '2019-04-01' last_updated: '2026-01-15' state_code: '03' state_name: Punjab address: line1: 'Number 234, 55 Wide Road, Ground Floor, Opposite Upkar Transport' line2: 'Godown Area, Zirakpur, SASNagar' city: Zirakpur district: SASNagar postal_code: '140603' state_code: '03' country_code: IN suggested_business: legal_name: 'Edsafe Logistics Private Limited' gst_registration_type: normal gstin: 03AAFCE1234J1Z0 address_line_1: 'Number 234, 55 Wide Road, Ground Floor, Opposite Upkar Transport' address_line_2: 'Godown Area, Zirakpur, SASNagar' city: Zirakpur state_code: '03' pincode: '140603' default_place_of_supply: '03' suggested_contact: name: 'EdLogistics Private Limited' company_name: 'EdLogistics Private Limited' gst_treatment: registered_regular gstin: 03AAFCE1234J1Z0 billing_address_line_1: 'Number 234, 55 Wide Road, Ground Floor, Opposite Upkar Transport' billing_address_line_2: 'Godown Area, Zirakpur, SASNagar' billing_city: Zirakpur billing_state_code: '03' billing_pincode: '140603' properties: data: type: object properties: gstin: type: string example: 03AAFCE1234J1Z0 legal_name: type: string example: 'Edsafe Logistics Private Limited' trade_name: type: string example: 'EdLogistics Private Limited' status: type: string example: Active registration_type: type: string example: Regular constitution: type: string example: 'Private Limited Company' einvoice_status: type: string example: 'Yes' registration_date: type: string example: '2019-04-01' last_updated: type: string example: '2026-01-15' state_code: type: string example: '03' state_name: type: string example: Punjab address: type: object properties: line1: type: string example: 'Number 234, 55 Wide Road, Ground Floor, Opposite Upkar Transport' line2: type: string example: 'Godown Area, Zirakpur, SASNagar' city: type: string example: Zirakpur district: type: string example: SASNagar postal_code: type: string example: '140603' state_code: type: string example: '03' country_code: type: string example: IN suggested_business: type: object properties: legal_name: type: string example: 'Edsafe Logistics Private Limited' gst_registration_type: type: string example: normal gstin: type: string example: 03AAFCE1234J1Z0 address_line_1: type: string example: 'Number 234, 55 Wide Road, Ground Floor, Opposite Upkar Transport' address_line_2: type: string example: 'Godown Area, Zirakpur, SASNagar' city: type: string example: Zirakpur state_code: type: string example: '03' pincode: type: string example: '140603' default_place_of_supply: type: string example: '03' suggested_contact: type: object properties: name: type: string example: 'EdLogistics Private Limited' company_name: type: string example: 'EdLogistics Private Limited' gst_treatment: type: string example: registered_regular gstin: type: string example: 03AAFCE1234J1Z0 billing_address_line_1: type: string example: 'Number 234, 55 Wide Road, Ground Floor, Opposite Upkar Transport' billing_address_line_2: type: string example: 'Godown Area, Zirakpur, SASNagar' billing_city: type: string example: Zirakpur billing_state_code: type: string example: '03' billing_pincode: type: string example: '140603' 422: description: '' content: application/json: schema: type: object example: message: 'We could not verify this GSTIN. Check it and try again.' properties: message: type: string example: 'We could not verify this GSTIN. Check it and try again.' 503: description: '' content: application/json: schema: type: object example: message: 'GST verification is temporarily unavailable. Please try again shortly.' properties: message: type: string example: 'GST verification is temporarily unavailable. Please try again shortly.' tags: - 'GST verification' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/contacts/{contact_id}/ledger.pdf': get: summary: 'Download a contact ledger statement PDF.' operationId: downloadAContactLedgerStatementPDF description: 'The statement includes dated movements, running receivable and payable balances, and period totals.' parameters: - in: query name: from description: 'Start date, inclusive.' example: '2026-04-01' required: false schema: type: string description: 'Start date, inclusive.' example: '2026-04-01' - in: query name: to description: 'End date, inclusive. Must be on or after `from`.' example: '2027-03-31' required: false schema: type: string description: 'End date, inclusive. Must be on or after `from`.' example: '2027-03-31' responses: 200: description: 'The generated ledger statement PDF.' content: application/pdf: schema: type: string format: binary tags: - Contacts parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: contact_id description: 'The ID of the contact.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/contacts/{contact_id}/ledger/email': post: summary: 'Email a contact ledger statement.' operationId: emailAContactLedgerStatement description: 'Sends one email to each chosen person at the party with the statement PDF for the period attached and a button to the signed public statement. Choose people by the `key` values in `send_recipients` from GET `contacts/{contact}`; each must have an email address. Leave `from` and `to` empty for all dates. Every email send is recorded in the audit log.' parameters: [] responses: 202: description: '' content: application/json: schema: type: object example: message: 'Emailed to Ravi Kumar.' data: sent_to: - key: primary name: 'Ravi Kumar' email: ravi@example.test properties: message: type: string example: 'Emailed to Ravi Kumar.' data: type: object properties: sent_to: type: array example: - key: primary name: 'Ravi Kumar' email: ravi@example.test items: type: object properties: key: type: string example: primary name: type: string example: 'Ravi Kumar' email: type: string example: ravi@example.test 422: description: '' content: application/json: schema: type: object example: message: 'Ravi Kumar has no email address.' errors: recipients.0: - 'Ravi Kumar has no email address.' properties: message: type: string example: 'Ravi Kumar has no email address.' errors: type: object properties: recipients.0: type: array example: - 'Ravi Kumar has no email address.' items: type: string tags: - Contacts requestBody: required: true content: application/json: schema: type: object properties: recipients: type: array description: 'A recipient key: `primary` for the party''s primary contact, or `person-{id}` for an additional contact. Must match the regex /^(primary|person-\d+)$/.' example: - primary - person-12 items: type: string recipient_emails: type: array description: 'Must be a valid email address. Must not be greater than 254 characters.' example: primary: accounts@example.com items: type: string nullable: true subject: type: string description: 'Email subject line. Must not be greater than 150 characters.' example: 'Sri Lakshmi Traders | Purchase order: PO-0007' message: type: string description: 'Message body. Blank lines separate paragraphs. The PDF is attached and a button links to the signed public page. Must not be greater than 2000 characters.' example: 'Hi Ravi, please find purchase order PO-0007 for ₹12,500.00. Please confirm the order and the delivery date.' from: type: string description: 'Ledger statements only: start date, inclusive. Must be a valid date.' example: '2026-04-01' nullable: true to: type: string description: 'Ledger statements only: end date, inclusive. Must be on or after `from`. Must be a valid date. Must be a date after or equal to from.' example: '2027-03-31' nullable: true required: - recipients - subject - message parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: contact_id description: 'The ID of the contact.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/contacts/{contact_id}/active': patch: summary: 'Stop trading with a party, or start again.' operationId: stopTradingWithAPartyOrStartAgain description: "Its own endpoint rather than a field on update, which re-records the opening balance and\nwill reverse and replace its accounting entry when it looks changed. Retiring a party is a one-word\ndecision and has no business touching their khata. An inactive party keeps its balance and\nevery document it already sits on; it is simply no longer offered when raising a new one." parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'The walk-in customer is where every untagged counter sale is billed, so it cannot be made inactive.' errors: is_active: - 'The walk-in customer is where every untagged counter sale is billed, so it cannot be made inactive.' properties: message: type: string example: 'The walk-in customer is where every untagged counter sale is billed, so it cannot be made inactive.' errors: type: object properties: is_active: type: array example: - 'The walk-in customer is where every untagged counter sale is billed, so it cannot be made inactive.' items: type: string tags: - Contacts requestBody: required: true content: application/json: schema: type: object properties: is_active: type: boolean description: 'Whether the shop still trades with this party.' example: false required: - is_active parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: contact_id description: 'The ID of the contact.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/contacts/bulk': patch: summary: 'Bulk update contact status or party role.' operationId: bulkUpdateContactStatusOrPartyRole description: 'Atomic: all IDs must belong to this business and the caller must be authorized for every current and target role. Missing/cross-business IDs return 404; denied roles return 403; invalid actions, duplicate IDs and protected walk-in changes return 422. Existing documents, balances, journals and profile fields are preserved. Use one ID for an individual role change.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: updated_count: 2 properties: data: type: object properties: updated_count: type: integer example: 2 tags: - Contacts requestBody: required: true content: application/json: schema: type: object properties: contact_ids: type: array description: '' example: - 1 - 2 items: type: integer action: type: string description: 'activate or deactivate changes only status. customer, supplier or both changes only the party role. Documents, opening balances, journals and other profile fields are preserved.' example: both enum: - activate - deactivate - customer - supplier - both required: - contact_ids - action parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/contacts/{contact}/photo': get: summary: "View the party's private profile photo." operationId: viewThePartysPrivateProfilePhoto description: "Requires membership, khata access and sales permission for customers or purchases permission\nfor suppliers. Both parties permit either permission. Missing photos return 404.\nNo public storage path or signed public link is exposed. Production photos use private S3;\nlegacy images are read from their recorded disk through this same authenticated endpoint." parameters: [] responses: 200: description: 'The saved JPEG, PNG or WebP photo, with private/no-store headers.' content: application/octet-stream: schema: type: string format: binary tags: - Contacts parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: contact description: 'The contact.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/contacts': get: summary: '' operationId: getApiV1BusinessesBusinessContacts description: '' parameters: - in: query name: status description: 'Which parties to list by state: `all` (the default) lists both, with inactive parties last, `active` lists only parties the shop still trades with, and `inactive` lists only inactive ones. Use `active` when building a picker.' example: "active\n\nThe `search` term also matches the name and phone of a contact's additional contacts." required: false schema: type: string description: 'Which parties to list by state: `all` (the default) lists both, with inactive parties last, `active` lists only parties the shop still trades with, and `inactive` lists only inactive ones. Use `active` when building a picker.' example: "active\n\nThe `search` term also matches the name and phone of a contact's additional contacts." responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Contacts requestBody: required: false content: application/json: schema: type: object properties: type: type: string description: '' example: customer enum: - customer - supplier - both nullable: true search: type: string description: 'Must not be greater than 128 characters.' example: b nullable: true status: type: string description: '' example: all enum: - all - active - inactive nullable: true per_page: type: integer description: 'Must be at least 1. Must not be greater than 100.' example: 22 nullable: true post: summary: 'Create a contact and record its opening balance.' operationId: createAContactAndRecordItsOpeningBalance description: "Customer opening balances are recorded in Accounts receivable; supplier opening balances are recorded in Accounts payable, offset by Owner equity. A customer may also carry a `credit_limit`, the most it may owe at once; sales invoices and manual khata credits past it are refused with HTTP 422.\n\nA business party keeps its primary contact in `contact_person`, `phone` and `email`, and may list more people in `additional_contacts`." parameters: [] responses: {} tags: - Contacts requestBody: required: true content: application/json: schema: type: object properties: type: type: string description: '' example: customer enum: - customer - supplier - both is_active: type: boolean description: '' example: false profile_type: type: string description: '' example: consumer enum: - consumer - business name: type: string description: 'Must not be greater than 128 characters.' example: b company_name: type: string description: 'Must not be greater than 191 characters.' example: 'n' nullable: true contact_person: type: string description: "Primary contact person at a business party. Ignored for consumer contacts. The primary contact phone and email are the contact's own `phone` and `email`. Must not be greater than 128 characters." example: 'Anita Rao' nullable: true phone: type: string description: 'Must not be greater than 32 characters.' example: g nullable: true email: type: string description: 'Must be a valid email address. Must not be greater than 255 characters.' example: rowan.gulgowski@example.com nullable: true gst_treatment: type: string description: 'GST registration status. Use unregistered for an Indian business without GST registration and overseas for a contact outside India.' example: unregistered enum: - unregistered - registered_regular - registered_composition - consumer - overseas - sez gstin: type: string description: '15-character Indian GSTIN. Required for registered_regular, registered_composition, and sez. Optional for an overseas contact registered under Indian GST; ignored for unregistered and consumer contacts. The same GSTIN may be used by multiple contacts. Must match the regex /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/. Must be 15 characters.' example: 29ABCDE1234F1Z5 nullable: true pan: type: string description: 'Optional Indian PAN. Ignored for consumer and overseas contacts. Must match the regex /^[A-Z]{5}[0-9]{4}[A-Z]$/. Must be 10 characters.' example: ABCDE1234F nullable: true country: type: string description: 'Country where the overseas business is registered. Required when gst_treatment is overseas; ignored otherwise. Must not be greater than 96 characters.' example: 'United Arab Emirates' nullable: true foreign_tax_id: type: string description: 'Optional foreign Tax, VAT, or government-issued business identification number. Used only for overseas contacts. Must not be greater than 64 characters.' example: '100123456700003' nullable: true latitude: type: number description: 'Optional contact location latitude in decimal degrees, from -90 to 90. Send both latitude and longitude together. Omit both on update to preserve the location; send both as null to remove it. Mobile map pickers submit the selected coordinates here. This field is required when longitude is present. Must be between -90 and 90.' example: 17.448583 nullable: true longitude: type: number description: 'Optional contact location longitude in decimal degrees, from -180 to 180. Required with latitude; send both fields together, including when clearing them. This field is required when latitude is present. Must be between -180 and 180.' example: 78.390803 nullable: true address: type: string description: 'Must not be greater than 1000 characters.' example: d nullable: true billing_address_line_1: type: string description: 'Must not be greater than 191 characters.' example: l nullable: true billing_address_line_2: type: string description: 'Must not be greater than 191 characters.' example: j nullable: true billing_city: type: string description: 'Must not be greater than 96 characters.' example: 'n' nullable: true billing_state_code: type: string description: '' example: 1 enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 26 - 27 - 29 - 30 - 31 - 32 - 33 - 34 - 35 - 36 - 37 - 38 - 96 - 97 - 99 nullable: true billing_region: type: string description: 'State, province, emirate, or other first-level region for an overseas billing address. Ignored for contacts in India. Must not be greater than 96 characters.' example: Dubai nullable: true billing_pincode: type: string description: 'Must be 6 digits.' example: '569775' nullable: true billing_postal_code: type: string description: 'Postal or ZIP code for an overseas billing address. Ignored for contacts in India. Must not be greater than 32 characters.' example: 'SW1A 1AA' nullable: true shipping_same_as_billing: type: boolean description: '' example: false shipping_address_line_1: type: string description: 'Must not be greater than 191 characters.' example: 'n' nullable: true shipping_address_line_2: type: string description: 'Must not be greater than 191 characters.' example: g nullable: true shipping_city: type: string description: 'Must not be greater than 96 characters.' example: z nullable: true shipping_state_code: type: string description: '' example: 1 enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 26 - 27 - 29 - 30 - 31 - 32 - 33 - 34 - 35 - 36 - 37 - 38 - 96 - 97 - 99 nullable: true shipping_region: type: string description: 'State, province, emirate, or other first-level region for an overseas shipping address. Ignored for contacts in India. Must not be greater than 96 characters.' example: Dubai nullable: true shipping_pincode: type: string description: 'Must be 6 digits.' example: '569775' nullable: true shipping_postal_code: type: string description: 'Postal or ZIP code for an overseas shipping address. Ignored for contacts in India. Must not be greater than 32 characters.' example: 'SW1A 1AA' nullable: true price_list_id: type: integer description: 'Rate card this party buys at. Item lines raised for the party take their rate from it unless the request quotes one. Requires the party pricing feature; leave empty to bill at the item master rate.' example: 7 nullable: true default_discount_percent: type: number description: 'Blanket percentage off whatever rate the party would otherwise pay, applied after the rate card. Defaults to 0. Must be at least 0. Must not be greater than 100.' example: 2 nullable: true credit_limit: type: number description: 'Most this customer may owe at any moment, in rupees. Omit or send null for no limit; send 0 to put the party on cash only. Ignored for supplier contacts. A sales invoice or manual khata credit that would take the receivable balance past it is refused with HTTP 422. Must be at least 0. Must not be greater than 999999999.' example: 50000 nullable: true opening_balance: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 22 nullable: true opening_balance_side: type: string description: '' example: receivable enum: - receivable - payable opening_balance_date: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true additional_contacts: type: array description: '' example: - name: 'Suresh Gupta' designation: Accounts phone: '9811111111' email: accounts@example.com items: type: object properties: name: type: string description: 'Must not be greater than 128 characters.' example: 'Suresh Gupta' designation: type: string description: 'Must not be greater than 96 characters.' example: Accounts nullable: true phone: type: string description: 'Must not be greater than 32 characters.' example: '9811111111' nullable: true email: type: string description: 'Must be a valid email address. Must not be greater than 255 characters.' example: accounts@example.com nullable: true required: - name remove_photo: type: boolean description: 'Set true to remove the saved photo. Cannot be combined with a photo upload. Defaults to false.' example: false required: - type - profile_type - name - gst_treatment - opening_balance_side multipart/form-data: schema: type: object properties: type: type: string description: '' example: customer enum: - customer - supplier - both is_active: type: boolean description: '' example: false profile_type: type: string description: '' example: consumer enum: - consumer - business name: type: string description: 'Must not be greater than 128 characters.' example: b company_name: type: string description: 'Must not be greater than 191 characters.' example: 'n' nullable: true contact_person: type: string description: "Primary contact person at a business party. Ignored for consumer contacts. The primary contact phone and email are the contact's own `phone` and `email`. Must not be greater than 128 characters." example: 'Anita Rao' nullable: true phone: type: string description: 'Must not be greater than 32 characters.' example: g nullable: true email: type: string description: 'Must be a valid email address. Must not be greater than 255 characters.' example: rowan.gulgowski@example.com nullable: true gst_treatment: type: string description: 'GST registration status. Use unregistered for an Indian business without GST registration and overseas for a contact outside India.' example: unregistered enum: - unregistered - registered_regular - registered_composition - consumer - overseas - sez gstin: type: string description: '15-character Indian GSTIN. Required for registered_regular, registered_composition, and sez. Optional for an overseas contact registered under Indian GST; ignored for unregistered and consumer contacts. The same GSTIN may be used by multiple contacts. Must match the regex /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/. Must be 15 characters.' example: 29ABCDE1234F1Z5 nullable: true pan: type: string description: 'Optional Indian PAN. Ignored for consumer and overseas contacts. Must match the regex /^[A-Z]{5}[0-9]{4}[A-Z]$/. Must be 10 characters.' example: ABCDE1234F nullable: true country: type: string description: 'Country where the overseas business is registered. Required when gst_treatment is overseas; ignored otherwise. Must not be greater than 96 characters.' example: 'United Arab Emirates' nullable: true foreign_tax_id: type: string description: 'Optional foreign Tax, VAT, or government-issued business identification number. Used only for overseas contacts. Must not be greater than 64 characters.' example: '100123456700003' nullable: true latitude: type: number description: 'Optional contact location latitude in decimal degrees, from -90 to 90. Send both latitude and longitude together. Omit both on update to preserve the location; send both as null to remove it. Mobile map pickers submit the selected coordinates here. This field is required when longitude is present. Must be between -90 and 90.' example: 17.448583 nullable: true longitude: type: number description: 'Optional contact location longitude in decimal degrees, from -180 to 180. Required with latitude; send both fields together, including when clearing them. This field is required when latitude is present. Must be between -180 and 180.' example: 78.390803 nullable: true address: type: string description: 'Must not be greater than 1000 characters.' example: d nullable: true billing_address_line_1: type: string description: 'Must not be greater than 191 characters.' example: l nullable: true billing_address_line_2: type: string description: 'Must not be greater than 191 characters.' example: j nullable: true billing_city: type: string description: 'Must not be greater than 96 characters.' example: 'n' nullable: true billing_state_code: type: string description: '' example: 1 enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 26 - 27 - 29 - 30 - 31 - 32 - 33 - 34 - 35 - 36 - 37 - 38 - 96 - 97 - 99 nullable: true billing_region: type: string description: 'State, province, emirate, or other first-level region for an overseas billing address. Ignored for contacts in India. Must not be greater than 96 characters.' example: Dubai nullable: true billing_pincode: type: string description: 'Must be 6 digits.' example: '569775' nullable: true billing_postal_code: type: string description: 'Postal or ZIP code for an overseas billing address. Ignored for contacts in India. Must not be greater than 32 characters.' example: 'SW1A 1AA' nullable: true shipping_same_as_billing: type: boolean description: '' example: false shipping_address_line_1: type: string description: 'Must not be greater than 191 characters.' example: 'n' nullable: true shipping_address_line_2: type: string description: 'Must not be greater than 191 characters.' example: g nullable: true shipping_city: type: string description: 'Must not be greater than 96 characters.' example: z nullable: true shipping_state_code: type: string description: '' example: 1 enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 26 - 27 - 29 - 30 - 31 - 32 - 33 - 34 - 35 - 36 - 37 - 38 - 96 - 97 - 99 nullable: true shipping_region: type: string description: 'State, province, emirate, or other first-level region for an overseas shipping address. Ignored for contacts in India. Must not be greater than 96 characters.' example: Dubai nullable: true shipping_pincode: type: string description: 'Must be 6 digits.' example: '569775' nullable: true shipping_postal_code: type: string description: 'Postal or ZIP code for an overseas shipping address. Ignored for contacts in India. Must not be greater than 32 characters.' example: 'SW1A 1AA' nullable: true price_list_id: type: integer description: 'Rate card this party buys at. Item lines raised for the party take their rate from it unless the request quotes one. Requires the party pricing feature; leave empty to bill at the item master rate.' example: 7 nullable: true default_discount_percent: type: number description: 'Blanket percentage off whatever rate the party would otherwise pay, applied after the rate card. Defaults to 0. Must be at least 0. Must not be greater than 100.' example: 2 nullable: true credit_limit: type: number description: 'Most this customer may owe at any moment, in rupees. Omit or send null for no limit; send 0 to put the party on cash only. Ignored for supplier contacts. A sales invoice or manual khata credit that would take the receivable balance past it is refused with HTTP 422. Must be at least 0. Must not be greater than 999999999.' example: 50000 nullable: true opening_balance: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 22 nullable: true opening_balance_side: type: string description: '' example: receivable enum: - receivable - payable opening_balance_date: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true additional_contacts: type: array description: '' example: - name: 'Suresh Gupta' designation: Accounts phone: '9811111111' email: accounts@example.com items: type: object properties: name: type: string description: 'Must not be greater than 128 characters.' example: 'Suresh Gupta' designation: type: string description: 'Must not be greater than 96 characters.' example: Accounts nullable: true phone: type: string description: 'Must not be greater than 32 characters.' example: '9811111111' nullable: true email: type: string description: 'Must be a valid email address. Must not be greater than 255 characters.' example: accounts@example.com nullable: true required: - name photo: type: string format: binary description: 'Optional private JPEG, PNG or WebP profile photo, up to 2 MB and 8192 pixels per side. Multipart upload. Omit to retain the saved photo; null does not remove it. Clients may crop before uploading. Must be an image. Must not be greater than 2048 kilobytes.' nullable: true remove_photo: type: boolean description: 'Set true to remove the saved photo. Cannot be combined with a photo upload. Defaults to false.' example: false required: - type - profile_type - name - gst_treatment - opening_balance_side parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/contacts/{id}': get: summary: '' operationId: getApiV1BusinessesBusinessContactsId description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Contacts put: summary: 'Update a contact and replace its opening-balance journal.' operationId: updateAContactAndReplaceItsOpeningBalanceJournal description: 'Omit `additional_contacts` to leave the list as it is; send the complete list to replace it, or an empty array to remove every additional contact.' parameters: [] responses: {} tags: - Contacts requestBody: required: true content: application/json: schema: type: object properties: type: type: string description: '' example: customer enum: - customer - supplier - both is_active: type: boolean description: '' example: false profile_type: type: string description: '' example: consumer enum: - consumer - business name: type: string description: 'Must not be greater than 128 characters.' example: b company_name: type: string description: 'Must not be greater than 191 characters.' example: 'n' nullable: true contact_person: type: string description: "Primary contact person at a business party. Ignored for consumer contacts. The primary contact phone and email are the contact's own `phone` and `email`. Must not be greater than 128 characters." example: 'Anita Rao' nullable: true phone: type: string description: 'Must not be greater than 32 characters.' example: g nullable: true email: type: string description: 'Must be a valid email address. Must not be greater than 255 characters.' example: rowan.gulgowski@example.com nullable: true gst_treatment: type: string description: 'GST registration status. Use unregistered for an Indian business without GST registration and overseas for a contact outside India.' example: unregistered enum: - unregistered - registered_regular - registered_composition - consumer - overseas - sez gstin: type: string description: '15-character Indian GSTIN. Required for registered_regular, registered_composition, and sez. Optional for an overseas contact registered under Indian GST; ignored for unregistered and consumer contacts. The same GSTIN may be used by multiple contacts. Must match the regex /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/. Must be 15 characters.' example: 29ABCDE1234F1Z5 nullable: true pan: type: string description: 'Optional Indian PAN. Ignored for consumer and overseas contacts. Must match the regex /^[A-Z]{5}[0-9]{4}[A-Z]$/. Must be 10 characters.' example: ABCDE1234F nullable: true country: type: string description: 'Country where the overseas business is registered. Required when gst_treatment is overseas; ignored otherwise. Must not be greater than 96 characters.' example: 'United Arab Emirates' nullable: true foreign_tax_id: type: string description: 'Optional foreign Tax, VAT, or government-issued business identification number. Used only for overseas contacts. Must not be greater than 64 characters.' example: '100123456700003' nullable: true latitude: type: number description: 'Optional contact location latitude in decimal degrees, from -90 to 90. Send both latitude and longitude together. Omit both on update to preserve the location; send both as null to remove it. Mobile map pickers submit the selected coordinates here. This field is required when longitude is present. Must be between -90 and 90.' example: 17.448583 nullable: true longitude: type: number description: 'Optional contact location longitude in decimal degrees, from -180 to 180. Required with latitude; send both fields together, including when clearing them. This field is required when latitude is present. Must be between -180 and 180.' example: 78.390803 nullable: true address: type: string description: 'Must not be greater than 1000 characters.' example: d nullable: true billing_address_line_1: type: string description: 'Must not be greater than 191 characters.' example: l nullable: true billing_address_line_2: type: string description: 'Must not be greater than 191 characters.' example: j nullable: true billing_city: type: string description: 'Must not be greater than 96 characters.' example: 'n' nullable: true billing_state_code: type: string description: '' example: 1 enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 26 - 27 - 29 - 30 - 31 - 32 - 33 - 34 - 35 - 36 - 37 - 38 - 96 - 97 - 99 nullable: true billing_region: type: string description: 'State, province, emirate, or other first-level region for an overseas billing address. Ignored for contacts in India. Must not be greater than 96 characters.' example: Dubai nullable: true billing_pincode: type: string description: 'Must be 6 digits.' example: '569775' nullable: true billing_postal_code: type: string description: 'Postal or ZIP code for an overseas billing address. Ignored for contacts in India. Must not be greater than 32 characters.' example: 'SW1A 1AA' nullable: true shipping_same_as_billing: type: boolean description: '' example: false shipping_address_line_1: type: string description: 'Must not be greater than 191 characters.' example: 'n' nullable: true shipping_address_line_2: type: string description: 'Must not be greater than 191 characters.' example: g nullable: true shipping_city: type: string description: 'Must not be greater than 96 characters.' example: z nullable: true shipping_state_code: type: string description: '' example: 1 enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 26 - 27 - 29 - 30 - 31 - 32 - 33 - 34 - 35 - 36 - 37 - 38 - 96 - 97 - 99 nullable: true shipping_region: type: string description: 'State, province, emirate, or other first-level region for an overseas shipping address. Ignored for contacts in India. Must not be greater than 96 characters.' example: Dubai nullable: true shipping_pincode: type: string description: 'Must be 6 digits.' example: '569775' nullable: true shipping_postal_code: type: string description: 'Postal or ZIP code for an overseas shipping address. Ignored for contacts in India. Must not be greater than 32 characters.' example: 'SW1A 1AA' nullable: true price_list_id: type: integer description: 'Rate card this party buys at. Item lines raised for the party take their rate from it unless the request quotes one. Requires the party pricing feature; leave empty to bill at the item master rate.' example: 7 nullable: true default_discount_percent: type: number description: 'Blanket percentage off whatever rate the party would otherwise pay, applied after the rate card. Defaults to 0. Must be at least 0. Must not be greater than 100.' example: 2 nullable: true credit_limit: type: number description: 'Most this customer may owe at any moment, in rupees. Omit or send null for no limit; send 0 to put the party on cash only. Ignored for supplier contacts. A sales invoice or manual khata credit that would take the receivable balance past it is refused with HTTP 422. Must be at least 0. Must not be greater than 999999999.' example: 50000 nullable: true opening_balance: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 22 nullable: true opening_balance_side: type: string description: '' example: receivable enum: - receivable - payable opening_balance_date: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true additional_contacts: type: array description: '' example: - name: 'Suresh Gupta' designation: Accounts phone: '9811111111' email: accounts@example.com items: type: object properties: name: type: string description: 'Must not be greater than 128 characters.' example: 'Suresh Gupta' designation: type: string description: 'Must not be greater than 96 characters.' example: Accounts nullable: true phone: type: string description: 'Must not be greater than 32 characters.' example: '9811111111' nullable: true email: type: string description: 'Must be a valid email address. Must not be greater than 255 characters.' example: accounts@example.com nullable: true required: - name remove_photo: type: boolean description: 'Set true to remove the saved photo. Cannot be combined with a photo upload. Defaults to false.' example: false required: - type - profile_type - name - gst_treatment - opening_balance_side multipart/form-data: schema: type: object properties: type: type: string description: '' example: customer enum: - customer - supplier - both is_active: type: boolean description: '' example: false profile_type: type: string description: '' example: consumer enum: - consumer - business name: type: string description: 'Must not be greater than 128 characters.' example: b company_name: type: string description: 'Must not be greater than 191 characters.' example: 'n' nullable: true contact_person: type: string description: "Primary contact person at a business party. Ignored for consumer contacts. The primary contact phone and email are the contact's own `phone` and `email`. Must not be greater than 128 characters." example: 'Anita Rao' nullable: true phone: type: string description: 'Must not be greater than 32 characters.' example: g nullable: true email: type: string description: 'Must be a valid email address. Must not be greater than 255 characters.' example: rowan.gulgowski@example.com nullable: true gst_treatment: type: string description: 'GST registration status. Use unregistered for an Indian business without GST registration and overseas for a contact outside India.' example: unregistered enum: - unregistered - registered_regular - registered_composition - consumer - overseas - sez gstin: type: string description: '15-character Indian GSTIN. Required for registered_regular, registered_composition, and sez. Optional for an overseas contact registered under Indian GST; ignored for unregistered and consumer contacts. The same GSTIN may be used by multiple contacts. Must match the regex /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/. Must be 15 characters.' example: 29ABCDE1234F1Z5 nullable: true pan: type: string description: 'Optional Indian PAN. Ignored for consumer and overseas contacts. Must match the regex /^[A-Z]{5}[0-9]{4}[A-Z]$/. Must be 10 characters.' example: ABCDE1234F nullable: true country: type: string description: 'Country where the overseas business is registered. Required when gst_treatment is overseas; ignored otherwise. Must not be greater than 96 characters.' example: 'United Arab Emirates' nullable: true foreign_tax_id: type: string description: 'Optional foreign Tax, VAT, or government-issued business identification number. Used only for overseas contacts. Must not be greater than 64 characters.' example: '100123456700003' nullable: true latitude: type: number description: 'Optional contact location latitude in decimal degrees, from -90 to 90. Send both latitude and longitude together. Omit both on update to preserve the location; send both as null to remove it. Mobile map pickers submit the selected coordinates here. This field is required when longitude is present. Must be between -90 and 90.' example: 17.448583 nullable: true longitude: type: number description: 'Optional contact location longitude in decimal degrees, from -180 to 180. Required with latitude; send both fields together, including when clearing them. This field is required when latitude is present. Must be between -180 and 180.' example: 78.390803 nullable: true address: type: string description: 'Must not be greater than 1000 characters.' example: d nullable: true billing_address_line_1: type: string description: 'Must not be greater than 191 characters.' example: l nullable: true billing_address_line_2: type: string description: 'Must not be greater than 191 characters.' example: j nullable: true billing_city: type: string description: 'Must not be greater than 96 characters.' example: 'n' nullable: true billing_state_code: type: string description: '' example: 1 enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 26 - 27 - 29 - 30 - 31 - 32 - 33 - 34 - 35 - 36 - 37 - 38 - 96 - 97 - 99 nullable: true billing_region: type: string description: 'State, province, emirate, or other first-level region for an overseas billing address. Ignored for contacts in India. Must not be greater than 96 characters.' example: Dubai nullable: true billing_pincode: type: string description: 'Must be 6 digits.' example: '569775' nullable: true billing_postal_code: type: string description: 'Postal or ZIP code for an overseas billing address. Ignored for contacts in India. Must not be greater than 32 characters.' example: 'SW1A 1AA' nullable: true shipping_same_as_billing: type: boolean description: '' example: false shipping_address_line_1: type: string description: 'Must not be greater than 191 characters.' example: 'n' nullable: true shipping_address_line_2: type: string description: 'Must not be greater than 191 characters.' example: g nullable: true shipping_city: type: string description: 'Must not be greater than 96 characters.' example: z nullable: true shipping_state_code: type: string description: '' example: 1 enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 26 - 27 - 29 - 30 - 31 - 32 - 33 - 34 - 35 - 36 - 37 - 38 - 96 - 97 - 99 nullable: true shipping_region: type: string description: 'State, province, emirate, or other first-level region for an overseas shipping address. Ignored for contacts in India. Must not be greater than 96 characters.' example: Dubai nullable: true shipping_pincode: type: string description: 'Must be 6 digits.' example: '569775' nullable: true shipping_postal_code: type: string description: 'Postal or ZIP code for an overseas shipping address. Ignored for contacts in India. Must not be greater than 32 characters.' example: 'SW1A 1AA' nullable: true price_list_id: type: integer description: 'Rate card this party buys at. Item lines raised for the party take their rate from it unless the request quotes one. Requires the party pricing feature; leave empty to bill at the item master rate.' example: 7 nullable: true default_discount_percent: type: number description: 'Blanket percentage off whatever rate the party would otherwise pay, applied after the rate card. Defaults to 0. Must be at least 0. Must not be greater than 100.' example: 2 nullable: true credit_limit: type: number description: 'Most this customer may owe at any moment, in rupees. Omit or send null for no limit; send 0 to put the party on cash only. Ignored for supplier contacts. A sales invoice or manual khata credit that would take the receivable balance past it is refused with HTTP 422. Must be at least 0. Must not be greater than 999999999.' example: 50000 nullable: true opening_balance: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 22 nullable: true opening_balance_side: type: string description: '' example: receivable enum: - receivable - payable opening_balance_date: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true additional_contacts: type: array description: '' example: - name: 'Suresh Gupta' designation: Accounts phone: '9811111111' email: accounts@example.com items: type: object properties: name: type: string description: 'Must not be greater than 128 characters.' example: 'Suresh Gupta' designation: type: string description: 'Must not be greater than 96 characters.' example: Accounts nullable: true phone: type: string description: 'Must not be greater than 32 characters.' example: '9811111111' nullable: true email: type: string description: 'Must be a valid email address. Must not be greater than 255 characters.' example: accounts@example.com nullable: true required: - name photo: type: string format: binary description: 'Optional private JPEG, PNG or WebP profile photo, up to 2 MB and 8192 pixels per side. Multipart upload. Omit to retain the saved photo; null does not remove it. Clients may crop before uploading. Must be an image. Must not be greater than 2048 kilobytes.' nullable: true remove_photo: type: boolean description: 'Set true to remove the saved photo. Cannot be combined with a photo upload. Defaults to false.' example: false required: - type - profile_type - name - gst_treatment - opening_balance_side delete: summary: '' operationId: deleteApiV1BusinessesBusinessContactsId description: '' parameters: [] responses: {} tags: - Contacts parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: id description: 'The ID of the contact.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/contact-imports/template': get: summary: 'Download the party import template.' operationId: downloadThePartyImportTemplate description: "A CSV carrying the documented headers and two example rows. The headers are not the only\nones accepted — common labels from other billing software are recognised too, and a\n`column_map` on upload settles anything that cannot be guessed." parameters: [] responses: 200: description: 'The party import template as CSV.' content: application/octet-stream: schema: type: string format: binary tags: - Contacts parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/contact-imports': post: summary: 'Upload a party file and stage it for review.' operationId: uploadAPartyFileAndStageItForReview description: "Creates no contacts. The file is stored, checksummed, and checked row by row away from this\nrequest, so the response may come back still `uploaded`; poll the read endpoint until the\nstatus settles on `validated` or `invalid`.\n\n`profile_type` and `gst_treatment` need not be in the file. A column stating either always\nwins; otherwise a row with a GSTIN is taken as a registered business, one with a company\nname as an unregistered business, and one with neither as a walk-in consumer.\n\nAn `Other Contacts` column lists a business party's additional contacts in one cell, each\nperson as `Name | Role | Phone | Email` and people separated by `;`. A file without the\ncolumn leaves an updated party's additional contacts alone; with it, the cell replaces them,\nand a blank cell removes them." parameters: [] responses: {} tags: - Contacts requestBody: required: true content: multipart/form-data: schema: type: object properties: file: type: string format: binary description: "The party list, `.csv` or `.xlsx`, up to 5 MB and 5,000 rows. The format is detected from the file's contents, not its name." duplicate_mode: type: string description: 'What to do with a row naming a party the books already hold: `skip`, `update`, or `create`. Defaults to `skip`.' example: skip nullable: true column_map: type: object description: 'Our field name to the column heading in your file, for a file whose headings cannot be guessed.' example: name: 'Party Name' phone: 'Mobile No' properties: {} required: - file parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/contact-imports/{contactImport_uuid}': get: summary: 'Read a staged import.' operationId: readAStagedImport description: "Returns the status, the row counts, every validation error with its row number in the\nsheet, and a preview sample saying what each row would do." parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Contacts delete: summary: 'Discard a staged import.' operationId: discardAStagedImport description: "Removes the stored file and its checked rows. An import that has already created parties\ncannot be discarded, because it is the record of what was loaded." parameters: [] responses: 409: description: '' content: application/json: schema: type: object example: message: 'An imported file cannot be discarded.' properties: message: type: string example: 'An imported file cannot be discarded.' tags: - Contacts parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: contactImport_uuid description: '' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string '/api/v1/businesses/{business}/contact-imports/{contactImport_uuid}/commit': post: summary: 'Create and update every checked row.' operationId: createAndUpdateEveryCheckedRow description: "The whole file is written in one transaction: a failure anywhere leaves the party list\nexactly as it was. Rows carrying an opening balance record a dated khata entry, never a\nsilent balance on the record." parameters: [] responses: 409: description: '' content: application/json: schema: oneOf: - description: '' type: object example: message: 'This file has not passed validation.' properties: message: type: string example: 'This file has not passed validation.' - description: '' type: object example: message: 'This file has already been imported.' properties: message: type: string example: 'This file has already been imported.' tags: - Contacts parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: contactImport_uuid description: '' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string '/api/v1/businesses/{business}/ledger-entries': get: summary: '' operationId: getApiV1BusinessesBusinessLedgerEntries description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Khata ledger' requestBody: required: false content: application/json: schema: type: object properties: contact_id: type: integer description: '' example: 16 nullable: true kind: type: string description: 'Must not be greater than 32 characters.' example: 'n' nullable: true from: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true to: type: string description: 'Must be a valid date. Must be a date after or equal to from.' example: '2026-01-15' nullable: true per_page: type: integer description: 'Must be at least 1. Must not be greater than 100.' example: 22 nullable: true post: summary: 'Record a manual khata adjustment.' operationId: recordAManualKhataAdjustment description: 'The receivable or payable adjustment is offset to Owner equity. Use invoice, purchase, expense, and payment endpoints for operational transactions.' parameters: [] responses: {} tags: - 'Khata ledger' requestBody: required: true content: application/json: schema: type: object properties: contact_id: type: integer description: '' example: 16 kind: type: string description: '' example: customer_credit enum: - customer_credit - customer_payment - supplier_credit - supplier_payment amount: type: number description: 'Must not be greater than 999999999.' example: 22 occurred_on: type: string description: 'Must be a valid date.' example: '2026-01-15' reference: type: string description: 'Must not be greater than 64 characters.' example: g nullable: true note: type: string description: 'Must not be greater than 1000 characters.' example: z nullable: true required: - contact_id - kind - amount - occurred_on parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/ledger-entries/{ledgerEntry_id}': get: summary: '' operationId: getApiV1BusinessesBusinessLedgerEntriesLedgerEntry_id description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Khata ledger' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: ledgerEntry_id description: 'The ID of the ledgerEntry.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/ledger-entries/{ledgerEntry_id}/reverse': post: summary: 'Reverse a manual khata adjustment and its journal.' operationId: reverseAManualKhataAdjustmentAndItsJournal description: '' parameters: [] responses: {} tags: - 'Khata ledger' requestBody: required: true content: application/json: schema: type: object properties: reason: type: string description: 'Must be at least 3 characters. Must not be greater than 255 characters.' example: b required: - reason parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: ledgerEntry_id description: 'The ID of the ledgerEntry.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/items/{item_id}/transactions': get: summary: 'List item transactions, newest first.' operationId: listItemTransactionsNewestFirst description: "Includes stock movements and invoices, purchase invoices, and returns with no\nrecorded movement for this item, including untracked goods and services.\nDocuments are restricted by tenant, workspace role, and plan. A document with\na stock movement is represented only by its movements.\nFully cancelled manual adjustments and their exact same-date, same-warehouse\nreversal are omitted; their audit records remain stored." parameters: - in: query name: limit description: 'Number of transactions to return, 1-100. Defaults to 25.' example: 10 required: false schema: type: integer description: 'Number of transactions to return, 1-100. Defaults to 25.' example: 10 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Inventory requestBody: required: false content: application/json: schema: type: object properties: limit: type: integer description: 'Must be at least 1. Must not be greater than 100.' example: 1 nullable: true parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: item_id description: 'The ID of the item.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/items/{item_id}/ledger': get: summary: 'Item stock ledger.' operationId: itemStockLedger description: "Effective stock movements of the item with their source documents, party, quantity in and out,\nthe stock held straight after it, and its cost. Closing stock is anchored to the item's\ncurrent stock, so the newest row always matches `stock_quantity`. `meta.summary` totals\nthe filtered movements. Document links respect workspace role and plan: `web_url` is null\nwhen the caller may not open that document.\nFully cancelled manual adjustments and their exact same-date, same-warehouse reversal\nare omitted from rows, totals and closing balances. The original audit records remain.\nReal opening stock, active adjustments, sales voids and invoice revisions remain visible." parameters: - in: query name: from description: 'date Movements on or after this date.' example: '2026-09-01' required: false schema: type: string description: 'date Movements on or after this date.' example: '2026-09-01' - in: query name: to description: 'date Movements on or before this date.' example: '2026-09-30' required: false schema: type: string description: 'date Movements on or before this date.' example: '2026-09-30' - in: query name: type description: 'Narrow to one document type: sale, purchase, sales_return, purchase_return, delivery_challan, adjustment, opening, or transfer.' example: purchase required: false schema: type: string description: 'Narrow to one document type: sale, purchase, sales_return, purchase_return, delivery_challan, adjustment, opening, or transfer.' example: purchase - in: query name: warehouse description: "Only movements in this warehouse. Closing stock then becomes that warehouse's balance." example: 1 required: false schema: type: integer description: "Only movements in this warehouse. Closing stock then becomes that warehouse's balance." example: 1 - in: query name: sort description: 'date (default), unit_cost, or total_cost.' example: date required: false schema: type: string description: 'date (default), unit_cost, or total_cost.' example: date - in: query name: direction description: 'desc (default) or asc.' example: desc required: false schema: type: string description: 'desc (default) or asc.' example: desc - in: query name: per_page description: 'Rows per page, 1-100. Defaults to 25.' example: 25 required: false schema: type: integer description: 'Rows per page, 1-100. Defaults to 25.' example: 25 - in: query name: page description: 'Page number.' example: 1 required: false schema: type: integer description: 'Page number.' example: 1 responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 1 occurred_on: '2026-10-02' movement_type: opening label: 'Opening stock' documents: [] party: null stock_in: 12.5 stock_out: 0 closing_stock: 12.5 unit: pcs unit_cost_paise: 8000 total_cost_paise: 100000 batch_number: null warehouse: id: 1 name: 'Main warehouse' location: null reason: 'Opening stock' can_edit_opening_stock: true meta: current_page: 1 per_page: 25 total: 1 summary: in_quantity: 12.5 in_value_paise: 100000 out_quantity: 0 out_value_paise: 0 net_quantity: 12.5 net_value_paise: 100000 count: 1 stock_quantity: 12.5 track_inventory: true properties: data: type: array example: - id: 1 occurred_on: '2026-10-02' movement_type: opening label: 'Opening stock' documents: [] party: null stock_in: 12.5 stock_out: 0 closing_stock: 12.5 unit: pcs unit_cost_paise: 8000 total_cost_paise: 100000 batch_number: null warehouse: id: 1 name: 'Main warehouse' location: null reason: 'Opening stock' can_edit_opening_stock: true items: type: object properties: id: type: integer example: 1 occurred_on: type: string example: '2026-10-02' movement_type: type: string example: opening label: type: string example: 'Opening stock' documents: type: array example: [] party: type: string example: null nullable: true stock_in: type: number example: 12.5 stock_out: type: integer example: 0 closing_stock: type: number example: 12.5 unit: type: string example: pcs unit_cost_paise: type: integer example: 8000 total_cost_paise: type: integer example: 100000 batch_number: type: string example: null nullable: true warehouse: type: object properties: id: type: integer example: 1 name: type: string example: 'Main warehouse' location: type: string example: null nullable: true reason: type: string example: 'Opening stock' can_edit_opening_stock: type: boolean example: true meta: type: object properties: current_page: type: integer example: 1 per_page: type: integer example: 25 total: type: integer example: 1 summary: type: object properties: in_quantity: type: number example: 12.5 description: 'Total effective quantity received in the filtered range, excluding fully cancelled manual adjustment pairs.' in_value_paise: type: integer example: 100000 description: 'Stock cost of what was received.' out_quantity: type: integer example: 0 description: 'Total quantity issued in the filtered range.' out_value_paise: type: integer example: 0 description: 'Stock cost of what was issued.' net_quantity: type: number example: 12.5 description: 'in_quantity minus out_quantity.' net_value_paise: type: integer example: 100000 description: 'in_value_paise minus out_value_paise.' count: type: integer example: 1 description: 'Number of movements in the filtered range. Without a warehouse filter, stock transfers are left out of the totals because they net to zero; filter by `type=transfer` or a warehouse to count them.' stock_quantity: type: number example: 12.5 track_inventory: type: boolean example: true tags: - Inventory requestBody: required: false content: application/json: schema: type: object properties: per_page: type: integer description: 'Must be at least 1. Must not be greater than 100.' example: 1 nullable: true parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: item_id description: 'The ID of the item.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/items/{item_id}/opening-stock/{movement_id}': patch: summary: 'Correct opening stock.' operationId: correctOpeningStock description: "Replaces this opening entry's quantity and cost, retaining its date, warehouse, and batch.\nStock and balanced Inventory / Owner equity journals are corrected atomically. Original\njournals and before/after values remain in the audit trail. No current-stock adjustment\nis created. Zero quantity removes the entry from ledger rows and totals. Corrections that\nwould make subsequent item, warehouse, or batch stock negative return 422. Only item-sourced,\nunreversed opening entries are supported; serialised items and stock placed into batches\nafter opening must use their unit/batch workflows. Requires inventory feature and permission." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 1 quantity: 12.5 unit_cost_paise: 8000 total_cost_paise: 100000 stock_quantity: 12.5 properties: data: type: object properties: id: type: integer example: 1 quantity: type: number example: 12.5 unit_cost_paise: type: integer example: 8000 total_cost_paise: type: integer example: 100000 stock_quantity: type: number example: 12.5 422: description: '' content: application/json: schema: type: object example: message: 'This opening quantity is needed by later stock movements.' errors: quantity: - 'This opening quantity is needed by later stock movements.' properties: message: type: string example: 'This opening quantity is needed by later stock movements.' errors: type: object properties: quantity: type: array example: - 'This opening quantity is needed by later stock movements.' items: type: string tags: - Inventory requestBody: required: true content: application/json: schema: type: object properties: quantity: type: number description: 'Corrected opening quantity, with at most three decimal places. Zero removes the entry from the visible ledger. Must be at least 0. Must not be greater than 999999999.' example: 12.5 unit_cost: type: number description: 'Cost per opening unit in rupees, with at most two decimal places. The original date, warehouse, and batch are retained. Must be at least 0. Must not be greater than 999999999.' example: 80 reason: type: string description: 'Reason retained with the old and new values in the audit trail. Must not be greater than 500 characters.' example: 'Corrected the initial stock count' required: - quantity - unit_cost - reason delete: summary: 'Remove opening stock.' operationId: removeOpeningStock description: "Sets this opening entry's quantity and value to zero and omits it from ledger rows,\ntransaction history, and totals. Adjusts current stock in its original warehouse and batch,\nreverses its opening journal on the original date, and retains the audit trail. Does not\ndelete purchases or sales. The same eligibility and nonnegative-stock rules as correction\napply. Repeating removal is safe. Requires inventory feature and permission." parameters: [] responses: 204: description: '' content: application/json: schema: type: object example: {} properties: {} tags: - Inventory requestBody: required: true content: application/json: schema: type: object properties: reason: type: string description: 'Reason recorded in the audit trail, maximum 500 characters.' example: 'Opening stock was entered twice' required: - reason parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: item_id description: 'The ID of the item.' example: 1 required: true schema: type: integer - in: path name: movement_id description: "Opening stock movement ID from the item's ledger." example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/items/{item_id}/suppliers': get: summary: "List an item's suppliers." operationId: listAnItemsSuppliers description: "Linked suppliers and every supplier a purchase invoice shows the item was bought from,\npreferred first, then linked, then most recently bought from. A supplier seen only in\npurchase history has `linked` false and a null `id`; link it with the create endpoint." parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Inventory post: summary: 'Link a supplier to an item.' operationId: linkASupplierToAnItem description: "Marking a supplier preferred clears the flag on the item's other suppliers." parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'This supplier is already linked to the item.' errors: contact_id: - 'This supplier is already linked to the item.' properties: message: type: string example: 'This supplier is already linked to the item.' errors: type: object properties: contact_id: type: array example: - 'This supplier is already linked to the item.' items: type: string tags: - Inventory requestBody: required: true content: application/json: schema: type: object properties: contact_id: type: integer description: 'Supplier or customer-and-supplier contact.' example: 17 supplier_sku: type: string description: "The supplier's own code for the item." example: MC-TAP-01 nullable: true purchase_price: type: number description: 'Agreed rate in rupees.' example: 110.5 nullable: true minimum_order_quantity: type: number description: "Minimum order in the item's unit." example: 12.0 nullable: true lead_time_days: type: integer description: 'Days from order to delivery, 0-365.' example: 7 nullable: true is_preferred: type: boolean description: "Use this supplier's rate when no supplier is chosen." example: true notes: type: string description: 'Free-text notes, up to 1000 characters.' example: 'Delivers on Tuesdays.' nullable: true required: - contact_id parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: item_id description: 'The ID of the item.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/items/{item_id}/suppliers/{itemSupplier_id}': patch: summary: 'Update the terms agreed with a supplier.' operationId: updateTheTermsAgreedWithASupplier description: 'Only the fields sent change. Send a field as null to clear it.' parameters: [] responses: {} tags: - Inventory requestBody: required: false content: application/json: schema: type: object properties: contact_id: type: integer description: 'Must be at least 1.' example: 16 supplier_sku: type: string description: 'Must not be greater than 64 characters.' example: 'n' nullable: true purchase_price: type: number description: 'Agreed rate in rupees.' example: 108.0 nullable: true minimum_order_quantity: type: number description: 'Must be at least 0. Must not be greater than 99999999999.' example: 16 nullable: true lead_time_days: type: integer description: 'Days from order to delivery, 0-365.' example: 5 nullable: true is_preferred: type: boolean description: '' example: true notes: type: string description: 'Must not be greater than 1000 characters.' example: i nullable: true delete: summary: 'Unlink a supplier from an item.' operationId: unlinkASupplierFromAnItem description: "Purchase history is kept, so the supplier still appears in the list with `linked` false\nif a purchase invoice shows the item was bought from them." parameters: [] responses: {} tags: - Inventory parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: item_id description: 'The ID of the item.' example: 1 required: true schema: type: integer - in: path name: itemSupplier_id description: 'The ID of the itemSupplier.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/warehouses': get: summary: 'List warehouses.' operationId: listWarehouses description: 'Default first, then by name, including inactive ones.' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Inventory post: summary: 'Create a warehouse.' operationId: createAWarehouse description: 'Marking it default takes the flag off the current default.' parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'Another warehouse already has this name.' errors: name: - 'Another warehouse already has this name.' properties: message: type: string example: 'Another warehouse already has this name.' errors: type: object properties: name: type: array example: - 'Another warehouse already has this name.' items: type: string tags: - Inventory requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Unique within the business, up to 100 characters.' example: 'Sanitary godown' location: type: string description: 'Short location, up to 150 characters.' example: 'Gomaty Nagar' nullable: true address: type: string description: 'Optional address, up to 1000 characters.' example: '12 Station Road, Lucknow' nullable: true is_default: type: boolean description: 'Make this the default warehouse.' example: false is_active: type: boolean description: '' example: false required: - name parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/warehouses/{id}': put: summary: 'Update a warehouse.' operationId: updateAWarehouse description: "Only the fields sent change. The default warehouse cannot be deactivated or un-defaulted\ndirectly: make another warehouse the default instead. A warehouse holding stock cannot be\ndeactivated until its stock is transferred out." parameters: [] responses: {} tags: - Inventory requestBody: required: false content: application/json: schema: type: object properties: name: type: string description: 'Must not be greater than 100 characters.' example: b location: type: string description: 'Must not be greater than 150 characters.' example: 'n' nullable: true address: type: string description: 'Must not be greater than 1000 characters.' example: g nullable: true is_default: type: boolean description: '' example: false is_active: type: boolean description: '' example: false delete: summary: 'Delete a warehouse.' operationId: deleteAWarehouse description: "Only a warehouse that is not the default and never held stock can be deleted; otherwise\ndeactivate it." parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'This warehouse has stock history. Deactivate it instead.' errors: warehouse: - 'This warehouse has stock history. Deactivate it instead.' properties: message: type: string example: 'This warehouse has stock history. Deactivate it instead.' errors: type: object properties: warehouse: type: array example: - 'This warehouse has stock history. Deactivate it instead.' items: type: string tags: - Inventory parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: id description: 'The ID of the warehouse.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/items/{item_id}/stock-transfers': post: summary: 'Transfer stock between warehouses.' operationId: transferStockBetweenWarehouses description: "Moves stock of one item from one warehouse to another. It records a `transfer_out` and a\n`transfer_in` movement in the item ledger; the item's total stock, cost, and accounts are\nunchanged. The source warehouse must hold the quantity." parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'Main warehouse holds only 2 PCS of Tap.' errors: quantity: - 'Main warehouse holds only 2 PCS of Tap.' properties: message: type: string example: 'Main warehouse holds only 2 PCS of Tap.' errors: type: object properties: quantity: type: array example: - 'Main warehouse holds only 2 PCS of Tap.' items: type: string tags: - Inventory requestBody: required: true content: application/json: schema: type: object properties: from_warehouse_id: type: integer description: 'Active warehouse the stock leaves.' example: 1 to_warehouse_id: type: integer description: 'A different active warehouse.' example: 2 quantity: type: number description: 'More than 0, up to three decimals.' example: 5.0 transferred_on: type: date description: 'Defaults to today; cannot be in the future.' example: '2026-09-29' nullable: true note: type: string description: 'Up to 255 characters.' example: 'Moved for the weekend sale' nullable: true item_batch_id: type: integer description: 'For a batch-tracked item, the one lot to move. Left out, lots in the source warehouse move earliest expiry first.' example: 3 nullable: true serial_numbers: type: array description: 'For a serial-tracked item, the units to move, one per unit of quantity. Left out, the oldest units in the source warehouse move.' example: - IMEI-001 items: type: string required: - from_warehouse_id - to_warehouse_id - quantity parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: item_id description: 'The ID of the item.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/items/{item_id}/batches': get: summary: "List an item's batches." operationId: listAnItemsBatches description: "Ordered first-expiry-first-out, so the first sellable row is the lot the counter should\npre-select. Expired lots are listed — the shop still has to see and clear them — but\ncarry `is_expired`, and they can never be billed." parameters: - in: query name: in_stock description: 'Only batches still holding stock.' example: true required: false schema: type: boolean description: 'Only batches still holding stock.' example: true - in: query name: sellable description: 'Only batches that can be billed today: in stock, not expired, not on hold or recalled, and — when the business blocks short expiry — clear of the minimum shelf life.' example: true required: false schema: type: boolean description: 'Only batches that can be billed today: in stock, not expired, not on hold or recalled, and — when the business blocks short expiry — clear of the minimum shelf life.' example: true responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Inventory requestBody: required: false content: application/json: schema: type: object properties: in_stock: type: boolean description: '' example: false nullable: true sellable: type: boolean description: '' example: false nullable: true post: summary: 'Open a batch, optionally with the stock already on the shelf.' operationId: openABatchOptionallyWithTheStockAlreadyOnTheShelf description: "Batches are usually created implicitly by a purchase invoice. This is for opening stock\nand for a lot the shop is recording by hand. A positive `quantity` records an opening stock\nmovement and its inventory journal, exactly as opening stock on the item master does." parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'Turn on batch tracking for this item first.' errors: item_id: - 'Turn on batch tracking for this item first.' properties: message: type: string example: 'Turn on batch tracking for this item first.' errors: type: object properties: item_id: type: array example: - 'Turn on batch tracking for this item first.' items: type: string tags: - Inventory requestBody: required: true content: application/json: schema: type: object properties: warehouse_id: type: integer description: 'Must be at least 1.' example: 16 nullable: true batch_number: type: string description: 'Must not be greater than 64 characters.' example: 'n' expiry_date: type: string description: 'Must be a valid date.' example: '2026-01-15' manufactured_on: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true mrp: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 7 nullable: true purchase_price: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 16 nullable: true quantity: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 17 nullable: true required: - batch_number parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: item_id description: 'The ID of the item.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/items/{item_id}/batches/{itemBatch_id}': patch: summary: 'Correct a batch, and adjust the stock it holds.' operationId: correctABatchAndAdjustTheStockItHolds description: "A changed `quantity` records an inventory adjustment for the difference, the same journal an\nitem-master stock correction records, so the books follow the shelf.\n\n`status` pulls the lot from sale or puts it back. `on_hold` and `recalled` both stop it being\nbilled — on invoices, POS, and delivery challans — while the stock stays on the shelf and in\nthe books; a `status_reason` is required for either and is shown to whoever tries to sell\nthe lot. `available` releases it. Sales returns into the lot and purchase returns out of it\nstill work, so recalled stock can go back to the supplier." parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'Say why this batch is being held, so the counter knows.' errors: status_reason: - 'Say why this batch is being held, so the counter knows.' properties: message: type: string example: 'Say why this batch is being held, so the counter knows.' errors: type: object properties: status_reason: type: array example: - 'Say why this batch is being held, so the counter knows.' items: type: string tags: - Inventory requestBody: required: false content: application/json: schema: type: object properties: warehouse_id: type: integer description: 'Must be at least 1.' example: 16 nullable: true batch_number: type: string description: 'Must not be greater than 64 characters.' example: 'n' expiry_date: type: string description: 'Must be a valid date.' example: '2026-01-15' manufactured_on: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true mrp: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 7 nullable: true purchase_price: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 16 nullable: true quantity: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 17 status: type: string description: '`available`, `on_hold`, or `recalled`.' example: on_hold status_reason: type: string description: 'Why the lot is held or recalled, up to 255 characters. Required with `on_hold` or `recalled`.' example: 'Customer complaint, checking the carton' nullable: true delete: summary: 'Remove an empty batch.' operationId: removeAnEmptyBatch description: "A lot that ever held stock is part of the audit trail, so only an empty batch with no\nmovements against it can be deleted. Everything else stays and simply reads as zero." parameters: [] responses: {} tags: - Inventory parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: item_id description: 'The ID of the item.' example: 1 required: true schema: type: integer - in: path name: itemBatch_id description: 'The ID of the itemBatch.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/items/{item_id}/batches/{itemBatch_id}/write-offs': post: summary: 'Write off stock from a batch.' operationId: writeOffStockFromABatch description: "For goods leaving without a sale — expired, damaged, or recalled stock being destroyed. The\nquantity leaves the lot and the item, a `write_off` stock movement is recorded, and the cost\nat the item's weighted average moves from Inventory to the Stock written off expense\naccount. Any lot may be written off, including an expired, held, or recalled one. It cannot\nbe undone; stock found again is added back with a batch quantity correction." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: batch: id: 18 batch_number: AMX-2291 quantity: 0 status: available sale_state: empty is_sellable: false write_off: quantity: 4 value_paise: 32000 reason: expired note: null stock_movement_id: 311 occurred_on: '2026-09-28' properties: data: type: object properties: batch: type: object properties: id: type: integer example: 18 batch_number: type: string example: AMX-2291 quantity: type: integer example: 0 status: type: string example: available sale_state: type: string example: empty is_sellable: type: boolean example: false description: 'The lot after the write-off, in the same shape as the batch list.' write_off: type: object properties: quantity: type: integer example: 4 description: 'Quantity removed.' value_paise: type: integer example: 32000 description: 'Loss booked at cost, in paise.' reason: type: string example: expired description: 'The reason code sent.' note: type: string example: null nullable: true stock_movement_id: type: integer example: 311 description: 'The `write_off` stock movement recorded.' occurred_on: type: string example: '2026-09-28' 422: description: '' content: application/json: schema: type: object example: message: 'Batch AMX-2291 holds only 4 — you cannot write off more than that.' errors: quantity: - 'Batch AMX-2291 holds only 4 — you cannot write off more than that.' properties: message: type: string example: 'Batch AMX-2291 holds only 4 — you cannot write off more than that.' errors: type: object properties: quantity: type: array example: - 'Batch AMX-2291 holds only 4 — you cannot write off more than that.' items: type: string tags: - Inventory requestBody: required: true content: application/json: schema: type: object properties: quantity: type: number description: 'Quantity to remove, no more than the lot holds.' example: 4.0 reason: type: string description: 'Why: `expired`, `damaged`, `recalled`, or `other`.' example: expired note: type: string description: 'Free text kept with the movement, up to 255 characters.' example: "Destroyed with the chemists' association" nullable: true required: - quantity - reason parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: item_id description: 'The ID of the item.' example: 1 required: true schema: type: integer - in: path name: itemBatch_id description: 'The ID of the itemBatch.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/items/{item_id}/serials': get: summary: "List an item's units." operationId: listAnItemsUnits description: "Ordered oldest first, so the shelf clears in the order it filled and the first row is a\nsensible default pick. Sold and written-off units are listed too — the shop still has to\nsee them — but carry `on_shelf: false` and can never be billed." parameters: - in: query name: on_shelf description: 'Only units that can be billed: in stock or returned.' example: true required: false schema: type: boolean description: 'Only units that can be billed: in stock or returned.' example: true - in: query name: status description: 'One exact status: in_stock, sold, returned, or void.' example: sold required: false schema: type: string description: 'One exact status: in_stock, sold, returned, or void.' example: sold - in: query name: q description: 'Match part of a serial number.' example: '3567' required: false schema: type: string description: 'Match part of a serial number.' example: '3567' responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Inventory requestBody: required: false content: application/json: schema: type: object properties: on_shelf: type: boolean description: '' example: false nullable: true status: type: string description: '' example: null nullable: true q: type: string description: 'Must not be greater than 64 characters.' example: b nullable: true post: summary: 'Record units the shop is already holding.' operationId: recordUnitsTheShopIsAlreadyHolding description: "Units normally arrive on a purchase invoice, which captures the serial off each box. This\nis for opening stock and for a unit being recorded by hand. Every serial listed records an\nopening stock movement and its inventory journal, exactly as opening stock does." parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'Turn on serial tracking for this item first.' errors: item_id: - 'Turn on serial tracking for this item first.' properties: message: type: string example: 'Turn on serial tracking for this item first.' errors: type: object properties: item_id: type: array example: - 'Turn on serial tracking for this item first.' items: type: string tags: - Inventory requestBody: required: true content: application/json: schema: type: object properties: warehouse_id: type: integer description: 'Must be at least 1.' example: 16 nullable: true serial_numbers: type: array description: 'One per unit, up to 100 at a time.' example: - '356938035643809' - '356938035643810' items: type: string warranty_until: type: date description: 'Cover end date applied to every unit in this call.' example: '2027-09-13' nullable: true received_on: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true required: - serial_numbers parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: item_id description: 'The ID of the item.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/item-serials': get: summary: 'Search every unit in the catalogue.' operationId: searchEveryUnitInTheCatalogue description: "This is the lookup the counter actually uses: a customer walks in with a handset and the\nshop needs to know whether it sold it, to whom, when, and whether cover still runs.\nSerial history and item names remain available after the item is soft deleted." parameters: - in: query name: q description: 'Part or all of a serial number.' example: '356938' required: false schema: type: string description: 'Part or all of a serial number.' example: '356938' - in: query name: item_id description: 'Narrow to one item.' example: 12 required: false schema: type: integer description: 'Narrow to one item.' example: 12 - in: query name: status description: 'One exact status: in_stock, sold, returned, or void.' example: sold required: false schema: type: string description: 'One exact status: in_stock, sold, returned, or void.' example: sold responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Inventory requestBody: required: false content: application/json: schema: type: object properties: q: type: string description: 'Must not be greater than 64 characters.' example: b nullable: true item_id: type: integer description: '' example: 16 nullable: true status: type: string description: '' example: null nullable: true parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/item-serials/{serial}': get: summary: 'One unit by its exact serial number.' operationId: oneUnitByItsExactSerialNumber description: "Exact match only, ignoring case and surrounding space — a scanner that silently picks the\nnearest handset is worse than one that says it does not know this unit. A miss answers\n`404` carrying the code back, so the app can offer to record it.\nSerial history and item names remain available after the item is soft deleted." parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 404: description: '' content: application/json: schema: type: object example: message: 'No unit is recorded with this serial number.' serial_number: '356938035643809' properties: message: type: string example: 'No unit is recorded with this serial number.' serial_number: type: string example: '356938035643809' tags: - Inventory parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: serial description: 'The serial number as scanned.' example: '356938035643809' required: true schema: type: string '/api/v1/businesses/{business}/items/{item_id}/serials/{itemSerial_id}': patch: summary: 'Correct one unit.' operationId: correctOneUnit description: "The serial number and its warranty date can be corrected — a mistyped IMEI is a real and\ncommon problem. Where the unit *is* cannot: that follows the documents it moved on, and a\nstatus typed by hand would put the shelf and the books out of step." parameters: [] responses: {} tags: - Inventory requestBody: required: false content: application/json: schema: type: object properties: serial_number: type: string description: 'Must not be greater than 64 characters.' example: b warranty_until: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true received_on: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true delete: summary: 'Remove a unit that was never sold.' operationId: removeAUnitThatWasNeverSold description: "A unit that has been billed is part of the audit trail and stays, however it was later\nreturned. A unit on the shelf can be removed — a serial typed twice, or a box that turned\nout to be empty — and doing so takes it out of stock with an adjustment, not silently." parameters: [] responses: {} tags: - Inventory parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: item_id description: 'The ID of the item.' example: 1 required: true schema: type: integer - in: path name: itemSerial_id description: 'The ID of the itemSerial.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/items/by-barcode/{code}': get: summary: 'One item by its exact barcode.' operationId: oneItemByItsExactBarcode description: "The catalogue search is a guess machine: it matches part of a name, part of an SKU, and\nhands back a list. That is right for a person typing \"parle\" and wrong for a scanner. An\napp that silently bills the first of three near-matches costs the shop a wrong bill and a\nstock count that stops tallying, and nobody notices for a month — so this answers with one\nitem or with nothing, and never guesses.\n\nMatching ignores surrounding space and reads a GS1 code the way GS1 does, so the 12-digit\nUPC-A a camera reports and the 13-digit EAN-13 printed on the same pack find the same\nitem. A miss answers 404 carrying the code back, so the app can offer to create an item\nwith it rather than make the shopkeeper retype thirteen digits off a crumpled packet.\n\nLots and units ride along exactly as they do in the POS feed, because a batched or\nserialised item cannot be billed without naming which one leaves, and the counter has to\nwork without signal." parameters: - in: query name: contact description: 'Customer id. Adds `party_price`, the rate that party pays under its rate card.' example: 42 required: false schema: type: integer description: 'Customer id. Adds `party_price`, the rate that party pays under its rate card.' example: 42 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 404: description: '' content: application/json: schema: type: object example: message: 'No item is recorded with this barcode.' barcode: '8901234567890' properties: message: type: string example: 'No item is recorded with this barcode.' barcode: type: string example: '8901234567890' tags: - Inventory requestBody: required: false content: application/json: schema: type: object properties: contact: type: integer description: 'Must be at least 1.' example: 16 nullable: true parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: code description: 'The barcode as scanned.' example: '8901234567890' required: true schema: type: string '/api/v1/businesses/{business}/items/by-qr': get: summary: 'One item by the QR sticker the shop printed for it.' operationId: oneItemByTheQRStickerTheShopPrintedForIt description: "The companion to scanning a barcode, for the half of the shelf a barcode can never cover.\nA barcode belongs to whoever made the pack, so loose rice, re-packed dal and the shop's\nown mixture have no code to scan and never will; a QR is the shop's own label for its own\ngoods, so Dukanam mints it and the counter can scan it like anything else.\n\nSend the payload exactly as the scanner read it. A Dukanam sticker encodes a URL, so that\nis usually what arrives, but the bare token is accepted too — a client should never have\nto take a URL apart to scan a label. A payload that is not one of ours is a miss, not an\nerror, and is never guessed at: like the barcode endpoint, this answers with one item or\nwith nothing.\n\nThe code travels as a query parameter, not in the path, and that is the whole reason this\nendpoint is shaped differently from `items/by-barcode/{code}`. A barcode is usually digits\nand only rarely holds a slash; a QR payload is a URL and holds slashes every time, and an\nencoded slash in a path segment is refused or quietly collapsed by a fair number of\nproxies. Percent-encode the value as you would any query parameter.\n\nA token is minted per item and never reissued, because once a sticker is on a shelf it is\nout of our hands and a rotated token would strand every label already stuck down. Tokens\nare unique platform-wide but resolved within the workspace in the URL, so one shop's\nsticker cannot read an item out of another shop.\n\nLots and units ride along exactly as they do for a barcode scan, because a batched or\nserialised item cannot be billed without naming which one leaves." parameters: - in: query name: code description: 'The QR payload as scanned — the whole URL, or just the token.' example: 'https://dukanam.com/q/7Fq2bXm9KdLp3RtVw8ZaCe' required: true schema: type: string description: 'The QR payload as scanned — the whole URL, or just the token.' example: 'https://dukanam.com/q/7Fq2bXm9KdLp3RtVw8ZaCe' - in: query name: contact description: 'Customer id. Adds `party_price`, the rate that party pays under its rate card.' example: 42 required: false schema: type: integer description: 'Customer id. Adds `party_price`, the rate that party pays under its rate card.' example: 42 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 404: description: '' content: application/json: schema: type: object example: message: 'No item is recorded with this QR code.' properties: message: type: string example: 'No item is recorded with this QR code.' tags: - Inventory requestBody: required: true content: application/json: schema: type: object properties: code: type: string description: 'Must not be greater than 2048 characters.' example: b contact: type: integer description: 'Must be at least 1.' example: 22 nullable: true required: - code parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/items/barcode-labels': get: summary: 'Barcode labels for one item or for many.' operationId: barcodeLabelsForOneItemOrForMany description: "Two shapes of the same job. `GET items/{item}/barcode-labels` prints one item's sticker —\nwhat an app offers from an item screen after assigning a barcode. `GET items/barcode-labels?items[]=`\nprints a run of them, which is what a shop actually does: a delivery arrives and forty\nlines need shelf labels before opening. Both are this one endpoint, so a client that\nlearns the options once can print either.\n\n`format=json`, the default, returns the code as an inline SVG per item together with the\nchosen stationery's measurements, so an app can lay the sheet out itself or send one\nlabel to a Bluetooth label printer. `format=pdf` returns the whole sheet already laid out\non the chosen stationery, for handing to the platform print service.\n\n`code_kind` chooses what the sticker carries, and the two are not interchangeable. A\nbarcode reprints a number somebody else issued for a pack they made, so an item that\nholds no barcode cannot have one invented for it and is skipped. A QR carries a token\nDukanam mints for the item, so it labels the loose rice, the re-packed dal and everything\nelse no manufacturer ever numbered — a QR run skips nothing. Scan them back with\n`items/by-barcode/{code}` and `items/by-qr?code=` respectively.\n\nCopies are per item, because a delivery is never uniform: `copies[12]=6&copies[19]=2`\nprints six of one line and two of another. A single `copies=10` prints ten of everything.\nItems not named in the map print once.\n\nAn item that cannot be labelled is never silently dropped from the run. It comes back in\n`meta.skipped` with a reason, so a shopkeeper counting stickers against a delivery note is\ntold which two are missing and why, rather than finding out at the shelf." parameters: - in: query name: items description: 'Item ids to label. Required on the collection route, ignored when the URL already names an item. Maximum 200.' example: - 12 - 19 required: false schema: type: array description: 'Item ids to label. Required on the collection route, ignored when the URL already names an item. Maximum 200.' example: - 12 - 19 items: type: integer - in: query name: copies description: 'Copies of every label, 1-100. Send `copies[]` instead to set a count per item.' example: 10 required: false schema: type: integer description: 'Copies of every label, 1-100. Send `copies[]` instead to set a count per item.' example: 10 - in: query name: layout description: 'Stationery to lay the sheet out on: a4-65, a4-40, a4-24, roll-50x25, roll-38x25 or custom (one sticker per page). Defaults to a4-65.' example: a4-65 required: false schema: type: string description: 'Stationery to lay the sheet out on: a4-65, a4-40, a4-24, roll-50x25, roll-38x25 or custom (one sticker per page). Defaults to a4-65.' example: a4-65 - in: query name: width_mm description: 'Required for layout=custom. Sticker and page width in millimetres, 25-200. Ignored for presets.' example: 50.0 required: false schema: type: number description: 'Required for layout=custom. Sticker and page width in millimetres, 25-200. Ignored for presets.' example: 50.0 - in: query name: height_mm description: 'Required for layout=custom. Sticker and page height in millimetres, 20-200. Must fit selected text plus a QR of at least 14mm or bars of at least 5mm; otherwise returns 422. Ignored for presets.' example: 30.0 required: false schema: type: number description: 'Required for layout=custom. Sticker and page height in millimetres, 20-200. Must fit selected text plus a QR of at least 14mm or bars of at least 5mm; otherwise returns 422. Ignored for presets.' example: 30.0 - in: query name: code_kind description: '`barcode` to reprint the code the item already carries, or `qr` to print a Dukanam QR sticker. Defaults to barcode. A barcode run skips items holding no code; a QR run skips nothing, because the token is minted for the item.' example: qr required: false schema: type: string description: '`barcode` to reprint the code the item already carries, or `qr` to print a Dukanam QR sticker. Defaults to barcode. A barcode run skips items holding no code; a QR run skips nothing, because the token is minted for the item.' example: qr - in: query name: show_name description: 'Print the item name. Defaults to true.' example: true required: false schema: type: boolean description: 'Print the item name. Defaults to true.' example: true - in: query name: show_price description: 'Print the selling price. Defaults to true.' example: true required: false schema: type: boolean description: 'Print the selling price. Defaults to true.' example: true - in: query name: show_mrp description: 'Print the MRP when the item carries one. Defaults to false.' example: false required: false schema: type: boolean description: 'Print the MRP when the item carries one. Defaults to false.' example: false - in: query name: show_sku description: 'Print the SKU. Defaults to false.' example: false required: false schema: type: boolean description: 'Print the SKU. Defaults to false.' example: false - in: query name: show_business description: 'Print the shop name. Defaults to false.' example: false required: false schema: type: boolean description: 'Print the shop name. Defaults to false.' example: false - in: query name: format description: '`json` for per-item SVG, or `pdf` for the laid-out sheet. Defaults to json.' example: json required: false schema: type: string description: '`json` for per-item SVG, or `pdf` for the laid-out sheet. Defaults to json.' example: json responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 422: description: '' content: application/json: schema: type: object example: message: 'That would print 720 labels. Print at most 500 at a time.' errors: copies: - 'That would print 720 labels. Print at most 500 at a time.' properties: message: type: string example: 'That would print 720 labels. Print at most 500 at a time.' errors: type: object properties: copies: type: array example: - 'That would print 720 labels. Print at most 500 at a time.' items: type: string tags: - Inventory requestBody: required: true content: application/json: schema: type: object properties: items: type: array description: 'Must be at least 1.' example: - 16 items: type: integer copies: type: array description: 'Must be at least 1. Must not be greater than 100.' example: - 22 items: type: integer layout: type: string description: '' example: a4-65 enum: - a4-65 - a4-40 - a4-24 - roll-50x25 - roll-38x25 - custom nullable: true width_mm: type: number description: 'Must be between 25 and 200.' example: 25 height_mm: type: number description: 'Must be between 20 and 200.' example: 21 code_kind: type: string description: '' example: barcode enum: - barcode - qr nullable: true show_business: type: boolean description: '' example: false nullable: true show_name: type: boolean description: '' example: false nullable: true show_sku: type: boolean description: '' example: false nullable: true show_price: type: boolean description: '' example: false nullable: true show_mrp: type: boolean description: '' example: false nullable: true format: type: string description: '' example: json enum: - json - pdf nullable: true search: type: string description: 'Must not be greater than 128 characters.' example: m nullable: true required: - width_mm - height_mm parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/items/{item_id}/barcode-labels': get: summary: 'Barcode labels for one item or for many.' operationId: barcodeLabelsForOneItemOrForMany description: "Two shapes of the same job. `GET items/{item}/barcode-labels` prints one item's sticker —\nwhat an app offers from an item screen after assigning a barcode. `GET items/barcode-labels?items[]=`\nprints a run of them, which is what a shop actually does: a delivery arrives and forty\nlines need shelf labels before opening. Both are this one endpoint, so a client that\nlearns the options once can print either.\n\n`format=json`, the default, returns the code as an inline SVG per item together with the\nchosen stationery's measurements, so an app can lay the sheet out itself or send one\nlabel to a Bluetooth label printer. `format=pdf` returns the whole sheet already laid out\non the chosen stationery, for handing to the platform print service.\n\n`code_kind` chooses what the sticker carries, and the two are not interchangeable. A\nbarcode reprints a number somebody else issued for a pack they made, so an item that\nholds no barcode cannot have one invented for it and is skipped. A QR carries a token\nDukanam mints for the item, so it labels the loose rice, the re-packed dal and everything\nelse no manufacturer ever numbered — a QR run skips nothing. Scan them back with\n`items/by-barcode/{code}` and `items/by-qr?code=` respectively.\n\nCopies are per item, because a delivery is never uniform: `copies[12]=6&copies[19]=2`\nprints six of one line and two of another. A single `copies=10` prints ten of everything.\nItems not named in the map print once.\n\nAn item that cannot be labelled is never silently dropped from the run. It comes back in\n`meta.skipped` with a reason, so a shopkeeper counting stickers against a delivery note is\ntold which two are missing and why, rather than finding out at the shelf." parameters: - in: query name: items description: 'Item ids to label. Required on the collection route, ignored when the URL already names an item. Maximum 200.' example: - 12 - 19 required: false schema: type: array description: 'Item ids to label. Required on the collection route, ignored when the URL already names an item. Maximum 200.' example: - 12 - 19 items: type: integer - in: query name: copies description: 'Copies of every label, 1-100. Send `copies[]` instead to set a count per item.' example: 10 required: false schema: type: integer description: 'Copies of every label, 1-100. Send `copies[]` instead to set a count per item.' example: 10 - in: query name: layout description: 'Stationery to lay the sheet out on: a4-65, a4-40, a4-24, roll-50x25, roll-38x25 or custom (one sticker per page). Defaults to a4-65.' example: a4-65 required: false schema: type: string description: 'Stationery to lay the sheet out on: a4-65, a4-40, a4-24, roll-50x25, roll-38x25 or custom (one sticker per page). Defaults to a4-65.' example: a4-65 - in: query name: width_mm description: 'Required for layout=custom. Sticker and page width in millimetres, 25-200. Ignored for presets.' example: 50.0 required: false schema: type: number description: 'Required for layout=custom. Sticker and page width in millimetres, 25-200. Ignored for presets.' example: 50.0 - in: query name: height_mm description: 'Required for layout=custom. Sticker and page height in millimetres, 20-200. Must fit selected text plus a QR of at least 14mm or bars of at least 5mm; otherwise returns 422. Ignored for presets.' example: 30.0 required: false schema: type: number description: 'Required for layout=custom. Sticker and page height in millimetres, 20-200. Must fit selected text plus a QR of at least 14mm or bars of at least 5mm; otherwise returns 422. Ignored for presets.' example: 30.0 - in: query name: code_kind description: '`barcode` to reprint the code the item already carries, or `qr` to print a Dukanam QR sticker. Defaults to barcode. A barcode run skips items holding no code; a QR run skips nothing, because the token is minted for the item.' example: qr required: false schema: type: string description: '`barcode` to reprint the code the item already carries, or `qr` to print a Dukanam QR sticker. Defaults to barcode. A barcode run skips items holding no code; a QR run skips nothing, because the token is minted for the item.' example: qr - in: query name: show_name description: 'Print the item name. Defaults to true.' example: true required: false schema: type: boolean description: 'Print the item name. Defaults to true.' example: true - in: query name: show_price description: 'Print the selling price. Defaults to true.' example: true required: false schema: type: boolean description: 'Print the selling price. Defaults to true.' example: true - in: query name: show_mrp description: 'Print the MRP when the item carries one. Defaults to false.' example: false required: false schema: type: boolean description: 'Print the MRP when the item carries one. Defaults to false.' example: false - in: query name: show_sku description: 'Print the SKU. Defaults to false.' example: false required: false schema: type: boolean description: 'Print the SKU. Defaults to false.' example: false - in: query name: show_business description: 'Print the shop name. Defaults to false.' example: false required: false schema: type: boolean description: 'Print the shop name. Defaults to false.' example: false - in: query name: format description: '`json` for per-item SVG, or `pdf` for the laid-out sheet. Defaults to json.' example: json required: false schema: type: string description: '`json` for per-item SVG, or `pdf` for the laid-out sheet. Defaults to json.' example: json responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 422: description: '' content: application/json: schema: type: object example: message: 'That would print 720 labels. Print at most 500 at a time.' errors: copies: - 'That would print 720 labels. Print at most 500 at a time.' properties: message: type: string example: 'That would print 720 labels. Print at most 500 at a time.' errors: type: object properties: copies: type: array example: - 'That would print 720 labels. Print at most 500 at a time.' items: type: string tags: - Inventory requestBody: required: true content: application/json: schema: type: object properties: items: type: array description: 'Must be at least 1.' example: - 16 items: type: integer copies: type: array description: 'Must be at least 1. Must not be greater than 100.' example: - 22 items: type: integer layout: type: string description: '' example: a4-65 enum: - a4-65 - a4-40 - a4-24 - roll-50x25 - roll-38x25 - custom nullable: true width_mm: type: number description: 'Must be between 25 and 200.' example: 25 height_mm: type: number description: 'Must be between 20 and 200.' example: 21 code_kind: type: string description: '' example: barcode enum: - barcode - qr nullable: true show_business: type: boolean description: '' example: false nullable: true show_name: type: boolean description: '' example: false nullable: true show_sku: type: boolean description: '' example: false nullable: true show_price: type: boolean description: '' example: false nullable: true show_mrp: type: boolean description: '' example: false nullable: true format: type: string description: '' example: json enum: - json - pdf nullable: true search: type: string description: 'Must not be greater than 128 characters.' example: m nullable: true required: - width_mm - height_mm parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: item_id description: 'The ID of the item.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/items/{item_id}/active': patch: summary: 'Take an item out of circulation, or put it back.' operationId: takeAnItemOutOfCirculationOrPutItBack description: "Its own endpoint rather than a field on update, which reconciles the submitted quantity\nagainst the shelf and can record a stock adjustment and a journal with it. Retiring a line is\na one-word decision and must not be able to move stock. An inactive item keeps its stock and\nevery document it already sits on; it is simply no longer offered at the counter or when\nraising a new document." parameters: [] responses: {} tags: - Inventory requestBody: required: true content: application/json: schema: type: object properties: is_active: type: boolean description: 'Whether the shop still offers this item.' example: false required: - is_active parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: item_id description: 'The ID of the item.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/items/organisation': patch: summary: 'Assign classifications to multiple items atomically.' operationId: assignClassificationsToMultipleItemsAtomically description: "Inventory permission required. Omitted fields stay unchanged; null clears that field.\nChanging category clears the old subcategory unless a matching new subcategory is supplied." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: updated: 2 properties: data: type: object properties: updated: type: integer example: 2 tags: - Inventory requestBody: required: true content: application/json: schema: type: object properties: item_ids: type: array description: 'Up to 100 distinct item IDs owned by this workspace.' example: - 1 - 2 items: type: integer category_id: type: integer description: 'Category ID, or null to clear it.' example: 1 subcategory_id: type: integer description: 'Subcategory belonging to the selected category.' example: 2 brand_id: type: integer description: 'Brand ID, or null to clear it.' example: 3 manufacturer_id: type: integer description: 'Manufacturer ID, or null to clear it.' example: 4 required: - item_ids parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/items': get: summary: 'List items.' operationId: listItems description: 'Item resources include `sale_tax` (enabled, tax_rate_basis_points, cess_rate_basis_points, price_includes_tax). Use this for new sale previews. Raw catalogue tax metadata is retained for history and future registration; it never authorizes GST collection. Unregistered businesses have sale_tax.enabled=false, zero sale rates and price_includes_tax=false.' parameters: - in: query name: category_id description: 'Filter by a tenant-owned category.' example: 1 required: false schema: type: integer description: 'Filter by a tenant-owned category.' example: 1 - in: query name: subcategory_id description: 'Filter by a tenant-owned subcategory.' example: 2 required: false schema: type: integer description: 'Filter by a tenant-owned subcategory.' example: 2 - in: query name: brand_id description: 'Filter by a tenant-owned brand.' example: 3 required: false schema: type: integer description: 'Filter by a tenant-owned brand.' example: 3 - in: query name: manufacturer_id description: 'Filter by a tenant-owned manufacturer.' example: 4 required: false schema: type: integer description: 'Filter by a tenant-owned manufacturer.' example: 4 - in: query name: gst_rate_id description: 'Filter by a tenant-owned GST percentage master.' example: 1 required: false schema: type: integer description: 'Filter by a tenant-owned GST percentage master.' example: 1 - in: query name: attention description: 'low_stock, expiring (next 30 days), expired, uncategorised or warranty.' example: warranty required: false schema: type: string description: 'low_stock, expiring (next 30 days), expired, uncategorised or warranty.' example: warranty - in: query name: contact description: 'Customer id. Adds `party_price` to every item, showing what that party pays under its rate card.' example: 42 required: false schema: type: integer description: 'Customer id. Adds `party_price` to every item, showing what that party pays under its rate card.' example: 42 - in: query name: supplier description: 'Supplier contact id, or `preferred`. Adds `supplier_price` to every item that supplier has an agreed or last-billed rate for, to open a purchase line at.' example: '17' required: false schema: type: string description: 'Supplier contact id, or `preferred`. Adds `supplier_price` to every item that supplier has an agreed or last-billed rate for, to open a purchase line at.' example: '17' - in: query name: status description: 'Which items to list by state: `all` (the default) lists both, with inactive items last, `active` lists only items the shop still offers, and `inactive` lists only inactive ones. Use `active` when building a picker.' example: active required: false schema: type: string description: 'Which items to list by state: `all` (the default) lists both, with inactive items last, `active` lists only items the shop still offers, and `inactive` lists only inactive ones. Use `active` when building a picker.' example: active responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Inventory requestBody: required: false content: application/json: schema: type: object properties: search: type: string description: 'Must not be greater than 128 characters.' example: b nullable: true low_stock: type: boolean description: '' example: false nullable: true status: type: string description: '' example: all enum: - all - active - inactive nullable: true contact: type: integer description: 'Must be at least 1.' example: 22 nullable: true supplier: type: string description: '' example: null per_page: type: integer description: 'Must be at least 1. Must not be greater than 100.' example: 7 nullable: true post: summary: 'Create an item and record opening inventory.' operationId: createAnItemAndRecordOpeningInventory description: "Tracked opening stock creates a balanced Inventory / Owner equity journal at purchase cost.\nStock and reorder quantities accept up to three decimal places.\nOptional category_id/subcategory_id/brand_id/manufacturer_id must belong to this tenant\nand have the matching kind. Alternatively use their *_name fields to create or reuse names.\nWarranty duration requires warranty_unit and warranty_provider and is capped at 10 years.\nNew/changed brand, manufacturer or both policies require a support name and at least one\nphone, email or HTTP(S) website. Omitted fields are retained on update; null clears optional fields.\nShop/no-warranty policies clear external contact fields. Issued warranties retain contact snapshots.\nGST master selection is authoritative for taxable items. Numeric tax_rate inputs create/reuse a\nbusiness percentage master for compatibility. Omitted rate inputs preserve the item rate on update.\nrequires_expiry needs enabled batch tracking; expiry is stored per physical batch." parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'Your current plan includes up to 5 items. Upgrade to Smart Books to add more.' errors: plan: - 'Your current plan includes up to 5 items. Upgrade to Smart Books to add more.' properties: message: type: string example: 'Your current plan includes up to 5 items. Upgrade to Smart Books to add more.' errors: type: object properties: plan: type: array example: - 'Your current plan includes up to 5 items. Upgrade to Smart Books to add more.' items: type: string tags: - Inventory requestBody: required: true content: multipart/form-data: schema: type: object properties: category_id: type: integer description: 'Must match an existing stored value.' example: 16 nullable: true category_name: type: string description: 'Optional category to create or reuse.' example: Electrical nullable: true subcategory_id: type: integer description: 'Must match an existing stored value.' example: 16 nullable: true subcategory_name: type: string description: 'Optional subcategory under the selected category.' example: 'LED lighting' nullable: true brand_id: type: integer description: 'Must match an existing stored value.' example: 16 nullable: true brand_name: type: string description: 'Optional brand to create or reuse.' example: 'Sample Brand' nullable: true manufacturer_id: type: integer description: 'Must match an existing stored value.' example: 16 nullable: true manufacturer_name: type: string description: 'Optional manufacturer to create or reuse.' example: 'Sample Manufacturer' nullable: true gst_rate_id: type: integer description: 'Optional business GST percentage master. Product tax metadata does not authorize collecting GST; unregistered/composition sales apply zero. Numeric inputs only create percentage masters for Regular GST businesses. Overrides tax_rate for taxable items; cross-business IDs return 422.' example: 1 nullable: true warranty_duration: type: integer description: 'Optional whole-number duration.' example: 6 nullable: true warranty_unit: type: string description: 'days, months or years; required with warranty_duration.' example: months nullable: true warranty_provider: type: string description: 'brand, manufacturer, shop or both; required with warranty_duration.' example: shop nullable: true warranty_terms: type: string description: 'Coverage conditions, at most 4000 characters.' example: 'Replacement for manufacturing defects.' nullable: true warranty_provider_name: type: string description: 'Support name, required for new/changed external warranties.' example: 'Sample Brand Support' nullable: true warranty_provider_phone: type: string description: 'Support phone; at least one phone/email/website is required for external cover.' example: '+91 1800 123 4567' nullable: true warranty_provider_email: type: string description: 'Support email, at most 254 characters.' example: support@example.test nullable: true warranty_provider_website: type: string description: 'HTTP(S) support URL, at most 2048 characters.' example: 'https://example.test/support' nullable: true warranty_provider_address: type: string description: 'Optional service address, at most 1000 characters.' example: 'Example Service Centre, Hyderabad' nullable: true warranty_provider_notes: type: string description: 'Optional claim instructions, at most 2000 characters.' example: 'Keep your invoice and product serial number ready.' nullable: true requires_expiry: type: boolean description: 'Require expiry on received stock lots.' example: false name: type: string description: 'Must not be greater than 128 characters.' example: k sku: type: string description: 'Must not be greater than 64 characters.' example: h nullable: true barcode: type: string description: 'Must not be greater than 64 characters.' example: w nullable: true item_type: type: string description: '' example: goods enum: - goods - service hsn_sac: type: string description: 'Must not be greater than 8 characters.' example: aykcmyuw nullable: true unit: type: string description: 'Must not be greater than 24 characters.' example: pwlvqwrsitcpscql uqc: type: string description: 'Must not be greater than 16 characters.' example: dzsnrwtujwvlxjkl gst_taxability: type: string description: '' example: taxable enum: - taxable - nil_rated - exempt - non_gst sale_price: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 8 purchase_price: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 10 nullable: true mrp: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 3 nullable: true tax_rate: type: number description: 'Must be at least 0. Must not be greater than 100.' example: 14 nullable: true cess_rate: type: number description: 'Optional product cess percentage, from 0 to 100; omitted updates preserve the current item value. Unregistered/composition sales apply zero cess.' example: 0.0 nullable: true stock_quantity: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 4 warehouse_id: type: integer description: 'Must be at least 1.' example: 35 nullable: true reorder_level: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 9 nullable: true is_active: type: boolean description: '' example: false track_inventory: type: boolean description: '' example: false track_batches: type: boolean description: '' example: false min_shelf_life_days: type: integer description: 'Must be at least 0. Must not be greater than 730.' example: 6 nullable: true track_serials: type: boolean description: '' example: false opening_batch_number: type: string description: 'Must not be greater than 64 characters.' example: 'n' nullable: true opening_batch_expiry_date: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true opening_serial_numbers: type: array description: 'Must not be greater than 64 characters.' example: - 'n' items: type: string opening_warranty_until: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true price_includes_tax: type: boolean description: '' example: false photos: type: array description: 'Must be an image. Must not be greater than 5120 kilobytes.' items: type: string format: binary remove_photos: type: array description: '' example: - 16 items: type: integer required: - name - item_type - unit - uqc - gst_taxability - sale_price - stock_quantity parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/items/{id}': get: summary: '' operationId: getApiV1BusinessesBusinessItemsId description: '' parameters: - in: query name: contact description: 'Customer id. Adds `party_price` to the item, showing what that party pays under its rate card.' example: 42 required: false schema: type: integer description: 'Customer id. Adds `party_price` to the item, showing what that party pays under its rate card.' example: 42 - in: query name: supplier description: 'Supplier contact id, or `preferred`. Adds `supplier_price`, the rate to open a purchase line at.' example: '17' required: false schema: type: string description: 'Supplier contact id, or `preferred`. Adds `supplier_price`, the rate to open a purchase line at.' example: '17' responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Inventory requestBody: required: false content: application/json: schema: type: object properties: contact: type: integer description: 'Must be at least 1.' example: 16 nullable: true supplier: type: string description: '' example: null put: summary: 'Update an item and account for stock corrections.' operationId: updateAnItemAndAccountForStockCorrections description: "Quantity increases record inventory-adjustment income; decreases record an operating expense.\nStock and reorder quantities accept up to three decimal places." parameters: [] responses: {} tags: - Inventory requestBody: required: true content: multipart/form-data: schema: type: object properties: category_id: type: integer description: 'Must match an existing stored value.' example: 16 nullable: true category_name: type: string description: 'Must not be greater than 100 characters.' example: 'n' nullable: true subcategory_id: type: integer description: 'Must match an existing stored value.' example: 16 nullable: true subcategory_name: type: string description: 'Must not be greater than 100 characters.' example: 'n' nullable: true brand_id: type: integer description: 'Must match an existing stored value.' example: 16 nullable: true brand_name: type: string description: 'Must not be greater than 100 characters.' example: 'n' nullable: true manufacturer_id: type: integer description: 'Must match an existing stored value.' example: 16 nullable: true manufacturer_name: type: string description: 'Must not be greater than 100 characters.' example: 'n' nullable: true gst_rate_id: type: integer description: 'Must match an existing stored value.' example: 16 nullable: true warranty_duration: type: integer description: 'Must be at least 1. Must not be greater than 3650.' example: 22 nullable: true warranty_unit: type: string description: 'This field is required when warranty_duration is present.' example: days enum: - days - months - years nullable: true warranty_provider: type: string description: 'This field is required when warranty_duration is present.' example: brand enum: - brand - manufacturer - shop - both nullable: true warranty_terms: type: string description: 'Must not be greater than 4000 characters.' example: g nullable: true warranty_provider_name: type: string description: 'Must not be greater than 160 characters.' example: z nullable: true warranty_provider_phone: type: string description: 'Must match the regex /^[+0-9][0-9\s().\-xX#]{4,49}$/. Must not be greater than 50 characters.' example: m nullable: true warranty_provider_email: type: string description: 'Must be a valid email address. Must not be greater than 254 characters.' example: gulgowski.asia@example.com nullable: true warranty_provider_website: type: string description: 'Must be a valid URL. Must not be greater than 2048 characters.' example: j nullable: true warranty_provider_address: type: string description: 'Must not be greater than 1000 characters.' example: 'n' nullable: true warranty_provider_notes: type: string description: 'Must not be greater than 2000 characters.' example: i nullable: true requires_expiry: type: boolean description: '' example: false name: type: string description: 'Must not be greater than 128 characters.' example: k sku: type: string description: 'Must not be greater than 64 characters.' example: h nullable: true barcode: type: string description: 'Must not be greater than 64 characters.' example: w nullable: true item_type: type: string description: '' example: goods enum: - goods - service hsn_sac: type: string description: 'Must not be greater than 8 characters.' example: aykcmyuw nullable: true unit: type: string description: 'Must not be greater than 24 characters.' example: pwlvqwrsitcpscql uqc: type: string description: 'Must not be greater than 16 characters.' example: dzsnrwtujwvlxjkl gst_taxability: type: string description: '' example: taxable enum: - taxable - nil_rated - exempt - non_gst sale_price: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 8 purchase_price: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 10 nullable: true mrp: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 3 nullable: true tax_rate: type: number description: 'Must be at least 0. Must not be greater than 100.' example: 14 nullable: true cess_rate: type: number description: 'Must be at least 0. Must not be greater than 100.' example: 7 nullable: true stock_quantity: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 4 warehouse_id: type: integer description: 'Must be at least 1.' example: 35 nullable: true reorder_level: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 9 nullable: true is_active: type: boolean description: '' example: false track_inventory: type: boolean description: '' example: false track_batches: type: boolean description: '' example: false min_shelf_life_days: type: integer description: 'Must be at least 0. Must not be greater than 730.' example: 6 nullable: true track_serials: type: boolean description: '' example: false opening_batch_number: type: string description: 'Must not be greater than 64 characters.' example: 'n' nullable: true opening_batch_expiry_date: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true opening_serial_numbers: type: array description: 'Must not be greater than 64 characters.' example: - 'n' items: type: string opening_warranty_until: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true price_includes_tax: type: boolean description: '' example: false photos: type: array description: 'Must be an image. Must not be greater than 5120 kilobytes.' items: type: string format: binary remove_photos: type: array description: '' example: - 16 items: type: integer required: - name - item_type - unit - uqc - gst_taxability - sale_price - stock_quantity delete: summary: 'Soft delete an item.' operationId: softDeleteAnItem description: "Items used on documents can be deleted. The item is removed from the catalogue and new\nitem selections; existing document lines, stock history, quantities, and accounting\nentries are retained. Existing documents can keep their deleted items when edited,\nand returns still use the original item. Requires inventory permission.\nSubsequent item reads, updates, or deletions return 404, as do foreign-tenant items." parameters: [] responses: 204: description: '' content: application/json: schema: type: object example: {} properties: {} 403: description: '' content: application/json: schema: type: object example: message: 'Your role does not allow this action.' properties: message: type: string example: 'Your role does not allow this action.' 404: description: '' content: application/json: schema: type: object example: message: 'Not Found' properties: message: type: string example: 'Not Found' tags: - Inventory parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: id description: 'The ID of the item.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/gst-rates': get: summary: 'List business GST percentage masters for item lookups.' operationId: listBusinessGSTPercentageMastersForItemLookups description: "Returns an empty list for unregistered and composition businesses; historical masters remain stored.\nThese are configured percentages, not a tax-advice or statutory-rate catalogue." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 1 percentage: 18 basis_points: 1800 label: 18% properties: data: type: array example: - id: 1 percentage: 18 basis_points: 1800 label: 18% items: type: object properties: id: type: integer example: 1 percentage: type: integer example: 18 basis_points: type: integer example: 1800 label: type: string example: 18% tags: - Inventory post: summary: 'Create or reuse a business GST percentage.' operationId: createOrReuseABusinessGSTPercentage description: 'Regular GST registration is required. Unregistered and composition businesses return 422.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 1 percentage: 18 basis_points: 1800 label: 18% properties: data: type: object properties: id: type: integer example: 1 percentage: type: integer example: 18 basis_points: type: integer example: 1800 label: type: string example: 18% 201: description: '' content: application/json: schema: type: object example: data: id: 1 percentage: 18 basis_points: 1800 label: 18% properties: data: type: object properties: id: type: integer example: 1 percentage: type: integer example: 18 basis_points: type: integer example: 1800 label: type: string example: 18% 422: description: '' content: application/json: schema: type: object example: message: 'Only a business with Regular GST registration can add or edit GST percentages.' errors: percentage: - 'Only a business with Regular GST registration can add or edit GST percentages.' properties: message: type: string example: 'Only a business with Regular GST registration can add or edit GST percentages.' errors: type: object properties: percentage: type: array example: - 'Only a business with Regular GST registration can add or edit GST percentages.' items: type: string tags: - Inventory requestBody: required: true content: application/json: schema: type: object properties: percentage: type: number description: 'Percentage from 0 to 100, at most two decimal places.' example: 18.0 required: - percentage parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/gst-rates/{gstRate_id}': patch: summary: 'Change an unused GST percentage.' operationId: changeAnUnusedGSTPercentage description: "Requires Regular GST registration; unregistered and composition businesses return 422.\nA rate referenced by any item (including deleted items) cannot change. Create a new master instead." parameters: [] responses: 409: description: '' content: application/json: schema: type: object example: message: 'This rate is used by an item. Add a new percentage instead.' properties: message: type: string example: 'This rate is used by an item. Add a new percentage instead.' 422: description: '' content: application/json: schema: type: object example: message: 'Only a business with Regular GST registration can add or edit GST percentages.' errors: percentage: - 'Only a business with Regular GST registration can add or edit GST percentages.' properties: message: type: string example: 'Only a business with Regular GST registration can add or edit GST percentages.' errors: type: object properties: percentage: type: array example: - 'Only a business with Regular GST registration can add or edit GST percentages.' items: type: string tags: - Inventory requestBody: required: true content: application/json: schema: type: object properties: percentage: type: number description: 'Percentage from 0 to 100, at most two decimal places.' example: 12.0 required: - percentage delete: summary: 'Delete an unused GST percentage. Used rates return 409.' operationId: deleteAnUnusedGSTPercentageUsedRatesReturn409 description: 'Requires Regular GST registration; unregistered and composition businesses return 422.' parameters: [] responses: 204: description: '' content: application/json: schema: type: object example: {} properties: {} 409: description: '' content: application/json: schema: type: object example: message: 'This rate is still used by an item.' properties: message: type: string example: 'This rate is still used by an item.' 422: description: '' content: application/json: schema: type: object example: message: 'Only a business with Regular GST registration can add or edit GST percentages.' errors: percentage: - 'Only a business with Regular GST registration can add or edit GST percentages.' properties: message: type: string example: 'Only a business with Regular GST registration can add or edit GST percentages.' errors: type: object properties: percentage: type: array example: - 'Only a business with Regular GST registration can add or edit GST percentages.' items: type: string tags: - Inventory parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: gstRate_id description: 'The ID of the gstRate.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/item-classifications': get: summary: 'List categories, subcategories, brands and manufacturers.' operationId: listCategoriesSubcategoriesBrandsAndManufacturers description: '' parameters: - in: query name: kind description: 'Optional classification kind.' example: category required: false schema: type: string description: 'Optional classification kind.' example: category - in: query name: parent_id description: 'Optional parent category for subcategory lookups; must belong to this business.' example: 1 required: false schema: type: integer description: 'Optional parent category for subcategory lookups; must belong to this business.' example: 1 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Inventory requestBody: required: false content: application/json: schema: type: object properties: kind: type: string description: '' example: null nullable: true parent_id: type: integer description: '' example: 16 nullable: true post: summary: 'Create or reuse a classification.' operationId: createOrReuseAClassification description: 'Names ignore case and repeated spaces. Subcategories require a tenant-owned category parent.' parameters: [] responses: {} tags: - Inventory requestBody: required: true content: application/json: schema: type: object properties: kind: type: string description: 'category, subcategory, brand or manufacturer.' example: category name: type: string description: 'Display name, at most 100 characters.' example: Lighting parent_id: type: integer description: 'Parent category for a subcategory.' example: 1 nullable: true required: - kind - name parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/item-classifications/{itemClassification_id}': patch: summary: 'Rename a classification without changing its hierarchy.' operationId: renameAClassificationWithoutChangingItsHierarchy description: '' parameters: [] responses: {} tags: - Inventory requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'New display name, at most 100 characters.' example: 'Electrical lighting' required: - name delete: summary: 'Delete an unused classification. Used classifications and categories with children return 409.' operationId: deleteAnUnusedClassificationUsedClassificationsAndCategoriesWithChildrenReturn409 description: '' parameters: [] responses: 204: description: '' content: application/json: schema: type: object example: {} properties: {} tags: - Inventory parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: itemClassification_id description: 'The ID of the itemClassification.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/item-warranties': get: summary: 'List warranty records for this workspace.' operationId: listWarrantyRecordsForThisWorkspace description: 'Contact details are frozen when coverage is issued, including after invoice corrections.' parameters: - in: query name: search description: 'Search item, buyer, invoice or serial number.' example: LED required: false schema: type: string description: 'Search item, buyer, invoice or serial number.' example: LED - in: query name: item_id description: 'Filter one item.' example: 1 required: false schema: type: integer description: 'Filter one item.' example: 1 - in: query name: contact_id description: 'Filter one customer.' example: 1 required: false schema: type: integer description: 'Filter one customer.' example: 1 - in: query name: per_page description: 'Page size, 1-100.' example: 25 required: false schema: type: integer description: 'Page size, 1-100.' example: 25 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Inventory requestBody: required: false content: application/json: schema: type: object properties: search: type: string description: 'Must not be greater than 128 characters.' example: b nullable: true item_id: type: integer description: 'Must be at least 1.' example: 22 nullable: true contact_id: type: integer description: 'Must be at least 1.' example: 67 nullable: true per_page: type: integer description: 'Must be at least 1. Must not be greater than 100.' example: 16 nullable: true parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/item-warranties/{itemWarranty_id}': get: summary: 'Read a warranty, including coverage, its private buyer link and issued support contacts.' operationId: readAWarrantyIncludingCoverageItsPrivateBuyerLinkAndIssuedSupportContacts description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Inventory parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: itemWarranty_id description: 'The ID of the itemWarranty.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/item-imports/template': get: summary: 'Download the product import template.' operationId: downloadTheProductImportTemplate description: "The workbook carries the `Products` sheet and header row the upload expects. Row 1 must\nsurvive unedited — a renamed or reordered header is reported as a file-level error.\nOptional product columns include category, subcategory, brand, manufacturer, warranty\nduration/unit/provider/terms and batch/serial/expiry flags. The original 16-column template\nremains supported. Stock batches and Serial units sheets link by SKU; quantities/counts\nmust equal Opening Stock. Each optional stock sheet allows up to 2,000 rows. Required\nexpiry, normalised serial uniqueness, tracked-goods constraints and plan features apply.\nCreate category, subcategory, brand and manufacturer masters first. This business-specific\nworkbook includes a Masters reference sheet and per-item dropdowns for existing records.\nSubcategory uses a plain saved-master list, without dependent INDIRECT formulas.\nThe Masters sheet shows each subcategory's parent; upload validates that it matches the category.\nSpreadsheet apps that do not preserve dropdowns can copy the saved names from Masters.\nRegular GST businesses also receive\nGST treatment choices and existing GST percentage masters; other businesses have tax columns hidden.\nDownload again after changing masters. Organisation columns may be left blank and assigned later." parameters: [] responses: 200: description: 'The product import template workbook.' content: application/octet-stream: schema: type: string format: binary tags: - Inventory parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/item-imports': post: summary: 'Upload a product file and stage it for review.' operationId: uploadAProductFileAndStageItForReview description: "Creates no items. The file is stored, checksummed, and validated row by row.\nImports never create masters. Nonblank classification names must match existing records\nin this business (case and repeated spaces are ignored); a subcategory must match its category.\nTaxable GST percentages supplied by a Regular GST business must match existing GST masters.\nBlank organisation/rate cells remain unassigned. Unknown records produce row validation errors.\nOptional warranty units are days/months/years and providers brand/manufacturer/shop/both.\nExternal policies need Warranty provider name and at least one Warranty provider phone/email/website.\nOptional Warranty provider address and Warranty provider notes appear on issued slips.\nThe response reports how many rows passed, which rows failed and why, and a sample of the normalized\nrows that would be created. Call the confirm endpoint once `error_count` is zero.\n\nRe-uploading a file this workspace has already uploaded does not stage it a second time:\nthe existing record is returned with `duplicate` set to `true` and HTTP `200`. Unconfirmed\nfiles are revalidated against current masters; imported files remain unchanged." parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'The file must be a file of type: xlsx.' errors: file: - 'The file must be a file of type: xlsx.' properties: message: type: string example: 'The file must be a file of type: xlsx.' errors: type: object properties: file: type: array example: - 'The file must be a file of type: xlsx.' items: type: string tags: - Inventory requestBody: required: true content: multipart/form-data: schema: type: object properties: file: type: string format: binary description: 'The product workbook, `.xlsx`, up to 5 MB and 2,000 rows.' required: - file parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/item-imports/{itemImport_id}': get: summary: 'Read a staged import.' operationId: readAStagedImport description: 'Returns the current status, the row counts, every validation error, and the preview sample.' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Inventory delete: summary: 'Discard a staged import.' operationId: discardAStagedImport description: "Deletes the stored file and its staged rows. An import that has already created items\ncannot be discarded — its checksum is what stops the same file being imported twice." parameters: [] responses: 409: description: '' content: application/json: schema: type: object example: message: 'An imported file cannot be discarded.' properties: message: type: string example: 'An imported file cannot be discarded.' tags: - Inventory parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: itemImport_id description: 'The ID of the itemImport.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/item-imports/{itemImport_id}/confirm': post: summary: 'Create every validated row.' operationId: createEveryValidatedRow description: "All rows are created inside one transaction, so the catalogue is either wholly imported or\nuntouched. Rows carrying opening stock also record an opening stock movement and its\ninventory journal, exactly as the setup wizard does.\n\nThe workbook is checked again before writing. Conflicts introduced since preview (including\nSKUs or barcodes reserved by deleted items) return `422` under `file` and create nothing.\nMalformed numeric and boolean values are rejected instead of being converted to zero or false.\n\nExisting classification and GST masters are revalidated at confirmation; renamed/deleted\nmasters return 422 under file. No masters are created during preview or confirmation.\nUnregistered/composition imports retain product tax metadata without creating new masters;\nthat metadata never authorizes collecting GST on new sales." parameters: [] responses: 409: description: '' content: application/json: schema: oneOf: - description: '' type: object example: message: 'This file has already been imported.' properties: message: type: string example: 'This file has already been imported.' - description: '' type: object example: message: 'This file has not passed validation.' properties: message: type: string example: 'This file has not passed validation.' 422: description: '' content: application/json: schema: type: object example: message: 'Your current plan includes up to 5 items. Upgrade to Smart Books to add more.' errors: plan: - 'Your current plan includes up to 5 items. Upgrade to Smart Books to add more.' properties: message: type: string example: 'Your current plan includes up to 5 items. Upgrade to Smart Books to add more.' errors: type: object properties: plan: type: array example: - 'Your current plan includes up to 5 items. Upgrade to Smart Books to add more.' items: type: string tags: - Inventory parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: itemImport_id description: 'The ID of the itemImport.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/invoices/{invoice_id}/warranty.pdf': get: summary: 'Download the warranty slip PDF for an invoice.' operationId: downloadTheWarrantySlipPDFForAnInvoice description: 'Requires sales permission. Includes current warranty records, their terms and buyer tracking links.' parameters: [] responses: 200: description: '' content: text/plain: schema: type: string example: 'binary application/pdf' tags: - Inventory parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: invoice_id description: 'The ID of the invoice.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/invoices/{invoice_id}/warranty/send': post: summary: "Send or resend the invoice and warranty slip to the buyer's primary email." operationId: sendOrResendTheInvoiceAndWarrantySlipToTheBuyersPrimaryEmail description: "Requires sales permission, configured mail and a valid customer email. Queued or sending\ndeliveries are reused. Explicit resends may duplicate a message already accepted by mail." parameters: [] responses: 202: description: '' content: application/json: schema: type: object example: data: status: queued sent_at: null properties: data: type: object properties: status: type: string example: queued sent_at: type: string example: null nullable: true tags: - Inventory parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: invoice_id description: 'The ID of the invoice.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/invoices': get: summary: 'List saved sales invoices.' operationId: listSavedSalesInvoices description: 'Resources include `show_gst_details`, based on the registration frozen at issue. When false, hide GSTINs, HSN/tax columns, place of supply, reverse charge, GST amounts and tax summaries in invoice and document UI. New unregistered documents force GST/cess to zero and tax-inclusive/reverse-charge flags false. Previously issued registered documents keep their original tax treatment.' parameters: - in: query name: status description: 'Filter by status. saved aliases saved.' example: saved required: false schema: type: string description: 'Filter by status. saved aliases saved.' example: saved - in: query name: outstanding description: 'Only non-void invoices with a positive balance after payments and returns. Filtered before pagination.' example: true required: false schema: type: boolean description: 'Only non-void invoices with a positive balance after payments and returns. Filtered before pagination.' example: true responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Sales invoices' requestBody: required: false content: application/json: schema: type: object properties: status: type: string description: '' example: saved enum: - saved - posted - partially_paid - paid - void - partially_returned - returned nullable: true outstanding: type: boolean description: '' example: false nullable: true contact_id: type: integer description: '' example: 16 nullable: true from: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true to: type: string description: 'Must be a valid date. Must be a date after or equal to from.' example: '2026-01-15' nullable: true search: type: string description: 'Must not be greater than 128 characters.' example: 'n' nullable: true per_page: type: integer description: 'Must be at least 1. Must not be greater than 100.' example: 7 nullable: true post: summary: 'Save one or many sales invoices.' operationId: saveOneOrManySalesInvoices description: "Send a single invoice payload, or wrap up to 25 of them in an `invoices` array to save a batch in one atomic request. A batch either saves completely or not at all, and returns its invoices in request order under `data` with `meta.count`.\n\nMonthly invoice allowances count all invoices created in the application-calendar month, including trial\nand voided invoices. A downgrade does not reset usage. At the limit, new invoices return HTTP 422\nagainst `plan` with an upgrade/reset message. Existing idempotency keys can still be retried at the limit.\nA batch that exceeds the remaining allowance is rolled back completely. Existing invoices remain accessible.\n\nA customer carrying a credit limit is refused with HTTP 422 against `contact_id` when the invoice would take what it owes past that limit. A batch is weighed cumulatively: each entry sees the exposure the earlier entries created.\n\nGST heads follow the place of supply: CGST and SGST (UTGST in Chandigarh, Dadra and Nagar Haveli and Daman and Diu, Lakshadweep, Andaman and Nicobar Islands and Ladakh) when it is the business's own state, otherwise IGST. A customer with `gst_treatment` `sez` is always charged IGST, and an overseas customer defaults to place of supply `96`." parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'The selected record was not found for this business.' errors: invoices.1.contact_id: - 'The selected record was not found for this business.' properties: message: type: string example: 'The selected record was not found for this business.' errors: type: object properties: invoices.1.contact_id: type: array example: - 'The selected record was not found for this business.' items: type: string tags: - 'Sales invoices' requestBody: required: true content: application/json: schema: type: object properties: line_layout: type: array description: 'Optional display rows. Empty or null uses the line array order. On update, omission retains the layout when the line count is unchanged.' example: - type: header title: 'Kitchen essentials' - type: item line_index: 0 - type: subtotal items: type: object properties: type: type: string description: 'item, header, or subtotal.' example: item line_index: type: integer description: 'Zero-based index in lines, required for item rows. Every line must appear exactly once.' example: 0 nullable: true title: type: string description: 'Heading required for header rows, maximum 100 characters.' example: 'Kitchen essentials' nullable: true required: - type show_section_totals: type: boolean description: 'Show calculated header totals on document output; defaults true.' example: true workers: type: array description: 'Optional complete multi-worker assignment set with commission_kind, fixed_amount_paise or rate_basis_points, and owner-entered attribution_basis_points totalling 10000. Requires Advanced Plan and owner/admin. Earnings commit atomically with the invoice.' example: null items: type: object properties: worker_id: type: integer description: 'Must be at least 1.' example: 66 commission_kind: type: string description: '' example: percentage enum: - percentage - fixed rate_basis_points: type: integer description: 'Must be between 0 and 10000.' example: 0 nullable: true fixed_amount_paise: type: integer description: 'Must be between 0 and 99999999900.' example: 0 nullable: true attribution_basis_points: type: integer description: 'Must be between 1 and 10000.' example: 1 required: - worker_id - commission_kind - attribution_basis_points worker_attribution_basis_points: type: integer description: 'Required with worker_id; owner-entered single-worker share must be 10000.' example: null nullable: true worker_id: type: integer description: 'Optional referral worker. Advanced Plan and owner/administrator required; commission is earned atomically when this sale is saved.' example: null nullable: true worker_rate_basis_points: type: integer description: 'Optional rate override from 0 to 10000; omitted uses the worker default. Earnings use discounted value before tax, with automatic return debit notes.' example: null nullable: true contact_id: type: integer description: '' example: 16 issue_date: type: string description: 'Must be a valid date.' example: '2026-01-15' due_date: type: string description: 'Must be a valid date. Must be a date after or equal to issue_date.' example: '2026-01-15' nullable: true notes: type: string description: 'Must not be greater than 2000 characters.' example: 'n' nullable: true place_of_supply_state_code: type: string description: 'Two-digit GST state code, or 96 for an export. 99 is refused. Defaults from the customer, then the business.' example: '29' nullable: true reverse_charge: type: boolean description: '' example: false prices_include_tax: type: boolean description: '' example: false channel: type: string description: '' example: backoffice enum: - backoffice - pos nullable: true idempotency_key: type: string description: 'Must be a valid UUID.' example: 6b72fe4a-5b40-307c-bc24-f79acf9a1bb9 nullable: true lines: type: array description: 'Must have at least 1 items. Must not have more than 50 items.' example: - [] items: type: object properties: details: type: string description: 'Optional additional description, maximum 2000 characters.' example: 'Deliver in sealed packs.' nullable: true item_id: type: integer description: '' example: 16 nullable: true warehouse_id: type: integer description: 'Must be at least 1.' example: 22 nullable: true item_batch_id: type: integer description: '' example: 16 nullable: true serial_numbers: type: array description: 'Must not be greater than 64 characters.' example: - 'n' items: type: string description: type: string description: 'Must not be greater than 255 characters.' example: 'Animi quos velit et fugiat.' hsn_sac: type: string description: "HSN (4, 6 or 8 digits) or SAC (6 digits) for this line. Dots and spaces are stripped. Defaults to the item's code on an item line." example: '100630' nullable: true quantity: type: number description: 'Must not be greater than 999999.' example: 18 unit_price: type: number description: 'This field is required when lines.*.item_id is not present. Must be at least 0. Must not be greater than 999999999.' example: 22 nullable: true tax_rate: type: number description: 'Must be at least 0. Must not be greater than 100.' example: 24 nullable: true cess_rate: type: number description: 'Must be at least 0. Must not be greater than 100.' example: 18 nullable: true discount: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 8 nullable: true price_includes_tax: type: boolean description: '' example: false required: - description - quantity required: - contact_id - issue_date - lines parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/invoices/payments': post: summary: 'Record one or many customer payments.' operationId: recordOneOrManyCustomerPayments description: 'Post a single payment to `invoices/{invoice}/payments`, or send up to 25 payments — each naming its own `invoice_id` — as a `payments` array to `invoices/payments` to settle several invoices in one atomic request.' parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'Enter an amount up to the current outstanding balance.' errors: payments.1.amount: - 'Enter an amount up to the current outstanding balance.' properties: message: type: string example: 'Enter an amount up to the current outstanding balance.' errors: type: object properties: payments.1.amount: type: array example: - 'Enter an amount up to the current outstanding balance.' items: type: string tags: - 'Sales invoices' requestBody: required: true content: application/json: schema: type: object properties: payments: type: array description: 'Must have at least 1 items. Must not have more than 25 items.' example: - [] items: type: object properties: invoice_id: type: integer description: '' example: 16 amount: type: number description: 'Must not be greater than 999999999.' example: 22 method: type: string description: '' example: cash enum: - cash - bank - upi - card - cheque - other payment_account_id: type: integer description: '' example: 16 paid_on: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true reference: type: string description: 'Must not be greater than 64 characters.' example: 'n' nullable: true notes: type: string description: 'Must not be greater than 1000 characters.' example: g nullable: true required: - invoice_id - amount - method - payment_account_id required: - payments parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/invoices/void': post: summary: 'Void one or many unpaid invoices that have no returns.' operationId: voidOneOrManyUnpaidInvoicesThatHaveNoReturns description: "Invoices with a payment or a sales return must be settled through the corresponding payment or return workflow and cannot be voided.\n\nVoid a single invoice at `invoices/{invoice}/void`, or send up to 25 entries — each naming its own `invoice_id` — as an `invoices` array to `invoices/void`. A batch may carry one shared request-level `reason` instead of repeating it per entry, and either voids completely or not at all." parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'Only unpaid, active invoices without returns can be voided.' errors: invoice: - 'Only unpaid, active invoices without returns can be voided.' properties: message: type: string example: 'Only unpaid, active invoices without returns can be voided.' errors: type: object properties: invoice: type: array example: - 'Only unpaid, active invoices without returns can be voided.' items: type: string tags: - 'Sales invoices' requestBody: required: true content: application/json: schema: type: object properties: invoices: type: array description: 'Must have at least 1 items. Must not have more than 25 items.' example: - [] items: type: object properties: invoice_id: type: integer description: '' example: 16 reason: type: string description: 'This field is required when reason is not present. Must be at least 3 characters. Must not be greater than 255 characters.' example: 'n' nullable: true required: - invoice_id reason: type: string description: 'Must be at least 3 characters. Must not be greater than 255 characters.' example: b nullable: true required: - invoices parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/invoices/{invoice_id}': get: summary: 'Read an invoice and its change history.' operationId: readAnInvoiceAndItsChangeHistory description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: customer_pdf_url: 'https://dukanam.com/invoices/1/shared?expires=1790000000&signature=example' send: share_url: 'https://dukanam.com/shared/invoices/1?expires=1790000000&signature=example' message: "Hi Ravi Kumar, please find invoice INV-1001 for ₹2,500.00 from Sri Lakshmi Traders. Thank you.\n\nView invoice PDF: https://dukanam.com/shared/invoices/1?expires=1790000000&signature=example" email_subject: 'Invoice INV-1001 from Sri Lakshmi Traders' email_available: true payment_reminder: message: 'Hi Ravi Kumar, a payment of ₹2,500.00 is pending on invoice INV-1001. Pay to Sri Lakshmi Traders: Account number: 001234567890, IFSC: HDFC0000123. View invoice PDF: https://dukanam.com/shared/invoices/1?expires=1790000000&signature=example' email_subject: 'Sri Lakshmi Traders | Payment reminder: INV-1001' recipients: - key: primary name: 'Ravi Kumar' designation: 'Primary contact' phone: '98765 43210' whatsapp_phone: '919876543210' email: ravi@example.test - key: person-12 name: Priya designation: Accounts phone: null whatsapp_phone: null email: priya@example.test properties: data: type: object properties: customer_pdf_url: type: string example: 'https://dukanam.com/invoices/1/shared?expires=1790000000&signature=example' description: 'Signed public invoice link showing the complete document with print and PDF sharing actions. The link expires after 90 days.' send: type: object properties: share_url: type: string example: 'https://dukanam.com/shared/invoices/1?expires=1790000000&signature=example' description: 'Signed public invoice link to include in a WhatsApp or SMS message.' message: type: string example: "Hi Ravi Kumar, please find invoice INV-1001 for ₹2,500.00 from Sri Lakshmi Traders. Thank you.\n\nView invoice PDF: https://dukanam.com/shared/invoices/1?expires=1790000000&signature=example" description: "Suggested message, ending with the public link. Uses the business's invoice sharing template." email_subject: type: string example: 'Invoice INV-1001 from Sri Lakshmi Traders' description: 'Suggested email subject.' email_available: type: boolean example: true description: 'Whether this app can send email; when false, offer WhatsApp and SMS only.' payment_reminder: type: object properties: message: type: string example: 'Hi Ravi Kumar, a payment of ₹2,500.00 is pending on invoice INV-1001. Pay to Sri Lakshmi Traders: Account number: 001234567890, IFSC: HDFC0000123. View invoice PDF: https://dukanam.com/shared/invoices/1?expires=1790000000&signature=example' email_subject: type: string example: 'Sri Lakshmi Traders | Payment reminder: INV-1001' description: 'Suggested customer payment reminder with message and email_subject, including the outstanding amount, due date, visible receiving accounts and invoice link. Null for settled or void invoices. Clients let the shop review and send through the existing email endpoint or WhatsApp/SMS; scheduled push alerts still go to workspace staff.' recipients: type: array example: - key: primary name: 'Ravi Kumar' designation: 'Primary contact' phone: '98765 43210' whatsapp_phone: '919876543210' email: ravi@example.test - key: person-12 name: Priya designation: Accounts phone: null whatsapp_phone: null email: priya@example.test description: 'People at the customer with a phone or email: the primary contact (`key` `primary`) then each additional contact (`key` `person-{id}`), each with `name`, `designation`, `phone`, `whatsapp_phone` (digits with country code, or null) and `email`.' items: type: object properties: key: type: string example: primary name: type: string example: 'Ravi Kumar' designation: type: string example: 'Primary contact' phone: type: string example: '98765 43210' whatsapp_phone: type: string example: '919876543210' email: type: string example: ravi@example.test description: 'What to send and who can receive it. Returned on this endpoint only.' tags: - 'Sales invoices' put: summary: 'Save edits to an invoice at any time.' operationId: saveEditsToAnInvoiceAtAnyTime description: "PUT and PATCH both replace the editable invoice fields and lines in full. Send the latest revision from GET and include each existing line's id to retain it; omit id for a new line. Missing existing lines are removed. A stale revision returns 422 on revision. The invoice keeps its identifier, number, payment records, and return records. All statuses, including paid, returned, and void, remain editable with no time limit; a void invoice remains void.\n\nThe customer cannot change when payments or returns exist. Returned lines cannot be removed, assigned another item, or reduced below their returned quantity or recorded amounts. Reducing the total below payments retains customer credit. Totals, stock and accounting reconcile atomically, and each save records complete before/after snapshots with the actor and timestamp. Accounting corrections retain original entries and dated reversals/replacements; report date ranges honor each entry's date. Original business tax registration and stored line metadata are preserved.\n\nInvoice edits keep the original invoice date unless the owner explicitly changes issue_date. Revision reversals use the previous transaction dates; replacements use the submitted invoice date, restating that period rather than moving the correction to today. The edit timestamp remains in audit history. There is no invoice-age limit on changing the date." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 1 number: INV-001 status: posted status_label: Saved revision: 2 total_paise: 20000 history: - id: 2 action: invoice.updated user: id: 1 name: 'Shop owner' old_values: revision: 1 total_paise: 10000 lines: - id: 1 quantity: '1.000' new_values: revision: 2 total_paise: 20000 lines: - id: 1 quantity: '2.000' reason: null created_at: '2026-09-13T10:00:00.000000Z' properties: data: type: object properties: id: type: integer example: 1 number: type: string example: INV-001 status: type: string example: posted status_label: type: string example: Saved description: 'Display label; the legacy saved status is labelled Saved.' revision: type: integer example: 2 description: 'New revision after the edit is saved.' total_paise: type: integer example: 20000 history: type: array example: - id: 2 action: invoice.updated user: id: 1 name: 'Shop owner' old_values: revision: 1 total_paise: 10000 lines: - id: 1 quantity: '1.000' new_values: revision: 2 total_paise: 20000 lines: - id: 1 quantity: '2.000' reason: null created_at: '2026-09-13T10:00:00.000000Z' description: 'Invoice audit records with id, action, user (id and name), old_values, new_values, reason, and created_at. IP addresses, user agents, and internal audit metadata are omitted.' items: type: object properties: id: type: integer example: 2 action: type: string example: invoice.updated user: type: object properties: id: type: integer example: 1 name: type: string example: 'Shop owner' old_values: type: object properties: revision: type: integer example: 1 total_paise: type: integer example: 10000 lines: type: array example: - id: 1 quantity: '1.000' items: type: object properties: id: { type: integer, example: 1 } quantity: { type: string, example: '1.000' } new_values: type: object properties: revision: type: integer example: 2 total_paise: type: integer example: 20000 lines: type: array example: - id: 1 quantity: '2.000' items: type: object properties: id: { type: integer, example: 1 } quantity: { type: string, example: '2.000' } reason: type: string example: null nullable: true created_at: type: string example: '2026-09-13T10:00:00.000000Z' 422: description: '' content: application/json: schema: type: object example: message: 'This invoice was changed by someone else. Reload it before saving your changes.' errors: revision: - 'This invoice was changed by someone else. Reload it before saving your changes.' properties: message: type: string example: 'This invoice was changed by someone else. Reload it before saving your changes.' errors: type: object properties: revision: type: array example: - 'This invoice was changed by someone else. Reload it before saving your changes.' items: type: string tags: - 'Sales invoices' requestBody: required: true content: application/json: schema: type: object properties: line_layout: type: array description: 'Optional display rows. Empty or null uses the line array order. On update, omission retains the layout when the line count is unchanged.' example: - type: header title: 'Kitchen essentials' - type: item line_index: 0 - type: subtotal items: type: object properties: type: type: string description: 'item, header, or subtotal.' example: item line_index: type: integer description: 'Zero-based index in lines, required for item rows. Every line must appear exactly once.' example: 0 nullable: true title: type: string description: 'Heading required for header rows, maximum 100 characters.' example: 'Kitchen essentials' nullable: true required: - type show_section_totals: type: boolean description: 'Show calculated header totals on document output; defaults true.' example: true workers: type: array description: 'Must not have more than 10 items.' example: null items: type: object properties: worker_id: type: integer description: 'Must be at least 1.' example: 27 commission_kind: type: string description: '' example: percentage enum: - percentage - fixed rate_basis_points: type: integer description: 'Must be between 0 and 10000.' example: 0 nullable: true fixed_amount_paise: type: integer description: 'Must be between 0 and 99999999900.' example: 0 nullable: true attribution_basis_points: type: integer description: 'Must be between 1 and 10000.' example: 2 required: - worker_id - commission_kind - attribution_basis_points worker_attribution_basis_points: type: integer description: 'This field is required when worker_id is present.' example: 10000 enum: - 10000 nullable: true worker_id: type: integer description: 'This field is required when worker_rate_basis_points is present. Must be at least 1.' example: 16 nullable: true worker_rate_basis_points: type: integer description: 'Must be between 0 and 10000.' example: 1 nullable: true contact_id: type: integer description: '' example: 16 issue_date: type: string description: 'Must be a valid date.' example: '2026-01-15' due_date: type: string description: 'Must be a valid date. Must be a date after or equal to issue_date.' example: '2026-01-15' nullable: true notes: type: string description: 'Must not be greater than 2000 characters.' example: 'n' nullable: true place_of_supply_state_code: type: string description: '' example: 1 enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 26 - 27 - 29 - 30 - 31 - 32 - 33 - 34 - 35 - 36 - 37 - 38 - 96 - 97 nullable: true reverse_charge: type: boolean description: '' example: false prices_include_tax: type: boolean description: '' example: false lines: type: array description: 'Must have at least 1 items. Must not have more than 50 items.' example: - [] items: type: object properties: details: type: string description: 'Optional additional description, maximum 2000 characters.' example: 'Deliver in sealed packs.' nullable: true item_id: type: integer description: '' example: 16 nullable: true warehouse_id: type: integer description: 'Must be at least 1.' example: 22 nullable: true item_batch_id: type: integer description: '' example: 16 nullable: true serial_numbers: type: array description: 'Must not be greater than 64 characters.' example: - 'n' items: type: string description: type: string description: 'Must not be greater than 255 characters.' example: 'Animi quos velit et fugiat.' hsn_sac: type: string description: "HSN or SAC for this line; omit to keep the line's current code." example: '100630' nullable: true quantity: type: number description: 'Must not be greater than 999999.' example: 18 unit_price: type: number description: 'This field is required when lines.*.item_id is not present. Must be at least 0. Must not be greater than 999999999.' example: 22 nullable: true tax_rate: type: number description: 'Must be at least 0. Must not be greater than 100.' example: 24 nullable: true cess_rate: type: number description: 'Must be at least 0. Must not be greater than 100.' example: 18 nullable: true discount: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 8 nullable: true price_includes_tax: type: boolean description: '' example: false id: type: integer description: 'Existing line identifier belonging to this invoice; omit for a new line. Values must be distinct.' example: 1 nullable: true required: - description - quantity revision: type: integer description: 'Current invoice revision from GET; prevents overwriting another edit.' example: 1 required: - contact_id - issue_date - lines - revision parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: invoice_id description: 'The ID of the invoice.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/invoices/{invoice_id}/pdf': get: summary: 'Download a sales invoice PDF.' operationId: downloadASalesInvoicePDF description: 'Returns the same tenant-scoped invoice document used by the web sharing flow.' parameters: [] responses: 200: description: 'The generated invoice PDF.' content: application/pdf: schema: type: string format: binary tags: - 'Sales invoices' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: invoice_id description: 'The ID of the invoice.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/invoices/{invoice_id}/receipt': get: summary: 'Get a sales invoice as a thermal printer receipt.' operationId: getASalesInvoiceAsAThermalPrinterReceipt description: "Returns the receipt as ESC/POS printer commands for a 58mm or 80mm thermal printer, so the app can print over Bluetooth, USB or the network with no print dialog. Decode `content_base64` and write the bytes to the printer unchanged. The same bytes are served to the web counter. The supermarket layout includes bill/date/cashier, aligned description/quantity/rate/amount columns, per-line discounts, item/quantity counts, payment methods, savings and a GST-rate summary using stored invoice values. Long descriptions wrap and large amounts retain all digits.\n\nPass `cash_register_id` to use that counter's saved printer settings. The cash drawer opens only when the counter allows it and the sale has a cash payment. A UPI QR prints only while a balance is due, for that balance. Text prints in the printer's built-in ASCII font; the rupee sign prints as `Rs.` and other characters are transliterated.\n\nAvailable to members who can make sales or use the POS." parameters: - in: query name: cash_register_id description: 'Counter the receipt prints at. Its saved printer settings (paper, cut, cash drawer) apply. Omit to use the defaults: 80mm, cut, drawer opens for cash sales. Must match an existing stored value.' example: 1 required: false schema: type: integer description: 'Counter the receipt prints at. Its saved printer settings (paper, cut, cash drawer) apply. Omit to use the defaults: 80mm, cut, drawer opens for cash sales. Must match an existing stored value.' example: 1 nullable: true - in: query name: paper description: "Overrides the counter's paper width: `58mm` (32 characters a line) or `80mm` (48 characters a line)." example: 58mm required: false schema: type: string description: "Overrides the counter's paper width: `58mm` (32 characters a line) or `80mm` (48 characters a line)." example: 58mm enum: - 58mm - 80mm nullable: true - in: query name: cut description: 'Overrides whether the paper is cut at the end: `1` or `0`.' example: false required: false schema: type: boolean description: 'Overrides whether the paper is cut at the end: `1` or `0`.' example: false nullable: true - in: query name: open_drawer description: 'Overrides whether the cash drawer opens: `1` always opens it, `0` never does. When omitted, the drawer opens only if the counter allows it and the sale has a cash payment.' example: false required: false schema: type: boolean description: 'Overrides whether the cash drawer opens: `1` always opens it, `0` never does. When omitted, the drawer opens only if the counter allows it and the sale has a cash payment.' example: false nullable: true responses: 200: description: '' content: application/json: schema: type: object example: data: format: escpos invoice_id: 1 invoice_number: INV-0001 cash_register_id: 1 paper: 80mm characters_per_line: 48 cut: true opens_drawer: true auto_print: false content_base64: G0AbcAAZ+htHYQE... byte_length: 812 text: "Sri Lakshmi Traders\nTAX INVOICE\nBill: INV-0001 28 Sep 2026" properties: data: type: object properties: format: type: string example: escpos description: 'Always `escpos`.' invoice_id: type: integer example: 1 invoice_number: type: string example: INV-0001 cash_register_id: type: integer example: 1 paper: type: string example: 80mm description: 'Paper width used: `58mm` or `80mm`.' characters_per_line: type: integer example: 48 description: '32 on 58mm, 48 on 80mm.' cut: type: boolean example: true description: 'Whether the bytes end with a paper cut.' opens_drawer: type: boolean example: true description: 'Whether the bytes open the cash drawer.' auto_print: type: boolean example: false description: "The counter's setting to print as soon as a sale is saved. False without a counter." content_base64: type: string example: G0AbcAAZ+htHYQE... description: 'ESC/POS bytes, base64-encoded. Send to the printer as-is.' byte_length: type: integer example: 812 description: 'Length of the decoded bytes.' text: type: string example: "Sri Lakshmi Traders\nTAX INVOICE\nBill: INV-0001 28 Sep 2026" description: 'Plain-text copy of what prints, including customer-visible bank/payment instructions, for an on-screen preview.' 422: description: '' content: application/json: schema: type: object example: message: 'Choose 58mm or 80mm paper.' errors: paper: - 'Choose 58mm or 80mm paper.' properties: message: type: string example: 'Choose 58mm or 80mm paper.' errors: type: object properties: paper: type: array example: - 'Choose 58mm or 80mm paper.' items: type: string tags: - 'Sales invoices' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: invoice_id description: 'The ID of the invoice.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/invoices/{invoice_id}/email': post: summary: 'Email an invoice to people at the customer.' operationId: emailAnInvoiceToPeopleAtTheCustomer description: "Sends one email to each chosen person with the invoice PDF attached and a button to the signed public invoice page. Choose people by the `key` values in `send.recipients` from GET `invoices/{invoice}`; each must have an email address. To send on WhatsApp or SMS instead, open the chat on the device with `send.message` and a recipient's `whatsapp_phone` or `phone`. Every email send is recorded in the audit log." parameters: [] responses: 202: description: '' content: application/json: schema: type: object example: message: 'Emailed to Ravi Kumar and Priya.' data: sent_to: - key: primary name: 'Ravi Kumar' email: ravi@example.test - key: person-12 name: Priya email: priya@example.test properties: message: type: string example: 'Emailed to Ravi Kumar and Priya.' data: type: object properties: sent_to: type: array example: - key: primary name: 'Ravi Kumar' email: ravi@example.test - key: person-12 name: Priya email: priya@example.test items: type: object properties: key: type: string example: primary name: type: string example: 'Ravi Kumar' email: type: string example: ravi@example.test 422: description: '' content: application/json: schema: type: object example: message: 'Priya has no email address.' errors: recipients.1: - 'Priya has no email address.' properties: message: type: string example: 'Priya has no email address.' errors: type: object properties: recipients.1: type: array example: - 'Priya has no email address.' items: type: string tags: - 'Sales invoices' requestBody: required: true content: application/json: schema: type: object properties: recipients: type: array description: 'A recipient key: `primary` for the party''s primary contact, or `person-{id}` for an additional contact. Must match the regex /^(primary|person-\d+)$/.' example: - primary - person-12 items: type: string recipient_emails: type: array description: 'Must be a valid email address. Must not be greater than 254 characters.' example: primary: accounts@example.com items: type: string nullable: true subject: type: string description: 'Email subject line. Must not be greater than 150 characters.' example: 'Sri Lakshmi Traders | Purchase order: PO-0007' message: type: string description: 'Message body. Blank lines separate paragraphs. The PDF is attached and a button links to the signed public page. Must not be greater than 2000 characters.' example: 'Hi Ravi, please find purchase order PO-0007 for ₹12,500.00. Please confirm the order and the delivery date.' from: type: string description: 'Ledger statements only: start date, inclusive. Must be a valid date.' example: '2026-04-01' nullable: true to: type: string description: 'Ledger statements only: end date, inclusive. Must be on or after `from`. Must be a valid date. Must be a date after or equal to from.' example: '2027-03-31' nullable: true required: - recipients - subject - message parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: invoice_id description: 'The ID of the invoice.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/invoices/{invoice_id}/payments': post: summary: 'Record one or many customer payments.' operationId: recordOneOrManyCustomerPayments description: 'Post a single payment to `invoices/{invoice}/payments`, or send up to 25 payments — each naming its own `invoice_id` — as a `payments` array to `invoices/payments` to settle several invoices in one atomic request.' parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'Enter an amount up to the current outstanding balance.' errors: payments.1.amount: - 'Enter an amount up to the current outstanding balance.' properties: message: type: string example: 'Enter an amount up to the current outstanding balance.' errors: type: object properties: payments.1.amount: type: array example: - 'Enter an amount up to the current outstanding balance.' items: type: string tags: - 'Sales invoices' requestBody: required: true content: application/json: schema: type: object properties: payments: type: array description: 'Must have at least 1 items. Must not have more than 25 items.' example: - [] items: type: object properties: invoice_id: type: integer description: '' example: 16 amount: type: number description: 'Must not be greater than 999999999.' example: 22 method: type: string description: '' example: cash enum: - cash - bank - upi - card - cheque - other payment_account_id: type: integer description: '' example: 16 paid_on: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true reference: type: string description: 'Must not be greater than 64 characters.' example: 'n' nullable: true notes: type: string description: 'Must not be greater than 1000 characters.' example: g nullable: true required: - invoice_id - amount - method - payment_account_id required: - payments parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: invoice_id description: 'The ID of the invoice.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/invoices/{invoice_id}/void': post: summary: 'Void one or many unpaid invoices that have no returns.' operationId: voidOneOrManyUnpaidInvoicesThatHaveNoReturns description: "Invoices with a payment or a sales return must be settled through the corresponding payment or return workflow and cannot be voided.\n\nVoid a single invoice at `invoices/{invoice}/void`, or send up to 25 entries — each naming its own `invoice_id` — as an `invoices` array to `invoices/void`. A batch may carry one shared request-level `reason` instead of repeating it per entry, and either voids completely or not at all." parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'Only unpaid, active invoices without returns can be voided.' errors: invoice: - 'Only unpaid, active invoices without returns can be voided.' properties: message: type: string example: 'Only unpaid, active invoices without returns can be voided.' errors: type: object properties: invoice: type: array example: - 'Only unpaid, active invoices without returns can be voided.' items: type: string tags: - 'Sales invoices' requestBody: required: true content: application/json: schema: type: object properties: invoices: type: array description: 'Must have at least 1 items. Must not have more than 25 items.' example: - [] items: type: object properties: invoice_id: type: integer description: '' example: 16 reason: type: string description: 'This field is required when reason is not present. Must be at least 3 characters. Must not be greater than 255 characters.' example: 'n' nullable: true required: - invoice_id reason: type: string description: 'Must be at least 3 characters. Must not be greater than 255 characters.' example: b nullable: true required: - invoices parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: invoice_id description: 'The ID of the invoice.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/documents/{document}/recurring-settings': get: summary: 'Get recurring schedule settings and generation history.' operationId: getRecurringScheduleSettingsAndGenerationHistory description: "Requires sales permission and recurring-invoice entitlement. Returns the schedule,\ncustomer recipients (including contacts missing an email) and the latest 25 occurrences.\nOwner notification preference defaults on; a missing owner email is not configured." parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Business documents' put: summary: 'Update a recurring schedule and automatic email preferences.' operationId: updateARecurringScheduleAndAutomaticEmailPreferences description: "Changes affect future invoices only. Start/next date must follow the last generated date.\nMonthly dates stay anchored to their original day, clamped to shorter months. Creation\nuses the business timezone. Customer auto-email requires selected valid emails; missing\naddresses can be saved with recipient_emails. Existing emails cannot be overwritten.\nOwner notifications default on but missing email never blocks invoice creation. Pause\nwith schedule_status=paused, resume with active; set a future next date to skip backlog.\nRequires sales permission and recurring-invoice entitlement; foreign schedules return 404." parameters: [] responses: {} tags: - 'Business documents' requestBody: required: true content: application/json: schema: type: object properties: frequency: type: string description: 'weekly, monthly, quarterly or yearly.' example: monthly next_issue_date: type: date description: 'Next invoice date in business timezone.' example: '2026-10-31' end_date: type: date description: 'Nullable inclusive last date.' example: '2027-10-31' repeat_every: type: integer description: 'Repeat every 1–52 selected periods.' example: 1 due_after_days: type: integer description: 'Payment due 0–365 days after creation date.' example: 7 auto_email: type: boolean description: 'Automatically email generated invoices to selected customer contacts.' example: true email_recipients: type: array description: 'Selected primary or person-ID keys belonging to this customer.' example: - primary items: type: string recipient_emails: type: object description: 'Missing emails to save for selected recipients.' example: primary: accounts@example.com properties: {} notify_owner: type: boolean description: 'Notify owner separately; defaults true.' example: true owner_email: type: string description: 'Nullable notification override, not a change to account/login email.' example: owner@example.com schedule_status: type: string description: 'active or paused.' example: active required: - frequency - next_issue_date parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: document description: 'The document.' example: architecto required: true schema: type: string '/api/v1/businesses/{business}/documents/{document}/recurring-runs/{run}/retry': post: summary: 'Retry failed emails for an existing recurring invoice.' operationId: retryFailedEmailsForAnExistingRecurringInvoice description: "Never creates another invoice or resends recipients already marked sent. Missing-address\nskips require updating the contact and sending that invoice manually. Queue retries are\nautomatic (three attempts); this endpoint restarts exhausted pending/failed delivery.\nRequires sales permission and recurring-invoice entitlement; foreign runs return 404." parameters: [] responses: 202: description: '' content: application/json: schema: type: object example: message: 'Unsent emails queued for retry.' properties: message: type: string example: 'Unsent emails queued for retry.' tags: - 'Business documents' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: document description: 'The document.' example: architecto required: true schema: type: string - in: path name: run description: '' example: architecto required: true schema: type: string '/api/v1/businesses/{business}/documents': get: summary: 'List business documents.' operationId: listBusinessDocuments description: "Saving a bill updates stock and account balances automatically. There is no extra confirmation step.\nDisplay status_label in screens; raw status values are retained for existing integrations." parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Business documents' requestBody: required: true content: application/json: schema: type: object properties: type: type: string description: '' example: quote enum: - quote - purchase_order - purchase_invoice - recurring_invoice - sales_return - purchase_return - delivery_challan status: type: string description: 'Must not be greater than 32 characters.' example: b nullable: true contact_id: type: integer description: '' example: 16 nullable: true per_page: type: integer description: 'Must be at least 1. Must not be greater than 100.' example: 22 nullable: true required: - type parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/documents/{document_type}': post: summary: 'Create a business document.' operationId: createABusinessDocument description: "Document resources include `show_gst_details`, based on the registration frozen at issue. When false, hide GST identifiers, HSN/tax columns, supply and reverse-charge details, GST amounts and tax summaries. Previously issued registered documents keep their original tax treatment.\n\nCreate a business document or recurring invoice schedule.\n\nTax is split by place of supply as on sales invoices: CGST and SGST (or UTGST) within the business's state, IGST across states and for SEZ parties. Responses carry `place_of_supply_state_code`, `supply_type`, `state_tax_label`, the `cgst_total_paise`, `sgst_total_paise`, `igst_total_paise` and `cess_total_paise` totals, and per-line `hsn_sac` and tax heads.\nSales and purchase returns retain the source line's item even after it has been soft deleted,\nincluding its inventory, batch, and serial handling. Deleted items cannot be selected on new documents." parameters: [] responses: {} tags: - 'Business documents' requestBody: required: true content: application/json: schema: type: object properties: line_layout: type: array description: 'Optional display rows. Empty or null uses the line array order. On update, omission retains the layout when the line count is unchanged.' example: - type: header title: 'Kitchen essentials' - type: item line_index: 0 - type: subtotal items: type: object properties: type: type: string description: 'item, header, or subtotal.' example: item line_index: type: integer description: 'Zero-based index in lines, required for item rows. Every line must appear exactly once.' example: 0 nullable: true title: type: string description: 'Heading required for header rows, maximum 100 characters.' example: 'Kitchen essentials' nullable: true required: - type show_section_totals: type: boolean description: 'Show calculated header totals on document output; defaults true.' example: true contact_id: type: integer description: '' example: 16 source_invoice_id: type: integer description: '' example: 16 nullable: true source_document_id: type: integer description: '' example: 16 nullable: true issue_date: type: string description: 'Must be a valid date.' example: '2026-01-15' due_date: type: string description: 'Must be a valid date. Must be a date after or equal to issue_date.' example: '2026-01-15' nullable: true frequency: type: string description: '' example: weekly enum: - weekly - monthly - quarterly - yearly nullable: true next_issue_date: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true end_date: type: string description: 'Must be a valid date. Must be a date after or equal to next_issue_date.' example: '2026-01-15' nullable: true notes: type: string description: 'Must not be greater than 2000 characters.' example: 'n' nullable: true external_reference: type: string description: 'Must not be greater than 64 characters.' example: g nullable: true challan_reason: type: string description: '' example: job_work enum: - job_work - branch_transfer - approval - line_sale - other nullable: true destination_warehouse_id: type: integer description: 'Must be at least 1.' example: 66 nullable: true expected_return_on: type: string description: 'Must be a valid date. Must be a date after or equal to issue_date.' example: '2026-01-15' nullable: true place_of_supply_state_code: type: string description: 'Two-digit GST state code, or 96 for an export. 99 is refused.' example: '29' nullable: true reverse_charge: type: boolean description: '' example: false prices_include_tax: type: boolean description: '' example: false lines: type: array description: 'Must have at least 1 items. Must not have more than 100 items.' example: - [] items: type: object properties: details: type: string description: 'Optional additional description, maximum 2000 characters.' example: 'Deliver in sealed packs.' nullable: true item_id: type: integer description: '' example: 16 nullable: true warehouse_id: type: integer description: 'Must be at least 1.' example: 22 nullable: true item_batch_id: type: integer description: '' example: 16 nullable: true batch_number: type: string description: 'Must not be greater than 64 characters.' example: 'n' nullable: true batch_expiry_date: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true batch_manufactured_on: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true batch_mrp: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 7 nullable: true serial_numbers: type: array description: 'Must not be greater than 64 characters.' example: - z items: type: string warranty_until: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true source_invoice_line_id: type: integer description: '' example: 16 nullable: true source_document_line_id: type: integer description: '' example: 16 nullable: true description: type: string description: 'Must not be greater than 255 characters.' example: 'Et animi quos velit et fugiat.' hsn_sac: type: string description: "HSN (4, 6 or 8 digits) or SAC (6 digits) for this line; defaults to the item's code." example: "998719\n\nRecurring schedules support the options below, with the same validation as the\nrecurring-settings endpoint. Owner notification defaults on; without an owner email,\nhistory remains available and invoice creation continues. Customer auto-email defaults off." nullable: true quantity: type: number description: 'Must not be greater than 999999.' example: 18 unit_price: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 22 tax_rate: type: number description: 'Must be at least 0. Must not be greater than 100.' example: 24 nullable: true cess_rate: type: number description: 'Must be at least 0. Must not be greater than 100.' example: 18 nullable: true discount: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 8 nullable: true price_includes_tax: type: boolean description: '' example: false required: - description - quantity - unit_price repeat_every: type: integer description: 'Recurring only: every 1–52 selected periods.' example: 1 due_after_days: type: integer description: 'Recurring only: 0–365 days after issue.' example: 7 auto_email: type: boolean description: 'Recurring only: automatically email generated invoices.' example: false email_recipients: type: array description: 'Recurring only: primary or person-ID keys on this customer.' example: - primary items: type: string recipient_emails: type: object description: 'Recurring only: missing emails to save for selected contacts.' example: primary: accounts@example.com properties: {} notify_owner: type: boolean description: 'Recurring only: separate owner notification, default true.' example: true owner_email: type: string description: 'Recurring only: nullable notification address; defaults to owner account email.' example: owner@example.com schedule_status: type: string description: 'Recurring only: active or paused.' example: active required: - contact_id - issue_date - lines parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: document_type description: '' example: quote|purchase_order|purchase_invoice|recurring_invoice|sales_return|purchase_return|delivery_challan required: true schema: type: string '/api/v1/businesses/{business}/documents/{document_id}': get: summary: '' operationId: getApiV1BusinessesBusinessDocumentsDocument_id description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Business documents' patch: summary: 'Update a quotation, purchase order, purchase invoice, or delivery challan.' operationId: updateAQuotationPurchaseOrderPurchaseInvoiceOrDeliveryChallan description: "Replaces the party, dates, note, and every line of a quotation, purchase order, or delivery challan that\nhas not been converted yet. A quotation that the customer already answered returns to `open` and its\nrecorded decision is cleared, so the same approval link asks for a fresh decision on the updated figures.\nA delivery challan that had already taken goods off the shelf returns them before the replacement lines\ntake out their own, so stock is never held out twice for one challan.\nPurchase invoices replace stock, supplier ledger, and accounting entries atomically, retaining their number.\nInvoice edits keep the original invoice date unless the owner explicitly changes issue_date. Purchase revision reversals use the previous transaction dates and replacements use the submitted invoice date, restating that period. The audit timestamp records when the edit occurred. There is no invoice-age limit on changing the date; existing stock, return, and payment safeguards still apply.\nReturns, batch or serial tracked goods, and later stock activity prevent purchase invoice edits (422).\nPaid invoices retain payments: the supplier cannot change and the total cannot fall below applied payments.\nThe complete contact_id, issue_date, and lines payload is required; omitted optional fields are cleared.\nItems already on this document can be retained after soft deletion. Other deleted items\nand foreign-tenant items return 404; deletion never bypasses the existing edit restrictions." parameters: [] responses: {} tags: - 'Business documents' requestBody: required: true content: application/json: schema: type: object properties: line_layout: type: array description: 'Optional display rows. Empty or null uses the line array order. On update, omission retains the layout when the line count is unchanged.' example: - type: header title: 'Kitchen essentials' - type: item line_index: 0 - type: subtotal items: type: object properties: type: type: string description: 'item, header, or subtotal.' example: item line_index: type: integer description: 'Zero-based index in lines, required for item rows. Every line must appear exactly once.' example: 0 nullable: true title: type: string description: 'Heading required for header rows, maximum 100 characters.' example: 'Kitchen essentials' nullable: true required: - type show_section_totals: type: boolean description: 'Show calculated header totals on document output; defaults true.' example: true contact_id: type: integer description: '' example: 16 issue_date: type: string description: 'Must be a valid date.' example: '2026-01-15' due_date: type: string description: 'Must be a valid date. Must be a date after or equal to issue_date.' example: '2026-01-15' nullable: true notes: type: string description: 'Must not be greater than 2000 characters.' example: 'n' nullable: true external_reference: type: string description: 'Must not be greater than 64 characters.' example: g nullable: true challan_reason: type: string description: '' example: job_work enum: - job_work - branch_transfer - approval - line_sale - other nullable: true destination_warehouse_id: type: integer description: 'Must be at least 1.' example: 66 nullable: true expected_return_on: type: string description: 'Must be a valid date. Must be a date after or equal to issue_date.' example: '2026-01-15' nullable: true place_of_supply_state_code: type: string description: '' example: 1 enum: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - 23 - 24 - 26 - 27 - 29 - 30 - 31 - 32 - 33 - 34 - 35 - 36 - 37 - 38 - 96 - 97 nullable: true reverse_charge: type: boolean description: '' example: false prices_include_tax: type: boolean description: '' example: false lines: type: array description: 'Must have at least 1 items. Must not have more than 100 items.' example: - [] items: type: object properties: details: type: string description: 'Optional additional description, maximum 2000 characters.' example: 'Deliver in sealed packs.' nullable: true item_id: type: integer description: '' example: 16 nullable: true warehouse_id: type: integer description: 'Must be at least 1.' example: 22 nullable: true item_batch_id: type: integer description: '' example: 16 nullable: true serial_numbers: type: array description: 'Must not be greater than 64 characters.' example: - 'n' items: type: string description: type: string description: 'Must not be greater than 255 characters.' example: 'Animi quos velit et fugiat.' hsn_sac: type: string description: 'Must match the regex /^[0-9]{4}(?:[0-9]{2}){0,2}$/.' example: '5593:14):23)' nullable: true quantity: type: number description: 'Must not be greater than 999999.' example: 18 unit_price: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 22 tax_rate: type: number description: 'Must be at least 0. Must not be greater than 100.' example: 24 nullable: true cess_rate: type: number description: 'Must be at least 0. Must not be greater than 100.' example: 18 nullable: true discount: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 8 nullable: true price_includes_tax: type: boolean description: '' example: false required: - description - quantity - unit_price required: - contact_id - issue_date - lines delete: summary: 'Delete a purchase invoice.' operationId: deleteAPurchaseInvoice description: "Requires purchases permission. Reverses stock, supplier ledger and accounting atomically,\nretaining the document with status `void` for audit. Outstanding becomes zero.\nPayments, applied advances, purchase returns, batch or serial tracked goods, later stock\nactivity, and already voided invoices prevent deletion (422). Other document types return 404.\nAny failed deletion leaves the invoice and all balances unchanged." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Purchase invoice deleted. Audit record retained.' properties: message: type: string example: 'Purchase invoice deleted. Audit record retained.' 422: description: '' content: application/json: schema: type: object example: message: 'This invoice has payments or applied advances and cannot be deleted.' errors: status: - 'This invoice has payments or applied advances and cannot be deleted.' properties: message: type: string example: 'This invoice has payments or applied advances and cannot be deleted.' errors: type: object properties: status: type: array example: - 'This invoice has payments or applied advances and cannot be deleted.' items: type: string tags: - 'Business documents' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: document_id description: 'The ID of the document.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/documents/{document_id}/convert': post: summary: 'Convert a document into the bill it becomes.' operationId: convertADocumentIntoTheBillItBecomes description: "A quotation and a delivery challan both become a tax invoice; a purchase order becomes a purchase\ninvoice. A delivery challan may be converted exactly once: a second attempt answers `422`, leaving the\nfirst invoice as the only bill for those goods. GST on a converted challan is computed at the conversion\ndate, because the price is frequently not settled when the goods leave the shop." parameters: [] responses: {} tags: - 'Business documents' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: document_id description: 'The ID of the document.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/documents/{document_id}/pdf': get: summary: 'Download a document PDF.' operationId: downloadADocumentPDF description: 'The same A4 paper the party receives, for quotations, purchase orders, delivery challans, sales returns and purchase returns. Other document types answer `404`.' parameters: [] responses: 200: description: 'The generated document PDF.' content: application/pdf: schema: type: string format: binary tags: - 'Business documents' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: document_id description: 'The ID of the document.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/documents/{document_id}/email': post: summary: 'Email a document to people at the party.' operationId: emailADocumentToPeopleAtTheParty description: "Sends one email to each chosen person with the document PDF attached and a button to its signed public page. Works for quotations, purchase orders, purchase invoices, recurring invoice schedules, delivery challans, sales returns and purchase returns that are not voided. Delivery is queued through platform Cloudflare email with the shop’s branding. Choose people by the `key` values in `send.recipients` from GET `documents/{document}`; each must have an email address. To send on WhatsApp or SMS instead, open the chat on the device with `send.message` and a recipient's `whatsapp_phone` or `phone`. Every email send is recorded in the audit log." parameters: [] responses: 202: description: '' content: application/json: schema: type: object example: message: 'Emailed to Ravi Kumar.' data: sent_to: - key: primary name: 'Ravi Kumar' email: ravi@example.test properties: message: type: string example: 'Emailed to Ravi Kumar.' data: type: object properties: sent_to: type: array example: - key: primary name: 'Ravi Kumar' email: ravi@example.test items: type: object properties: key: type: string example: primary name: type: string example: 'Ravi Kumar' email: type: string example: ravi@example.test 422: description: '' content: application/json: schema: type: object example: message: 'Choose people listed on this party.' errors: recipients.0: - 'Choose people listed on this party.' properties: message: type: string example: 'Choose people listed on this party.' errors: type: object properties: recipients.0: type: array example: - 'Choose people listed on this party.' items: type: string tags: - 'Business documents' requestBody: required: true content: application/json: schema: type: object properties: recipients: type: array description: 'A recipient key: `primary` for the party''s primary contact, or `person-{id}` for an additional contact. Must match the regex /^(primary|person-\d+)$/.' example: - primary - person-12 items: type: string recipient_emails: type: array description: 'Must be a valid email address. Must not be greater than 254 characters.' example: primary: accounts@example.com items: type: string nullable: true subject: type: string description: 'Email subject line. Must not be greater than 150 characters.' example: 'Sri Lakshmi Traders | Purchase order: PO-0007' message: type: string description: 'Message body. Blank lines separate paragraphs. The PDF is attached and a button links to the signed public page. Must not be greater than 2000 characters.' example: 'Hi Ravi, please find purchase order PO-0007 for ₹12,500.00. Please confirm the order and the delivery date.' from: type: string description: 'Ledger statements only: start date, inclusive. Must be a valid date.' example: '2026-04-01' nullable: true to: type: string description: 'Ledger statements only: end date, inclusive. Must be on or after `from`. Must be a valid date. Must be a date after or equal to from.' example: '2027-03-31' nullable: true required: - recipients - subject - message parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: document_id description: 'The ID of the document.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business_id}/documents/{document_id}/attachments': post: summary: 'Attach a supplier invoice.' operationId: attachASupplierInvoice description: "Upload one PDF, JPEG or PNG using multipart/form-data. Maximum 10 MB per file and\n10 attachments per purchase invoice. Requires purchases permission. Files are private\nto this business. Voided invoices are read-only (422); other document types return 404.\nAttachments do not change stock, balances or the invoice's accounting entries." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: id: 1 name: supplier-invoice.pdf mime_type: application/pdf size_bytes: 2048 created_at: '2026-09-28T05:00:00.000000Z' download_url: 'https://dukanam.com/api/v1/businesses/1/documents/1/attachments/1' properties: data: type: object properties: id: type: integer example: 1 name: type: string example: supplier-invoice.pdf mime_type: type: string example: application/pdf size_bytes: type: integer example: 2048 created_at: type: string example: '2026-09-28T05:00:00.000000Z' download_url: type: string example: 'https://dukanam.com/api/v1/businesses/1/documents/1/attachments/1' 422: description: '' content: application/json: schema: type: object example: message: 'A purchase invoice can have up to 10 attachments.' errors: file: - 'A purchase invoice can have up to 10 attachments.' properties: message: type: string example: 'A purchase invoice can have up to 10 attachments.' errors: type: object properties: file: type: array example: - 'A purchase invoice can have up to 10 attachments.' items: type: string tags: - 'Business documents' requestBody: required: true content: multipart/form-data: schema: type: object properties: file: type: string format: binary description: 'Supplier invoice PDF, JPEG or PNG, up to 10240 KB.' required: - file parameters: - in: path name: business_id description: 'The ID of the business.' example: 1 required: true schema: type: integer - in: path name: document_id description: 'The ID of the document.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business_id}/documents/{document_id}/attachments/{attachment_id}': get: summary: 'Download an attached supplier invoice.' operationId: downloadAnAttachedSupplierInvoice description: "Requires authentication and purchases permission in the owning business, including when\nfollowing download_url. Available on voided invoices for audit. Missing files, foreign-tenant\ninvoices and attachments belonging to a different invoice return 404. No public storage URL is exposed." parameters: - in: query name: preview description: 'Use 1 to display the PDF or image inline instead of downloading it. Authentication and tenant checks still apply.' example: true required: false schema: type: boolean description: 'Use 1 to display the PDF or image inline instead of downloading it. Authentication and tenant checks still apply.' example: true responses: 200: description: 'The original supplier invoice file, inline when preview=1 or downloaded otherwise.' content: application/octet-stream: schema: type: string format: binary tags: - 'Business documents' requestBody: required: false content: application/json: schema: type: object properties: preview: type: boolean description: '' example: false delete: summary: 'Remove an attached supplier invoice.' operationId: removeAnAttachedSupplierInvoice description: "Requires purchases permission. Removes the attachment without changing the invoice or\nits balances. Voided invoices are read-only (422). Tenant and invoice mismatches return 404." parameters: [] responses: 204: description: '' content: application/json: schema: type: object example: {} properties: {} tags: - 'Business documents' parameters: - in: path name: business_id description: 'The ID of the business.' example: 1 required: true schema: type: integer - in: path name: document_id description: 'The ID of the document.' example: 1 required: true schema: type: integer - in: path name: attachment_id description: 'The ID of the attachment.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/payments': get: summary: 'List payments.' operationId: listPayments description: "Payments received from customers or made to suppliers, newest first. Advances — money taken\nor paid before there was a bill — appear here too, with `is_advance` set and the part not\nyet put towards a bill in `unapplied_paise`." parameters: - in: query name: direction description: '`received` for customer payments, `made` for supplier payments.' example: received required: true schema: type: string description: '`received` for customer payments, `made` for supplier payments.' example: received - in: query name: from description: 'Earliest payment date.' example: '2026-09-01' required: false schema: type: string description: 'Earliest payment date.' example: '2026-09-01' - in: query name: to description: 'Latest payment date.' example: '2026-09-30' required: false schema: type: string description: 'Latest payment date.' example: '2026-09-30' - in: query name: contact_id description: "Only this customer's or supplier's payments." example: 42 required: false schema: type: integer description: "Only this customer's or supplier's payments." example: 42 - in: query name: is_advance description: '`1` for advances only, `0` to leave advances out.' example: true required: false schema: type: boolean description: '`1` for advances only, `0` to leave advances out.' example: true - in: query name: unapplied description: '`1` for advances that still have money left to apply.' example: true required: false schema: type: boolean description: '`1` for advances that still have money left to apply.' example: true - in: query name: per_page description: 'Page size, up to 100.' example: 20 required: false schema: type: integer description: 'Page size, up to 100.' example: 20 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Payments requestBody: required: true content: application/json: schema: type: object properties: direction: type: string description: '' example: received enum: - received - made from: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true to: type: string description: 'Must be a valid date. Must be a date after or equal to from.' example: '2026-01-15' nullable: true contact_id: type: integer description: '' example: 16 nullable: true is_advance: type: boolean description: '' example: false nullable: true unapplied: type: boolean description: '' example: false nullable: true per_page: type: integer description: 'Must be at least 1. Must not be greater than 100.' example: 22 nullable: true required: - direction post: summary: 'Record a payment.' operationId: recordAPayment description: "Settles a bill, or — with `is_advance` — records money taken from a customer or paid to a\nsupplier before any bill exists. An advance moves the account balance, the cash register,\nthe party's khata and the books the day it is recorded, so the party carries a credit\nbalance until a bill takes it up through `POST /payments/{payment}/apply`." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: id: 9 direction: received is_advance: true amount_paise: 500000 applied_paise: 0 unapplied_paise: 500000 paid_on: '2026-09-25' method: upi invoice_id: null business_document_id: null properties: data: type: object properties: id: type: integer example: 9 direction: type: string example: received is_advance: type: boolean example: true description: 'Whether this is an advance.' amount_paise: type: integer example: 500000 applied_paise: type: integer example: 0 unapplied_paise: type: integer example: 500000 description: 'How much of an advance is still available to apply.' paid_on: type: string example: '2026-09-25' method: type: string example: upi invoice_id: type: string example: null nullable: true business_document_id: type: string example: null nullable: true 422: description: '' content: application/json: schema: type: object example: message: 'Choose the customer who paid this advance.' errors: contact_id: - 'Choose the customer who paid this advance.' properties: message: type: string example: 'Choose the customer who paid this advance.' errors: type: object properties: contact_id: type: array example: - 'Choose the customer who paid this advance.' items: type: string tags: - Payments requestBody: required: true content: application/json: schema: type: object properties: is_advance: type: boolean description: 'Record an advance against the party rather than a payment against a bill.' example: false nullable: true contact_id: type: integer description: 'The customer (received) or supplier (made) who paid or was paid the advance. Required when `is_advance` is true; ignored otherwise.' example: 42 nullable: true invoice_id: type: integer description: 'The sales invoice being settled. Required for a `received` payment that is not an advance.' example: 16 nullable: true business_document_id: type: integer description: 'The purchase invoice being settled. Required for a `made` payment that is not an advance.' example: 16 nullable: true amount: type: number description: 'Must not be greater than 999999999.' example: 22 paid_on: type: string description: 'Must be a valid date.' example: '2026-01-15' method: type: string description: '' example: cash enum: - cash - bank - upi - card - cheque - other payment_account_id: type: integer description: '' example: 16 nullable: true reference: type: string description: 'Must not be greater than 64 characters.' example: 'n' nullable: true notes: type: string description: 'Must not be greater than 1000 characters.' example: g nullable: true direction: type: string description: '`received` for a customer payment, `made` for a supplier payment.' example: received required: - amount - paid_on - method - direction parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/payments/{payment_id}': get: summary: 'Get a payment and its email/share options.' operationId: getAPaymentAndItsEmailshareOptions description: "Requires sales permission for received payments and purchases permission for made\npayments. Includes advances. Foreign business payments return 404." parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Payments patch: summary: 'Edit an allocated payment.' operationId: editAnAllocatedPayment description: "Correct a payment allocated directly to a sales or purchase invoice, including a POS\nreceipt. Requires sales permission for received payments or purchases for made payments.\nThe party, direction, advance designation and invoice allocation cannot change. Advances\nand voided invoices return 422. Amount is capped at the current payment plus the bill's\noutstanding balance (an existing overpayment may be retained or reduced).\nUpdates bill balances/status, khata, account journals and cash movements atomically;\noriginal financial entries are reversed and replacements saved, with payment.updated\naudit values. Reconciled payments cannot change amount, date, method or account. Payments\nin closed cash sessions cannot change amount, method or account; reference and notes can\nstill be corrected. Moving to cash requires an open register. Foreign records return 404.\nOmitted fields retain their values; null reference/notes clear them." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 9 direction: received is_advance: false amount_paise: 100000 paid_on: '2026-09-30' method: upi payment_account_id: 3 reference: UTR-123 notes: 'Corrected receipt' invoice_id: 16 properties: data: type: object properties: id: type: integer example: 9 direction: type: string example: received is_advance: type: boolean example: false amount_paise: type: integer example: 100000 paid_on: type: string example: '2026-09-30' method: type: string example: upi payment_account_id: type: integer example: 3 reference: type: string example: UTR-123 notes: type: string example: 'Corrected receipt' invoice_id: type: integer example: 16 422: description: '' content: application/json: schema: type: object example: message: 'This payment is reconciled. Its amount, date, method and account cannot be changed.' errors: payment: - 'This payment is reconciled. Its amount, date, method and account cannot be changed.' properties: message: type: string example: 'This payment is reconciled. Its amount, date, method and account cannot be changed.' errors: type: object properties: payment: type: array example: - 'This payment is reconciled. Its amount, date, method and account cannot be changed.' items: type: string tags: - Payments requestBody: required: false content: application/json: schema: type: object properties: is_advance: type: boolean description: 'prohibited The original designation is fixed.' example: false contact_id: type: integer description: 'prohibited The original party is fixed.' example: null invoice_id: type: integer description: 'prohibited The original allocation is fixed.' example: null business_document_id: type: integer description: 'prohibited The original allocation is fixed.' example: null amount: type: number description: 'Payment amount in rupees, greater than zero.' example: 1000.0 paid_on: type: string description: 'Payment date.' example: '2026-09-30' method: type: string description: 'cash, bank, upi, card, cheque or other.' example: upi payment_account_id: type: integer description: 'Compatible account belonging to this business. A different account must be active.' example: 3 nullable: true reference: type: string description: 'Optional transaction reference, up to 64 characters.' example: UTR-123 nullable: true notes: type: string description: 'Optional note, up to 1000 characters.' example: 'Corrected receipt' nullable: true direction: type: string description: 'prohibited The original direction is fixed.' example: null parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: payment_id description: 'The ID of the payment.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/payments/{payment_id}/email': post: summary: 'Email a payment receipt or advice.' operationId: emailAPaymentReceiptOrAdvice description: "Queues a separate branded Dukanam email for each selected contact through the platform's\nCloudflare SMTP transport, with the shop's branding and receipt PDF attached.\nRequires sales permission for received payments or purchases for made payments.\nIncludes advances. Foreign tenant payments return 404, invalid or foreign recipient keys\nand disabled platform email return 422. No payment or ledger values are changed." parameters: [] responses: 202: description: '' content: application/json: schema: type: object example: message: 'Emailed to Ravi.' data: sent_to: - key: primary name: Ravi email: ravi@example.com properties: message: type: string example: 'Emailed to Ravi.' data: type: object properties: sent_to: type: array example: - key: primary name: Ravi email: ravi@example.com items: type: object properties: key: type: string example: primary name: type: string example: Ravi email: type: string example: ravi@example.com tags: - Payments requestBody: required: true content: application/json: schema: type: object properties: recipients: type: array description: 'A recipient key: `primary` for the party''s primary contact, or `person-{id}` for an additional contact. Must match the regex /^(primary|person-\d+)$/.' example: - primary - person-12 items: type: string recipient_emails: type: array description: 'Must be a valid email address. Must not be greater than 254 characters.' example: primary: accounts@example.com items: type: string nullable: true subject: type: string description: 'Email subject line. Must not be greater than 150 characters.' example: 'Sri Lakshmi Traders | Purchase order: PO-0007' message: type: string description: 'Message body. Blank lines separate paragraphs. The PDF is attached and a button links to the signed public page. Must not be greater than 2000 characters.' example: 'Hi Ravi, please find purchase order PO-0007 for ₹12,500.00. Please confirm the order and the delivery date.' from: type: string description: 'Ledger statements only: start date, inclusive. Must be a valid date.' example: '2026-04-01' nullable: true to: type: string description: 'Ledger statements only: end date, inclusive. Must be on or after `from`. Must be a valid date. Must be a date after or equal to from.' example: '2027-03-31' nullable: true required: - recipients - subject - message parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: payment_id description: 'The ID of the payment.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/payments/{payment_id}/apply': post: summary: 'Apply an advance to a bill.' operationId: applyAnAdvanceToABill description: "Puts part or all of an advance towards one unpaid bill of the same party: a sales invoice\nfor an advance received, a purchase invoice for an advance made. The bill's `paid_paise`\nrises and the advance's `unapplied_paise` falls; the account, cash register, khata and\nbooks do not move again, because they already did when the advance was recorded. Leave\n`amount` out to apply as much as both the advance and the bill balance allow." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 9 direction: received is_advance: true amount_paise: 500000 applied_paise: 150000 unapplied_paise: 350000 applications: - id: 1 payment_id: 9 amount_paise: 150000 applied_on: '2026-09-25' invoice_id: 16 invoice_number: INV-016 business_document_id: null properties: data: type: object properties: id: type: integer example: 9 direction: type: string example: received is_advance: type: boolean example: true amount_paise: type: integer example: 500000 applied_paise: type: integer example: 150000 unapplied_paise: type: integer example: 350000 applications: type: array example: - id: 1 payment_id: 9 amount_paise: 150000 applied_on: '2026-09-25' invoice_id: 16 invoice_number: INV-016 business_document_id: null description: 'Every bill this advance has been put towards, with amount_paise, applied_on, and the invoice or document it settled.' items: type: object properties: id: type: integer example: 1 payment_id: type: integer example: 9 amount_paise: type: integer example: 150000 applied_on: type: string example: '2026-09-25' invoice_id: type: integer example: 16 invoice_number: type: string example: INV-016 business_document_id: type: string example: null nullable: true 422: description: '' content: application/json: schema: type: object example: message: 'Enter an amount up to ₹3,500.00, the smaller of the unused advance and the bill balance.' errors: amount: - 'Enter an amount up to ₹3,500.00, the smaller of the unused advance and the bill balance.' properties: message: type: string example: 'Enter an amount up to ₹3,500.00, the smaller of the unused advance and the bill balance.' errors: type: object properties: amount: type: array example: - 'Enter an amount up to ₹3,500.00, the smaller of the unused advance and the bill balance.' items: type: string tags: - Payments requestBody: required: false content: application/json: schema: type: object properties: invoice_id: type: integer description: "The customer's sales invoice. Required for an advance received." example: 16 nullable: true business_document_id: type: integer description: "The supplier's purchase invoice. Required for an advance made." example: 16 nullable: true amount: type: number description: 'Rupees to apply, up to the smaller of the unused advance and the bill balance.' example: 1500.0 nullable: true applied_on: type: string description: 'Date the advance was set against the bill. Defaults to today.' example: '2026-09-25' nullable: true parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: payment_id description: 'The ID of the payment.' example: 1 required: true schema: type: integer - in: path name: payment description: 'The advance payment.' example: 9 required: true schema: type: integer '/api/v1/businesses/{business}/expenses': get: summary: 'List expenses.' operationId: listExpenses description: "Search and filter business expenses. The response includes totals for the current\nresult, the current month, and the retained voided audit trail." parameters: - in: query name: q description: 'Case-insensitive search across category, payee, or description.' example: internet required: false schema: type: string description: 'Case-insensitive search across category, payee, or description.' example: internet nullable: true - in: query name: category description: 'Filter by an exact category.' example: Utilities required: false schema: type: string description: 'Filter by an exact category.' example: Utilities nullable: true - in: query name: payment_account_id description: 'Filter by the payment account used.' example: 9 required: false schema: type: integer description: 'Filter by the payment account used.' example: 9 nullable: true - in: query name: from description: 'Include expenses on or after this date (`YYYY-MM-DD`).' example: '2026-08-01' required: false schema: type: string description: 'Include expenses on or after this date (`YYYY-MM-DD`).' example: '2026-08-01' nullable: true - in: query name: to description: 'Include expenses on or before this date (`YYYY-MM-DD`).' example: '2026-08-31' required: false schema: type: string description: 'Include expenses on or before this date (`YYYY-MM-DD`).' example: '2026-08-31' nullable: true - in: query name: status description: 'Filter by record status.' example: active required: false schema: type: string description: 'Filter by record status.' example: active enum: - active - voided nullable: true - in: query name: per_page description: 'Results per page, from 1 to 100.' example: 20 required: false schema: type: integer description: 'Results per page, from 1 to 100.' example: 20 nullable: true responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 41 category: Utilities payee: 'City Internet Services' amount_paise: 249900 occurred_on: '2026-08-12' payment_method: bank payment_account_id: 9 payment_account_name: 'HDFC Current Account' note: 'August internet bill for the main shop' voided_at: null void_reason: null created_at: '2026-08-12T10:30:00.000000Z' links: first: 'https://example.com/api/v1/businesses/1/expenses?page=1' last: 'https://example.com/api/v1/businesses/1/expenses?page=1' prev: null next: null meta: current_page: 1 from: 1 last_page: 1 path: 'https://example.com/api/v1/businesses/1/expenses' per_page: 20 to: 1 total: 1 summary: filtered_total_paise: 249900 month_total_paise: 384900 month_count: 3 voided_count: 1 properties: data: type: array example: - id: 41 category: Utilities payee: 'City Internet Services' amount_paise: 249900 occurred_on: '2026-08-12' payment_method: bank payment_account_id: 9 payment_account_name: 'HDFC Current Account' note: 'August internet bill for the main shop' voided_at: null void_reason: null created_at: '2026-08-12T10:30:00.000000Z' items: type: object properties: id: type: integer example: 41 category: type: string example: Utilities payee: type: string example: 'City Internet Services' amount_paise: type: integer example: 249900 occurred_on: type: string example: '2026-08-12' payment_method: type: string example: bank payment_account_id: type: integer example: 9 payment_account_name: type: string example: 'HDFC Current Account' note: type: string example: 'August internet bill for the main shop' voided_at: type: string example: null nullable: true void_reason: type: string example: null nullable: true created_at: type: string example: '2026-08-12T10:30:00.000000Z' links: type: object properties: first: type: string example: 'https://example.com/api/v1/businesses/1/expenses?page=1' last: type: string example: 'https://example.com/api/v1/businesses/1/expenses?page=1' prev: type: string example: null nullable: true next: type: string example: null nullable: true meta: type: object properties: current_page: type: integer example: 1 from: type: integer example: 1 last_page: type: integer example: 1 path: type: string example: 'https://example.com/api/v1/businesses/1/expenses' per_page: type: integer example: 20 to: type: integer example: 1 total: type: integer example: 1 summary: type: object properties: filtered_total_paise: type: integer example: 249900 description: 'Total of non-voided expenses matching the current filters.' month_total_paise: type: integer example: 384900 description: 'Total of all non-voided expenses in the current calendar month.' month_count: type: integer example: 3 description: 'Count of all non-voided expenses in the current calendar month.' voided_count: type: integer example: 1 description: 'Count of all retained voided expenses.' tags: - Expenses post: summary: 'Save an expense.' operationId: saveAnExpense description: 'The expense, general-ledger journal, cash-drawer movement, and audit record either all commit or all roll back.' parameters: [] responses: {} tags: - Expenses requestBody: required: true content: application/json: schema: type: object properties: category: type: string description: 'Must not be greater than 64 characters.' example: b payee: type: string description: 'Must not be greater than 128 characters.' example: 'n' nullable: true amount: type: number description: 'Must not be greater than 999999999.' example: 7 occurred_on: type: string description: 'Must be a valid date.' example: '2026-01-15' payment_method: type: string description: '' example: cash enum: - cash - bank - upi - card - other payment_account_id: type: integer description: '' example: 16 nullable: true note: type: string description: 'Must not be greater than 1000 characters.' example: 'n' idempotency_key: type: string description: 'Stable UUID used to make creation retries safe.' example: 6d61f406-f07d-482d-a284-3e06edfd7f55 nullable: true required: - category - amount - occurred_on - payment_method - note parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/expenses/{expense_id}': get: summary: '' operationId: getApiV1BusinessesBusinessExpensesExpense_id description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Expenses parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: expense_id description: 'The ID of the expense.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/expenses/{expense_id}/void': post: summary: 'Void an expense atomically.' operationId: voidAnExpenseAtomically description: 'The void marker, journal reversal, cash-drawer reversal, and audit record either all commit or all roll back.' parameters: [] responses: {} tags: - Expenses requestBody: required: true content: application/json: schema: type: object properties: reason: type: string description: 'Must be at least 3 characters. Must not be greater than 255 characters.' example: b required: - reason parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: expense_id description: 'The ID of the expense.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/pos/items': get: summary: "A barcode is matched whole, and read as GS1 reads it, so a wedge scanner emitting the\n12-digit UPC-A of a pack stored by its 13-digit EAN-13 still finds it. A name is matched\non any part of it. For a camera scan use `items/by-barcode/{code}`, which answers with one\nitem or a 404 rather than a list." operationId: aBarcodeIsMatchedWholeAndReadAsGS1ReadsItSoAWedgeScannerEmittingThe12DigitUPCAOfAPackStoredByIts13DigitEAN13StillFindsItANameIsMatchedOnAnyPartOfItForACameraScanUseitemsbyBarcodecodeWhichAnswersWithOneItemOrA404RatherThanAList description: '' parameters: - in: query name: q description: 'Barcode, SKU, or item name to search for.' example: atta required: false schema: type: string description: 'Barcode, SKU, or item name to search for.' example: atta - in: query name: contact description: 'Customer id. Adds `party_price` to every item, so the counter shows what this party pays before the sale is rung up.' example: 42 required: false schema: type: integer description: 'Customer id. Adds `party_price` to every item, so the counter shows what this party pays before the sale is rung up.' example: 42 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - POS requestBody: required: false content: application/json: schema: type: object properties: q: type: string description: 'Must not be greater than 128 characters.' example: b nullable: true contact: type: integer description: 'Must be at least 1.' example: 22 nullable: true parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/pos/upi': get: summary: '' operationId: getApiV1BusinessesBusinessPosUpi description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - POS requestBody: required: true content: application/json: schema: type: object properties: amount: type: number description: 'Must not be greater than 10000000.' example: 1 payment_account_id: type: integer description: '' example: 16 nullable: true required: - amount parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/pos/carts': get: summary: 'List held POS carts for the business.' operationId: listHeldPOSCartsForTheBusiness description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - POS post: summary: 'Hold a POS cart for later checkout.' operationId: holdAPOSCartForLaterCheckout description: '' parameters: [] responses: {} tags: - POS requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Must not be greater than 96 characters.' example: b contact_id: type: integer description: '' example: 16 nullable: true lines: type: array description: 'Must have at least 1 items. Must not have more than 100 items.' example: - [] items: type: object properties: item_id: type: integer description: '' example: 16 item_batch_id: type: integer description: '' example: 16 nullable: true serial_numbers: type: array description: 'Must not be greater than 64 characters.' example: - 'n' items: type: string quantity: type: number description: '' example: 4326.41688 required: - item_id - quantity required: - name - lines parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/pos/checkout': post: summary: '' operationId: postApiV1BusinessesBusinessPosCheckout description: '' parameters: [] responses: {} tags: - POS requestBody: required: true content: application/json: schema: type: object properties: contact_id: type: integer description: '' example: 16 idempotency_key: type: string description: 'Must be a valid UUID.' example: a4855dc5-0acb-33c3-b921-f4291f719ca0 warehouse_id: type: integer description: "The warehouse this sale's stock leaves; the default warehouse when left out. Must be at least 1." example: 66 nullable: true cart_id: type: integer description: '' example: 16 nullable: true lines: type: array description: 'Must have at least 1 items. Must not have more than 100 items.' example: - [] items: type: object properties: item_id: type: integer description: '' example: 16 item_batch_id: type: integer description: 'A batch-tracked item says which lot it is sold from; anything else must not.' example: 16 nullable: true serial_numbers: type: array description: 'Must not be greater than 64 characters.' example: - 'n' items: type: string quantity: type: number description: '' example: 4326.41688 discount: type: number description: 'Must be at least 0.' example: 77 nullable: true required: - item_id - quantity payments: type: array description: 'Must not have more than 4 items.' example: null items: type: object nullable: true properties: method: type: string description: '' example: cash enum: - cash - upi - card - bank - cheque payment_account_id: type: integer description: '' example: 16 nullable: true amount: type: number description: '' example: 4326.41688 required: - method - amount required: - contact_id - idempotency_key - lines parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/cash-register': get: summary: '' operationId: getApiV1BusinessesBusinessCashRegister description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Cash register' requestBody: required: false content: application/json: schema: type: object properties: cash_register_id: type: integer description: '' example: 16 nullable: true parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/cash-register/open': post: summary: '' operationId: postApiV1BusinessesBusinessCashRegisterOpen description: '' parameters: [] responses: {} tags: - 'Cash register' requestBody: required: true content: application/json: schema: type: object properties: cash_register_id: type: integer description: '' example: 16 nullable: true opening_float: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 22 required: - opening_float parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/cash-register/movements': post: summary: '' operationId: postApiV1BusinessesBusinessCashRegisterMovements description: '' parameters: [] responses: {} tags: - 'Cash register' requestBody: required: true content: application/json: schema: type: object properties: type: type: string description: '' example: cash_in enum: - cash_in - cash_out cash_register_id: type: integer description: '' example: 16 nullable: true amount: type: number description: 'Must not be greater than 999999999.' example: 22 notes: type: string description: 'Must not be greater than 255 characters.' example: g required: - type - amount - notes parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/cash-register/close': post: summary: '' operationId: postApiV1BusinessesBusinessCashRegisterClose description: '' parameters: [] responses: {} tags: - 'Cash register' requestBody: required: true content: application/json: schema: type: object properties: counted_cash: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 1 cash_register_id: type: integer description: '' example: 16 nullable: true closing_notes: type: string description: 'Must not be greater than 1000 characters.' example: 'n' nullable: true required: - counted_cash parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/cash-register/printer': patch: summary: "Save a counter's receipt printer settings." operationId: saveACountersReceiptPrinterSettings description: 'Every device billing at the counter reads these from `printer` on GET `cash-register` and from `cash_register.printer` on payment accounts, and GET `invoices/{invoice}/receipt` applies them. Send only the settings that change. The printer itself is paired on each device and is not stored.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: cash_register_id: 1 cash_register_name: 'Main counter' paper: 58mm auto_print: true cut: false open_drawer: true properties: data: type: object properties: cash_register_id: type: integer example: 1 cash_register_name: type: string example: 'Main counter' paper: type: string example: 58mm auto_print: type: boolean example: true cut: type: boolean example: false open_drawer: type: boolean example: true 422: description: '' content: application/json: schema: type: object example: message: 'Choose 58mm or 80mm paper.' errors: paper: - 'Choose 58mm or 80mm paper.' properties: message: type: string example: 'Choose 58mm or 80mm paper.' errors: type: object properties: paper: type: array example: - 'Choose 58mm or 80mm paper.' items: type: string tags: - 'Cash register' requestBody: required: true content: application/json: schema: type: object properties: cash_register_id: type: integer description: 'Counter whose printer settings change. Must match an existing stored value.' example: 1 paper: type: string description: 'Roll width: `58mm` (2-inch, 32 characters a line) or `80mm` (3-inch, 48 characters a line).' example: 80mm enum: - 58mm - 80mm auto_print: type: boolean description: 'Print the receipt as soon as a sale is saved, without asking.' example: true cut: type: boolean description: 'Cut the paper after each receipt. Turn off for printers without a cutter.' example: true open_drawer: type: boolean description: 'Open the cash drawer connected to the printer after a sale with a cash payment.' example: true required: - cash_register_id parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/cash-register/printer/test': get: summary: 'Get a printer test slip.' operationId: getAPrinterTestSlip description: 'ESC/POS bytes for a short slip showing the counter, paper width, cutter and drawer settings, with a numbered line that fills exactly one row when the paper width is right. Use it to check a newly paired printer before the first sale. The drawer opens only when `open_drawer=1` is sent. The response has the same shape as GET `invoices/{invoice}/receipt`, with `invoice_id` and `invoice_number` null.' parameters: - in: query name: cash_register_id description: 'Counter the receipt prints at. Its saved printer settings (paper, cut, cash drawer) apply. Omit to use the defaults: 80mm, cut, drawer opens for cash sales. Must match an existing stored value.' example: 1 required: false schema: type: integer description: 'Counter the receipt prints at. Its saved printer settings (paper, cut, cash drawer) apply. Omit to use the defaults: 80mm, cut, drawer opens for cash sales. Must match an existing stored value.' example: 1 nullable: true - in: query name: paper description: "Overrides the counter's paper width: `58mm` (32 characters a line) or `80mm` (48 characters a line)." example: 58mm required: false schema: type: string description: "Overrides the counter's paper width: `58mm` (32 characters a line) or `80mm` (48 characters a line)." example: 58mm enum: - 58mm - 80mm nullable: true - in: query name: cut description: 'Overrides whether the paper is cut at the end: `1` or `0`.' example: false required: false schema: type: boolean description: 'Overrides whether the paper is cut at the end: `1` or `0`.' example: false nullable: true - in: query name: open_drawer description: 'Overrides whether the cash drawer opens: `1` always opens it, `0` never does. When omitted, the drawer opens only if the counter allows it and the sale has a cash payment.' example: false required: false schema: type: boolean description: 'Overrides whether the cash drawer opens: `1` always opens it, `0` never does. When omitted, the drawer opens only if the counter allows it and the sale has a cash payment.' example: false nullable: true responses: 200: description: '' content: application/json: schema: type: object example: data: format: escpos invoice_id: null invoice_number: null cash_register_id: 1 paper: 80mm characters_per_line: 48 cut: true opens_drawer: false auto_print: false content_base64: G0AbYQEbRQEd... byte_length: 402 text: "API Shop\nPrinter test" properties: data: type: object properties: format: type: string example: escpos invoice_id: type: string example: null nullable: true invoice_number: type: string example: null nullable: true cash_register_id: type: integer example: 1 paper: type: string example: 80mm characters_per_line: type: integer example: 48 cut: type: boolean example: true opens_drawer: type: boolean example: false auto_print: type: boolean example: false content_base64: type: string example: G0AbYQEbRQEd... byte_length: type: integer example: 402 text: type: string example: "API Shop\nPrinter test" tags: - 'Cash register' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/reports': get: summary: 'View a business report.' operationId: viewABusinessReport description: "Reports update automatically when bills, returns, expenses and other transactions are saved.\nNo extra confirmation step is required. The P&L description is \"Income and expenses for the selected period\".\nReport descriptions use plain billing language. General ledger labels its entry-number column Reference.\n\nUnregistered businesses have no Taxes category in catalog; tax-summary returns 403.\n\n`warehouse` narrows stock summary, stock valuation, and low stock to one warehouse (low stock then compares that warehouse's quantity with each item's reorder level). The `stock-by-warehouse` report lists every item's stock in every warehouse.\nInventory values use recorded carrying costs independently of editable catalogue prices;\nincomplete legacy movement history retains catalogue estimates. Warehouse allocations\nuse cumulative rounding so their values sum to the whole inventory value.\nCash flow includes actual expense settlements and worker payouts with dated payment\nreversals; unpaid invoice expenses and earned-but-unpaid commissions are not outflows.\n\nFinancial reports include original journal entries and their dated reversals, so invoice edits and cancellations retain history while producing the correct net balance. Each entry is included according to its own accounting date.\nInvoice edits restate the invoice period: reversals use the previous invoice date and replacements use the submitted invoice date. Saving an edit does not move the invoice to today's date. Owners may explicitly change issue_date; cancellation reversals retain their cancellation date." parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Reports requestBody: required: false content: application/json: schema: type: object properties: from: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true to: type: string description: 'Must be a valid date. Must be a date after or equal to from.' example: '2026-01-15' nullable: true warehouse: type: integer description: 'Must be at least 1.' example: 22 nullable: true parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/reports/export': get: summary: 'Download a report as PDF or Excel.' operationId: downloadAReportAsPDFOrExcel description: "Supports every report in the report catalog, including profit-loss, balance-sheet,\ntrial-balance, general-ledger, cash-flow and tax-summary. Uses the same rows and totals\nas the web and JSON report, excluding the separate summary cards. Money in Excel is\nnumeric rupees, with signed amounts and two decimal places; dates are native Excel dates.\nProfit is green, loss is red, and zero is neutral in both formats.\nBoth formats include the current shop/legal name, address, contact details and GSTIN\nwhere registered, plus the saved invoice logo (when enabled), accent and header style.\nHeaders include the report dates and generation timestamp in the business timezone.\nMissing or unreadable logos are omitted without preventing download.\n\nRequires reports and exports permissions, reports.basic and data_export features.\nAdvanced reports also require reports.advanced. Unregistered businesses cannot export\ntax-summary. Tenant and warehouse isolation are enforced.\n\nDates are inclusive. Balance-sheet and trial-balance use to as their as-at date.\nCurrent-stock, payment-account and party-balance reports remain current snapshots,\nmatching the JSON report, and are labelled accordingly. Aging reports use the requested\ndocument period with aging as at today. Downloads are synchronous and online-only." parameters: - in: query name: report description: 'A report slug from the catalog.' example: profit-loss required: true schema: type: string description: 'A report slug from the catalog.' example: profit-loss - in: query name: format description: 'pdf or xlsx.' example: pdf required: true schema: type: string description: 'pdf or xlsx.' example: pdf enum: - pdf - xlsx - in: query name: from description: 'Start date, YYYY-MM-DD.' example: '2026-04-01' required: true schema: type: string description: 'Start date, YYYY-MM-DD.' example: '2026-04-01' - in: query name: to description: 'End date, YYYY-MM-DD, on or after from.' example: '2026-10-03' required: true schema: type: string description: 'End date, YYYY-MM-DD, on or after from.' example: '2026-10-03' - in: query name: warehouse description: 'Optional warehouse of this business; narrows stock-summary, stock-valuation and low-stock.' example: 1 required: false schema: type: integer description: 'Optional warehouse of this business; narrows stock-summary, stock-valuation and low-stock.' example: 1 nullable: true responses: 200: description: 'The report file, with attachment filename dukanam-{report}-{from}-{to}.{pdf|xlsx}.' content: application/pdf: schema: type: string format: binary application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: schema: type: string format: binary 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 403: description: '' content: application/json: schema: type: object example: message: 'Your role does not allow this action.' properties: message: type: string example: 'Your role does not allow this action.' 404: description: '' content: application/json: schema: type: object example: message: 'Not Found' properties: message: type: string example: 'Not Found' 422: description: '' content: application/json: schema: type: object example: message: 'The selected format is invalid.' errors: format: - 'The selected format is invalid.' properties: message: type: string example: 'The selected format is invalid.' errors: type: object properties: format: type: array example: - 'The selected format is invalid.' items: type: string tags: - Reports parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/reports/expiring': get: summary: 'Stock that is about to go out of date.' operationId: stockThatIsAboutToGoOutOfDate description: "Batches still holding stock whose expiry falls within `within_days`, soonest first.\nAlready-expired lots are included and flagged — the shop has to see and clear them — and\ncan never be billed. Requires a plan with advanced reports, like every other stock report." parameters: - in: query name: within_days description: 'How far ahead to look, 1-365. Defaults to 30.' example: 60 required: false schema: type: integer description: 'How far ahead to look, 1-365. Defaults to 30.' example: 60 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Reports requestBody: required: false content: application/json: schema: type: object properties: within_days: type: integer description: 'Must be at least 1. Must not be greater than 365.' example: 1 nullable: true parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business_id}/compliance': get: summary: 'Get the compliance profile, registrations, evidence metadata and assessed obligations.' operationId: getTheComplianceProfileRegistrationsEvidenceMetadataAndAssessedObligations description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: business_id: 16 state_code: '29' profile: constitution: proprietorship masked_pan: 'ABC****34F' district: 'Bengaluru Urban' local_body: BBMP premises_type: rented supply_type: both gst_registration_status: not_registered previous_year_turnover_paise: 180000000 other_pan_turnover_paise: 25000000 employee_count: 4 contract_worker_count: 0 female_employee_count: 2 interstate_sales: false ecommerce_sales: true imports: false exports: false manufactures: false food_business: true uses_weighing_scale: true packs_or_imports_goods: false pharmacy: false serves_alcohol: false fire_risk: false pollution_activity: false reminder_days: - 90 - 60 - 30 - 7 - 0 assessment_completed_at: '2026-08-20T12:00:00+05:30' next_review_at: '2026-11-20T12:00:00+05:30' categories: - id: 13 slug: food-service name: 'Restaurant, café and food service' nic_section: I gst_registrations: [] obligations: - id: 81 rule_code: fssai-registration-licence rule_version: 1 title: 'FSSAI registration or food licence' applicability_reason: 'Every food business operator must hold the matching registration or licence. Matched using Guidance Shop facts.' applicability_level: required obligation_type: licence authority_name: 'Food Safety and Standards Authority of India' source_url: 'https://fssai.gov.in/business/registration' application_url: 'https://foscos.fssai.gov.in/' status: action_required reference_number: null issued_on: null due_on: null expires_on: null notes: null manual_override: false last_assessed_at: '2026-08-20T12:00:00+05:30' documents: [] guidance_notice: 'Guidance is generated from configurable rules and recorded business facts. Review items marked professional review and verify official sources before relying on them.' options: constitutions: proprietorship: Proprietorship partnership: 'Partnership firm' llp: 'Limited Liability Partnership' private_limited: 'Private limited company' public_limited: 'Public limited company' opc: 'One Person Company' huf: 'Hindu Undivided Family' trust_society: 'Trust / society' cooperative: 'Co-operative society' government: 'Government entity' other: Other supply_types: goods: 'Goods only' services: 'Services only' both: 'Goods and services' gst_statuses: not_assessed: 'Not assessed' not_registered: 'Not registered' pending: 'Application pending' active: 'Registered / active' suspended: Suspended cancelled: 'Cancelled / surrendered' gst_registration_types: normal: 'Normal taxpayer' composition: 'Composition taxpayer' casual: 'Casual taxable person' non_resident: 'Non-resident taxable person' isd: 'Input Service Distributor (ISD)' tds: 'Tax Deductor (TDS)' tcs: 'Tax Collector / e-commerce operator (TCS)' sez_unit: 'SEZ unit' sez_developer: 'SEZ developer' oidar: 'OIDAR / online money gaming supplier' uin: 'Unique Identity Number holder' temporary: 'Temporary registration' meta: available_categories: - id: 13 slug: food-service name: 'Restaurant, café and food service' nic_section: I description: 'Restaurants, cafés, caterers, cloud kitchens and mobile food service.' properties: data: type: object properties: business_id: type: integer example: 16 state_code: type: string example: '29' profile: type: object properties: constitution: type: string example: proprietorship masked_pan: type: string example: 'ABC****34F' district: type: string example: 'Bengaluru Urban' local_body: type: string example: BBMP premises_type: type: string example: rented supply_type: type: string example: both gst_registration_status: type: string example: not_registered previous_year_turnover_paise: type: integer example: 180000000 other_pan_turnover_paise: type: integer example: 25000000 employee_count: type: integer example: 4 contract_worker_count: type: integer example: 0 female_employee_count: type: integer example: 2 interstate_sales: type: boolean example: false ecommerce_sales: type: boolean example: true imports: type: boolean example: false exports: type: boolean example: false manufactures: type: boolean example: false food_business: type: boolean example: true uses_weighing_scale: type: boolean example: true packs_or_imports_goods: type: boolean example: false pharmacy: type: boolean example: false serves_alcohol: type: boolean example: false fire_risk: type: boolean example: false pollution_activity: type: boolean example: false reminder_days: type: array example: - 90 - 60 - 30 - 7 - 0 items: type: integer assessment_completed_at: type: string example: '2026-08-20T12:00:00+05:30' next_review_at: type: string example: '2026-11-20T12:00:00+05:30' categories: type: array example: - id: 13 slug: food-service name: 'Restaurant, café and food service' nic_section: I items: type: object properties: id: type: integer example: 13 slug: type: string example: food-service name: type: string example: 'Restaurant, café and food service' nic_section: type: string example: I gst_registrations: type: array example: [] obligations: type: array example: - id: 81 rule_code: fssai-registration-licence rule_version: 1 title: 'FSSAI registration or food licence' applicability_reason: 'Every food business operator must hold the matching registration or licence. Matched using Guidance Shop facts.' applicability_level: required obligation_type: licence authority_name: 'Food Safety and Standards Authority of India' source_url: 'https://fssai.gov.in/business/registration' application_url: 'https://foscos.fssai.gov.in/' status: action_required reference_number: null issued_on: null due_on: null expires_on: null notes: null manual_override: false last_assessed_at: '2026-08-20T12:00:00+05:30' documents: [] items: type: object properties: id: type: integer example: 81 rule_code: type: string example: fssai-registration-licence rule_version: type: integer example: 1 title: type: string example: 'FSSAI registration or food licence' applicability_reason: type: string example: 'Every food business operator must hold the matching registration or licence. Matched using Guidance Shop facts.' applicability_level: type: string example: required obligation_type: type: string example: licence authority_name: type: string example: 'Food Safety and Standards Authority of India' source_url: type: string example: 'https://fssai.gov.in/business/registration' application_url: type: string example: 'https://foscos.fssai.gov.in/' status: type: string example: action_required reference_number: type: string example: null nullable: true issued_on: type: string example: null nullable: true due_on: type: string example: null nullable: true expires_on: type: string example: null nullable: true notes: type: string example: null nullable: true manual_override: type: boolean example: false last_assessed_at: type: string example: '2026-08-20T12:00:00+05:30' documents: type: array example: [] guidance_notice: type: string example: 'Guidance is generated from configurable rules and recorded business facts. Review items marked professional review and verify official sources before relying on them.' options: type: object properties: constitutions: type: object properties: proprietorship: type: string example: Proprietorship partnership: type: string example: 'Partnership firm' llp: type: string example: 'Limited Liability Partnership' private_limited: type: string example: 'Private limited company' public_limited: type: string example: 'Public limited company' opc: type: string example: 'One Person Company' huf: type: string example: 'Hindu Undivided Family' trust_society: type: string example: 'Trust / society' cooperative: type: string example: 'Co-operative society' government: type: string example: 'Government entity' other: type: string example: Other supply_types: type: object properties: goods: type: string example: 'Goods only' services: type: string example: 'Services only' both: type: string example: 'Goods and services' gst_statuses: type: object properties: not_assessed: type: string example: 'Not assessed' not_registered: type: string example: 'Not registered' pending: type: string example: 'Application pending' active: type: string example: 'Registered / active' suspended: type: string example: Suspended cancelled: type: string example: 'Cancelled / surrendered' gst_registration_types: type: object properties: normal: type: string example: 'Normal taxpayer' composition: type: string example: 'Composition taxpayer' casual: type: string example: 'Casual taxable person' non_resident: type: string example: 'Non-resident taxable person' isd: type: string example: 'Input Service Distributor (ISD)' tds: type: string example: 'Tax Deductor (TDS)' tcs: type: string example: 'Tax Collector / e-commerce operator (TCS)' sez_unit: type: string example: 'SEZ unit' sez_developer: type: string example: 'SEZ developer' oidar: type: string example: 'OIDAR / online money gaming supplier' uin: type: string example: 'Unique Identity Number holder' temporary: type: string example: 'Temporary registration' meta: type: object properties: available_categories: type: array example: - id: 13 slug: food-service name: 'Restaurant, café and food service' nic_section: I description: 'Restaurants, cafés, caterers, cloud kitchens and mobile food service.' items: type: object properties: id: type: integer example: 13 slug: type: string example: food-service name: type: string example: 'Restaurant, café and food service' nic_section: type: string example: I description: type: string example: 'Restaurants, cafés, caterers, cloud kitchens and mobile food service.' tags: - 'Business compliance guidance' parameters: - in: path name: business_id description: 'The ID of the business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business_id}/compliance/profile': put: summary: 'Update business facts and immediately run the deterministic compliance assessment.' operationId: updateBusinessFactsAndImmediatelyRunTheDeterministicComplianceAssessment description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: business_id: 16 state_code: '29' profile: constitution: proprietorship masked_pan: 'ABC****34F' district: 'Bengaluru Urban' local_body: BBMP premises_type: rented supply_type: both gst_registration_status: not_registered previous_year_turnover_paise: 180000000 other_pan_turnover_paise: 25000000 employee_count: 4 contract_worker_count: 0 female_employee_count: 2 interstate_sales: false ecommerce_sales: true imports: false exports: false manufactures: false food_business: true uses_weighing_scale: true packs_or_imports_goods: false pharmacy: false serves_alcohol: false fire_risk: false pollution_activity: false reminder_days: - 90 - 60 - 30 - 7 - 0 assessment_completed_at: '2026-08-20T12:00:00+05:30' next_review_at: '2026-11-20T12:00:00+05:30' categories: - id: 13 slug: food-service name: 'Restaurant, café and food service' nic_section: I gst_registrations: [] obligations: - id: 81 rule_code: fssai-registration-licence rule_version: 1 title: 'FSSAI registration or food licence' applicability_reason: 'Every food business operator must hold the matching registration or licence. Matched using Guidance Shop facts.' applicability_level: required obligation_type: licence authority_name: 'Food Safety and Standards Authority of India' source_url: 'https://fssai.gov.in/business/registration' application_url: 'https://foscos.fssai.gov.in/' status: action_required reference_number: null issued_on: null due_on: null expires_on: null notes: null manual_override: false last_assessed_at: '2026-08-20T12:00:00+05:30' documents: [] guidance_notice: 'Guidance is generated from configurable rules and recorded business facts. Review items marked professional review and verify official sources before relying on them.' options: constitutions: proprietorship: Proprietorship partnership: 'Partnership firm' llp: 'Limited Liability Partnership' private_limited: 'Private limited company' public_limited: 'Public limited company' opc: 'One Person Company' huf: 'Hindu Undivided Family' trust_society: 'Trust / society' cooperative: 'Co-operative society' government: 'Government entity' other: Other supply_types: goods: 'Goods only' services: 'Services only' both: 'Goods and services' gst_statuses: not_assessed: 'Not assessed' not_registered: 'Not registered' pending: 'Application pending' active: 'Registered / active' suspended: Suspended cancelled: 'Cancelled / surrendered' gst_registration_types: normal: 'Normal taxpayer' composition: 'Composition taxpayer' casual: 'Casual taxable person' non_resident: 'Non-resident taxable person' isd: 'Input Service Distributor (ISD)' tds: 'Tax Deductor (TDS)' tcs: 'Tax Collector / e-commerce operator (TCS)' sez_unit: 'SEZ unit' sez_developer: 'SEZ developer' oidar: 'OIDAR / online money gaming supplier' uin: 'Unique Identity Number holder' temporary: 'Temporary registration' meta: available_categories: - id: 13 slug: food-service name: 'Restaurant, café and food service' nic_section: I description: 'Restaurants, cafés, caterers, cloud kitchens and mobile food service.' properties: data: type: object properties: business_id: type: integer example: 16 state_code: type: string example: '29' profile: type: object properties: constitution: type: string example: proprietorship masked_pan: type: string example: 'ABC****34F' district: type: string example: 'Bengaluru Urban' local_body: type: string example: BBMP premises_type: type: string example: rented supply_type: type: string example: both gst_registration_status: type: string example: not_registered previous_year_turnover_paise: type: integer example: 180000000 other_pan_turnover_paise: type: integer example: 25000000 employee_count: type: integer example: 4 contract_worker_count: type: integer example: 0 female_employee_count: type: integer example: 2 interstate_sales: type: boolean example: false ecommerce_sales: type: boolean example: true imports: type: boolean example: false exports: type: boolean example: false manufactures: type: boolean example: false food_business: type: boolean example: true uses_weighing_scale: type: boolean example: true packs_or_imports_goods: type: boolean example: false pharmacy: type: boolean example: false serves_alcohol: type: boolean example: false fire_risk: type: boolean example: false pollution_activity: type: boolean example: false reminder_days: type: array example: - 90 - 60 - 30 - 7 - 0 items: type: integer assessment_completed_at: type: string example: '2026-08-20T12:00:00+05:30' next_review_at: type: string example: '2026-11-20T12:00:00+05:30' categories: type: array example: - id: 13 slug: food-service name: 'Restaurant, café and food service' nic_section: I items: type: object properties: id: type: integer example: 13 slug: type: string example: food-service name: type: string example: 'Restaurant, café and food service' nic_section: type: string example: I gst_registrations: type: array example: [] obligations: type: array example: - id: 81 rule_code: fssai-registration-licence rule_version: 1 title: 'FSSAI registration or food licence' applicability_reason: 'Every food business operator must hold the matching registration or licence. Matched using Guidance Shop facts.' applicability_level: required obligation_type: licence authority_name: 'Food Safety and Standards Authority of India' source_url: 'https://fssai.gov.in/business/registration' application_url: 'https://foscos.fssai.gov.in/' status: action_required reference_number: null issued_on: null due_on: null expires_on: null notes: null manual_override: false last_assessed_at: '2026-08-20T12:00:00+05:30' documents: [] items: type: object properties: id: type: integer example: 81 rule_code: type: string example: fssai-registration-licence rule_version: type: integer example: 1 title: type: string example: 'FSSAI registration or food licence' applicability_reason: type: string example: 'Every food business operator must hold the matching registration or licence. Matched using Guidance Shop facts.' applicability_level: type: string example: required obligation_type: type: string example: licence authority_name: type: string example: 'Food Safety and Standards Authority of India' source_url: type: string example: 'https://fssai.gov.in/business/registration' application_url: type: string example: 'https://foscos.fssai.gov.in/' status: type: string example: action_required reference_number: type: string example: null nullable: true issued_on: type: string example: null nullable: true due_on: type: string example: null nullable: true expires_on: type: string example: null nullable: true notes: type: string example: null nullable: true manual_override: type: boolean example: false last_assessed_at: type: string example: '2026-08-20T12:00:00+05:30' documents: type: array example: [] guidance_notice: type: string example: 'Guidance is generated from configurable rules and recorded business facts. Review items marked professional review and verify official sources before relying on them.' options: type: object properties: constitutions: type: object properties: proprietorship: type: string example: Proprietorship partnership: type: string example: 'Partnership firm' llp: type: string example: 'Limited Liability Partnership' private_limited: type: string example: 'Private limited company' public_limited: type: string example: 'Public limited company' opc: type: string example: 'One Person Company' huf: type: string example: 'Hindu Undivided Family' trust_society: type: string example: 'Trust / society' cooperative: type: string example: 'Co-operative society' government: type: string example: 'Government entity' other: type: string example: Other supply_types: type: object properties: goods: type: string example: 'Goods only' services: type: string example: 'Services only' both: type: string example: 'Goods and services' gst_statuses: type: object properties: not_assessed: type: string example: 'Not assessed' not_registered: type: string example: 'Not registered' pending: type: string example: 'Application pending' active: type: string example: 'Registered / active' suspended: type: string example: Suspended cancelled: type: string example: 'Cancelled / surrendered' gst_registration_types: type: object properties: normal: type: string example: 'Normal taxpayer' composition: type: string example: 'Composition taxpayer' casual: type: string example: 'Casual taxable person' non_resident: type: string example: 'Non-resident taxable person' isd: type: string example: 'Input Service Distributor (ISD)' tds: type: string example: 'Tax Deductor (TDS)' tcs: type: string example: 'Tax Collector / e-commerce operator (TCS)' sez_unit: type: string example: 'SEZ unit' sez_developer: type: string example: 'SEZ developer' oidar: type: string example: 'OIDAR / online money gaming supplier' uin: type: string example: 'Unique Identity Number holder' temporary: type: string example: 'Temporary registration' meta: type: object properties: available_categories: type: array example: - id: 13 slug: food-service name: 'Restaurant, café and food service' nic_section: I description: 'Restaurants, cafés, caterers, cloud kitchens and mobile food service.' items: type: object properties: id: type: integer example: 13 slug: type: string example: food-service name: type: string example: 'Restaurant, café and food service' nic_section: type: string example: I description: type: string example: 'Restaurants, cafés, caterers, cloud kitchens and mobile food service.' tags: - 'Business compliance guidance' requestBody: required: true content: application/json: schema: type: object properties: state_code: type: string description: 'Two-character GST state/UT code.' example: '29' category_ids: type: array description: 'Must match an existing stored value.' example: - 13 items: type: integer constitution: type: string description: 'Legal constitution of the business.' example: proprietorship enum: - proprietorship - partnership - llp - private_limited - public_limited - opc - huf - trust_society - cooperative - government - other pan: type: string description: 'PAN. Required for a business marked not registered under GST. Omit to keep the saved PAN. Must match the regex /^[A-Z]{5}[0-9]{4}[A-Z]$/. Must be 10 characters.' example: ABCDE1234F nullable: true district: type: string description: 'District containing the principal place of business. Must not be greater than 96 characters.' example: 'Bengaluru Urban' nullable: true local_body: type: string description: 'Municipality, corporation or panchayat. Must not be greater than 128 characters.' example: BBMP nullable: true premises_type: type: string description: 'How the principal premises is occupied.' example: rented enum: - owned - rented - leased - home - mobile - virtual - other supply_type: type: string description: 'Whether the business supplies goods, services or both.' example: both enum: - goods - services - both gst_registration_status: type: string description: 'Declared GST registration status.' example: not_registered enum: - not_assessed - not_registered - pending - active - suspended - cancelled previous_year_turnover: type: number description: 'Previous financial-year PAN-wide aggregate turnover in rupees. Must be at least 0. Must not be greater than 999999999999.99.' example: 1800000 other_pan_turnover: type: number description: 'Current-year PAN-wide turnover not recorded in this workspace, in rupees. Must be at least 0. Must not be greater than 999999999999.99.' example: 250000 employee_count: type: integer description: 'Direct employee count. Must be at least 0. Must not be greater than 1000000.' example: 4 contract_worker_count: type: integer description: 'Contract worker count. Must be at least 0. Must not be greater than 1000000.' example: 0 female_employee_count: type: integer description: 'Women employees, not exceeding employee_count. Must be at least 0. Must not be greater than 1000000.' example: 2 interstate_sales: type: boolean description: '' example: false ecommerce_sales: type: boolean description: '' example: true imports: type: boolean description: '' example: false exports: type: boolean description: '' example: false manufactures: type: boolean description: '' example: false food_business: type: boolean description: '' example: true uses_weighing_scale: type: boolean description: '' example: true packs_or_imports_goods: type: boolean description: '' example: false pharmacy: type: boolean description: '' example: false serves_alcohol: type: boolean description: '' example: false fire_risk: type: boolean description: '' example: false pollution_activity: type: boolean description: '' example: false reminder_days: type: array description: 'Must be at least 0. Must not be greater than 365.' example: - 90 - 60 - 30 - 7 - 0 items: type: integer required: - state_code - category_ids - constitution - premises_type - supply_type - gst_registration_status - previous_year_turnover - other_pan_turnover - employee_count - contract_worker_count - female_employee_count - interstate_sales - ecommerce_sales - imports - exports - manufactures - food_business - uses_weighing_scale - packs_or_imports_goods - pharmacy - serves_alcohol - fire_risk - pollution_activity parameters: - in: path name: business_id description: 'The ID of the business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business_id}/compliance/assess': post: summary: 'Re-run assessment against the latest published rule revisions.' operationId: reRunAssessmentAgainstTheLatestPublishedRuleRevisions description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: matched: 8 closed: 1 properties: data: type: object properties: matched: type: integer example: 8 closed: type: integer example: 1 tags: - 'Business compliance guidance' parameters: - in: path name: business_id description: 'The ID of the business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business_id}/compliance/gst-registrations': post: summary: '' operationId: postApiV1BusinessesBusiness_idComplianceGstRegistrations description: '' parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: id: 32 business_id: 16 registration_type: composition registration_status: active gstin: 29ABCDE1234F1Z5 uin: null state_code: '29' is_primary: true valid_from: '2026-04-01T00:00:00.000000Z' valid_until: null created_at: '2026-08-20T06:30:00.000000Z' updated_at: '2026-08-20T06:30:00.000000Z' properties: data: type: object properties: id: type: integer example: 32 business_id: type: integer example: 16 registration_type: type: string example: composition registration_status: type: string example: active gstin: type: string example: 29ABCDE1234F1Z5 uin: type: string example: null nullable: true state_code: type: string example: '29' is_primary: type: boolean example: true valid_from: type: string example: '2026-04-01T00:00:00.000000Z' valid_until: type: string example: null nullable: true created_at: type: string example: '2026-08-20T06:30:00.000000Z' updated_at: type: string example: '2026-08-20T06:30:00.000000Z' tags: - 'Business compliance guidance' requestBody: required: true content: application/json: schema: type: object properties: registration_type: type: string description: 'GST taxpayer/registration profile.' example: composition enum: - normal - composition - casual - non_resident - isd - tds - tcs - sez_unit - sez_developer - oidar - uin - temporary registration_status: type: string description: 'Current portal status.' example: active enum: - pending - active - suspended - cancelled gstin: type: string description: '15-character GSTIN. Required except for a UIN record. This field is required unless registration_type is in uin. Must match the regex /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/. Must be 15 characters.' example: 29ABCDE1234F1Z5 nullable: true uin: type: string description: 'Unique Identity Number for UIN records. This field is required when registration_type is uin. Must not be greater than 32 characters.' example: null nullable: true state_code: type: string description: 'Two-character state/UT code; must match the GSTIN prefix.' example: '29' nullable: true is_primary: type: boolean description: 'Whether this is the primary invoicing registration.' example: true valid_from: type: string description: 'Must be a valid date.' example: '2026-04-01' nullable: true valid_until: type: string description: 'Must be a valid date. Must be a date after or equal to valid_from.' example: null nullable: true required: - registration_type - registration_status - is_primary parameters: - in: path name: business_id description: 'The ID of the business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business_id}/compliance/gst-registrations/{gstRegistration_id}': delete: summary: '' operationId: deleteApiV1BusinessesBusiness_idComplianceGstRegistrationsGstRegistration_id description: '' parameters: [] responses: 204: description: '' content: application/json: schema: type: object example: {} properties: {} tags: - 'Business compliance guidance' parameters: - in: path name: business_id description: 'The ID of the business.' example: 1 required: true schema: type: integer - in: path name: gstRegistration_id description: 'The ID of the gstRegistration.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business_id}/compliance/obligations/{complianceObligation_id}': get: summary: '' operationId: getApiV1BusinessesBusiness_idComplianceObligationsComplianceObligation_id description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 81 rule_code: fssai-registration-licence rule_version: 1 title: 'FSSAI registration or food licence' applicability_reason: 'Every food business operator must hold the matching registration or licence. Matched using Guidance Shop facts.' applicability_level: required obligation_type: licence authority_name: 'Food Safety and Standards Authority of India' source_url: 'https://fssai.gov.in/business/registration' application_url: 'https://foscos.fssai.gov.in/' status: obtained reference_number: FSSAI-10010022000123 issued_on: '2026-04-01' due_on: null expires_on: '2027-03-31' notes: 'Certificate verified.' manual_override: false last_assessed_at: '2026-08-20T12:00:00+05:30' documents: - id: 44 original_name: fssai-certificate.pdf mime_type: application/pdf size: 102400 document_number: FSSAI-10010022000123 issued_on: '2026-04-01' expires_on: '2027-03-31' is_current: true download_url: 'https://billing.example.com/api/v1/businesses/16/compliance/documents/44/download' properties: data: type: object properties: id: type: integer example: 81 rule_code: type: string example: fssai-registration-licence rule_version: type: integer example: 1 title: type: string example: 'FSSAI registration or food licence' applicability_reason: type: string example: 'Every food business operator must hold the matching registration or licence. Matched using Guidance Shop facts.' applicability_level: type: string example: required obligation_type: type: string example: licence authority_name: type: string example: 'Food Safety and Standards Authority of India' source_url: type: string example: 'https://fssai.gov.in/business/registration' application_url: type: string example: 'https://foscos.fssai.gov.in/' status: type: string example: obtained reference_number: type: string example: FSSAI-10010022000123 issued_on: type: string example: '2026-04-01' due_on: type: string example: null nullable: true expires_on: type: string example: '2027-03-31' notes: type: string example: 'Certificate verified.' manual_override: type: boolean example: false last_assessed_at: type: string example: '2026-08-20T12:00:00+05:30' documents: type: array example: - id: 44 original_name: fssai-certificate.pdf mime_type: application/pdf size: 102400 document_number: FSSAI-10010022000123 issued_on: '2026-04-01' expires_on: '2027-03-31' is_current: true download_url: 'https://billing.example.com/api/v1/businesses/16/compliance/documents/44/download' items: type: object properties: id: type: integer example: 44 original_name: type: string example: fssai-certificate.pdf mime_type: type: string example: application/pdf size: type: integer example: 102400 document_number: type: string example: FSSAI-10010022000123 issued_on: type: string example: '2026-04-01' expires_on: type: string example: '2027-03-31' is_current: type: boolean example: true download_url: type: string example: 'https://billing.example.com/api/v1/businesses/16/compliance/documents/44/download' tags: - 'Business compliance guidance' patch: summary: '' operationId: patchApiV1BusinessesBusiness_idComplianceObligationsComplianceObligation_id description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 81 rule_code: fssai-registration-licence rule_version: 1 title: 'FSSAI registration or food licence' applicability_reason: 'Every food business operator must hold the matching registration or licence. Matched using Guidance Shop facts.' applicability_level: required obligation_type: licence authority_name: 'Food Safety and Standards Authority of India' source_url: 'https://fssai.gov.in/business/registration' application_url: 'https://foscos.fssai.gov.in/' status: obtained reference_number: FSSAI-10010022000123 issued_on: '2026-04-01' due_on: null expires_on: '2027-03-31' notes: 'Certificate verified.' manual_override: false last_assessed_at: '2026-08-20T12:00:00+05:30' documents: - id: 44 original_name: fssai-certificate.pdf mime_type: application/pdf size: 102400 document_number: FSSAI-10010022000123 issued_on: '2026-04-01' expires_on: '2027-03-31' is_current: true download_url: 'https://billing.example.com/api/v1/businesses/16/compliance/documents/44/download' properties: data: type: object properties: id: type: integer example: 81 rule_code: type: string example: fssai-registration-licence rule_version: type: integer example: 1 title: type: string example: 'FSSAI registration or food licence' applicability_reason: type: string example: 'Every food business operator must hold the matching registration or licence. Matched using Guidance Shop facts.' applicability_level: type: string example: required obligation_type: type: string example: licence authority_name: type: string example: 'Food Safety and Standards Authority of India' source_url: type: string example: 'https://fssai.gov.in/business/registration' application_url: type: string example: 'https://foscos.fssai.gov.in/' status: type: string example: obtained reference_number: type: string example: FSSAI-10010022000123 issued_on: type: string example: '2026-04-01' due_on: type: string example: null nullable: true expires_on: type: string example: '2027-03-31' notes: type: string example: 'Certificate verified.' manual_override: type: boolean example: false last_assessed_at: type: string example: '2026-08-20T12:00:00+05:30' documents: type: array example: - id: 44 original_name: fssai-certificate.pdf mime_type: application/pdf size: 102400 document_number: FSSAI-10010022000123 issued_on: '2026-04-01' expires_on: '2027-03-31' is_current: true download_url: 'https://billing.example.com/api/v1/businesses/16/compliance/documents/44/download' items: type: object properties: id: type: integer example: 44 original_name: type: string example: fssai-certificate.pdf mime_type: type: string example: application/pdf size: type: integer example: 102400 document_number: type: string example: FSSAI-10010022000123 issued_on: type: string example: '2026-04-01' expires_on: type: string example: '2027-03-31' is_current: type: boolean example: true download_url: type: string example: 'https://billing.example.com/api/v1/businesses/16/compliance/documents/44/download' tags: - 'Business compliance guidance' requestBody: required: true content: application/json: schema: type: object properties: status: type: string description: 'Owner-tracked status. not_applicable requires notes.' example: obtained enum: - action_required - in_progress - obtained - not_applicable - expired - no_longer_applicable reference_number: type: string description: 'Application, registration or licence number. Must not be greater than 128 characters.' example: LIC-2026-1001 nullable: true issued_on: type: string description: 'Must be a valid date.' example: '2026-04-01' nullable: true due_on: type: string description: 'Must be a valid date.' example: null nullable: true expires_on: type: string description: 'Must be a valid date. Must be a date after or equal to issued_on.' example: '2027-03-31' nullable: true notes: type: string description: 'Must not be greater than 5000 characters.' example: 'Renewal filed by the accountant.' nullable: true required: - status parameters: - in: path name: business_id description: 'The ID of the business.' example: 1 required: true schema: type: integer - in: path name: complianceObligation_id description: 'The ID of the complianceObligation.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business_id}/compliance/obligations/{complianceObligation_id}/documents': post: summary: '' operationId: postApiV1BusinessesBusiness_idComplianceObligationsComplianceObligation_idDocuments description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 81 rule_code: fssai-registration-licence rule_version: 1 title: 'FSSAI registration or food licence' applicability_reason: 'Every food business operator must hold the matching registration or licence. Matched using Guidance Shop facts.' applicability_level: required obligation_type: licence authority_name: 'Food Safety and Standards Authority of India' source_url: 'https://fssai.gov.in/business/registration' application_url: 'https://foscos.fssai.gov.in/' status: obtained reference_number: FSSAI-10010022000123 issued_on: '2026-04-01' due_on: null expires_on: '2027-03-31' notes: 'Certificate verified.' manual_override: false last_assessed_at: '2026-08-20T12:00:00+05:30' documents: - id: 44 original_name: fssai-certificate.pdf mime_type: application/pdf size: 102400 document_number: FSSAI-10010022000123 issued_on: '2026-04-01' expires_on: '2027-03-31' is_current: true download_url: 'https://billing.example.com/api/v1/businesses/16/compliance/documents/44/download' properties: data: type: object properties: id: type: integer example: 81 rule_code: type: string example: fssai-registration-licence rule_version: type: integer example: 1 title: type: string example: 'FSSAI registration or food licence' applicability_reason: type: string example: 'Every food business operator must hold the matching registration or licence. Matched using Guidance Shop facts.' applicability_level: type: string example: required obligation_type: type: string example: licence authority_name: type: string example: 'Food Safety and Standards Authority of India' source_url: type: string example: 'https://fssai.gov.in/business/registration' application_url: type: string example: 'https://foscos.fssai.gov.in/' status: type: string example: obtained reference_number: type: string example: FSSAI-10010022000123 issued_on: type: string example: '2026-04-01' due_on: type: string example: null nullable: true expires_on: type: string example: '2027-03-31' notes: type: string example: 'Certificate verified.' manual_override: type: boolean example: false last_assessed_at: type: string example: '2026-08-20T12:00:00+05:30' documents: type: array example: - id: 44 original_name: fssai-certificate.pdf mime_type: application/pdf size: 102400 document_number: FSSAI-10010022000123 issued_on: '2026-04-01' expires_on: '2027-03-31' is_current: true download_url: 'https://billing.example.com/api/v1/businesses/16/compliance/documents/44/download' items: type: object properties: id: type: integer example: 44 original_name: type: string example: fssai-certificate.pdf mime_type: type: string example: application/pdf size: type: integer example: 102400 document_number: type: string example: FSSAI-10010022000123 issued_on: type: string example: '2026-04-01' expires_on: type: string example: '2027-03-31' is_current: type: boolean example: true download_url: type: string example: 'https://billing.example.com/api/v1/businesses/16/compliance/documents/44/download' tags: - 'Business compliance guidance' requestBody: required: true content: multipart/form-data: schema: type: object properties: document: type: string format: binary description: 'Private PDF/JPEG/PNG/WebP evidence, maximum 10 MB. Must be a file. Must not be greater than 10240 kilobytes.' document_number: type: string description: 'Certificate or licence number. Must not be greater than 128 characters.' example: FSSAI-10010022000123 nullable: true issued_on: type: string description: 'Must be a valid date.' example: '2026-04-01' nullable: true expires_on: type: string description: 'Must be a valid date. Must be a date after or equal to issued_on.' example: '2027-03-31' nullable: true required: - document parameters: - in: path name: business_id description: 'The ID of the business.' example: 1 required: true schema: type: integer - in: path name: complianceObligation_id description: 'The ID of the complianceObligation.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business_id}/compliance/documents/{complianceDocument_id}/download': get: summary: '' operationId: getApiV1BusinessesBusiness_idComplianceDocumentsComplianceDocument_idDownload description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Business compliance guidance' parameters: - in: path name: business_id description: 'The ID of the business.' example: 1 required: true schema: type: integer - in: path name: complianceDocument_id description: 'The ID of the complianceDocument.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/gst': get: summary: 'View GST workspace.' operationId: viewGSTWorkspace description: 'Unregistered businesses cannot access this endpoint, regardless of plan.' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 403: description: '' content: application/json: schema: type: object example: message: 'GST features are unavailable for an unregistered business.' properties: message: type: string example: 'GST features are unavailable for an unregistered business.' tags: - 'GST compliance' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/gst/gstr1': get: summary: 'Export GSTR-1.' operationId: exportGSTR1 description: 'Unregistered businesses cannot access this endpoint, regardless of plan.' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 403: description: '' content: application/json: schema: type: object example: message: 'GST features are unavailable for an unregistered business.' properties: message: type: string example: 'GST features are unavailable for an unregistered business.' tags: - 'GST compliance' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/gst/gstr2b': post: summary: 'Import GSTR-2B.' operationId: importGSTR2B description: 'Unregistered businesses cannot access this endpoint, regardless of plan.' parameters: [] responses: 403: description: '' content: application/json: schema: type: object example: message: 'GST features are unavailable for an unregistered business.' properties: message: type: string example: 'GST features are unavailable for an unregistered business.' tags: - 'GST compliance' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/gst/gstr3b': get: summary: 'Unregistered businesses cannot access this endpoint, regardless of plan.' operationId: unregisteredBusinessesCannotAccessThisEndpointRegardlessOfPlan description: '' parameters: - in: query name: period description: 'The month, as YYYY-MM.' example: 2026-08 required: true schema: type: string description: 'The month, as YYYY-MM.' example: 2026-08 - in: query name: revision description: 'A specific revision. Defaults to the latest.' example: 1 required: false schema: type: integer description: 'A specific revision. Defaults to the latest.' example: 1 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 403: description: '' content: text/plain: schema: type: string example: "{\"message\":\"GST features are unavailable for an unregistered business.\"}\n\nPrepare GSTR-3B for a month.\n\nSections 3.1, 3.2, 4, 5 and 6.1 computed from saved documents — never from current\nmasters, so re-reading an old period reproduces it exactly. A locked revision answers with\nthe figures that were locked rather than recomputing; a draft recomputes on every read.\n\n`itc_basis` says where section 4's other ITC came from: `gstr2b_reconciled` when a GSTR-2B\nhas been imported for the period, `books` when it has not. `not_derived` lists the rows of\nthe form Dukanam cannot compute, which the filer enters on the portal.\n\nThis is preparation data. Dukanam does not file returns.\n\n**Offline:** online-only. It reads a month of recorded transactions, so it belongs in neither the\noffline cache nor the write queue." tags: - 'GST compliance' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/gst/gstr3b/{period}/export': get: summary: 'Unregistered businesses cannot access this endpoint, regardless of plan.' operationId: unregisteredBusinessesCannotAccessThisEndpointRegardlessOfPlan description: '' parameters: - in: query name: format description: '`json` or `xlsx`. Defaults to `json`.' example: xlsx required: false schema: type: string description: '`json` or `xlsx`. Defaults to `json`.' example: xlsx - in: query name: revision description: 'A specific revision. Defaults to the latest.' example: 1 required: false schema: type: integer description: 'A specific revision. Defaults to the latest.' example: 1 responses: 200: description: 'JSON export' content: application/json: schema: type: object example: period: 2026-08 status: locked sections: {} properties: period: type: string example: 2026-08 status: type: string example: locked sections: type: object properties: {} 403: description: '' content: text/plain: schema: type: string example: "{\"message\":\"GST features are unavailable for an unregistered business.\"}\n\nDownload a prepared GSTR-3B.\n\n`format=json` returns the same payload as the summary endpoint as a file; `format=xlsx`\nreturns a workbook with one sheet per section, money as a number in rupees and a frozen\nheader row, for handing to the shop's accountant.\n\n**Offline:** online-only. The file is built on the server from recorded transactions." tags: - 'GST compliance' requestBody: required: false content: application/json: schema: type: object properties: format: type: string description: '' example: json enum: - json - xlsx nullable: true revision: type: integer description: 'Must be at least 1.' example: 16 nullable: true parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: period description: 'The month, as YYYY-MM.' example: 2026-08 required: true schema: type: string '/api/v1/businesses/{business}/gst/gstr3b/{period}/lock': post: summary: 'Unregistered businesses cannot access this endpoint, regardless of plan.' operationId: unregisteredBusinessesCannotAccessThisEndpointRegardlessOfPlan description: '' parameters: [] responses: 403: description: '' content: text/plain: schema: type: string example: "{\"message\":\"GST features are unavailable for an unregistered business.\"}\n\nLock a GSTR-3B period after filing.\n\nComputes the figures once more, stores them and hashes them. The period then answers from\nthat snapshot however the books move afterwards. Locking twice answers `422`; so does\nlocking a period that has not ended, because it cannot have been filed yet.\n\n**Offline:** online-only. Nothing here may be replayed from the write queue — a lock\nrecords a filing that has already happened, and a stale replay would freeze stale figures." tags: - 'GST compliance' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: period description: 'The month, as YYYY-MM.' example: 2026-08 required: true schema: type: string '/api/v1/businesses/{business}/gst/gstr3b/{period}/amend': post: summary: 'Unregistered businesses cannot access this endpoint, regardless of plan.' operationId: unregisteredBusinessesCannotAccessThisEndpointRegardlessOfPlan description: '' parameters: [] responses: 403: description: '' content: text/plain: schema: type: string example: "{\"message\":\"GST features are unavailable for an unregistered business.\"}\n\nOpen the next revision of a locked period.\n\nThe locked revision stays readable exactly as it was filed and the new revision starts as\na draft that recomputes from the corrected books. Amending a period that is not locked\nanswers `422` — an unlocked period is simply edited by correcting the documents.\n\n**Offline:** online-only, for the same reason a lock is." tags: - 'GST compliance' requestBody: required: false content: application/json: schema: type: object properties: reason: type: string description: 'Why the period is being amended, up to 255 characters.' example: 'Missed a purchase invoice' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: period description: 'The month, as YYYY-MM.' example: 2026-08 required: true schema: type: string '/api/v1/businesses/{business}/gst/gstr9': get: summary: 'Unregistered businesses cannot access this endpoint, regardless of plan.' operationId: unregisteredBusinessesCannotAccessThisEndpointRegardlessOfPlan description: '' parameters: - in: query name: financial_year description: 'The year, as YYYY-YY, starting in April.' example: 2026-27 required: true schema: type: string description: 'The year, as YYYY-YY, starting in April.' example: 2026-27 - in: query name: revision description: 'A specific revision. Defaults to the latest.' example: 1 required: false schema: type: integer description: 'A specific revision. Defaults to the latest.' example: 1 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 403: description: '' content: text/plain: schema: type: string example: "{\"message\":\"GST features are unavailable for an unregistered business.\"}\n\nPrepare the GSTR-9 annual working for a financial year.\n\nThis is the **working data**, not the return: the year's outward supplies by nature\n(tables 4 and 5), ITC availed and reversed (6 and 7), tax paid as it was declared (9), the\nHSN summaries (17 and 18), an outward and inward summary by rate, and the month-by-month\nreconciliation of the sales register against GSTR-3B against the journals. The filer reads\nthese onto the portal; Dukanam submits nothing.\n\nTables 4, 5, 6, 7, 17 and 18 are computed from the year's saved documents. Table 9 and the\nreconciliation are rolled up from the twelve monthly GSTR-3B returns, using a month's\n**locked** revision where it has one — that table reports what was declared, not what the\nbooks say today. `sections.9.filed_months` says how many of the twelve were locked, and\nevery reconciliation row carries `gstr3b_source` of `filed` or `computed`.\n\n`applicability` measures this workspace's turnover against the configured threshold. It is\nnever a gate: aggregate turnover is a PAN-wide figure Dukanam cannot see, and the threshold\nitself moves. A locked revision answers with the figures that were locked; a draft\nrecomputes on every read." tags: - 'GST compliance' requestBody: required: true content: application/json: schema: type: object properties: financial_year: type: string description: 'Must not be greater than 7 characters.' example: bngzmiy revision: type: integer description: 'Must be at least 1.' example: 16 nullable: true required: - financial_year parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/gst/gstr9/{financialYear}/export': get: summary: 'Unregistered businesses cannot access this endpoint, regardless of plan.' operationId: unregisteredBusinessesCannotAccessThisEndpointRegardlessOfPlan description: '' parameters: - in: query name: format description: '`json` or `xlsx`. Defaults to `json`.' example: xlsx required: false schema: type: string description: '`json` or `xlsx`. Defaults to `json`.' example: xlsx - in: query name: revision description: 'A specific revision. Defaults to the latest.' example: 1 required: false schema: type: integer description: 'A specific revision. Defaults to the latest.' example: 1 responses: 200: description: 'JSON export' content: application/json: schema: type: object example: financial_year: 2026-27 status: locked sections: {} properties: financial_year: type: string example: 2026-27 status: type: string example: locked sections: type: object properties: {} 403: description: '' content: text/plain: schema: type: string example: "{\"message\":\"GST features are unavailable for an unregistered business.\"}\n\nDownload a prepared GSTR-9 working.\n\n`format=json` returns the same payload as the summary endpoint as a file; `format=xlsx`\nreturns the workbook to hand to the shop's accountant — one sheet per table plus the\nreconciliation, money as a number in rupees and a frozen header row." tags: - 'GST compliance' requestBody: required: false content: application/json: schema: type: object properties: format: type: string description: '' example: json enum: - json - xlsx nullable: true revision: type: integer description: 'Must be at least 1.' example: 16 nullable: true parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: financialYear description: 'The year, as YYYY-YY.' example: 2026-27 required: true schema: type: string '/api/v1/businesses/{business}/gst/gstr9/{financialYear}/lock': post: summary: 'Unregistered businesses cannot access this endpoint, regardless of plan.' operationId: unregisteredBusinessesCannotAccessThisEndpointRegardlessOfPlan description: '' parameters: [] responses: 403: description: '' content: text/plain: schema: type: string example: "{\"message\":\"GST features are unavailable for an unregistered business.\"}\n\nLock a GSTR-9 year after filing.\n\nComputes the working once more, stores it and hashes it. The year then answers from that\nsnapshot however the books move afterwards. Locking twice answers `422`; so does locking a\nyear that has not ended, because its annual return cannot have been filed yet." tags: - 'GST compliance' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: financialYear description: 'The year, as YYYY-YY.' example: 2026-27 required: true schema: type: string '/api/v1/businesses/{business}/gst/gstr9/{financialYear}/amend': post: summary: 'Unregistered businesses cannot access this endpoint, regardless of plan.' operationId: unregisteredBusinessesCannotAccessThisEndpointRegardlessOfPlan description: '' parameters: [] responses: 403: description: '' content: text/plain: schema: type: string example: "{\"message\":\"GST features are unavailable for an unregistered business.\"}\n\nOpen the next revision of a locked year.\n\nThe locked revision stays readable exactly as it was filed and the new revision starts as a\ndraft that recomputes from the corrected books. Amending a year that is not locked answers\n`422` — an unlocked year is simply corrected by correcting the documents." tags: - 'GST compliance' requestBody: required: false content: application/json: schema: type: object properties: reason: type: string description: 'Why the year is being amended, up to 255 characters.' example: 'Credit note saved after filing' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: financialYear description: 'The year, as YYYY-YY.' example: 2026-27 required: true schema: type: string '/api/v1/businesses/{business}/billing': get: summary: 'List plans and activation availability' operationId: listPlansAndActivationAvailability description: "Use `razorpay_checkout_enabled` for paid checkout. `paid_plan_activation_enabled` refers only to the\nsupport-only manual activation fallback, while each plan's `activation_available` covers either path.\nEnded paid subscriptions automatically fall back to Free Essentials, preserving paid history and trial ineligibility." parameters: [] responses: 200: description: 'Free fallback (excerpt)' content: application/json: schema: type: object example: data: business: id: 1 free_transition: subscription_id: 3 reason: trial_ended requires_acknowledgement: true invoice_allowance: limit: 50 used: 70 remaining: 0 can_create: false resets_at: '2026-09-30T18:30:00.000000Z' paid_plan_activation_enabled: false razorpay_checkout_enabled: true plans: [] properties: data: type: object properties: business: type: object properties: id: type: integer example: 1 free_transition: type: object properties: subscription_id: type: integer example: 3 description: 'Current Free subscription ID to acknowledge.' reason: type: string example: trial_ended description: 'Either trial_ended or subscription_ended.' requires_acknowledgement: type: boolean example: true description: 'Show a plan choice when true; never block access to existing invoices.' description: 'Owner-only automatic fallback notice; null when no automatic fallback applies.' invoice_allowance: type: object properties: limit: type: integer example: 50 description: 'Monthly limit; null means unlimited.' used: type: integer example: 70 description: 'Invoices created this application-calendar month, including voided invoices.' remaining: type: integer example: 0 description: 'Remaining allowance, clamped to zero; null means unlimited.' can_create: type: boolean example: false description: 'Whether another invoice fits in the allowance.' resets_at: type: string example: '2026-09-30T18:30:00.000000Z' description: 'ISO-8601 timestamp of the next monthly reset.' description: 'Calendar-month usage, including trial invoices. Downgrading never resets it.' paid_plan_activation_enabled: type: boolean example: false description: 'Whether customers may self-activate paid plans.' razorpay_checkout_enabled: type: boolean example: true description: 'Whether Razorpay subscription checkout is configured.' plans: type: array example: [] tags: - Billing parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/billing/payments': get: summary: 'List subscription payments' operationId: listSubscriptionPayments description: "Returns successful subscription charges and mandate authorisations for the business owner.\nOnly captured recurring charges expose an invoice download URL." parameters: - in: query name: per_page description: 'Results per page.' example: 20 required: false schema: type: integer description: 'Results per page.' example: 20 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Billing parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/billing/payments/{subscriptionPayment_id}/invoice': get: summary: 'Download a subscription invoice' operationId: downloadASubscriptionInvoice description: 'Downloads the immutable PDF invoice for a captured recurring subscription charge.' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Billing parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: subscriptionPayment_id description: 'The ID of the subscriptionPayment.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/billing/continue-free': post: summary: 'Continue on Free after a subscription ends' operationId: continueOnFreeAfterASubscriptionEnds description: "Owner only. Acknowledges the current automatic Free fallback without changing plans,\nresetting invoice usage, or granting another trial. Repeating the same request is safe.\nA stale or unrelated subscription ID returns HTTP 422; another tenant returns HTTP 404.\nExisting invoices remain accessible before and after acknowledgement." parameters: [] responses: 200: description: 'Acknowledged Free fallback (excerpt)' content: application/json: schema: type: object example: data: id: 1 free_transition: subscription_id: 3 reason: trial_ended requires_acknowledgement: false invoice_allowance: limit: 50 used: 70 remaining: 0 can_create: false resets_at: '2026-09-30T18:30:00.000000Z' properties: data: type: object properties: id: type: integer example: 1 free_transition: type: object properties: subscription_id: type: integer example: 3 reason: type: string example: trial_ended requires_acknowledgement: type: boolean example: false description: 'False after acknowledgement.' invoice_allowance: type: object properties: limit: type: integer example: 50 used: type: integer example: 70 remaining: type: integer example: 0 can_create: type: boolean example: false resets_at: type: string example: '2026-09-30T18:30:00.000000Z' 422: description: '' content: application/json: schema: type: object example: message: 'Your plan has changed. Refresh billing before continuing.' errors: subscription_id: - 'Your plan has changed. Refresh billing before continuing.' properties: message: type: string example: 'Your plan has changed. Refresh billing before continuing.' errors: type: object properties: subscription_id: type: array example: - 'Your plan has changed. Refresh billing before continuing.' items: type: string tags: - Billing requestBody: required: true content: application/json: schema: type: object properties: subscription_id: type: integer description: 'The current free_transition.subscription_id from the business resource.' example: 3 required: - subscription_id parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/billing/change': post: summary: 'Change the current plan' operationId: changeTheCurrentPlan description: "Paid plans return HTTP 422 while online paid-plan activation is disabled, except for an upgrade from an\nactive Razorpay paid plan. Smart Books to Business is updated in place immediately; Razorpay credits the\nunused current-plan value and charges the prorated difference without creating another trial.\nChanging away from a Razorpay-backed plan cancels that provider subscription immediately before the new\nplan is activated, so mobile clients must confirm an immediate loss of paid access before submitting. If\nRazorpay cannot confirm cancellation, the endpoint returns HTTP 422 and leaves the current plan unchanged.\nSend an `Idempotency-Key` when applying a prorated upgrade. Retrying with the same key returns the completed\noperation and never sends a second provider update; concurrent retries return HTTP 409 until it completes.\nA conflicting target or interval also returns HTTP 409 while another transition owns the subscription." parameters: - in: header name: Idempotency-Key description: '' example: upgrade-2026-08-25-01 schema: type: string responses: 409: description: '' content: application/json: schema: type: object example: message: 'This plan change is already processing. Retry with the same idempotency key.' properties: message: type: string example: 'This plan change is already processing. Retry with the same idempotency key.' 422: description: '' content: application/json: schema: type: object example: message: 'Online paid-plan activation is not configured. Contact support to change this plan.' errors: plan_id: - 'Online paid-plan activation is not configured. Contact support to change this plan.' properties: message: type: string example: 'Online paid-plan activation is not configured. Contact support to change this plan.' errors: type: object properties: plan_id: type: array example: - 'Online paid-plan activation is not configured. Contact support to change this plan.' items: type: string tags: - Billing requestBody: required: true content: application/json: schema: type: object properties: plan_id: type: string description: 'The active plan ID selected from the billing discovery endpoint. Must match an existing stored value.' example: 3 billing_interval: type: string description: 'The selected billing frequency.' example: monthly enum: - monthly - yearly idempotency_key: type: string description: 'Optional body equivalent of the Idempotency-Key header for safely retrying a prorated upgrade. Must be at least 8 characters. Must not be greater than 128 characters.' example: upgrade-2026-08-25-01 nullable: true required: - plan_id - billing_interval parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/billing/checkout': post: summary: 'Start Razorpay subscription checkout' operationId: startRazorpaySubscriptionCheckout description: "Creates a price-versioned Razorpay subscription. Mobile clients must pass the returned `subscription_id`\nto Razorpay Standard Checkout and then send the signed result to the confirm endpoint. The effective plan\nprice is calculated on the server and must be at least 100 paise; clients cannot submit an arbitrary amount." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: checkout_id: 1 key: rzp_test_example subscription_id: sub_example name: Dukanam description: 'Smart Books · Monthly' amount_paise: 99900 currency: INR trial_days: 14 prefill: name: 'Shop Owner' email: owner@example.com contact: '9876543210' properties: data: type: object properties: checkout_id: type: integer example: 1 key: type: string example: rzp_test_example subscription_id: type: string example: sub_example name: type: string example: Dukanam description: type: string example: 'Smart Books · Monthly' amount_paise: type: integer example: 99900 currency: type: string example: INR trial_days: type: integer example: 14 prefill: type: object properties: name: type: string example: 'Shop Owner' email: type: string example: owner@example.com contact: type: string example: '9876543210' 401: description: '' content: application/json: schema: type: object example: message: 'Razorpay rejected the configured API credentials.' properties: message: type: string example: 'Razorpay rejected the configured API credentials.' 422: description: '' content: application/json: schema: type: object example: message: 'Razorpay checkout requires a charge of at least 100 paise.' errors: plan_id: - 'Razorpay checkout requires a charge of at least 100 paise.' properties: message: type: string example: 'Razorpay checkout requires a charge of at least 100 paise.' errors: type: object properties: plan_id: type: array example: - 'Razorpay checkout requires a charge of at least 100 paise.' items: type: string tags: - Billing requestBody: required: true content: application/json: schema: type: object properties: plan_id: type: string description: 'The active plan ID selected from the billing discovery endpoint. Must match an existing stored value.' example: 3 billing_interval: type: string description: 'The selected billing frequency.' example: monthly enum: - monthly - yearly required: - plan_id - billing_interval parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/billing/checkouts/{billingCheckout_id}/confirm': post: summary: 'Confirm Razorpay subscription authorisation' operationId: confirmRazorpaySubscriptionAuthorisation description: "The server verifies the Razorpay HMAC signature and fetches provider state before granting access.\nTrial access starts only after successful payment-method authorisation; a plan without a trial waits\nfor Razorpay to report the subscription as active. When the checkout replaces another Razorpay plan,\nthe previous provider subscription is cancelled immediately before the replacement is activated." parameters: [] responses: {} tags: - Billing requestBody: required: true content: application/json: schema: type: object properties: razorpay_payment_id: type: string description: 'Must match the regex /^pay_[A-Za-z0-9]+$/. Must not be greater than 191 characters.' example: b razorpay_subscription_id: type: string description: 'Must match the regex /^sub_[A-Za-z0-9]+$/. Must not be greater than 191 characters.' example: 'n' razorpay_signature: type: string description: 'Must match the regex /^[a-f0-9]{64}$/i. Must be 64 characters.' example: gzmiyvdljnikhwaykcmyuwpwlvqwrsitcpscqldzsnrwtujwvlxjklqppwqbewtn required: - razorpay_payment_id - razorpay_subscription_id - razorpay_signature parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: billingCheckout_id description: 'The ID of the billingCheckout.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/billing/cancel': post: summary: 'Cancel the paid plan at the end of the current period.' operationId: cancelThePaidPlanAtTheEndOfTheCurrentPeriod description: "Owner only. Access continues until the provider's current cycle ends. Free referral\ntime (`subscription.provider` is `referral`) ends on its own date and cannot be cancelled." parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'Free referral time ends on its own date and does not need to be cancelled.' properties: message: type: string example: 'Free referral time ends on its own date and does not need to be cancelled.' tags: - Billing parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/billing/pos-seats': post: summary: 'Buy or give up POS logins.' operationId: buyOrGiveUpPOSLogins description: "A POS login costs the seat price per month and is added to the running subscription:\nRazorpay charges the prorated difference for the rest of the cycle immediately and\nevery renewal after it bills the new total. Owner only. The workspace must be on an\nactive paid plan billed online, and seats cannot drop below the logins already in use." parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'POS logins are billed on an active paid subscription. Choose a plan first, then add counter logins.' errors: pos_seats: - 'POS logins are billed on an active paid subscription. Choose a plan first, then add counter logins.' properties: message: type: string example: 'POS logins are billed on an active paid subscription. Choose a plan first, then add counter logins.' errors: type: object properties: pos_seats: type: array example: - 'POS logins are billed on an active paid subscription. Choose a plan first, then add counter logins.' items: type: string tags: - Billing requestBody: required: true content: application/json: schema: type: object properties: pos_seats: type: integer description: 'The total number of POS logins to pay for.' example: 2 required: - pos_seats parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/referrals/codes/{code}': get: summary: 'Check a referral code.' operationId: checkAReferralCode description: "For a registration screen opened from a shared link: tells the app whether the code\nwill be accepted and whose it is (first name only). The code may be a customer's\nreferral code or an influencer partner's link code; `referrer_type` says which.\nSend the same code as `referral_code` on registration." parameters: [] responses: 200: description: '' content: application/json: schema: oneOf: - description: '' type: object example: data: code: K7M2QX9A valid: true program_enabled: true referrer_type: customer referrer_first_name: Priya properties: data: type: object properties: code: type: string example: K7M2QX9A valid: type: boolean example: true program_enabled: type: boolean example: true referrer_type: type: string example: customer referrer_first_name: type: string example: Priya - description: 'Partner link code' type: object example: data: code: RAVI7K valid: true program_enabled: true referrer_type: partner referrer_first_name: Ravi properties: data: type: object properties: code: type: string example: RAVI7K valid: type: boolean example: true program_enabled: type: boolean example: true referrer_type: type: string example: partner referrer_first_name: type: string example: Ravi tags: - Referrals security: [] parameters: - in: path name: code description: 'The referral code.' example: K7M2QX9A required: true schema: type: string /api/v1/referrals: get: summary: 'Get the referral dashboard.' operationId: getTheReferralDashboard description: "Returns the account's referral code and link (created on first call), a ready-to-send\nWhatsApp message and `wa.me` URL, the counters for the Referral & Rewards screen, the\nrunning reward and the projected dates of every waiting one. Waiting rewards start\nonly after all paid time ends; when `paid_coverage.auto_renews` is true those dates\nmove forward each time the paid plan renews. POS counter logins cannot refer (403)." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: program: enabled: true reward_months: 1 reward_plan: id: 2 name: 'Smart Books' slug: smart reward_plan_source: program_default referral_code: K7M2QX9A referral_link: 'https://dukanam.com/register?ref=K7M2QX9A' share: code: K7M2QX9A link: 'https://dukanam.com/register?ref=K7M2QX9A' message: 'Hey! Check out Dukanam for billing, stock and khata. You can register using my referral link: https://dukanam.com/register?ref=K7M2QX9A' whatsapp_url: 'https://wa.me/?text=Hey%21%20Check%20out%20Dukanam' stats: total_referrals: 3 pending_referrals: 1 successful_referrals: 2 rewards_earned: 2 months_earned: 2 available_rewards: 1 available_months: 1 available_extra_days: 0 active_rewards: 1 used_rewards: 0 paid_coverage: ends_at: null auto_renews: false current_reward: id: 7 status: active months: 1 remaining_days: null plan: id: 2 name: 'Smart Books' slug: smart business: id: 17 name: 'Aarogya Medical Store' referred_name: 'Ravi Kumar' earned_at: '2026-09-01T10:00:00.000000Z' activated_at: '2026-09-01T10:00:00.000000Z' starts_at: '2026-09-01T10:00:00.000000Z' ends_at: '2026-10-01T10:00:00.000000Z' consumed_at: null upcoming_rewards: - id: 8 status: available months: 1 remaining_days: null plan: id: 2 name: 'Smart Books' slug: smart business: null referred_name: 'Meena S' earned_at: '2026-09-15T10:00:00.000000Z' activated_at: null starts_at: null ends_at: null consumed_at: null projected_starts_at: '2026-10-01T10:00:00.000000Z' projected_ends_at: '2026-11-01T10:00:00.000000Z' starts_after_paid_plan: false properties: data: type: object properties: program: type: object properties: enabled: type: boolean example: true reward_months: type: integer example: 1 reward_plan: type: object properties: id: type: integer example: 2 name: type: string example: 'Smart Books' slug: type: string example: smart description: "The plan this account's next reward grants: the plan it last paid for, otherwise the program plan." reward_plan_source: type: string example: program_default description: '`paid_plan` when the reward matches the plan this account last paid for, `program_default` when it uses the admin-configured plan.' referral_code: type: string example: K7M2QX9A description: "The account's unique referral code." referral_link: type: string example: 'https://dukanam.com/register?ref=K7M2QX9A' description: 'The registration link carrying the code as `ref`.' share: type: object properties: code: type: string example: K7M2QX9A link: type: string example: 'https://dukanam.com/register?ref=K7M2QX9A' message: type: string example: 'Hey! Check out Dukanam for billing, stock and khata. You can register using my referral link: https://dukanam.com/register?ref=K7M2QX9A' description: 'The admin-configured share message with the link filled in.' whatsapp_url: type: string example: 'https://wa.me/?text=Hey%21%20Check%20out%20Dukanam' description: 'Opens WhatsApp (app on phones, WhatsApp Web or Desktop on computers) with the message pre-filled.' stats: type: object properties: total_referrals: type: integer example: 3 pending_referrals: type: integer example: 1 description: 'Registered or subscription pending.' successful_referrals: type: integer example: 2 description: 'Bought a paid plan.' rewards_earned: type: integer example: 2 months_earned: type: integer example: 2 available_rewards: type: integer example: 1 available_months: type: integer example: 1 description: 'Whole months waiting to start.' available_extra_days: type: integer example: 0 description: 'Days carried over from rewards a paid plan interrupted.' active_rewards: type: integer example: 1 used_rewards: type: integer example: 0 paid_coverage: type: object properties: ends_at: type: string example: null nullable: true auto_renews: type: boolean example: false current_reward: type: object properties: id: type: integer example: 7 status: type: string example: active months: type: integer example: 1 remaining_days: type: string example: null nullable: true plan: type: object properties: id: type: integer example: 2 name: type: string example: 'Smart Books' slug: type: string example: smart business: type: object properties: id: type: integer example: 17 name: type: string example: 'Aarogya Medical Store' referred_name: type: string example: 'Ravi Kumar' earned_at: type: string example: '2026-09-01T10:00:00.000000Z' activated_at: type: string example: '2026-09-01T10:00:00.000000Z' starts_at: type: string example: '2026-09-01T10:00:00.000000Z' ends_at: type: string example: '2026-10-01T10:00:00.000000Z' consumed_at: type: string example: null nullable: true upcoming_rewards: type: array example: - id: 8 status: available months: 1 remaining_days: null plan: id: 2 name: 'Smart Books' slug: smart business: null referred_name: 'Meena S' earned_at: '2026-09-15T10:00:00.000000Z' activated_at: null starts_at: null ends_at: null consumed_at: null projected_starts_at: '2026-10-01T10:00:00.000000Z' projected_ends_at: '2026-11-01T10:00:00.000000Z' starts_after_paid_plan: false items: type: object properties: id: type: integer example: 8 status: type: string example: available months: type: integer example: 1 remaining_days: type: string example: null nullable: true plan: type: object properties: id: type: integer example: 2 name: type: string example: 'Smart Books' slug: type: string example: smart business: type: string example: null nullable: true referred_name: type: string example: 'Meena S' earned_at: type: string example: '2026-09-15T10:00:00.000000Z' activated_at: type: string example: null nullable: true starts_at: type: string example: null nullable: true ends_at: type: string example: null nullable: true consumed_at: type: string example: null nullable: true projected_starts_at: type: string example: '2026-10-01T10:00:00.000000Z' projected_ends_at: type: string example: '2026-11-01T10:00:00.000000Z' starts_after_paid_plan: type: boolean example: false 403: description: '' content: application/json: schema: type: object example: message: 'This action is unauthorized.' properties: message: type: string example: 'This action is unauthorized.' tags: - Referrals /api/v1/referrals/history: get: summary: 'List referral history.' operationId: listReferralHistory description: "The people who registered with the account's code, newest first, with each one's\nstatus (`registered`, `subscription_pending`, `subscription_purchased`,\n`reward_earned`, `reward_applied`) and the reward it earned." parameters: - in: query name: status description: 'Filter by one status.' example: reward_earned required: false schema: type: string description: 'Filter by one status.' example: reward_earned - in: query name: per_page description: 'Results per page, from 1 to 50.' example: 20 required: false schema: type: integer description: 'Results per page, from 1 to 50.' example: 20 responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 12 status: reward_earned status_label: 'Reward earned' steps: - status: registered label: Registered reached: true - status: subscription_pending label: 'Subscription pending' reached: true - status: subscription_purchased label: 'Subscription purchased' reached: true - status: reward_earned label: 'Reward earned' reached: true - status: reward_applied label: 'Reward applied' reached: false referred: name: 'Meena S' source: api registered_at: '2026-09-10T08:00:00.000000Z' subscription_pending_at: '2026-09-12T08:00:00.000000Z' subscription_purchased_at: '2026-09-15T10:00:00.000000Z' reward_earned_at: '2026-09-15T10:00:00.000000Z' reward_applied_at: null reward_skipped_reason: null reward: id: 8 status: available months: 1 remaining_days: null plan: id: 2 name: 'Smart Books' slug: smart business: null referred_name: null earned_at: '2026-09-15T10:00:00.000000Z' activated_at: null starts_at: null ends_at: null consumed_at: null links: {} meta: current_page: 1 per_page: 20 total: 1 properties: data: type: array example: - id: 12 status: reward_earned status_label: 'Reward earned' steps: - status: registered label: Registered reached: true - status: subscription_pending label: 'Subscription pending' reached: true - status: subscription_purchased label: 'Subscription purchased' reached: true - status: reward_earned label: 'Reward earned' reached: true - status: reward_applied label: 'Reward applied' reached: false referred: name: 'Meena S' source: api registered_at: '2026-09-10T08:00:00.000000Z' subscription_pending_at: '2026-09-12T08:00:00.000000Z' subscription_purchased_at: '2026-09-15T10:00:00.000000Z' reward_earned_at: '2026-09-15T10:00:00.000000Z' reward_applied_at: null reward_skipped_reason: null reward: id: 8 status: available months: 1 remaining_days: null plan: id: 2 name: 'Smart Books' slug: smart business: null referred_name: null earned_at: '2026-09-15T10:00:00.000000Z' activated_at: null starts_at: null ends_at: null consumed_at: null items: type: object properties: id: type: integer example: 12 status: type: string example: reward_earned status_label: type: string example: 'Reward earned' steps: type: array example: - status: registered label: Registered reached: true - status: subscription_pending label: 'Subscription pending' reached: true - status: subscription_purchased label: 'Subscription purchased' reached: true - status: reward_earned label: 'Reward earned' reached: true - status: reward_applied label: 'Reward applied' reached: false items: type: object properties: status: type: string example: registered label: type: string example: Registered reached: type: boolean example: true referred: type: object properties: name: type: string example: 'Meena S' source: type: string example: api registered_at: type: string example: '2026-09-10T08:00:00.000000Z' subscription_pending_at: type: string example: '2026-09-12T08:00:00.000000Z' subscription_purchased_at: type: string example: '2026-09-15T10:00:00.000000Z' reward_earned_at: type: string example: '2026-09-15T10:00:00.000000Z' reward_applied_at: type: string example: null nullable: true reward_skipped_reason: type: string example: null nullable: true reward: type: object properties: id: type: integer example: 8 status: type: string example: available months: type: integer example: 1 remaining_days: type: string example: null nullable: true plan: type: object properties: id: type: integer example: 2 name: type: string example: 'Smart Books' slug: type: string example: smart business: type: string example: null nullable: true referred_name: type: string example: null nullable: true earned_at: type: string example: '2026-09-15T10:00:00.000000Z' activated_at: type: string example: null nullable: true starts_at: type: string example: null nullable: true ends_at: type: string example: null nullable: true consumed_at: type: string example: null nullable: true links: type: object properties: {} meta: type: object properties: current_page: type: integer example: 1 per_page: type: integer example: 20 total: type: integer example: 1 tags: - Referrals requestBody: required: false content: application/json: schema: type: object properties: status: type: string description: '' example: architecto nullable: true per_page: type: integer description: 'Must be at least 1. Must not be greater than 50.' example: 22 nullable: true /api/v1/referrals/rewards: get: summary: 'List earned rewards.' operationId: listEarnedRewards description: "Every reward the account has earned. `available` rewards are earned and waiting,\n`active` is running now as free subscription time, and `consumed` ones are used up.\nA reward a paid plan interrupted is `available` again with `remaining_days` set." parameters: - in: query name: status description: 'Filter by available, active or consumed.' example: available required: false schema: type: string description: 'Filter by available, active or consumed.' example: available responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 8 status: available months: 1 remaining_days: null plan: id: 2 name: 'Smart Books' slug: smart business: null referred_name: 'Meena S' earned_at: '2026-09-15T10:00:00.000000Z' activated_at: null starts_at: null ends_at: null consumed_at: null properties: data: type: array example: - id: 8 status: available months: 1 remaining_days: null plan: id: 2 name: 'Smart Books' slug: smart business: null referred_name: 'Meena S' earned_at: '2026-09-15T10:00:00.000000Z' activated_at: null starts_at: null ends_at: null consumed_at: null items: type: object properties: id: type: integer example: 8 status: type: string example: available months: type: integer example: 1 remaining_days: type: string example: null nullable: true plan: type: object properties: id: type: integer example: 2 name: type: string example: 'Smart Books' slug: type: string example: smart business: type: string example: null nullable: true referred_name: type: string example: 'Meena S' earned_at: type: string example: '2026-09-15T10:00:00.000000Z' activated_at: type: string example: null nullable: true starts_at: type: string example: null nullable: true ends_at: type: string example: null nullable: true consumed_at: type: string example: null nullable: true tags: - Referrals requestBody: required: false content: application/json: schema: type: object properties: status: type: string description: '' example: architecto nullable: true /api/v1/referrals/rewards/apply: post: summary: 'Apply waiting rewards.' operationId: applyWaitingRewards description: "Optionally chooses which owned workspace waiting rewards run on, then starts the next\none immediately when no paid plan is running. With a paid plan running, nothing\nstarts: rewards never interrupt paid time and begin on their own once it ends.\nIdempotent. Returns the refreshed referral dashboard." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: program: enabled: true reward_months: 1 reward_plan: id: 2 name: 'Smart Books' slug: smart reward_plan_source: program_default referral_code: K7M2QX9A referral_link: 'https://dukanam.com/register?ref=K7M2QX9A' share: code: K7M2QX9A link: 'https://dukanam.com/register?ref=K7M2QX9A' message: 'Hey! You can register using my referral link: https://dukanam.com/register?ref=K7M2QX9A' whatsapp_url: 'https://wa.me/?text=Hey%21' stats: total_referrals: 1 pending_referrals: 0 successful_referrals: 1 rewards_earned: 1 months_earned: 1 available_rewards: 0 available_months: 0 available_extra_days: 0 active_rewards: 1 used_rewards: 0 paid_coverage: ends_at: null auto_renews: false current_reward: id: 8 status: active months: 1 remaining_days: null plan: id: 2 name: 'Smart Books' slug: smart business: id: 17 name: 'Aarogya Medical Store' referred_name: 'Meena S' earned_at: '2026-09-15T10:00:00.000000Z' activated_at: '2026-09-24T10:00:00.000000Z' starts_at: '2026-09-24T10:00:00.000000Z' ends_at: '2026-10-24T10:00:00.000000Z' consumed_at: null upcoming_rewards: [] properties: data: type: object properties: program: type: object properties: enabled: type: boolean example: true reward_months: type: integer example: 1 reward_plan: type: object properties: id: type: integer example: 2 name: type: string example: 'Smart Books' slug: type: string example: smart reward_plan_source: type: string example: program_default referral_code: type: string example: K7M2QX9A referral_link: type: string example: 'https://dukanam.com/register?ref=K7M2QX9A' share: type: object properties: code: type: string example: K7M2QX9A link: type: string example: 'https://dukanam.com/register?ref=K7M2QX9A' message: type: string example: 'Hey! You can register using my referral link: https://dukanam.com/register?ref=K7M2QX9A' whatsapp_url: type: string example: 'https://wa.me/?text=Hey%21' stats: type: object properties: total_referrals: type: integer example: 1 pending_referrals: type: integer example: 0 successful_referrals: type: integer example: 1 rewards_earned: type: integer example: 1 months_earned: type: integer example: 1 available_rewards: type: integer example: 0 available_months: type: integer example: 0 available_extra_days: type: integer example: 0 active_rewards: type: integer example: 1 used_rewards: type: integer example: 0 paid_coverage: type: object properties: ends_at: type: string example: null nullable: true auto_renews: type: boolean example: false current_reward: type: object properties: id: type: integer example: 8 status: type: string example: active months: type: integer example: 1 remaining_days: type: string example: null nullable: true plan: type: object properties: id: type: integer example: 2 name: type: string example: 'Smart Books' slug: type: string example: smart business: type: object properties: id: type: integer example: 17 name: type: string example: 'Aarogya Medical Store' referred_name: type: string example: 'Meena S' earned_at: type: string example: '2026-09-15T10:00:00.000000Z' activated_at: type: string example: '2026-09-24T10:00:00.000000Z' starts_at: type: string example: '2026-09-24T10:00:00.000000Z' ends_at: type: string example: '2026-10-24T10:00:00.000000Z' consumed_at: type: string example: null nullable: true upcoming_rewards: type: array example: [] 422: description: '' content: application/json: schema: type: object example: message: 'The selected business id is invalid.' errors: business_id: - 'The selected business id is invalid.' properties: message: type: string example: 'The selected business id is invalid.' errors: type: object properties: business_id: type: array example: - 'The selected business id is invalid.' items: type: string tags: - Referrals requestBody: required: false content: application/json: schema: type: object properties: business_id: type: integer description: 'An active workspace the signed-in account owns.' example: 17 nullable: true /api/v1/admin/referral-settings: get: summary: 'Get referral program settings.' operationId: getReferralProgramSettings description: "Super admin only. `reward_plan_id` is the configured plan, or the entry paid plan\nwhen none is configured. It is the fallback: a referrer who has paid before is\nrewarded with the plan they last paid for, and only a referrer who never paid gets\nthis plan. Program totals are included for the admin overview." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: enabled: true reward_plan_id: 2 reward_months: 1 whatsapp_message: 'Hey! Check out Dukanam. You can register using my referral link: {link}' placeholders: - '{link}' - '{code}' - '{name}' totals: referrals: 42 successful: 9 rewards: 9 months_granted: 9 rewards_running: 3 properties: data: type: object properties: enabled: type: boolean example: true reward_plan_id: type: integer example: 2 reward_months: type: integer example: 1 whatsapp_message: type: string example: 'Hey! Check out Dukanam. You can register using my referral link: {link}' placeholders: type: array example: - '{link}' - '{code}' - '{name}' items: type: string totals: type: object properties: referrals: type: integer example: 42 successful: type: integer example: 9 rewards: type: integer example: 9 months_granted: type: integer example: 9 rewards_running: type: integer example: 3 tags: - 'Referral program administration' put: summary: 'Update referral program settings.' operationId: updateReferralProgramSettings description: "Super admin only. Takes effect for the next reward earned; rewards already earned\nkeep the plan and months they were earned with. Turning the program off stops new\nsign-ups from being attributed and new rewards from being earned." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: enabled: true reward_plan_id: 3 reward_months: 2 whatsapp_message: 'Join me on Dukanam: {link}' placeholders: - '{link}' - '{code}' - '{name}' totals: referrals: 42 successful: 9 rewards: 9 months_granted: 9 rewards_running: 3 properties: data: type: object properties: enabled: type: boolean example: true reward_plan_id: type: integer example: 3 reward_months: type: integer example: 2 whatsapp_message: type: string example: 'Join me on Dukanam: {link}' placeholders: type: array example: - '{link}' - '{code}' - '{name}' items: type: string totals: type: object properties: referrals: type: integer example: 42 successful: type: integer example: 9 rewards: type: integer example: 9 months_granted: type: integer example: 9 rewards_running: type: integer example: 3 422: description: '' content: application/json: schema: type: object example: message: 'Choose an active paid plan as the referral reward.' errors: reward_plan_id: - 'Choose an active paid plan as the referral reward.' properties: message: type: string example: 'Choose an active paid plan as the referral reward.' errors: type: object properties: reward_plan_id: type: array example: - 'Choose an active paid plan as the referral reward.' items: type: string tags: - 'Referral program administration' requestBody: required: true content: application/json: schema: type: object properties: enabled: type: boolean description: 'Turn the referral program on or off. While off, new sign-ups are not attributed and no new rewards are earned; rewards already earned still apply.' example: true reward_plan_id: type: integer description: 'The active paid plan a reward grants when the referrer has never paid. Referrers who paid before get the plan they last paid for.' example: 2 reward_months: type: integer description: 'Free months granted per successful referral, from 1 to 24. Must be at least 1. Must not be greater than 24.' example: 1 whatsapp_message: type: string description: "The share message. `{link}`, `{code}` and `{name}` are replaced with the referrer's link, code and name; the link is appended when `{link}` is missing. Must not be greater than 1000 characters." example: 'Hey! Check out Dukanam. You can register using my referral link: {link}' required: - enabled - reward_plan_id - reward_months - whatsapp_message /api/v1/admin/referrals: get: summary: 'List all referrals.' operationId: listAllReferrals description: 'Super admin only. Every referral with both accounts, its status and any reward.' parameters: - in: query name: status description: 'Filter by registered, subscription_pending, subscription_purchased, reward_earned or reward_applied.' example: reward_earned required: false schema: type: string description: 'Filter by registered, subscription_pending, subscription_purchased, reward_earned or reward_applied.' example: reward_earned - in: query name: search description: "Match the referrer's or referred person's name or email, or the code." example: priya required: false schema: type: string description: "Match the referrer's or referred person's name or email, or the code." example: priya - in: query name: per_page description: 'Results per page, from 1 to 50.' example: 25 required: false schema: type: integer description: 'Results per page, from 1 to 50.' example: 25 responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 12 code: K7M2QX9A status: reward_earned status_label: 'Reward earned' source: web referrer: id: 4 name: 'Priya Rao' email: priya@example.com referred: id: 31 name: 'Meena S' email: meena@example.com qualifying_business: id: 40 name: 'Meena Stores' registered_at: '2026-09-10T08:00:00.000000Z' subscription_pending_at: '2026-09-12T08:00:00.000000Z' subscription_purchased_at: '2026-09-15T10:00:00.000000Z' reward_earned_at: '2026-09-15T10:00:00.000000Z' reward_applied_at: null reward_skipped_reason: null reward: id: 8 status: available months: 1 remaining_days: null plan: id: 2 name: 'Smart Books' slug: smart business: null referred_name: null earned_at: '2026-09-15T10:00:00.000000Z' activated_at: null starts_at: null ends_at: null consumed_at: null links: {} meta: current_page: 1 per_page: 25 total: 1 properties: data: type: array example: - id: 12 code: K7M2QX9A status: reward_earned status_label: 'Reward earned' source: web referrer: id: 4 name: 'Priya Rao' email: priya@example.com referred: id: 31 name: 'Meena S' email: meena@example.com qualifying_business: id: 40 name: 'Meena Stores' registered_at: '2026-09-10T08:00:00.000000Z' subscription_pending_at: '2026-09-12T08:00:00.000000Z' subscription_purchased_at: '2026-09-15T10:00:00.000000Z' reward_earned_at: '2026-09-15T10:00:00.000000Z' reward_applied_at: null reward_skipped_reason: null reward: id: 8 status: available months: 1 remaining_days: null plan: id: 2 name: 'Smart Books' slug: smart business: null referred_name: null earned_at: '2026-09-15T10:00:00.000000Z' activated_at: null starts_at: null ends_at: null consumed_at: null items: type: object properties: id: type: integer example: 12 code: type: string example: K7M2QX9A status: type: string example: reward_earned status_label: type: string example: 'Reward earned' source: type: string example: web referrer: type: object properties: id: type: integer example: 4 name: type: string example: 'Priya Rao' email: type: string example: priya@example.com referred: type: object properties: id: type: integer example: 31 name: type: string example: 'Meena S' email: type: string example: meena@example.com qualifying_business: type: object properties: id: type: integer example: 40 name: type: string example: 'Meena Stores' registered_at: type: string example: '2026-09-10T08:00:00.000000Z' subscription_pending_at: type: string example: '2026-09-12T08:00:00.000000Z' subscription_purchased_at: type: string example: '2026-09-15T10:00:00.000000Z' reward_earned_at: type: string example: '2026-09-15T10:00:00.000000Z' reward_applied_at: type: string example: null nullable: true reward_skipped_reason: type: string example: null nullable: true reward: type: object properties: id: type: integer example: 8 status: type: string example: available months: type: integer example: 1 remaining_days: type: string example: null nullable: true plan: type: object properties: id: type: integer example: 2 name: type: string example: 'Smart Books' slug: type: string example: smart business: type: string example: null nullable: true referred_name: type: string example: null nullable: true earned_at: type: string example: '2026-09-15T10:00:00.000000Z' activated_at: type: string example: null nullable: true starts_at: type: string example: null nullable: true ends_at: type: string example: null nullable: true consumed_at: type: string example: null nullable: true links: type: object properties: {} meta: type: object properties: current_page: type: integer example: 1 per_page: type: integer example: 25 total: type: integer example: 1 tags: - 'Referral program administration' requestBody: required: false content: application/json: schema: type: object properties: status: type: string description: '' example: architecto nullable: true search: type: string description: 'Must not be greater than 100 characters.' example: 'n' nullable: true per_page: type: integer description: 'Must be at least 1. Must not be greater than 50.' example: 7 nullable: true /api/v1/admin/referral-rewards: get: summary: 'List rewards granted.' operationId: listRewardsGranted description: "Super admin only. Who received referral rewards, on which workspace and plan, and\nwhether each is waiting, running or used." parameters: - in: query name: status description: 'Filter by available, active or consumed.' example: active required: false schema: type: string description: 'Filter by available, active or consumed.' example: active - in: query name: search description: "Match the receiving account's name or email." example: priya required: false schema: type: string description: "Match the receiving account's name or email." example: priya - in: query name: per_page description: 'Results per page, from 1 to 50.' example: 25 required: false schema: type: integer description: 'Results per page, from 1 to 50.' example: 25 responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 8 status: active months: 1 remaining_days: null plan: id: 2 name: 'Smart Books' slug: smart business: id: 17 name: 'Aarogya Medical Store' referred_name: 'Meena S' earned_at: '2026-09-15T10:00:00.000000Z' activated_at: '2026-09-24T10:00:00.000000Z' starts_at: '2026-09-24T10:00:00.000000Z' ends_at: '2026-10-24T10:00:00.000000Z' consumed_at: null user: id: 4 name: 'Priya Rao' email: priya@example.com referral_id: 12 subscription_id: 95 links: {} meta: current_page: 1 per_page: 25 total: 1 properties: data: type: array example: - id: 8 status: active months: 1 remaining_days: null plan: id: 2 name: 'Smart Books' slug: smart business: id: 17 name: 'Aarogya Medical Store' referred_name: 'Meena S' earned_at: '2026-09-15T10:00:00.000000Z' activated_at: '2026-09-24T10:00:00.000000Z' starts_at: '2026-09-24T10:00:00.000000Z' ends_at: '2026-10-24T10:00:00.000000Z' consumed_at: null user: id: 4 name: 'Priya Rao' email: priya@example.com referral_id: 12 subscription_id: 95 items: type: object properties: id: type: integer example: 8 status: type: string example: active months: type: integer example: 1 remaining_days: type: string example: null nullable: true plan: type: object properties: id: type: integer example: 2 name: type: string example: 'Smart Books' slug: type: string example: smart business: type: object properties: id: type: integer example: 17 name: type: string example: 'Aarogya Medical Store' referred_name: type: string example: 'Meena S' earned_at: type: string example: '2026-09-15T10:00:00.000000Z' activated_at: type: string example: '2026-09-24T10:00:00.000000Z' starts_at: type: string example: '2026-09-24T10:00:00.000000Z' ends_at: type: string example: '2026-10-24T10:00:00.000000Z' consumed_at: type: string example: null nullable: true user: type: object properties: id: type: integer example: 4 name: type: string example: 'Priya Rao' email: type: string example: priya@example.com referral_id: type: integer example: 12 subscription_id: type: integer example: 95 links: type: object properties: {} meta: type: object properties: current_page: type: integer example: 1 per_page: type: integer example: 25 total: type: integer example: 1 tags: - 'Referral program administration' requestBody: required: false content: application/json: schema: type: object properties: status: type: string description: '' example: architecto nullable: true search: type: string description: 'Must not be greater than 100 characters.' example: 'n' nullable: true per_page: type: integer description: 'Must be at least 1. Must not be greater than 50.' example: 7 nullable: true '/api/v1/businesses/{business}/exports': get: summary: 'Export business data' operationId: exportBusinessData description: "Reporting exports download as `dukanam-{type}-YYYY-MM-DD.{csv|xlsx}`. `format=csv` stays the default,\nso an existing caller sees no change. `format=xlsx` returns a workbook with one sheet per requested\nsection, a frozen header row, real dates and money as numbers a spreadsheet can sum.\n\n`type` accepts several sections at once — `type=invoices,expenses` or `type[]=invoices&type[]=expenses` —\nwhich requires `format=xlsx`, since a CSV file holds one table. Several sections download as\n`dukanam-export-YYYY-MM-DD.xlsx`.\n\n`from` and `to` narrow ledger, invoice and expense exports to a period. Contact exports carry lifetime\nbalances and refuse a date range rather than quietly ignoring it.\n\nAn export larger than the queued-export threshold is generated on the queue: the response is **202**\ncarrying the export record, which is polled until `status` is `completed` and then fetched from\n`download_url`. Pass `async=1` to ask for that regardless of size. The web workspace keeps streaming\nits downloads inline, since a browser link has nowhere to poll.\n\nInvoice CSV and Excel exports use readable status labels, such as Saved, instead of internal status codes.\n\nExports are online-only: the file is generated on the server from the recorded transactions, and nothing here\nbelongs in the app's offline cache or its write queue.\n\nThe legacy `business-backup` type returns a `dukanam-business-export-v2` owner/admin-only customer-data\nportability export named `dukanam-business-export-YYYY-MM-DD.json`. It includes item photos and compliance\nevidence content, item classifications, GST percentage masters, sale warranty snapshots and warranty delivery records,\nbut is not a restorable platform backup. Warranty tokens are private buyer access links;\nprotect the archive accordingly." parameters: - in: query name: type description: 'Export type, or several comma-separated.' example: invoices required: true schema: type: string description: 'Export type, or several comma-separated.' example: invoices - in: query name: format description: 'Output format, `csv` or `xlsx`. Defaults to `csv`.' example: xlsx required: false schema: type: string description: 'Output format, `csv` or `xlsx`. Defaults to `csv`.' example: xlsx - in: query name: from description: 'date Start of the period, for exports that carry a date.' example: '2026-04-01' required: false schema: type: string description: 'date Start of the period, for exports that carry a date.' example: '2026-04-01' - in: query name: to description: 'date End of the period, for exports that carry a date.' example: '2027-03-31' required: false schema: type: string description: 'date End of the period, for exports that carry a date.' example: '2027-03-31' - in: query name: async description: 'Generate on the queue and return the export to poll, whatever its size.' example: true required: false schema: type: boolean description: 'Generate on the queue and return the export to poll, whatever its size.' example: true responses: 202: description: '' content: application/json: schema: type: object example: data: uuid: 9b5f… types: - invoices format: xlsx status: queued download_url: null properties: data: type: object properties: uuid: type: string example: 9b5f… types: type: array example: - invoices items: type: string format: type: string example: xlsx status: type: string example: queued download_url: type: string example: null nullable: true 422: description: '' content: application/json: schema: type: object example: message: 'Several sections can only be exported together as a workbook. Use format=xlsx or export one type at a time.' errors: format: - 'Several sections can only be exported together as a workbook. Use format=xlsx or export one type at a time.' properties: message: type: string example: 'Several sections can only be exported together as a workbook. Use format=xlsx or export one type at a time.' errors: type: object properties: format: type: array example: - 'Several sections can only be exported together as a workbook. Use format=xlsx or export one type at a time.' items: type: string tags: - 'Data exports' requestBody: required: false content: application/json: schema: type: object properties: type: type: array description: '' example: - architecto items: type: string format: type: string description: '' example: csv enum: - csv - xlsx from: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true to: type: string description: 'Must be a valid date. Must be a date after or equal to from.' example: '2026-01-15' nullable: true parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/exports/tally': get: summary: 'Export the books for Tally Prime' operationId: exportTheBooksForTallyPrime description: "Returns Tally-compatible XML: the shop's chart of accounts, parties, units and stock items as\nmasters, and its accounting entries as accounting vouchers. The CA imports the file into a Tally\ncompany with **Gateway of Tally → Import → Vouchers**, which is how a shop hands its year over\nwithout keeping a second set of books.\n\n`mode` chooses what the file carries — `masters`, `vouchers`, or `both` (the default). A voucher\nexport needs `from` and `to` and covers at most one financial year per call; a masters export is\nthe whole chart and refuses a date range.\n\nVouchers are built from the same accounting entries Dukanam's own trial balance is built from, so the\ntwo agree. A voucher export starting after the books did also carries one opening journal dated the\nday before `from`, holding every ledger and party balance as it stood — without it the CA would\nreceive a year of movement with no starting position.\n\nEach voucher carries a `REMOTEID` derived from Dukanam's own ids, so **re-importing an overlapping\ndate range updates the vouchers already there instead of writing a second copy of the same sales**.\n\nThe file is accounting-only: stock items arrive as masters with their units and HSN codes, but the\nvouchers carry ledger entries rather than inventory allocations, so Tally's stock summary is not\npopulated from this export. The mapping from Dukanam's chart of accounts onto Tally's groups is\nwritten into the top of the file as a comment, for the accountant to check.\n\nA large export is generated on the queue exactly as the workbook export is: the response is **202**\ncarrying the export record, polled until `status` is `completed` and then fetched from `download_url`.\nPass `async=1` to ask for that whatever the size. This is online-only — nothing here belongs in the\napp's offline cache or its write queue." parameters: - in: query name: mode description: 'What to export: `masters`, `vouchers` or `both`. Defaults to `both`.' example: both required: false schema: type: string description: 'What to export: `masters`, `vouchers` or `both`. Defaults to `both`.' example: both - in: query name: from description: 'date Start of the period. Required unless `mode=masters`.' example: '2026-04-01' required: false schema: type: string description: 'date Start of the period. Required unless `mode=masters`.' example: '2026-04-01' - in: query name: to description: 'date End of the period. Required unless `mode=masters`.' example: '2027-03-31' required: false schema: type: string description: 'date End of the period. Required unless `mode=masters`.' example: '2027-03-31' - in: query name: async description: 'Generate on the queue and return the export to poll, whatever its size.' example: true required: false schema: type: boolean description: 'Generate on the queue and return the export to poll, whatever its size.' example: true responses: 200: description: 'The generated Tally XML.' content: application/octet-stream: schema: type: string format: binary 202: description: '' content: application/json: schema: type: object example: data: uuid: 9b5f… types: - tally-both format: tally status: queued filename: dukanam-tally-2026-09-15.xml download_url: null properties: data: type: object properties: uuid: type: string example: 9b5f… types: type: array example: - tally-both items: type: string format: type: string example: tally status: type: string example: queued filename: type: string example: dukanam-tally-2026-09-15.xml download_url: type: string example: null nullable: true 422: description: '' content: application/json: schema: type: object example: message: 'A Tally voucher export needs both a start and an end date.' errors: from: - 'A Tally voucher export needs both a start and an end date.' properties: message: type: string example: 'A Tally voucher export needs both a start and an end date.' errors: type: object properties: from: type: array example: - 'A Tally voucher export needs both a start and an end date.' items: type: string tags: - 'Data exports' requestBody: required: false content: application/json: schema: type: object properties: mode: type: string description: '' example: masters enum: - masters - vouchers - both from: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true to: type: string description: 'Must be a valid date. Must be a date after or equal to from.' example: '2026-01-15' nullable: true parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/exports/queued': get: summary: 'List queued exports.' operationId: listQueuedExports description: 'Newest first, whatever their status, so a failed export is visible rather than silent.' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Data exports' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/exports/queued/{dataExport_id}': get: summary: 'Read one queued export.' operationId: readOneQueuedExport description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Data exports' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: dataExport_id description: 'The ID of the dataExport.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/exports/queued/{dataExport_id}/download': get: summary: 'Download a generated export.' operationId: downloadAGeneratedExport description: '' parameters: [] responses: 200: description: 'The generated CSV or workbook.' content: application/octet-stream: schema: type: string format: binary 409: description: '' content: application/json: schema: type: object example: message: 'This export is not ready yet.' properties: message: type: string example: 'This export is not ready yet.' 410: description: '' content: application/json: schema: type: object example: message: 'This export has expired. Request it again.' properties: message: type: string example: 'This export has expired. Request it again.' tags: - 'Data exports' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: dataExport_id description: 'The ID of the dataExport.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/team': get: summary: 'List team members, pending invitations, and the 25 most recent expired invitations.' operationId: listTeamMembersPendingInvitationsAndThe25MostRecentExpiredInvitations description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: members: - id: 2 name: 'Priya Rao' email: priya@example.com role: cashier status: active joined_at: '2026-08-21T12:00:00.000000Z' invitations: [] roles: cashier: label: Cashier description: 'POS, sales, and cash register access.' accountant: label: Accountant description: 'Sales, purchases, inventory, accounting, reports, compliance, and exports.' seats: used: 2 limit: 5 active: 2 pending: 0 properties: data: type: object properties: members: type: array example: - id: 2 name: 'Priya Rao' email: priya@example.com role: cashier status: active joined_at: '2026-08-21T12:00:00.000000Z' items: type: object properties: id: type: integer example: 2 name: type: string example: 'Priya Rao' email: type: string example: priya@example.com role: type: string example: cashier status: type: string example: active joined_at: type: string example: '2026-08-21T12:00:00.000000Z' invitations: type: array example: [] roles: type: object properties: cashier: type: object properties: label: type: string example: Cashier description: type: string example: 'POS, sales, and cash register access.' accountant: type: object properties: label: type: string example: Accountant description: type: string example: 'Sales, purchases, inventory, accounting, reports, compliance, and exports.' seats: type: object properties: used: type: integer example: 2 limit: type: integer example: 5 description: 'Total workspace seat limit. A value of 0 means unlimited seats.' active: type: integer example: 2 pending: type: integer example: 0 tags: - 'Team access' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/team/invitations': post: summary: 'Invite a team member.' operationId: inviteATeamMember description: 'Pending invitations reserve a seat and expire after seven days.' parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: id: 12 email: cashier@example.com role: cashier status: pending expires_at: '2026-08-28T12:00:00.000000Z' accepted_at: null invited_by: id: 1 name: 'Workspace Owner' properties: data: type: object properties: id: type: integer example: 12 email: type: string example: cashier@example.com role: type: string example: cashier status: type: string example: pending expires_at: type: string example: '2026-08-28T12:00:00.000000Z' accepted_at: type: string example: null nullable: true invited_by: type: object properties: id: type: integer example: 1 name: type: string example: 'Workspace Owner' tags: - 'Team access' requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: "The invited person's email." example: cashier@example.com role: type: string description: 'One of admin, manager, cashier, accountant, staff, or viewer. The `pos` role is not invitable: POS logins are bought per seat and created with `POST /businesses/{business}/team/pos-logins`.' example: cashier required: - email - role parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/team/invitations/{teamInvitation_id}/resend': post: summary: 'Resend a pending or expired invitation with a new secure token.' operationId: resendAPendingOrExpiredInvitationWithANewSecureToken description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 12 email: cashier@example.com role: cashier status: pending expires_at: '2026-08-28T12:00:00.000000Z' accepted_at: null invited_by: id: 1 name: 'Workspace Owner' properties: data: type: object properties: id: type: integer example: 12 email: type: string example: cashier@example.com role: type: string example: cashier status: type: string example: pending expires_at: type: string example: '2026-08-28T12:00:00.000000Z' accepted_at: type: string example: null nullable: true invited_by: type: object properties: id: type: integer example: 1 name: type: string example: 'Workspace Owner' 422: description: '' content: application/json: schema: type: object example: message: 'All 2 workspace seats are already assigned or reserved.' errors: email: - 'All 2 workspace seats are already assigned or reserved.' properties: message: type: string example: 'All 2 workspace seats are already assigned or reserved.' errors: type: object properties: email: type: array example: - 'All 2 workspace seats are already assigned or reserved.' items: type: string tags: - 'Team access' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: teamInvitation_id description: 'The ID of the teamInvitation.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/team/invitations/{teamInvitation_id}': delete: summary: 'Cancel a pending invitation and release its reserved seat.' operationId: cancelAPendingInvitationAndReleaseItsReservedSeat description: '' parameters: [] responses: 204: description: '' content: application/json: schema: type: object example: {} properties: {} 403: description: '' content: application/json: schema: type: object example: message: 'Only the owner can manage administrator invitations.' properties: message: type: string example: 'Only the owner can manage administrator invitations.' tags: - 'Team access' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: teamInvitation_id description: 'The ID of the teamInvitation.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/team/members/{member_id}': patch: summary: "Change a member's role or suspend/reactivate access." operationId: changeAMembersRoleOrSuspendreactivateAccess description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 2 name: 'Priya Rao' email: priya@example.com role: manager status: active joined_at: '2026-08-21T12:00:00.000000Z' properties: data: type: object properties: id: type: integer example: 2 name: type: string example: 'Priya Rao' email: type: string example: priya@example.com role: type: string example: manager status: type: string example: active joined_at: type: string example: '2026-08-21T12:00:00.000000Z' 422: description: '' content: application/json: schema: type: object example: message: 'All 2 workspace seats are already assigned or reserved.' errors: role: - 'All 2 workspace seats are already assigned or reserved.' status: - 'All 2 workspace seats are already assigned or reserved.' properties: message: type: string example: 'All 2 workspace seats are already assigned or reserved.' errors: type: object properties: role: type: array example: - 'All 2 workspace seats are already assigned or reserved.' items: type: string status: type: array example: - 'All 2 workspace seats are already assigned or reserved.' items: type: string tags: - 'Team access' requestBody: required: true content: application/json: schema: type: object properties: role: type: string description: 'One of admin, manager, cashier, accountant, staff, or viewer.' example: manager status: type: string description: 'Either active or suspended.' example: active required: - role - status delete: summary: "Remove a member's workspace access without deleting their user account." operationId: removeAMembersWorkspaceAccessWithoutDeletingTheirUserAccount description: '' parameters: [] responses: 204: description: '' content: application/json: schema: type: object example: {} properties: {} tags: - 'Team access' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: member_id description: 'The ID of the member.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/team/pos-logins': post: summary: 'Create a POS login against a paid counter seat.' operationId: createAPOSLoginAgainstAPaidCounterSeat description: "A POS login can open the billing counter and its cash drawer and nothing else. Seats\nare bought with `POST /businesses/{business}/billing/pos-seats`; creating a login\nwithout a free paid seat returns `422`. Unlike an invitation the password is set here\nand handed to the counter staff directly." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: id: 7 name: 'Ravi Kumar' email: counter@example.com role: pos status: active joined_at: '2026-09-02T12:00:00.000000Z' properties: data: type: object properties: id: type: integer example: 7 name: type: string example: 'Ravi Kumar' email: type: string example: counter@example.com role: type: string example: pos status: type: string example: active joined_at: type: string example: '2026-09-02T12:00:00.000000Z' 422: description: '' content: application/json: schema: type: object example: message: 'Buy a POS login before creating counter staff.' errors: email: - 'Buy a POS login before creating counter staff.' properties: message: type: string example: 'Buy a POS login before creating counter staff.' errors: type: object properties: email: type: array example: - 'Buy a POS login before creating counter staff.' items: type: string tags: - 'Team access' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: "The counter staff member's name." example: 'Ravi Kumar' email: type: string description: 'The email they sign in with.' example: counter@example.com password: type: string description: 'At least eight characters.' example: counter-password required: - name - email - password parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/team/pos-logins/{member_id}/password': patch: summary: "Replace a POS login's password and sign its devices out." operationId: replaceAPOSLoginsPasswordAndSignItsDevicesOut description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 7 name: 'Ravi Kumar' email: counter@example.com role: pos status: active joined_at: '2026-09-02T12:00:00.000000Z' properties: data: type: object properties: id: type: integer example: 7 name: type: string example: 'Ravi Kumar' email: type: string example: counter@example.com role: type: string example: pos status: type: string example: active joined_at: type: string example: '2026-09-02T12:00:00.000000Z' 422: description: '' content: application/json: schema: type: object example: message: 'That member is not a POS login.' properties: message: type: string example: 'That member is not a POS login.' tags: - 'Team access' requestBody: required: true content: application/json: schema: type: object properties: password: type: string description: 'The new password, at least eight characters.' example: new-counter-password required: - password parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: member_id description: 'The ID of the member.' example: 1 required: true schema: type: integer '/api/v1/team-invitations/{token}/accept': post: summary: 'Accept a team invitation as an existing user.' operationId: acceptATeamInvitationAsAnExistingUser description: 'Send a valid Sanctum bearer token belonging to the invited email. This endpoint does not create a new token.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: user: id: 2 name: 'Priya Rao' email: cashier@example.com business: id: 1 name: 'Anika Stores' role: cashier properties: data: type: object properties: user: type: object properties: id: type: integer example: 2 name: type: string example: 'Priya Rao' email: type: string example: cashier@example.com business: type: object properties: id: type: integer example: 1 name: type: string example: 'Anika Stores' role: type: string example: cashier 404: description: '' content: application/json: schema: type: object example: message: 'Not Found' properties: message: type: string example: 'Not Found' 422: description: '' content: application/json: schema: type: object example: message: 'The given data was invalid.' errors: email: - 'Sign in with the email address that received this invitation.' invitation: - 'This workspace is no longer available.' properties: message: type: string example: 'The given data was invalid.' errors: type: object properties: email: type: array example: - 'Sign in with the email address that received this invitation.' items: type: string invitation: type: array example: - 'This workspace is no longer available.' items: type: string tags: - 'Team invitations' parameters: - in: path name: token description: 'The opaque token from the invitation email.' example: 4WvBr4F8Dmcx3FJkL1nEz7PcQs2Yu9Aa required: true schema: type: string '/api/v1/team-invitations/{token}': get: summary: 'Inspect a team invitation.' operationId: inspectATeamInvitation description: 'This endpoint does not require authentication. The opaque token is supplied by the invitation email.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: business: id: 1 name: 'Anika Stores' email: cashier@example.com role: cashier role_label: Cashier status: pending expires_at: '2026-08-28T12:00:00.000000Z' existing_account: false properties: data: type: object properties: business: type: object properties: id: type: integer example: 1 name: type: string example: 'Anika Stores' email: type: string example: cashier@example.com role: type: string example: cashier role_label: type: string example: Cashier status: type: string example: pending expires_at: type: string example: '2026-08-28T12:00:00.000000Z' existing_account: type: boolean example: false 404: description: '' content: application/json: schema: type: object example: message: 'Not Found' properties: message: type: string example: 'Not Found' tags: - 'Team invitations' security: [] parameters: - in: path name: token description: 'The opaque token from the invitation email.' example: 4WvBr4F8Dmcx3FJkL1nEz7PcQs2Yu9Aa required: true schema: type: string '/api/v1/team-invitations/{token}/register-and-accept': post: summary: 'Create an account and accept a team invitation.' operationId: createAnAccountAndAcceptATeamInvitation description: 'Use this unauthenticated endpoint only when the invited email does not already belong to a Dukanam user. The response includes a new Sanctum device token.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: user: id: 2 name: 'Priya Rao' email: cashier@example.com business: id: 1 name: 'Anika Stores' role: cashier token: 1|new-mobile-token properties: data: type: object properties: user: type: object properties: id: type: integer example: 2 name: type: string example: 'Priya Rao' email: type: string example: cashier@example.com business: type: object properties: id: type: integer example: 1 name: type: string example: 'Anika Stores' role: type: string example: cashier token: type: string example: 1|new-mobile-token 403: description: '' content: application/json: schema: type: object example: message: 'Sign in as the invited user to accept this invitation.' properties: message: type: string example: 'Sign in as the invited user to accept this invitation.' 404: description: '' content: application/json: schema: type: object example: message: 'Not Found' properties: message: type: string example: 'Not Found' 422: description: '' content: application/json: schema: type: object example: message: 'This invitation has expired or is no longer available.' errors: invitation: - 'This invitation has expired or is no longer available.' properties: message: type: string example: 'This invitation has expired or is no longer available.' errors: type: object properties: invitation: type: array example: - 'This invitation has expired or is no longer available.' items: type: string tags: - 'Team invitations' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: "The new user's name." example: 'Priya Rao' password: type: string description: "The new user's password; minimum eight characters." example: secret-pass-123 device_name: type: string description: 'The name for the new API token.' example: "Priya's phone" password_confirmation: type: string description: 'Password confirmation.' example: secret-pass-123 required: - name - password - device_name - password_confirmation security: [] parameters: - in: path name: token description: 'The opaque token from the invitation email.' example: 4WvBr4F8Dmcx3FJkL1nEz7PcQs2Yu9Aa required: true schema: type: string '/api/v1/businesses/{business_id}/testimonials': get: summary: 'List my workspace testimonials.' operationId: listMyWorkspaceTestimonials description: "Returns only testimonials submitted by the signed-in user for this workspace.\nIncludes admin-authored drafts linked to this business owner; this grants\nno workspace access. photo_url may be null for a text-only draft." parameters: - in: query name: per_page description: 'Results per page, from 1 to 24.' example: 12 required: false schema: type: integer description: 'Results per page, from 1 to 24.' example: 12 responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29 store_type: pharmacy feedback: 'Dukanam keeps our counter records together.' photo_url: 'https://dukanam.com/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29/photo' status: pending submitted_at: '2026-08-31T10:15:00.000000Z' published_at: null links: {} meta: current_page: 1 per_page: 12 total: 1 properties: data: type: array example: - id: 53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29 store_type: pharmacy feedback: 'Dukanam keeps our counter records together.' photo_url: 'https://dukanam.com/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29/photo' status: pending submitted_at: '2026-08-31T10:15:00.000000Z' published_at: null items: type: object properties: id: type: string example: 53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29 store_type: type: string example: pharmacy feedback: type: string example: 'Dukanam keeps our counter records together.' photo_url: type: string example: 'https://dukanam.com/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29/photo' status: type: string example: pending submitted_at: type: string example: '2026-08-31T10:15:00.000000Z' published_at: type: string example: null nullable: true links: type: object properties: {} meta: type: object properties: current_page: type: integer example: 1 per_page: type: integer example: 12 total: type: integer example: 1 tags: - Testimonials post: summary: 'Submit a testimonial from a subscribed workspace.' operationId: submitATestimonialFromASubscribedWorkspace description: "Requires a usable non-free subscription. Name, email and shop identity are\ntaken from the authenticated user and workspace, not request fields." parameters: [] responses: 202: description: '' content: application/json: schema: type: object example: message: 'Thank you. Your testimonial is awaiting review.' data: id: 53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29 store_type: pharmacy feedback: 'Dukanam keeps our counter records together.' photo_url: 'https://dukanam.com/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29/photo' status: pending submitted_at: '2026-08-31T10:15:00.000000Z' published_at: null properties: message: type: string example: 'Thank you. Your testimonial is awaiting review.' data: type: object properties: id: type: string example: 53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29 store_type: type: string example: pharmacy feedback: type: string example: 'Dukanam keeps our counter records together.' photo_url: type: string example: 'https://dukanam.com/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29/photo' status: type: string example: pending submitted_at: type: string example: '2026-08-31T10:15:00.000000Z' published_at: type: string example: null nullable: true tags: - Testimonials requestBody: required: true content: multipart/form-data: schema: type: object properties: store_type: type: string description: 'Industry page that should receive the testimonial after approval. Accepts configured industry slugs, including electrical-store, paint-store, sanitaryware-store and pesticide-store.' example: pharmacy enum: - kirana-store - pharmacy - supermarket - clothing-store - footwear-store - hardware-store - electronics-store - mobile-store - stationery-store - book-store - bakery - cosmetics-store - wholesale-business - auto-parts-store - home-appliance-store - gift-shop - electrical-store - paint-store - sanitaryware-store - pesticide-store feedback: type: string description: 'First-hand feedback, 40 to 1,200 characters. Must be at least 40 characters. Must not be greater than 1200 characters.' example: 'Dukanam made our daily medicine counter billing easier to review, and the purchase and stock records now stay together for closing.' photo: type: string format: binary description: 'JPEG, PNG or WebP shop photo, 320×320 to 6000×6000 pixels, maximum 5 MB. Must be an image. Must not be greater than 5120 kilobytes.' consent: type: boolean description: 'Must be true to confirm content rights, consent from people shown, and permission to moderate and publish the submitted name, shop, photo and feedback. Must be accepted.' example: true website: type: string description: 'Spam-trap field. Omit it or leave it empty.' example: null nullable: true required: - store_type - feedback - photo - consent parameters: - in: path name: business_id description: 'Workspace ID.' example: 17 required: true schema: type: integer /api/v1/testimonials: get: summary: 'List approved public testimonials.' operationId: listApprovedPublicTestimonials description: "Includes only consented stories; internal uxcrafts.com owners and businesses\nare excluded. Admin-authored stories may be text-only." parameters: - in: query name: store_type description: 'Filter by one configured industry slug, including electrical-store, paint-store, sanitaryware-store and pesticide-store.' example: electrical-store required: false schema: type: string description: 'Filter by one configured industry slug, including electrical-store, paint-store, sanitaryware-store and pesticide-store.' example: electrical-store - in: query name: per_page description: 'Results per page, from 1 to 24.' example: 12 required: false schema: type: integer description: 'Results per page, from 1 to 24.' example: 12 responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29 store_type: pharmacy owner_name: 'Dr. Kavitha Reddy' shop_name: 'Aarogya Medical & General Store' feedback: 'Dukanam made our daily medicine counter billing easier to review.' photo_url: 'https://dukanam.com/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29/photo' published_at: '2026-08-31T10:30:00.000000Z' location: null links: first: 'https://dukanam.com/api/v1/testimonials?page=1' last: 'https://dukanam.com/api/v1/testimonials?page=1' prev: null next: null meta: current_page: 1 from: 1 last_page: 1 links: [] path: 'https://dukanam.com/api/v1/testimonials' per_page: 12 to: 1 total: 1 properties: data: type: array example: - id: 53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29 store_type: pharmacy owner_name: 'Dr. Kavitha Reddy' shop_name: 'Aarogya Medical & General Store' feedback: 'Dukanam made our daily medicine counter billing easier to review.' photo_url: 'https://dukanam.com/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29/photo' published_at: '2026-08-31T10:30:00.000000Z' location: null items: type: object properties: id: type: string example: 53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29 store_type: type: string example: pharmacy owner_name: type: string example: 'Dr. Kavitha Reddy' shop_name: type: string example: 'Aarogya Medical & General Store' feedback: type: string example: 'Dukanam made our daily medicine counter billing easier to review.' photo_url: type: string example: 'https://dukanam.com/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29/photo' published_at: type: string example: '2026-08-31T10:30:00.000000Z' location: type: string example: null nullable: true links: type: object properties: first: type: string example: 'https://dukanam.com/api/v1/testimonials?page=1' last: type: string example: 'https://dukanam.com/api/v1/testimonials?page=1' prev: type: string example: null nullable: true next: type: string example: null nullable: true meta: type: object properties: current_page: type: integer example: 1 from: type: integer example: 1 last_page: type: integer example: 1 links: type: array example: [] path: type: string example: 'https://dukanam.com/api/v1/testimonials' per_page: type: integer example: 12 to: type: integer example: 1 total: type: integer example: 1 tags: - Testimonials security: [] /api/v1/admin/testimonials: get: summary: 'List testimonial submissions for moderation.' operationId: listTestimonialSubmissionsForModeration description: '' parameters: - in: query name: status description: 'Filter by pending, approved or rejected.' example: pending required: false schema: type: string description: 'Filter by pending, approved or rejected.' example: pending - in: query name: store_type description: 'Filter by one configured industry slug, including electrical-store, paint-store, sanitaryware-store and pesticide-store.' example: electrical-store required: false schema: type: string description: 'Filter by one configured industry slug, including electrical-store, paint-store, sanitaryware-store and pesticide-store.' example: electrical-store - in: query name: per_page description: 'Results per page, from 1 to 50.' example: 24 required: false schema: type: integer description: 'Results per page, from 1 to 50.' example: 24 responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29 store_type: pharmacy user_id: 42 business_id: 17 owner_name: 'Dr. Kavitha Reddy' shop_name: 'Aarogya Medical & General Store' contact_email: kavitha@example.com feedback: 'Dukanam made our daily medicine counter billing easier to review.' photo_url: 'https://dukanam.com/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29/photo' photo_mime_type: image/webp photo_size: 98214 status: pending consent_at: '2026-08-31T10:15:00.000000Z' review_note: null reviewed_by: null reviewed_at: null published_at: null submitted_at: '2026-08-31T10:15:00.000000Z' location: null source: customer consent_note: null links: first: 'https://dukanam.com/api/v1/admin/testimonials?page=1' last: 'https://dukanam.com/api/v1/admin/testimonials?page=1' prev: null next: null meta: current_page: 1 from: 1 last_page: 1 links: [] path: 'https://dukanam.com/api/v1/admin/testimonials' per_page: 24 to: 1 total: 1 properties: data: type: array example: - id: 53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29 store_type: pharmacy user_id: 42 business_id: 17 owner_name: 'Dr. Kavitha Reddy' shop_name: 'Aarogya Medical & General Store' contact_email: kavitha@example.com feedback: 'Dukanam made our daily medicine counter billing easier to review.' photo_url: 'https://dukanam.com/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29/photo' photo_mime_type: image/webp photo_size: 98214 status: pending consent_at: '2026-08-31T10:15:00.000000Z' review_note: null reviewed_by: null reviewed_at: null published_at: null submitted_at: '2026-08-31T10:15:00.000000Z' location: null source: customer consent_note: null items: type: object properties: id: type: string example: 53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29 store_type: type: string example: pharmacy user_id: type: integer example: 42 business_id: type: integer example: 17 owner_name: type: string example: 'Dr. Kavitha Reddy' shop_name: type: string example: 'Aarogya Medical & General Store' contact_email: type: string example: kavitha@example.com feedback: type: string example: 'Dukanam made our daily medicine counter billing easier to review.' photo_url: type: string example: 'https://dukanam.com/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29/photo' photo_mime_type: type: string example: image/webp photo_size: type: integer example: 98214 status: type: string example: pending consent_at: type: string example: '2026-08-31T10:15:00.000000Z' review_note: type: string example: null nullable: true reviewed_by: type: string example: null nullable: true reviewed_at: type: string example: null nullable: true published_at: type: string example: null nullable: true submitted_at: type: string example: '2026-08-31T10:15:00.000000Z' location: type: string example: null nullable: true source: type: string example: customer consent_note: type: string example: null nullable: true links: type: object properties: first: type: string example: 'https://dukanam.com/api/v1/admin/testimonials?page=1' last: type: string example: 'https://dukanam.com/api/v1/admin/testimonials?page=1' prev: type: string example: null nullable: true next: type: string example: null nullable: true meta: type: object properties: current_page: type: integer example: 1 from: type: integer example: 1 last_page: type: integer example: 1 links: type: array example: [] path: type: string example: 'https://dukanam.com/api/v1/admin/testimonials' per_page: type: integer example: 24 to: type: integer example: 1 total: type: integer example: 1 tags: - 'Testimonial moderation' post: summary: 'Create a customer testimonial draft.' operationId: createACustomerTestimonialDraft description: "Super admins may draft on behalf of a customer on any plan. Identity and\nprivate contact email come from the selected business and its owner;\ndisplay-name corrections are allowed. uxcrafts.com and its subdomains are\nexcluded for both the owner and business email. Drafts always start pending.\nPhotos are optional. Permission for the exact wording and attribution must\nbe recorded with consent=true and a private consent_note before approval." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: id: 53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29 business_id: 17 user_id: 42 store_type: pharmacy owner_name: 'Kavitha Reddy' shop_name: 'Aarogya Medical Store' location: Hyderabad feedback: 'We have had a positive experience using Dukanam for our pharmacy in Hyderabad.' source: admin status: pending photo_url: null photo_mime_type: null photo_size: null consent_at: null consent_note: null published_at: null properties: data: type: object properties: id: type: string example: 53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29 business_id: type: integer example: 17 user_id: type: integer example: 42 store_type: type: string example: pharmacy description: 'Optional industry page; required before approval.' owner_name: type: string example: 'Kavitha Reddy' shop_name: type: string example: 'Aarogya Medical Store' location: type: string example: Hyderabad description: 'Optional public city or region.' feedback: type: string example: 'We have had a positive experience using Dukanam for our pharmacy in Hyderabad.' source: type: string example: admin description: 'customer for owner submissions, admin for administrator-authored drafts.' status: type: string example: pending photo_url: type: string example: null description: 'Null for text-only testimonials.' photo_mime_type: type: string example: null nullable: true photo_size: type: string example: null nullable: true consent_at: type: string example: null description: 'Null when permission has not been recorded.' consent_note: type: string example: null description: 'Private permission evidence, never returned publicly.' published_at: type: string example: null nullable: true 422: description: '' content: application/json: schema: type: object example: message: 'Choose a customer business with an owner outside uxcrafts.com and its subdomains.' errors: business_id: - 'Choose a customer business with an owner outside uxcrafts.com and its subdomains.' properties: message: type: string example: 'Choose a customer business with an owner outside uxcrafts.com and its subdomains.' errors: type: object properties: business_id: type: array example: - 'Choose a customer business with an owner outside uxcrafts.com and its subdomains.' items: type: string tags: - 'Testimonial moderation' requestBody: required: true content: multipart/form-data: schema: type: object properties: business_id: type: integer description: 'Existing customer business ID. Required on creation; prohibited on editing. Owners or businesses with uxcrafts.com or subdomain emails are excluded. Must match an existing stored value.' example: 17 store_type: type: string description: 'Optional configured industry page slug. An unassigned draft can be saved but requires a page before approval.' example: pharmacy enum: - kirana-store - pharmacy - supermarket - clothing-store - footwear-store - hardware-store - electronics-store - mobile-store - stationery-store - book-store - bakery - cosmetics-store - wholesale-business - auto-parts-store - home-appliance-store - gift-shop - electrical-store - paint-store - sanitaryware-store - pesticide-store nullable: true owner_name: type: string description: 'Optional display-name correction. Defaults to the business owner on creation and keeps the current name on editing. Must be at least 1 character. Must not be greater than 120 characters.' example: 'Kavitha Reddy' nullable: true shop_name: type: string description: 'Optional business display name; defaults to the registered business name. Must be at least 1 character. Must not be greater than 160 characters.' example: 'Aarogya Medical Store' nullable: true location: type: string description: 'Optional public city/region, up to 160 characters. Defaults to the business city on creation. Must not be greater than 160 characters.' example: 'Hyderabad, Telangana' nullable: true feedback: type: string description: 'Customer-supported wording, 40–1200 characters. Always saved pending; editing unpublishes the previous version. Must be at least 40 characters. Must not be greater than 1200 characters.' example: 'We have had a positive experience using Dukanam for our pharmacy in Hyderabad.' photo: type: string format: binary description: 'Optional real shop photo. JPEG, PNG or WebP, 320–6000 pixels per dimension, maximum 5 MB. Omit to keep the existing photo on editing. Must be an image. Must not be greater than 5120 kilobytes.' nullable: true remove_photo: type: boolean description: 'Remove the existing photo on editing; cannot be combined with a new photo.' example: false consent: type: boolean description: 'Confirm permission for the exact wording, attribution, location and any photo. False or omitted clears consent; drafts without consent cannot be approved.' example: true consent_note: type: string description: 'Private source/evidence of permission, maximum 1000 characters. Required when consent is true; never public. This field is required when consent is 1 or true. Must not be greater than 1000 characters.' example: 'Owner approved this wording and public attribution in our customer conversation.' nullable: true required: - business_id - feedback '/api/v1/admin/testimonials/{public_id}': put: summary: 'Edit a testimonial and return it to pending review.' operationId: editATestimonialAndReturnItToPendingReview description: "The business reference cannot be changed. Every edit unpublishes the old\nversion from matching web pages and the public API. Omit photo to keep it,\nor set remove_photo=true to remove it. Confirm consent and supply its private\nnote again for the revised wording; omitted/false consent clears permission.\nFor multipart edits, POST this URL with _method=PUT." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29 business_id: 17 user_id: 42 store_type: pharmacy owner_name: 'Kavitha Reddy' shop_name: 'Aarogya Medical Store' location: Hyderabad feedback: 'We have had a positive experience using Dukanam for our pharmacy in Hyderabad.' source: admin status: pending photo_url: null photo_mime_type: null photo_size: null consent_at: null consent_note: null published_at: null properties: data: type: object properties: id: type: string example: 53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29 business_id: type: integer example: 17 user_id: type: integer example: 42 store_type: type: string example: pharmacy owner_name: type: string example: 'Kavitha Reddy' shop_name: type: string example: 'Aarogya Medical Store' location: type: string example: Hyderabad feedback: type: string example: 'We have had a positive experience using Dukanam for our pharmacy in Hyderabad.' source: type: string example: admin status: type: string example: pending photo_url: type: string example: null nullable: true photo_mime_type: type: string example: null nullable: true photo_size: type: string example: null nullable: true consent_at: type: string example: null nullable: true consent_note: type: string example: null nullable: true published_at: type: string example: null nullable: true tags: - 'Testimonial moderation' requestBody: required: true content: multipart/form-data: schema: type: object properties: store_type: type: string description: 'Optional configured industry page slug. An unassigned draft can be saved but requires a page before approval.' example: pharmacy enum: - kirana-store - pharmacy - supermarket - clothing-store - footwear-store - hardware-store - electronics-store - mobile-store - stationery-store - book-store - bakery - cosmetics-store - wholesale-business - auto-parts-store - home-appliance-store - gift-shop - electrical-store - paint-store - sanitaryware-store - pesticide-store nullable: true owner_name: type: string description: 'Optional display-name correction. Defaults to the business owner on creation and keeps the current name on editing. Must be at least 1 character. Must not be greater than 120 characters.' example: 'Kavitha Reddy' nullable: true shop_name: type: string description: 'Optional business display name; defaults to the registered business name. Must be at least 1 character. Must not be greater than 160 characters.' example: 'Aarogya Medical Store' nullable: true location: type: string description: 'Optional public city/region, up to 160 characters. Defaults to the business city on creation. Must not be greater than 160 characters.' example: 'Hyderabad, Telangana' nullable: true feedback: type: string description: 'Customer-supported wording, 40–1200 characters. Always saved pending; editing unpublishes the previous version. Must be at least 40 characters. Must not be greater than 1200 characters.' example: 'We have had a positive experience using Dukanam for our pharmacy in Hyderabad.' photo: type: string format: binary description: 'Optional real shop photo. JPEG, PNG or WebP, 320–6000 pixels per dimension, maximum 5 MB. Omit to keep the existing photo on editing. Must be an image. Must not be greater than 5120 kilobytes.' nullable: true remove_photo: type: boolean description: 'Remove the existing photo on editing; cannot be combined with a new photo.' example: false consent: type: boolean description: 'Confirm permission for the exact wording, attribution, location and any photo. False or omitted clears consent; drafts without consent cannot be approved.' example: true consent_note: type: string description: 'Private source/evidence of permission, maximum 1000 characters. Required when consent is true; never public. This field is required when consent is 1 or true. Must not be greater than 1000 characters.' example: 'Owner approved this wording and public attribution in our customer conversation.' nullable: true required: - feedback patch: summary: 'Approve or reject a testimonial.' operationId: approveOrRejectATestimonial description: "Approval publishes the story on its matching industry page.\nRejection removes it from public API and web responses while preserving the\nprivate moderation record.\nApproval returns 422 without recorded permission or for internal uxcrafts.com\nbusinesses/owners. Rejection is the reversible Unpublish action." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29 store_type: pharmacy user_id: 42 business_id: 17 owner_name: 'Dr. Kavitha Reddy' shop_name: 'Aarogya Medical & General Store' contact_email: kavitha@example.com feedback: 'Dukanam made our daily medicine counter billing easier to review.' photo_url: 'https://dukanam.com/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29/photo' photo_mime_type: image/webp photo_size: 98214 status: approved consent_at: '2026-08-31T10:15:00.000000Z' review_note: Verified reviewed_by: id: 1 name: 'Platform Admin' reviewed_at: '2026-08-31T10:30:00.000000Z' published_at: '2026-08-31T10:30:00.000000Z' submitted_at: '2026-08-31T10:15:00.000000Z' location: null source: customer consent_note: null properties: data: type: object properties: id: type: string example: 53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29 store_type: type: string example: pharmacy user_id: type: integer example: 42 business_id: type: integer example: 17 owner_name: type: string example: 'Dr. Kavitha Reddy' shop_name: type: string example: 'Aarogya Medical & General Store' contact_email: type: string example: kavitha@example.com feedback: type: string example: 'Dukanam made our daily medicine counter billing easier to review.' photo_url: type: string example: 'https://dukanam.com/testimonials/53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29/photo' photo_mime_type: type: string example: image/webp photo_size: type: integer example: 98214 status: type: string example: approved consent_at: type: string example: '2026-08-31T10:15:00.000000Z' review_note: type: string example: Verified reviewed_by: type: object properties: id: type: integer example: 1 name: type: string example: 'Platform Admin' reviewed_at: type: string example: '2026-08-31T10:30:00.000000Z' published_at: type: string example: '2026-08-31T10:30:00.000000Z' submitted_at: type: string example: '2026-08-31T10:15:00.000000Z' location: type: string example: null nullable: true source: type: string example: customer consent_note: type: string example: null nullable: true tags: - 'Testimonial moderation' requestBody: required: true content: application/json: schema: type: object properties: status: type: string description: 'Moderation decision. Must be approved or rejected.' example: approved enum: - approved - rejected review_note: type: string description: 'Private moderation note. Must not be greater than 1000 characters.' example: 'Photo and first-hand statement verified.' nullable: true required: - status parameters: - in: path name: public_id description: 'Public testimonial UUID.' example: 53d5597b-4a34-4cf0-9dd3-7a0ae9f64c29 required: true schema: type: string '/api/v1/businesses/{business_id}/feedback': get: summary: 'List my private feedback.' operationId: listMyPrivateFeedback description: '' parameters: - in: query name: per_page description: 'Results per page, from 1 to 24.' example: 12 required: false schema: type: integer description: 'Results per page, from 1 to 24.' example: 12 responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 82fdb616-df56-4547-9e09-5f58f4740acd feedback_type: suggestion message: 'Please add a daily counter export to the mobile dashboard.' photo_url: null status: pending response: null submitted_at: '2026-08-31T10:15:00.000000Z' reviewed_at: null links: {} meta: current_page: 1 per_page: 12 total: 1 properties: data: type: array example: - id: 82fdb616-df56-4547-9e09-5f58f4740acd feedback_type: suggestion message: 'Please add a daily counter export to the mobile dashboard.' photo_url: null status: pending response: null submitted_at: '2026-08-31T10:15:00.000000Z' reviewed_at: null items: type: object properties: id: type: string example: 82fdb616-df56-4547-9e09-5f58f4740acd feedback_type: type: string example: suggestion message: type: string example: 'Please add a daily counter export to the mobile dashboard.' photo_url: type: string example: null nullable: true status: type: string example: pending response: type: string example: null nullable: true submitted_at: type: string example: '2026-08-31T10:15:00.000000Z' reviewed_at: type: string example: null nullable: true links: type: object properties: {} meta: type: object properties: current_page: type: integer example: 1 per_page: type: integer example: 12 total: type: integer example: 1 tags: - Feedback post: summary: 'Submit private feedback.' operationId: submitPrivateFeedback description: "Available to every authenticated workspace user, including free plans.\nFeedback is never added to public testimonial pages." parameters: [] responses: 202: description: '' content: application/json: schema: type: object example: message: 'Thank you. Your feedback has been sent to the Dukanam team.' data: id: 82fdb616-df56-4547-9e09-5f58f4740acd feedback_type: suggestion message: 'Please add a daily counter export to the mobile dashboard.' photo_url: null status: pending response: null submitted_at: '2026-08-31T10:15:00.000000Z' reviewed_at: null properties: message: type: string example: 'Thank you. Your feedback has been sent to the Dukanam team.' data: type: object properties: id: type: string example: 82fdb616-df56-4547-9e09-5f58f4740acd feedback_type: type: string example: suggestion message: type: string example: 'Please add a daily counter export to the mobile dashboard.' photo_url: type: string example: null nullable: true status: type: string example: pending response: type: string example: null nullable: true submitted_at: type: string example: '2026-08-31T10:15:00.000000Z' reviewed_at: type: string example: null nullable: true tags: - Feedback requestBody: required: true content: multipart/form-data: schema: type: object properties: feedback_type: type: string description: 'Improvement, complaint or suggestion.' example: suggestion enum: - improvement - complaint - suggestion message: type: string description: 'Private product feedback, 20 to 2,000 characters. Must be at least 20 characters. Must not be greater than 2000 characters.' example: 'Please add an option to export the daily counter summary directly from the mobile dashboard.' photo: type: string format: binary description: 'Optional JPEG, PNG or WebP screenshot or shop photo, 320×320 to 6000×6000 pixels, maximum 5 MB. Must be an image. Must not be greater than 5120 kilobytes.' nullable: true website: type: string description: 'Spam-trap field. Omit it or leave it empty.' example: null nullable: true required: - feedback_type - message parameters: - in: path name: business_id description: 'Workspace ID.' example: 17 required: true schema: type: integer /api/v1/admin/feedback: get: summary: 'List private customer feedback.' operationId: listPrivateCustomerFeedback description: '' parameters: - in: query name: status description: 'Filter by pending, reviewed or resolved.' example: pending required: false schema: type: string description: 'Filter by pending, reviewed or resolved.' example: pending - in: query name: feedback_type description: 'Filter by improvement, complaint or suggestion.' example: complaint required: false schema: type: string description: 'Filter by improvement, complaint or suggestion.' example: complaint - in: query name: per_page description: 'Results per page, from 1 to 50.' example: 24 required: false schema: type: integer description: 'Results per page, from 1 to 50.' example: 24 responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 82fdb616-df56-4547-9e09-5f58f4740acd feedback_type: complaint message: 'The mobile report did not load after closing the counter.' photo_url: null photo_mime_type: null photo_size: null status: pending submitter: id: 42 name: 'Kavitha Reddy' email: kavitha@example.com business: id: 17 name: 'Aarogya Medical Store' response: null reviewed_by: null submitted_at: '2026-08-31T10:15:00.000000Z' reviewed_at: null links: {} meta: current_page: 1 per_page: 24 total: 1 properties: data: type: array example: - id: 82fdb616-df56-4547-9e09-5f58f4740acd feedback_type: complaint message: 'The mobile report did not load after closing the counter.' photo_url: null photo_mime_type: null photo_size: null status: pending submitter: id: 42 name: 'Kavitha Reddy' email: kavitha@example.com business: id: 17 name: 'Aarogya Medical Store' response: null reviewed_by: null submitted_at: '2026-08-31T10:15:00.000000Z' reviewed_at: null items: type: object properties: id: type: string example: 82fdb616-df56-4547-9e09-5f58f4740acd feedback_type: type: string example: complaint message: type: string example: 'The mobile report did not load after closing the counter.' photo_url: type: string example: null nullable: true photo_mime_type: type: string example: null nullable: true photo_size: type: string example: null nullable: true status: type: string example: pending submitter: type: object properties: id: type: integer example: 42 name: type: string example: 'Kavitha Reddy' email: type: string example: kavitha@example.com business: type: object properties: id: type: integer example: 17 name: type: string example: 'Aarogya Medical Store' response: type: string example: null nullable: true reviewed_by: type: string example: null nullable: true submitted_at: type: string example: '2026-08-31T10:15:00.000000Z' reviewed_at: type: string example: null nullable: true links: type: object properties: {} meta: type: object properties: current_page: type: integer example: 1 per_page: type: integer example: 24 total: type: integer example: 1 tags: - 'Feedback moderation' requestBody: required: false content: application/json: schema: type: object properties: status: type: string description: '' example: null nullable: true feedback_type: type: string description: '' example: null nullable: true per_page: type: integer description: 'Must be at least 1. Must not be greater than 50.' example: 1 nullable: true '/api/v1/admin/feedback/{public_id}': patch: summary: 'Mark customer feedback reviewed or resolved.' operationId: markCustomerFeedbackReviewedOrResolved description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 82fdb616-df56-4547-9e09-5f58f4740acd feedback_type: suggestion message: 'Please add a mobile daily summary export.' photo_url: null photo_mime_type: null photo_size: null status: reviewed submitter: id: 42 name: 'Kavitha Reddy' email: kavitha@example.com business: id: 17 name: 'Aarogya Medical Store' response: 'Added to the mobile backlog.' reviewed_by: id: 1 name: 'Platform Admin' submitted_at: '2026-08-31T10:15:00.000000Z' reviewed_at: '2026-08-31T11:00:00.000000Z' properties: data: type: object properties: id: type: string example: 82fdb616-df56-4547-9e09-5f58f4740acd feedback_type: type: string example: suggestion message: type: string example: 'Please add a mobile daily summary export.' photo_url: type: string example: null nullable: true photo_mime_type: type: string example: null nullable: true photo_size: type: string example: null nullable: true status: type: string example: reviewed submitter: type: object properties: id: type: integer example: 42 name: type: string example: 'Kavitha Reddy' email: type: string example: kavitha@example.com business: type: object properties: id: type: integer example: 17 name: type: string example: 'Aarogya Medical Store' response: type: string example: 'Added to the mobile backlog.' reviewed_by: type: object properties: id: type: integer example: 1 name: type: string example: 'Platform Admin' submitted_at: type: string example: '2026-08-31T10:15:00.000000Z' reviewed_at: type: string example: '2026-08-31T11:00:00.000000Z' tags: - 'Feedback moderation' requestBody: required: true content: application/json: schema: type: object properties: status: type: string description: 'Feedback workflow status. Must be reviewed or resolved.' example: reviewed enum: - reviewed - resolved review_note: type: string description: 'Private administrator response or handling note. Must not be greater than 1000 characters.' example: 'Added to the mobile dashboard backlog.' nullable: true required: - status parameters: - in: path name: public_id description: 'Public feedback UUID.' example: 82fdb616-df56-4547-9e09-5f58f4740acd required: true schema: type: string /api/v1/apple/subscription/verify: post: summary: 'Verify an App Store subscription purchase' operationId: verifyAnAppStoreSubscriptionPurchase description: "Send either StoreKit's signed transaction JWS or a transaction ID plus its environment. The business owner\nmust pass the `app_account_token` returned by the current-subscription endpoint to StoreKit when purchasing.\nThe plan, dates, and status always come from Apple's signed data." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: success: true business_id: 1 app_account_token: 2ee48be5-b23e-46e8-b84d-5e21850d42d2 subscription: provider: apple plan: business billing_period: yearly product_id: com.dukanam.business.yearly status: active is_active: true expires_at: '2027-09-04T10:30:00.000000Z' auto_renew_status: true environment: production properties: success: type: boolean example: true business_id: type: integer example: 1 app_account_token: type: string example: 2ee48be5-b23e-46e8-b84d-5e21850d42d2 subscription: type: object properties: provider: type: string example: apple plan: type: string example: business billing_period: type: string example: yearly product_id: type: string example: com.dukanam.business.yearly status: type: string example: active is_active: type: boolean example: true expires_at: type: string example: '2027-09-04T10:30:00.000000Z' auto_renew_status: type: boolean example: true environment: type: string example: production 403: description: '' content: application/json: schema: type: object example: message: 'Only the business owner can manage this subscription.' properties: message: type: string example: 'Only the business owner can manage this subscription.' 409: description: '' content: application/json: schema: type: object example: message: 'The Apple transaction is not linked to this Dukanam business.' properties: message: type: string example: 'The Apple transaction is not linked to this Dukanam business.' 422: description: '' content: application/json: schema: type: object example: message: 'This Apple product is not supported.' properties: message: type: string example: 'This Apple product is not supported.' 503: description: '' content: application/json: schema: type: object example: message: 'Apple subscriptions are not enabled.' properties: message: type: string example: 'Apple subscriptions are not enabled.' tags: - 'Apple subscriptions' requestBody: required: true content: application/json: schema: type: object properties: business_id: type: integer description: 'The business receiving this entitlement.' example: 1 signedTransactionInfo: type: string description: 'StoreKit 2 transaction JWS. Required without transactionId.' example: eyJhbGciOiJFUzI1NiIsIng1YyI6WyIuLi4iXX0.eyJ0cmFuc2FjdGlvbklkIjoiMjAwMDAwMTIzNDU2Nzg5MCJ9.signature nullable: true transactionId: type: string description: 'App Store transaction ID. Required without signedTransactionInfo.' example: '2000001234567890' nullable: true environment: type: string description: 'Required with transactionId. Must be `sandbox` or `production`.' example: sandbox nullable: true required: - business_id /api/v1/apple/subscription/sync: post: summary: 'Synchronize an App Store subscription' operationId: synchronizeAnAppStoreSubscription description: "Reconciles the linked subscription with the App Store Server API. Use after Restore Purchases or to recover\nfrom a missed server notification." parameters: [] responses: 403: description: '' content: application/json: schema: type: object example: message: 'Only the business owner can manage this subscription.' properties: message: type: string example: 'Only the business owner can manage this subscription.' 422: description: '' content: application/json: schema: type: object example: message: 'No Apple subscription has been linked to this business yet.' properties: message: type: string example: 'No Apple subscription has been linked to this business yet.' 503: description: '' content: application/json: schema: type: object example: message: 'App Store Server API credentials are not configured.' properties: message: type: string example: 'App Store Server API credentials are not configured.' tags: - 'Apple subscriptions' requestBody: required: true content: application/json: schema: type: object properties: business_id: type: integer description: 'The current business.' example: 1 required: - business_id /api/v1/me/subscription: get: summary: 'Get the current subscription entitlement' operationId: getTheCurrentSubscriptionEntitlement description: "Returns the backend-authoritative plan and the stable `app_account_token` that Flutter must attach to every\nStoreKit purchase for this business. Free Essentials is returned when there is no usable paid entitlement." parameters: - in: query name: business_id description: 'The current business.' example: 1 required: true schema: type: integer description: 'The current business.' example: 1 responses: 200: description: '' content: application/json: schema: type: object example: success: true business_id: 1 app_account_token: 2ee48be5-b23e-46e8-b84d-5e21850d42d2 subscription: provider: null plan: free_essentials billing_period: null product_id: null status: active is_active: true expires_at: null auto_renew_status: null environment: null properties: success: type: boolean example: true business_id: type: integer example: 1 app_account_token: type: string example: 2ee48be5-b23e-46e8-b84d-5e21850d42d2 subscription: type: object properties: provider: type: string example: null nullable: true plan: type: string example: free_essentials billing_period: type: string example: null nullable: true product_id: type: string example: null nullable: true status: type: string example: active is_active: type: boolean example: true expires_at: type: string example: null nullable: true auto_renew_status: type: string example: null nullable: true environment: type: string example: null nullable: true 403: description: '' content: application/json: schema: type: object example: message: 'Only the business owner can manage this subscription.' properties: message: type: string example: 'Only the business owner can manage this subscription.' tags: - 'Apple subscriptions' /api/v1/apple/notifications: post: summary: 'Receive App Store Server Notifications V2' operationId: receiveAppStoreServerNotificationsV2 description: "Public Apple webhook. Every payload and nested transaction is cryptographically verified and processed\nidempotently by notification UUID. This endpoint does not use Dukanam bearer authentication." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: success: true properties: success: type: boolean example: true 400: description: '' content: application/json: schema: type: object example: message: 'Apple could not verify the signed notification.' properties: message: type: string example: 'Apple could not verify the signed notification.' tags: - 'Apple subscriptions' requestBody: required: true content: application/json: schema: type: object properties: signedPayload: type: string description: "Apple's App Store Server Notifications V2 JWS." example: eyJhbGciOiJFUzI1NiIsIng1YyI6WyIuLi4iXX0.eyJub3RpZmljYXRpb25UeXBlIjoiVEVTVCJ9.signature required: - signedPayload security: [] /api/v1/account/deletion: get: summary: 'Show the deletion status and what a deletion would destroy.' operationId: showTheDeletionStatusAndWhatADeletionWouldDestroy description: "`owned_workspaces` are erased entirely, including every team member's access.\n`joined_workspaces` survive; only this user's membership in them is removed.\nApple billing is not cancelled by deletion. Display the warning and management URL\nwhen present; active, trialing and billing-retry Apple records trigger the warning." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: pending: false grace_days: 7 scheduled_for: null owned_workspaces: - name: 'Sri Balaji Stores' team_members: 2 has_billing_records: true has_apple_subscription: true joined_workspaces: - 'Anand Traders' apple_subscription_warning: 'Deleting your Dukanam account does not cancel your Apple subscription. Apple may continue billing you until you cancel it in your Apple subscription settings.' apple_subscription_management_url: 'https://apps.apple.com/account/subscriptions' properties: data: type: object properties: pending: type: boolean example: false grace_days: type: integer example: 7 scheduled_for: type: string example: null nullable: true owned_workspaces: type: array example: - name: 'Sri Balaji Stores' team_members: 2 has_billing_records: true has_apple_subscription: true items: type: object properties: name: type: string example: 'Sri Balaji Stores' team_members: type: integer example: 2 has_billing_records: type: boolean example: true has_apple_subscription: type: boolean example: true joined_workspaces: type: array example: - 'Anand Traders' items: type: string apple_subscription_warning: type: string example: 'Deleting your Dukanam account does not cancel your Apple subscription. Apple may continue billing you until you cancel it in your Apple subscription settings.' apple_subscription_management_url: type: string example: 'https://apps.apple.com/account/subscriptions' tags: - 'Account deletion' post: summary: 'Request deletion of the account and its data.' operationId: requestDeletionOfTheAccountAndItsData description: "The account stays usable during the grace period so the request can be\ncancelled. Push notifications stop immediately.\nThe confirmation email warns owners of outstanding Apple subscriptions and links\nto Apple's subscription settings. Deletion never cancels Apple billing." parameters: [] responses: 202: description: '' content: application/json: schema: type: object example: message: 'Your account is scheduled for deletion.' data: scheduled_for: '2026-09-06T10:15:00+00:00' properties: message: type: string example: 'Your account is scheduled for deletion.' data: type: object properties: scheduled_for: type: string example: '2026-09-06T10:15:00+00:00' 403: description: '' content: application/json: schema: type: object example: message: 'Administrator accounts cannot be deleted from the app. Contact support.' properties: message: type: string example: 'Administrator accounts cannot be deleted from the app. Contact support.' 422: description: '' content: application/json: schema: type: object example: message: 'The given data was invalid.' errors: confirmation: - 'Type DELETE to confirm that you want the account erased.' properties: message: type: string example: 'The given data was invalid.' errors: type: object properties: confirmation: type: array example: - 'Type DELETE to confirm that you want the account erased.' items: type: string tags: - 'Account deletion' requestBody: required: true content: application/json: schema: type: object properties: current_password: type: string description: 'The signed-in account password.' example: correct-horse-battery confirmation: type: string description: 'Must be the word DELETE.' example: DELETE reason: type: string description: 'An optional reason, kept only until the purge runs.' example: 'Closing the shop' nullable: true required: - current_password - confirmation delete: summary: 'Cancel a pending deletion request.' operationId: cancelAPendingDeletionRequest description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Account deletion cancelled.' properties: message: type: string example: 'Account deletion cancelled.' tags: - 'Account deletion' /api/v1/admin/app-link/analytics: get: summary: 'Summarise app link opens.' operationId: summariseAppLinkOpens description: "Totals, store-page engagement, per-page rows, a zero-filled daily series, a weekday × hour\ngrid (ISO weekday 1 = Monday, hours 0–23), top-15 breakdowns by source, medium, campaign, platform, device, operating\nsystem, browser, in-app browser, referrer, country, region, language, page language,\ncall to action and screen size, the top 50\ncities with approximate coordinates, and link-preview fetches. `previous_period` covers\nthe same number of days immediately before `from`, for comparison.\n\nSource keys: whatsapp, instagram, facebook, meta (a Meta ad click whose app is unknown),\nqr, sms, google, youtube, x, linkedin, telegram, email, referral (another website),\ninternal (a link on dukanam.com) and direct (untagged with no referrer). Any other `utm_source` value is kept as sent, normalised.\nOutcomes: play_store and app_store are store redirects; landing_page means the page was shown." parameters: - in: query name: from description: 'First calendar day, YYYY-MM-DD. Defaults to 29 days before `to`. Must be a valid date in the format Y-m-d.' example: '2026-01-15' required: false schema: type: string description: 'First calendar day, YYYY-MM-DD. Defaults to 29 days before `to`. Must be a valid date in the format Y-m-d.' example: '2026-01-15' nullable: true - in: query name: to description: 'Last calendar day, YYYY-MM-DD. Defaults to today. The range may cover at most 366 days. Must be a valid date in the format Y-m-d.' example: '2026-01-15' required: false schema: type: string description: 'Last calendar day, YYYY-MM-DD. Defaults to today. The range may cover at most 366 days. Must be a valid date in the format Y-m-d.' example: '2026-01-15' nullable: true - in: query name: source description: 'Only opens from this source key, such as whatsapp, instagram, qr, sms, facebook, meta, google, referral or direct. Must not be greater than 50 characters.' example: whatsapp required: false schema: type: string description: 'Only opens from this source key, such as whatsapp, instagram, qr, sms, facebook, meta, google, referral or direct. Must not be greater than 50 characters.' example: whatsapp nullable: true - in: query name: campaign description: 'Only opens tagged with this campaign, as normalised (lower case, underscores). Must not be greater than 120 characters.' example: diwali_2026 required: false schema: type: string description: 'Only opens tagged with this campaign, as normalised (lower case, underscores). Must not be greater than 120 characters.' example: diwali_2026 nullable: true - in: query name: platform description: 'Only opens from android, ios, desktop or other.' example: android required: false schema: type: string description: 'Only opens from android, ios, desktop or other.' example: android enum: - android - ios - desktop - other nullable: true - in: query name: tracked_page description: 'Only opens of this page: app (the /app download link), stores (the store-type index), or a store-type key such as pharmacy or kirana-store.' example: app required: false schema: type: string description: 'Only opens of this page: app (the /app download link), stores (the store-type index), or a store-type key such as pharmacy or kirana-store.' example: app enum: - app - stores - kirana-store - pharmacy - supermarket - clothing-store - footwear-store - hardware-store - electronics-store - mobile-store - stationery-store - book-store - bakery - cosmetics-store - wholesale-business - auto-parts-store - home-appliance-store - gift-shop - electrical-store - paint-store - sanitaryware-store - pesticide-store nullable: true - in: query name: per_page description: 'Visits per page, 1 to 100. Visit lists only. Defaults to 25. Must be at least 1. Must not be greater than 100.' example: 25 required: false schema: type: integer description: 'Visits per page, 1 to 100. Visit lists only. Defaults to 25. Must be at least 1. Must not be greater than 100.' example: 25 nullable: true - in: query name: page description: 'Page number. Visit lists only. Must be at least 1.' example: 1 required: false schema: type: integer description: 'Page number. Visit lists only. Must be at least 1.' example: 1 nullable: true responses: 200: description: '' content: application/json: schema: type: object example: data: range: from: '2026-09-20' to: '2026-09-26' days: 7 filters: source: null campaign: null platform: null tracked_page: null totals: opens: 5 unique_visitors: 5 new_visitors: 4 returning_visitors: 1 store_redirects: 3 play_store: 2 app_store: 1 landing_page: 2 link_previews: 1 engagement: page_views: 2 average_seconds: 100 average_scroll_depth: 80 call_to_action_opens: 2 call_to_action_rate: 100 signup_or_app_opens: 1 previous_period: from: '2026-09-13' to: '2026-09-19' opens: 0 unique_visitors: 0 store_redirects: 0 daily: - date: '2026-09-20' opens: 0 unique_visitors: 0 play_store: 0 app_store: 0 landing_page: 0 - date: '2026-09-21' opens: 0 unique_visitors: 0 play_store: 0 app_store: 0 landing_page: 0 - date: '2026-09-22' opens: 0 unique_visitors: 0 play_store: 0 app_store: 0 landing_page: 0 - date: '2026-09-23' opens: 1 unique_visitors: 1 play_store: 1 app_store: 0 landing_page: 0 - date: '2026-09-24' opens: 1 unique_visitors: 1 play_store: 1 app_store: 0 landing_page: 0 - date: '2026-09-25' opens: 2 unique_visitors: 2 play_store: 0 app_store: 1 landing_page: 1 - date: '2026-09-26' opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 weekday_hours: - weekday: 1 hours: - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - weekday: 2 hours: - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - weekday: 3 hours: - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 1 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - weekday: 4 hours: - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 1 - 0 - 0 - 0 - 0 - weekday: 5 hours: - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 1 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 1 - 0 - 0 - weekday: 6 hours: - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 1 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - weekday: 7 hours: - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 pages: - key: app label: 'App download link (/app)' opens: 3 unique_visitors: 3 play_store: 2 app_store: 1 landing_page: 0 average_seconds: null average_scroll_depth: null call_to_action_opens: 0 signup_clicks: 0 - key: stores label: 'All store types' opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 average_seconds: 74 average_scroll_depth: 88 call_to_action_opens: 1 signup_clicks: 0 - key: pharmacy label: Pharmacies opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 average_seconds: 126 average_scroll_depth: 71 call_to_action_opens: 1 signup_clicks: 1 breakdowns: sources: - key: instagram label: Instagram opens: 2 unique_visitors: 2 play_store: 1 app_store: 0 landing_page: 1 - key: whatsapp label: WhatsApp opens: 1 unique_visitors: 1 play_store: 1 app_store: 0 landing_page: 0 - key: qr label: 'QR code' opens: 1 unique_visitors: 1 play_store: 0 app_store: 1 landing_page: 0 - key: google label: Google opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 mediums: - key: paid label: paid opens: 2 unique_visitors: 2 play_store: 1 app_store: 0 landing_page: 1 - key: social label: social opens: 1 unique_visitors: 1 play_store: 1 app_store: 0 landing_page: 0 - key: organic label: organic opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 - key: offline label: offline opens: 1 unique_visitors: 1 play_store: 0 app_store: 1 landing_page: 0 campaigns: - key: diwali_2026 label: diwali_2026 opens: 2 unique_visitors: 2 play_store: 2 app_store: 0 landing_page: 0 - key: poster_vijayawada label: poster_vijayawada opens: 1 unique_visitors: 1 play_store: 0 app_store: 1 landing_page: 0 - key: pharmacy_launch label: pharmacy_launch opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 - key: null label: 'Not tagged' opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 platforms: - key: android label: Android opens: 3 unique_visitors: 3 play_store: 2 app_store: 0 landing_page: 1 - key: ios label: 'iPhone / iPad' opens: 1 unique_visitors: 1 play_store: 0 app_store: 1 landing_page: 0 - key: desktop label: Desktop opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 device_types: - key: smartphone label: Smartphone opens: 4 unique_visitors: 4 play_store: 2 app_store: 1 landing_page: 1 - key: desktop label: Desktop opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 device_brands: - key: Realme label: Realme opens: 2 unique_visitors: 2 play_store: 1 app_store: 0 landing_page: 1 - key: Samsung label: Samsung opens: 1 unique_visitors: 1 play_store: 1 app_store: 0 landing_page: 0 - key: Apple label: Apple opens: 1 unique_visitors: 1 play_store: 0 app_store: 1 landing_page: 0 device_models: - key: 'Realme · C55' label: 'Realme C55' opens: 2 unique_visitors: 2 play_store: 1 app_store: 0 landing_page: 1 - key: null label: Unknown opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 - key: 'Apple · iPhone' label: 'Apple iPhone' opens: 1 unique_visitors: 1 play_store: 0 app_store: 1 landing_page: 0 - key: 'Samsung · Galaxy A15' label: 'Samsung Galaxy A15' opens: 1 unique_visitors: 1 play_store: 1 app_store: 0 landing_page: 0 operating_systems: - key: 'Android · 13' label: 'Android 13' opens: 2 unique_visitors: 2 play_store: 1 app_store: 0 landing_page: 1 - key: 'Android · 14' label: 'Android 14' opens: 1 unique_visitors: 1 play_store: 1 app_store: 0 landing_page: 0 - key: 'Windows · 10' label: 'Windows 10' opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 - key: 'iOS · 17.4' label: 'iOS 17.4' opens: 1 unique_visitors: 1 play_store: 0 app_store: 1 landing_page: 0 browsers: - key: 'Mobile Safari' label: 'Mobile Safari' opens: 1 unique_visitors: 1 play_store: 0 app_store: 1 landing_page: 0 - key: Chromium label: Chromium opens: 1 unique_visitors: 1 play_store: 1 app_store: 0 landing_page: 0 - key: 'Chrome Mobile' label: 'Chrome Mobile' opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 - key: Chrome label: Chrome opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 in_app_browsers: - key: Instagram label: Instagram opens: 1 unique_visitors: 1 play_store: 1 app_store: 0 landing_page: 0 referrers: - key: l.instagram.com label: l.instagram.com opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 - key: www.google.com label: www.google.com opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 countries: - key: IN label: India opens: 5 unique_visitors: 5 play_store: 2 app_store: 1 landing_page: 2 regions: - key: 'IN · Andhra Pradesh' label: 'Andhra Pradesh, IN' opens: 3 unique_visitors: 3 play_store: 1 app_store: 1 landing_page: 1 - key: 'IN · Karnataka' label: 'Karnataka, IN' opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 - key: 'IN · Telangana' label: 'Telangana, IN' opens: 1 unique_visitors: 1 play_store: 1 app_store: 0 landing_page: 0 cities: - key: 'IN · Andhra Pradesh · Vijayawada' label: Vijayawada region: 'Andhra Pradesh' country_code: IN opens: 2 unique_visitors: 2 latitude: 16.5062 longitude: 80.648 - key: 'IN · Karnataka · Bengaluru' label: Bengaluru region: Karnataka country_code: IN opens: 1 unique_visitors: 1 latitude: 12.9716 longitude: 77.5946 - key: 'IN · Andhra Pradesh · Guntur' label: Guntur region: 'Andhra Pradesh' country_code: IN opens: 1 unique_visitors: 1 latitude: 16.3067 longitude: 80.4365 - key: 'IN · Telangana · Hyderabad' label: Hyderabad region: Telangana country_code: IN opens: 1 unique_visitors: 1 latitude: 17.385 longitude: 78.4867 languages: - key: te-IN label: te-IN opens: 5 unique_visitors: 5 play_store: 2 app_store: 1 landing_page: 2 page_languages: - key: te label: తెలుగు opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 calls_to_action: - key: signup label: 'Start free / sign up' opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 - key: pricing label: Pricing opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 screen_sizes: - key: '412 · 915' label: '412 × 915' opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 - key: '1920 · 1080' label: '1920 × 1080' opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 link_previews: - label: WhatsApp fetches: 1 properties: data: type: object properties: range: type: object properties: from: type: string example: '2026-09-20' to: type: string example: '2026-09-26' days: type: integer example: 7 filters: type: object properties: source: type: string example: null nullable: true campaign: type: string example: null nullable: true platform: type: string example: null nullable: true tracked_page: type: string example: null nullable: true totals: type: object properties: opens: type: integer example: 5 unique_visitors: type: integer example: 5 new_visitors: type: integer example: 4 returning_visitors: type: integer example: 1 store_redirects: type: integer example: 3 play_store: type: integer example: 2 app_store: type: integer example: 1 landing_page: type: integer example: 2 link_previews: type: integer example: 1 engagement: type: object properties: page_views: type: integer example: 2 average_seconds: type: integer example: 100 average_scroll_depth: type: integer example: 80 call_to_action_opens: type: integer example: 2 call_to_action_rate: type: integer example: 100 signup_or_app_opens: type: integer example: 1 previous_period: type: object properties: from: type: string example: '2026-09-13' to: type: string example: '2026-09-19' opens: type: integer example: 0 unique_visitors: type: integer example: 0 store_redirects: type: integer example: 0 daily: type: array example: - date: '2026-09-20' opens: 0 unique_visitors: 0 play_store: 0 app_store: 0 landing_page: 0 - date: '2026-09-21' opens: 0 unique_visitors: 0 play_store: 0 app_store: 0 landing_page: 0 - date: '2026-09-22' opens: 0 unique_visitors: 0 play_store: 0 app_store: 0 landing_page: 0 - date: '2026-09-23' opens: 1 unique_visitors: 1 play_store: 1 app_store: 0 landing_page: 0 - date: '2026-09-24' opens: 1 unique_visitors: 1 play_store: 1 app_store: 0 landing_page: 0 - date: '2026-09-25' opens: 2 unique_visitors: 2 play_store: 0 app_store: 1 landing_page: 1 - date: '2026-09-26' opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 items: type: object properties: date: type: string example: '2026-09-20' opens: type: integer example: 0 unique_visitors: type: integer example: 0 play_store: type: integer example: 0 app_store: type: integer example: 0 landing_page: type: integer example: 0 weekday_hours: type: array example: - weekday: 1 hours: - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - weekday: 2 hours: - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - weekday: 3 hours: - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 1 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - weekday: 4 hours: - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 1 - 0 - 0 - 0 - 0 - weekday: 5 hours: - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 1 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 1 - 0 - 0 - weekday: 6 hours: - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 1 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - weekday: 7 hours: - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 items: type: object properties: weekday: type: integer example: 1 hours: type: array example: - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 items: type: integer pages: type: array example: - key: app label: 'App download link (/app)' opens: 3 unique_visitors: 3 play_store: 2 app_store: 1 landing_page: 0 average_seconds: null average_scroll_depth: null call_to_action_opens: 0 signup_clicks: 0 - key: stores label: 'All store types' opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 average_seconds: 74 average_scroll_depth: 88 call_to_action_opens: 1 signup_clicks: 0 - key: pharmacy label: Pharmacies opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 average_seconds: 126 average_scroll_depth: 71 call_to_action_opens: 1 signup_clicks: 1 items: type: object properties: key: type: string example: app label: type: string example: 'App download link (/app)' opens: type: integer example: 3 unique_visitors: type: integer example: 3 play_store: type: integer example: 2 app_store: type: integer example: 1 landing_page: type: integer example: 0 average_seconds: type: string example: null nullable: true average_scroll_depth: type: string example: null nullable: true call_to_action_opens: type: integer example: 0 signup_clicks: type: integer example: 0 breakdowns: type: object properties: sources: type: array example: - key: instagram label: Instagram opens: 2 unique_visitors: 2 play_store: 1 app_store: 0 landing_page: 1 - key: whatsapp label: WhatsApp opens: 1 unique_visitors: 1 play_store: 1 app_store: 0 landing_page: 0 - key: qr label: 'QR code' opens: 1 unique_visitors: 1 play_store: 0 app_store: 1 landing_page: 0 - key: google label: Google opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 items: type: object properties: key: type: string example: instagram label: type: string example: Instagram opens: type: integer example: 2 unique_visitors: type: integer example: 2 play_store: type: integer example: 1 app_store: type: integer example: 0 landing_page: type: integer example: 1 mediums: type: array example: - key: paid label: paid opens: 2 unique_visitors: 2 play_store: 1 app_store: 0 landing_page: 1 - key: social label: social opens: 1 unique_visitors: 1 play_store: 1 app_store: 0 landing_page: 0 - key: organic label: organic opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 - key: offline label: offline opens: 1 unique_visitors: 1 play_store: 0 app_store: 1 landing_page: 0 items: type: object properties: key: type: string example: paid label: type: string example: paid opens: type: integer example: 2 unique_visitors: type: integer example: 2 play_store: type: integer example: 1 app_store: type: integer example: 0 landing_page: type: integer example: 1 campaigns: type: array example: - key: diwali_2026 label: diwali_2026 opens: 2 unique_visitors: 2 play_store: 2 app_store: 0 landing_page: 0 - key: poster_vijayawada label: poster_vijayawada opens: 1 unique_visitors: 1 play_store: 0 app_store: 1 landing_page: 0 - key: pharmacy_launch label: pharmacy_launch opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 - key: null label: 'Not tagged' opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 items: type: object properties: key: type: string example: diwali_2026 label: type: string example: diwali_2026 opens: type: integer example: 2 unique_visitors: type: integer example: 2 play_store: type: integer example: 2 app_store: type: integer example: 0 landing_page: type: integer example: 0 platforms: type: array example: - key: android label: Android opens: 3 unique_visitors: 3 play_store: 2 app_store: 0 landing_page: 1 - key: ios label: 'iPhone / iPad' opens: 1 unique_visitors: 1 play_store: 0 app_store: 1 landing_page: 0 - key: desktop label: Desktop opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 items: type: object properties: key: type: string example: android label: type: string example: Android opens: type: integer example: 3 unique_visitors: type: integer example: 3 play_store: type: integer example: 2 app_store: type: integer example: 0 landing_page: type: integer example: 1 device_types: type: array example: - key: smartphone label: Smartphone opens: 4 unique_visitors: 4 play_store: 2 app_store: 1 landing_page: 1 - key: desktop label: Desktop opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 items: type: object properties: key: type: string example: smartphone label: type: string example: Smartphone opens: type: integer example: 4 unique_visitors: type: integer example: 4 play_store: type: integer example: 2 app_store: type: integer example: 1 landing_page: type: integer example: 1 device_brands: type: array example: - key: Realme label: Realme opens: 2 unique_visitors: 2 play_store: 1 app_store: 0 landing_page: 1 - key: Samsung label: Samsung opens: 1 unique_visitors: 1 play_store: 1 app_store: 0 landing_page: 0 - key: Apple label: Apple opens: 1 unique_visitors: 1 play_store: 0 app_store: 1 landing_page: 0 items: type: object properties: key: type: string example: Realme label: type: string example: Realme opens: type: integer example: 2 unique_visitors: type: integer example: 2 play_store: type: integer example: 1 app_store: type: integer example: 0 landing_page: type: integer example: 1 device_models: type: array example: - key: 'Realme · C55' label: 'Realme C55' opens: 2 unique_visitors: 2 play_store: 1 app_store: 0 landing_page: 1 - key: null label: Unknown opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 - key: 'Apple · iPhone' label: 'Apple iPhone' opens: 1 unique_visitors: 1 play_store: 0 app_store: 1 landing_page: 0 - key: 'Samsung · Galaxy A15' label: 'Samsung Galaxy A15' opens: 1 unique_visitors: 1 play_store: 1 app_store: 0 landing_page: 0 items: type: object properties: key: type: string example: 'Realme · C55' label: type: string example: 'Realme C55' opens: type: integer example: 2 unique_visitors: type: integer example: 2 play_store: type: integer example: 1 app_store: type: integer example: 0 landing_page: type: integer example: 1 operating_systems: type: array example: - key: 'Android · 13' label: 'Android 13' opens: 2 unique_visitors: 2 play_store: 1 app_store: 0 landing_page: 1 - key: 'Android · 14' label: 'Android 14' opens: 1 unique_visitors: 1 play_store: 1 app_store: 0 landing_page: 0 - key: 'Windows · 10' label: 'Windows 10' opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 - key: 'iOS · 17.4' label: 'iOS 17.4' opens: 1 unique_visitors: 1 play_store: 0 app_store: 1 landing_page: 0 items: type: object properties: key: type: string example: 'Android · 13' label: type: string example: 'Android 13' opens: type: integer example: 2 unique_visitors: type: integer example: 2 play_store: type: integer example: 1 app_store: type: integer example: 0 landing_page: type: integer example: 1 browsers: type: array example: - key: 'Mobile Safari' label: 'Mobile Safari' opens: 1 unique_visitors: 1 play_store: 0 app_store: 1 landing_page: 0 - key: Chromium label: Chromium opens: 1 unique_visitors: 1 play_store: 1 app_store: 0 landing_page: 0 - key: 'Chrome Mobile' label: 'Chrome Mobile' opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 - key: Chrome label: Chrome opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 items: type: object properties: key: type: string example: 'Mobile Safari' label: type: string example: 'Mobile Safari' opens: type: integer example: 1 unique_visitors: type: integer example: 1 play_store: type: integer example: 0 app_store: type: integer example: 1 landing_page: type: integer example: 0 in_app_browsers: type: array example: - key: Instagram label: Instagram opens: 1 unique_visitors: 1 play_store: 1 app_store: 0 landing_page: 0 items: type: object properties: key: type: string example: Instagram label: type: string example: Instagram opens: type: integer example: 1 unique_visitors: type: integer example: 1 play_store: type: integer example: 1 app_store: type: integer example: 0 landing_page: type: integer example: 0 referrers: type: array example: - key: l.instagram.com label: l.instagram.com opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 - key: www.google.com label: www.google.com opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 items: type: object properties: key: type: string example: l.instagram.com label: type: string example: l.instagram.com opens: type: integer example: 1 unique_visitors: type: integer example: 1 play_store: type: integer example: 0 app_store: type: integer example: 0 landing_page: type: integer example: 1 countries: type: array example: - key: IN label: India opens: 5 unique_visitors: 5 play_store: 2 app_store: 1 landing_page: 2 items: type: object properties: key: type: string example: IN label: type: string example: India opens: type: integer example: 5 unique_visitors: type: integer example: 5 play_store: type: integer example: 2 app_store: type: integer example: 1 landing_page: type: integer example: 2 regions: type: array example: - key: 'IN · Andhra Pradesh' label: 'Andhra Pradesh, IN' opens: 3 unique_visitors: 3 play_store: 1 app_store: 1 landing_page: 1 - key: 'IN · Karnataka' label: 'Karnataka, IN' opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 - key: 'IN · Telangana' label: 'Telangana, IN' opens: 1 unique_visitors: 1 play_store: 1 app_store: 0 landing_page: 0 items: type: object properties: key: type: string example: 'IN · Andhra Pradesh' label: type: string example: 'Andhra Pradesh, IN' opens: type: integer example: 3 unique_visitors: type: integer example: 3 play_store: type: integer example: 1 app_store: type: integer example: 1 landing_page: type: integer example: 1 cities: type: array example: - key: 'IN · Andhra Pradesh · Vijayawada' label: Vijayawada region: 'Andhra Pradesh' country_code: IN opens: 2 unique_visitors: 2 latitude: 16.5062 longitude: 80.648 - key: 'IN · Karnataka · Bengaluru' label: Bengaluru region: Karnataka country_code: IN opens: 1 unique_visitors: 1 latitude: 12.9716 longitude: 77.5946 - key: 'IN · Andhra Pradesh · Guntur' label: Guntur region: 'Andhra Pradesh' country_code: IN opens: 1 unique_visitors: 1 latitude: 16.3067 longitude: 80.4365 - key: 'IN · Telangana · Hyderabad' label: Hyderabad region: Telangana country_code: IN opens: 1 unique_visitors: 1 latitude: 17.385 longitude: 78.4867 items: type: object properties: key: type: string example: 'IN · Andhra Pradesh · Vijayawada' label: type: string example: Vijayawada region: type: string example: 'Andhra Pradesh' country_code: type: string example: IN opens: type: integer example: 2 unique_visitors: type: integer example: 2 latitude: type: number example: 16.5062 longitude: type: number example: 80.648 languages: type: array example: - key: te-IN label: te-IN opens: 5 unique_visitors: 5 play_store: 2 app_store: 1 landing_page: 2 items: type: object properties: key: type: string example: te-IN label: type: string example: te-IN opens: type: integer example: 5 unique_visitors: type: integer example: 5 play_store: type: integer example: 2 app_store: type: integer example: 1 landing_page: type: integer example: 2 page_languages: type: array example: - key: te label: తెలుగు opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 items: type: object properties: key: type: string example: te label: type: string example: తెలుగు opens: type: integer example: 1 unique_visitors: type: integer example: 1 play_store: type: integer example: 0 app_store: type: integer example: 0 landing_page: type: integer example: 1 calls_to_action: type: array example: - key: signup label: 'Start free / sign up' opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 - key: pricing label: Pricing opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 items: type: object properties: key: type: string example: signup label: type: string example: 'Start free / sign up' opens: type: integer example: 1 unique_visitors: type: integer example: 1 play_store: type: integer example: 0 app_store: type: integer example: 0 landing_page: type: integer example: 1 screen_sizes: type: array example: - key: '412 · 915' label: '412 × 915' opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 - key: '1920 · 1080' label: '1920 × 1080' opens: 1 unique_visitors: 1 play_store: 0 app_store: 0 landing_page: 1 items: type: object properties: key: type: string example: '412 · 915' label: type: string example: '412 × 915' opens: type: integer example: 1 unique_visitors: type: integer example: 1 play_store: type: integer example: 0 app_store: type: integer example: 0 landing_page: type: integer example: 1 link_previews: type: array example: - label: WhatsApp fetches: 1 items: type: object properties: label: type: string example: WhatsApp fetches: type: integer example: 1 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 403: description: '' content: application/json: schema: type: object example: message: 'This action is unauthorized.' properties: message: type: string example: 'This action is unauthorized.' 422: description: '' content: application/json: schema: type: object example: message: 'Choose a range of at most 366 days.' errors: from: - 'Choose a range of at most 366 days.' properties: message: type: string example: 'Choose a range of at most 366 days.' errors: type: object properties: from: type: array example: - 'Choose a range of at most 366 days.' items: type: string tags: - 'App link analytics' /api/v1/admin/app-link/visits: get: summary: 'List individual app link opens.' operationId: listIndividualAppLinkOpens description: "Newest first, including link previews (`is_link_preview: true`). No raw IP address, name,\nphone number or email is stored or returned. Location is approximate and may be null." parameters: - in: query name: from description: 'First calendar day, YYYY-MM-DD. Defaults to 29 days before `to`. Must be a valid date in the format Y-m-d.' example: '2026-01-15' required: false schema: type: string description: 'First calendar day, YYYY-MM-DD. Defaults to 29 days before `to`. Must be a valid date in the format Y-m-d.' example: '2026-01-15' nullable: true - in: query name: to description: 'Last calendar day, YYYY-MM-DD. Defaults to today. The range may cover at most 366 days. Must be a valid date in the format Y-m-d.' example: '2026-01-15' required: false schema: type: string description: 'Last calendar day, YYYY-MM-DD. Defaults to today. The range may cover at most 366 days. Must be a valid date in the format Y-m-d.' example: '2026-01-15' nullable: true - in: query name: source description: 'Only opens from this source key, such as whatsapp, instagram, qr, sms, facebook, meta, google, referral or direct. Must not be greater than 50 characters.' example: whatsapp required: false schema: type: string description: 'Only opens from this source key, such as whatsapp, instagram, qr, sms, facebook, meta, google, referral or direct. Must not be greater than 50 characters.' example: whatsapp nullable: true - in: query name: campaign description: 'Only opens tagged with this campaign, as normalised (lower case, underscores). Must not be greater than 120 characters.' example: diwali_2026 required: false schema: type: string description: 'Only opens tagged with this campaign, as normalised (lower case, underscores). Must not be greater than 120 characters.' example: diwali_2026 nullable: true - in: query name: platform description: 'Only opens from android, ios, desktop or other.' example: android required: false schema: type: string description: 'Only opens from android, ios, desktop or other.' example: android enum: - android - ios - desktop - other nullable: true - in: query name: tracked_page description: 'Only opens of this page: app (the /app download link), stores (the store-type index), or a store-type key such as pharmacy or kirana-store.' example: app required: false schema: type: string description: 'Only opens of this page: app (the /app download link), stores (the store-type index), or a store-type key such as pharmacy or kirana-store.' example: app enum: - app - stores - kirana-store - pharmacy - supermarket - clothing-store - footwear-store - hardware-store - electronics-store - mobile-store - stationery-store - book-store - bakery - cosmetics-store - wholesale-business - auto-parts-store - home-appliance-store - gift-shop - electrical-store - paint-store - sanitaryware-store - pesticide-store nullable: true - in: query name: per_page description: 'Visits per page, 1 to 100. Visit lists only. Defaults to 25. Must be at least 1. Must not be greater than 100.' example: 25 required: false schema: type: integer description: 'Visits per page, 1 to 100. Visit lists only. Defaults to 25. Must be at least 1. Must not be greater than 100.' example: 25 nullable: true - in: query name: page description: 'Page number. Visit lists only. Must be at least 1.' example: 1 required: false schema: type: integer description: 'Page number. Visit lists only. Must be at least 1.' example: 1 nullable: true responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 6 visited_at: '2026-09-26T11:20:00+05:30' page: app page_label: 'App download link (/app)' page_locale: null outcome: landing_page is_link_preview: true preview_agent: WhatsApp is_new_visitor: true source: whatsapp source_label: WhatsApp medium: social campaign: diwali_2026 utm: source: whatsapp medium: null campaign: diwali_2026 content: null term: null ad_click_id: null referrer_host: null referrer_app: null platform: other device: type: null brand: null model: null os: name: Android version: null browser: name: WhatsApp version: '2.23' in_app: null location: country_code: IN region: Maharashtra city: Mumbai postal_code: '400001' latitude: 19.076 longitude: 72.8777 timezone: Asia/Kolkata language: te-IN screen: null engagement: seconds: null scroll_depth: null call_to_action: null - id: 5 visited_at: '2026-09-26T10:05:00+05:30' page: pharmacy page_label: Pharmacies page_locale: te outcome: landing_page is_link_preview: false preview_agent: null is_new_visitor: true source: instagram source_label: Instagram medium: paid campaign: pharmacy_launch utm: source: instagram medium: paid campaign: pharmacy_launch content: null term: null ad_click_id: null referrer_host: l.instagram.com referrer_app: null platform: android device: type: smartphone brand: Realme model: C55 os: name: Android version: '13' browser: name: 'Chrome Mobile' version: '124' in_app: null location: country_code: IN region: 'Andhra Pradesh' city: Guntur postal_code: '522001' latitude: 16.3067 longitude: 80.4365 timezone: Asia/Kolkata language: te-IN screen: width: 412 height: 915 engagement: seconds: 126 scroll_depth: 71 call_to_action: signup meta: current_page: 1 last_page: 3 per_page: 2 total: 6 properties: data: type: array example: - id: 6 visited_at: '2026-09-26T11:20:00+05:30' page: app page_label: 'App download link (/app)' page_locale: null outcome: landing_page is_link_preview: true preview_agent: WhatsApp is_new_visitor: true source: whatsapp source_label: WhatsApp medium: social campaign: diwali_2026 utm: source: whatsapp medium: null campaign: diwali_2026 content: null term: null ad_click_id: null referrer_host: null referrer_app: null platform: other device: type: null brand: null model: null os: name: Android version: null browser: name: WhatsApp version: '2.23' in_app: null location: country_code: IN region: Maharashtra city: Mumbai postal_code: '400001' latitude: 19.076 longitude: 72.8777 timezone: Asia/Kolkata language: te-IN screen: null engagement: seconds: null scroll_depth: null call_to_action: null - id: 5 visited_at: '2026-09-26T10:05:00+05:30' page: pharmacy page_label: Pharmacies page_locale: te outcome: landing_page is_link_preview: false preview_agent: null is_new_visitor: true source: instagram source_label: Instagram medium: paid campaign: pharmacy_launch utm: source: instagram medium: paid campaign: pharmacy_launch content: null term: null ad_click_id: null referrer_host: l.instagram.com referrer_app: null platform: android device: type: smartphone brand: Realme model: C55 os: name: Android version: '13' browser: name: 'Chrome Mobile' version: '124' in_app: null location: country_code: IN region: 'Andhra Pradesh' city: Guntur postal_code: '522001' latitude: 16.3067 longitude: 80.4365 timezone: Asia/Kolkata language: te-IN screen: width: 412 height: 915 engagement: seconds: 126 scroll_depth: 71 call_to_action: signup items: type: object properties: id: type: integer example: 6 visited_at: type: string example: '2026-09-26T11:20:00+05:30' page: type: string example: app page_label: type: string example: 'App download link (/app)' page_locale: type: string example: null nullable: true outcome: type: string example: landing_page is_link_preview: type: boolean example: true preview_agent: type: string example: WhatsApp is_new_visitor: type: boolean example: true source: type: string example: whatsapp source_label: type: string example: WhatsApp medium: type: string example: social campaign: type: string example: diwali_2026 utm: type: object properties: source: type: string example: whatsapp medium: type: string example: null nullable: true campaign: type: string example: diwali_2026 content: type: string example: null nullable: true term: type: string example: null nullable: true ad_click_id: type: string example: null nullable: true referrer_host: type: string example: null nullable: true referrer_app: type: string example: null nullable: true platform: type: string example: other device: type: object properties: type: type: string example: null nullable: true brand: type: string example: null nullable: true model: type: string example: null nullable: true os: type: object properties: name: type: string example: Android version: type: string example: null nullable: true browser: type: object properties: name: type: string example: WhatsApp version: type: string example: '2.23' in_app: type: string example: null nullable: true location: type: object properties: country_code: type: string example: IN region: type: string example: Maharashtra city: type: string example: Mumbai postal_code: type: string example: '400001' latitude: type: number example: 19.076 longitude: type: number example: 72.8777 timezone: type: string example: Asia/Kolkata language: type: string example: te-IN screen: type: string example: null nullable: true engagement: type: object properties: seconds: type: string example: null nullable: true scroll_depth: type: string example: null nullable: true call_to_action: type: string example: null nullable: true meta: type: object properties: current_page: type: integer example: 1 last_page: type: integer example: 3 per_page: type: integer example: 2 total: type: integer example: 6 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 403: description: '' content: application/json: schema: type: object example: message: 'This action is unauthorized.' properties: message: type: string example: 'This action is unauthorized.' 422: description: '' content: application/json: schema: type: object example: message: 'The selected platform is invalid.' errors: platform: - 'The selected platform is invalid.' properties: message: type: string example: 'The selected platform is invalid.' errors: type: object properties: platform: type: array example: - 'The selected platform is invalid.' items: type: string tags: - 'App link analytics' /api/v1/webhooks/razorpay: post: summary: 'Receive Razorpay subscription events' operationId: receiveRazorpaySubscriptionEvents description: "This public provider callback requires a valid `X-Razorpay-Signature` HMAC header and is idempotent.\nWorkers fetch current Razorpay state. Cancelled, completed, or expired subscriptions end paid access\nand restore Free Essentials only when no newer subscription has replaced the ended subscription." parameters: - in: header name: X-Razorpay-Signature description: '' example: 'string required Razorpay webhook HMAC signature. Example: 0123456789abcdef' schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: received: true properties: received: type: boolean example: true 401: description: '' content: application/json: schema: type: object example: message: 'Invalid webhook signature.' properties: message: type: string example: 'Invalid webhook signature.' tags: - 'Billing webhooks' security: [] '/api/v1/businesses/{business}/settings/payment-details': get: summary: 'The shareable business and bank details.' operationId: theShareableBusinessAndBankDetails description: "With no `payment_account_id`, the account returned is the one the shop\nalready prints on its bills, else its default receiving bank, else any\nactive bank account it has. A workspace that has not saved a bank account\nyet gets `bank_account: null` — show the \"add bank details\" path rather\nthan an empty card.\n\n`share_message` is the same details as plain text, ready for WhatsApp or a\nshare sheet. Render the card from the fields; do not parse this string.\n\nNot available offline: bank credentials are fetched fresh each time and\nmust not be cached." parameters: - in: query name: payment_account_id description: 'An active payment account of this workspace to show instead of the default one.' example: 8 required: false schema: type: integer description: 'An active payment account of this workspace to show instead of the default one.' example: 8 responses: 200: description: '' content: application/json: schema: type: object example: data: business: id: 17 name: Haniot legal_name: 'HANIOT PRIVATE LIMITED' gstin: 37AAFCH7061N1Z5 gst_registration_status: active is_gst_registered: true phone: '9876543210' email: accounts@haniot.in logo_url: null address: line_1: '12 MG Road' line_2: null city: Visakhapatnam state_code: '37' pincode: '530001' bank_account: payment_account_id: 8 name: 'ICICI Current' type: bank bank_name: 'ICICI Bank' account_name: 'Haniot Private Limited' account_number: '236001505028' account_last_four: '5028' ifsc: ICIC0012360 branch: 'Dwaraka Nagar' upi_id: null upi_id: haniot@icici share_message: "HANIOT PRIVATE LIMITED\nGSTIN: 37AAFCH7061N1Z5\n\nAccount name: Haniot Private Limited\nAccount number: 236001505028\nBank: ICICI Bank\nIFSC: ICIC0012360\nBranch: Dwaraka Nagar\nUPI: haniot@icici" properties: data: type: object properties: business: type: object properties: id: type: integer example: 17 name: type: string example: Haniot legal_name: type: string example: 'HANIOT PRIVATE LIMITED' description: 'The registered name to print on the card. Falls back to the workspace name when no legal name is saved.' gstin: type: string example: 37AAFCH7061N1Z5 description: "The primary GST registration's GSTIN, else the one saved on the workspace." gst_registration_status: type: string example: active description: 'One of `not_assessed`, `not_registered`, `pending`, `active`, `suspended`, `cancelled`.' is_gst_registered: type: boolean example: true description: 'Whether to show the verified tick: an active registration with a GSTIN on record. It does not re-check the GST portal.' phone: type: string example: '9876543210' email: type: string example: accounts@haniot.in logo_url: type: string example: null nullable: true address: type: object properties: line_1: type: string example: '12 MG Road' line_2: type: string example: null nullable: true city: type: string example: Visakhapatnam state_code: type: string example: '37' pincode: type: string example: '530001' bank_account: type: object properties: payment_account_id: type: integer example: 8 name: type: string example: 'ICICI Current' type: type: string example: bank bank_name: type: string example: 'ICICI Bank' account_name: type: string example: 'Haniot Private Limited' account_number: type: string example: '236001505028' description: 'The full account number. Available here and in business receiving_bank_account for accounting members; fetch fresh rather than persisting it on the device.' account_last_four: type: string example: '5028' ifsc: type: string example: ICIC0012360 branch: type: string example: 'Dwaraka Nagar' upi_id: type: string example: null nullable: true description: 'The account to transfer into, or null when the workspace has saved none.' upi_id: type: string example: haniot@icici description: "The account's UPI ID, else the workspace's." share_message: type: string example: "HANIOT PRIVATE LIMITED\nGSTIN: 37AAFCH7061N1Z5\n\nAccount name: Haniot Private Limited\nAccount number: 236001505028\nBank: ICICI Bank\nIFSC: ICIC0012360\nBranch: Dwaraka Nagar\nUPI: haniot@icici" description: 'The same details as plain text for a share sheet.' 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 403: description: '' content: application/json: schema: type: object example: message: 'This action is unauthorized.' properties: message: type: string example: 'This action is unauthorized.' 422: description: '' content: application/json: schema: type: object example: message: 'The selected payment account id is invalid.' errors: payment_account_id: - 'The selected payment account id is invalid.' properties: message: type: string example: 'The selected payment account id is invalid.' errors: type: object properties: payment_account_id: type: array example: - 'The selected payment account id is invalid.' items: type: string tags: - 'Business details' requestBody: required: false content: application/json: schema: type: object properties: payment_account_id: type: integer description: '' example: 16 nullable: true parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: business_id description: 'Workspace ID.' example: 17 required: true schema: type: integer /api/v1/plugin/session: get: summary: 'Inspect the connected plugin account and shop.' operationId: inspectTheConnectedPluginAccountAndShop description: "Requires a plugin OAuth token with `dukanam:read`; ordinary mobile tokens return 403.\nMembership and plan/seat eligibility are rechecked on every request." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: user: id: 1 name: Priya business: id: 1 name: 'Priya Store' scope: 'dukanam:read' resource: 'https://mcp.example.com/mcp' timezone: Asia/Kolkata properties: data: type: object properties: user: type: object properties: id: type: integer example: 1 name: type: string example: Priya business: type: object properties: id: type: integer example: 1 name: type: string example: 'Priya Store' scope: type: string example: 'dukanam:read' resource: type: string example: 'https://mcp.example.com/mcp' timezone: type: string example: Asia/Kolkata 403: description: '' content: application/json: schema: type: object example: message: 'A plugin OAuth token is required.' properties: message: type: string example: 'A plugin OAuth token is required.' tags: - 'ChatGPT plugin' '/api/v1/businesses/{business}/invoices/{invoice}/expenses': get: summary: 'List expenses on an invoice.' operationId: listExpensesOnAnInvoice description: '' parameters: - in: query name: per_page description: 'From 1 to 100.' example: 20 required: false schema: type: integer description: 'From 1 to 100.' example: 20 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Invoice-linked expenses' requestBody: required: false content: application/json: schema: type: object properties: per_page: type: integer description: 'Must be between 1 and 100.' example: 2 nullable: true post: summary: 'Record a sales or purchase invoice expense.' operationId: recordASalesOrPurchaseInvoiceExpense description: '' parameters: [] responses: {} tags: - 'Invoice-linked expenses' requestBody: required: true content: application/json: schema: type: object properties: category: type: string description: 'Category, up to 64 characters.' example: Transport payee: type: string description: 'Payee, up to 128 characters.' example: 'Local transport' nullable: true amount_paise: type: integer description: 'Positive expense amount in paise.' example: 30000 occurred_on: type: string description: 'Expense date, Y-m-d.' example: '2026-10-04' note: type: string description: 'Description, up to 1000 characters.' example: 'Transport for this invoice' paid: type: boolean description: 'Whether paid at creation; defaults true. False accrues an expense payable without moving cash.' example: true payment_method: type: string description: 'Required if paid.' example: bank enum: - cash - bank - upi - card - other nullable: true payment_account_id: type: integer description: 'Active compatible payment account.' example: null nullable: true idempotency_key: type: string description: 'Stable UUID; retain for retries.' example: fdf5dfdf-aead-473b-8c98-dcfbbda6c02a required: - category - amount_paise - occurred_on - note - idempotency_key parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: invoice description: 'The invoice.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/documents/{document}/expenses': get: summary: 'List expenses on an invoice.' operationId: listExpensesOnAnInvoice description: '' parameters: - in: query name: per_page description: 'From 1 to 100.' example: 20 required: false schema: type: integer description: 'From 1 to 100.' example: 20 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Invoice-linked expenses' requestBody: required: false content: application/json: schema: type: object properties: per_page: type: integer description: 'Must be between 1 and 100.' example: 2 nullable: true post: summary: 'Record a sales or purchase invoice expense.' operationId: recordASalesOrPurchaseInvoiceExpense description: '' parameters: [] responses: {} tags: - 'Invoice-linked expenses' requestBody: required: true content: application/json: schema: type: object properties: category: type: string description: 'Category, up to 64 characters.' example: Transport payee: type: string description: 'Payee, up to 128 characters.' example: 'Local transport' nullable: true amount_paise: type: integer description: 'Positive expense amount in paise.' example: 30000 occurred_on: type: string description: 'Expense date, Y-m-d.' example: '2026-10-04' note: type: string description: 'Description, up to 1000 characters.' example: 'Transport for this invoice' paid: type: boolean description: 'Whether paid at creation; defaults true. False accrues an expense payable without moving cash.' example: true payment_method: type: string description: 'Required if paid.' example: bank enum: - cash - bank - upi - card - other nullable: true payment_account_id: type: integer description: 'Active compatible payment account.' example: null nullable: true idempotency_key: type: string description: 'Stable UUID; retain for retries.' example: fdf5dfdf-aead-473b-8c98-dcfbbda6c02a required: - category - amount_paise - occurred_on - note - idempotency_key parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: document description: 'The document.' example: architecto required: true schema: type: string '/api/v1/businesses/{business}/expenses/{expense}/payments': post: summary: 'Pay an outstanding invoice expense.' operationId: payAnOutstandingInvoiceExpense description: "Recognized expense stays on its original date. Payment reduces the expense payable and\ncash/bank only. Partial payments are allowed; amount cannot exceed outstanding." parameters: [] responses: {} tags: - 'Invoice-linked expenses' requestBody: required: true content: application/json: schema: type: object properties: amount_paise: type: integer description: 'Positive paise within the outstanding amount.' example: 10000 paid_on: type: string description: 'Payment date, Y-m-d.' example: '2026-10-04' payment_method: type: string description: '' example: bank enum: - cash - bank - upi - card - other payment_account_id: type: integer description: 'Active compatible account.' example: null nullable: true reference: type: string description: 'Payment reference, up to 128 characters.' example: null nullable: true idempotency_key: type: string description: 'Stable UUID for retries.' example: fdf5dfdf-aead-473b-8c98-dcfbbda6c02b required: - amount_paise - paid_on - payment_method - idempotency_key parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: expense description: 'The expense.' example: architecto required: true schema: type: string '/api/v1/businesses/{business}/expenses/{expense}/payments/{payment}/void': post: summary: 'Void an invoice expense payment.' operationId: voidAnInvoiceExpensePayment description: 'Keeps the expense and restores its payable; reverses the bank/cash movement once.' parameters: [] responses: {} tags: - 'Invoice-linked expenses' requestBody: required: true content: application/json: schema: type: object properties: reason: type: string description: 'Correction reason, up to 1000 characters.' example: 'Wrong transfer' required: - reason parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: expense description: 'The expense.' example: architecto required: true schema: type: string - in: path name: payment description: 'The payment.' example: architecto required: true schema: type: string '/api/v1/businesses/{business_id}/settings/invoice-branding': get: summary: 'Current invoice branding and the choices available.' operationId: currentInvoiceBrandingAndTheChoicesAvailable description: "`templates`, `layout_choices` and `accents` are the pickers — render them rather\nthan hard-coding values, because they can grow. `accents` is a set of suggested\nswatches to offer beside a colour input, not the set of allowed values: any\n`#RRGGBB` is accepted. `invoice_accent_palette` carries the colours that hex\nactually prints as, so a client can show the result without redoing the maths.\n\nAvailable offline: cached. Branding changes rarely and the counter needs it\nto print without signal." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: invoice_template: classic invoice_accent_colour: '#10505E' invoice_footer_note: 'Goods once sold are not returnable.' bank_details: 'HDFC Bank 5010 1234 5678, IFSC HDFC0000123' authorized_signatory: 'Priya Sharma' show_upi_qr_on_invoice: false show_logo_on_invoice: true upi_id: priyatextiles@hdfcbank logo_url: 'https://cdn.dukanam.com/business-logos/17/logo.png' signature_url: null templates: - value: classic label: Classic summary: 'Boxed party details under a ruled header. The layout Dukanam has always printed.' - value: compact label: Compact summary: 'Tighter type and flat rows, so a long bill fits on fewer pages.' - value: accent label: Accent summary: 'A filled colour band across the head, for a shop that wants its colour seen.' accents: - value: '#10505E' label: 'Dukanam teal' max_image_kilobytes: 1024 properties: data: type: object properties: invoice_template: type: string example: classic invoice_accent_colour: type: string example: '#10505E' invoice_footer_note: type: string example: 'Goods once sold are not returnable.' bank_details: type: string example: 'HDFC Bank 5010 1234 5678, IFSC HDFC0000123' authorized_signatory: type: string example: 'Priya Sharma' show_upi_qr_on_invoice: type: boolean example: false show_logo_on_invoice: type: boolean example: true upi_id: type: string example: priyatextiles@hdfcbank logo_url: type: string example: 'https://cdn.dukanam.com/business-logos/17/logo.png' description: 'Temporary signed object-storage URL when the logo is stored on S3. Refresh the resource after it expires.' signature_url: type: string example: null description: 'As logo_url, for the authorised signature image.' templates: type: array example: - value: classic label: Classic summary: 'Boxed party details under a ruled header. The layout Dukanam has always printed.' - value: compact label: Compact summary: 'Tighter type and flat rows, so a long bill fits on fewer pages.' - value: accent label: Accent summary: 'A filled colour band across the head, for a shop that wants its colour seen.' items: type: object properties: value: type: string example: classic label: type: string example: Classic summary: type: string example: 'Boxed party details under a ruled header. The layout Dukanam has always printed.' accents: type: array example: - value: '#10505E' label: 'Dukanam teal' items: type: object properties: value: type: string example: '#10505E' label: type: string example: 'Dukanam teal' max_image_kilobytes: type: integer example: 1024 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 403: description: '' content: application/json: schema: type: object example: message: 'This action is unauthorized.' properties: message: type: string example: 'This action is unauthorized.' tags: - 'Invoice branding' put: summary: 'Update invoice branding.' operationId: updateInvoiceBranding description: "Requires the `accounting` permission, the same as the rest of business\nsettings. Send only the fields you are changing. Saved documents are\nuntouched: each froze its own branding when it was raised.\n\nOnline only." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: invoice_template: accent invoice_accent_colour: '#047857' show_upi_qr_on_invoice: true properties: data: type: object properties: invoice_template: type: string example: accent invoice_accent_colour: type: string example: '#047857' show_upi_qr_on_invoice: type: boolean example: true 403: description: '' content: application/json: schema: type: object example: message: 'Your role does not allow this action.' properties: message: type: string example: 'Your role does not allow this action.' 422: description: '' content: application/json: schema: type: object example: message: 'Add a UPI ID in business settings before printing a UPI QR on invoices.' errors: show_upi_qr_on_invoice: - 'Add a UPI ID in business settings before printing a UPI QR on invoices.' properties: message: type: string example: 'Add a UPI ID in business settings before printing a UPI QR on invoices.' errors: type: object properties: show_upi_qr_on_invoice: type: array example: - 'Add a UPI ID in business settings before printing a UPI QR on invoices.' items: type: string tags: - 'Invoice branding' requestBody: required: false content: application/json: schema: type: object properties: invoice_template: type: string description: 'One of `classic`, `compact`, `accent`, or `custom`.' example: accent invoice_accent_colour: type: string description: 'Any seven-character hex, `#RRGGBB`. The `accents` list returned by the GET is a set of suggestions, not the limit. Send `null` to return to the default teal.' example: '#047857' nullable: true invoice_layout: type: object description: 'The knobs behind the `custom` template. Every key is optional and merges over what is saved, so one switch can be moved on its own. Send `null` to reset the whole layout to its defaults.' example: [] properties: header: type: string description: '`ruled`, `band`, or `minimal`.' example: band density: type: string description: '`roomy`, `normal`, or `compact`.' example: compact table: type: string description: '`ruled`, `flat`, or `zebra`.' example: flat logo: type: object description: '' example: null properties: height: type: integer description: 'Logo height on A4 in pixels, 24-96. An 80mm roll keeps its own size.' example: 64 align: type: string description: '`left`, `center`, or `right`.' example: center columns: type: object description: '' example: null properties: hsn: type: boolean description: 'Print the HSN/SAC code under each line. A GST tax invoice must carry it above the turnover threshold.' example: true uqc: type: boolean description: 'Print the unit beside the quantity.' example: true discount: type: boolean description: 'Print the per-line discount column. The discount total still prints either way.' example: true nullable: true invoice_footer_note: type: string description: 'Terms, a returns policy, or a thank-you line, up to 1000 characters.' example: 'Goods once sold are not returnable.' nullable: true bank_details: type: string description: 'Payment instructions printed above the signature, up to 1000 characters.' example: 'HDFC Bank 5010 1234 5678, IFSC HDFC0000123' nullable: true authorized_signatory: type: string description: 'The name printed under the signature, up to 128 characters.' example: 'Priya Sharma' nullable: true show_upi_qr_on_invoice: type: boolean description: 'Print an exact-amount UPI QR on the bill. Requires a UPI ID on the workspace.' example: true show_logo_on_invoice: type: boolean description: 'Print the shop logo on invoices and documents. On by default. The image is the workspace logo captured during onboarding, so this only controls whether it appears on the paper.' example: true parameters: - in: path name: business_id description: 'Workspace ID.' example: 17 required: true schema: type: integer '/api/v1/businesses/{business_id}/settings/invoice-branding/preview': get: summary: 'Preview a sample bill under branding that has not been saved.' operationId: previewASampleBillUnderBrandingThatHasNotBeenSaved description: "Renders a plausible invoice — never created, never numbered, never stored —\nso the settings screen can show the real document while the shop is still\nchoosing. Returns a PDF by default; pass `as=html` for a WebView.\n\nOnline only." parameters: - in: query name: template description: 'Preview this template instead of the saved one.' example: accent required: false schema: type: string description: 'Preview this template instead of the saved one.' example: accent - in: query name: accent description: 'Preview this accent instead of the saved one, as any seven-character hex.' example: '#047857' required: false schema: type: string description: 'Preview this accent instead of the saved one, as any seven-character hex.' example: '#047857' - in: query name: layout description: 'Preview these layout knobs instead of the saved ones, as `layout[header]=band&layout[density]=compact`. Merged over what is saved.' example: [] required: false schema: type: object description: 'Preview these layout knobs instead of the saved ones, as `layout[header]=band&layout[density]=compact`. Merged over what is saved.' example: [] properties: {} - in: query name: format description: 'Paper size: `a4` (default) or `80mm`.' example: 80mm required: false schema: type: string description: 'Paper size: `a4` (default) or `80mm`.' example: 80mm - in: query name: as description: 'Response body: `pdf` (default) or `html`.' example: html required: false schema: type: string description: 'Response body: `pdf` (default) or `html`.' example: html responses: 200: description: '' content: text/plain: schema: type: string example: 'A PDF or HTML document, not JSON.' 422: description: '' content: application/json: schema: type: object example: message: 'The selected template is invalid.' errors: template: - 'The selected template is invalid.' properties: message: type: string example: 'The selected template is invalid.' errors: type: object properties: template: type: array example: - 'The selected template is invalid.' items: type: string tags: - 'Invoice branding' parameters: - in: path name: business_id description: 'Workspace ID.' example: 17 required: true schema: type: integer '/api/v1/businesses/{business_id}/settings/invoice-branding/{kind}': post: summary: 'Upload the shop logo or the authorised signature.' operationId: uploadTheShopLogoOrTheAuthorisedSignature description: "Multipart. PNG or JPEG, up to 1 MB. Replaces whatever is stored under that\nname; the previous file is deleted once the new one is committed.\n\nOnline only." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: invoice_template: classic logo_url: 'https://cdn.dukanam.com/business-logos/17/logo.png' signature_url: null properties: data: type: object properties: invoice_template: type: string example: classic logo_url: type: string example: 'https://cdn.dukanam.com/business-logos/17/logo.png' signature_url: type: string example: null nullable: true 422: description: '' content: application/json: schema: type: object example: message: 'The image must not be greater than 1024 kilobytes.' errors: image: - 'The image must not be greater than 1024 kilobytes.' properties: message: type: string example: 'The image must not be greater than 1024 kilobytes.' errors: type: object properties: image: type: array example: - 'The image must not be greater than 1024 kilobytes.' items: type: string tags: - 'Invoice branding' requestBody: required: true content: multipart/form-data: schema: type: object properties: image: type: string format: binary description: 'The PNG or JPEG to store, up to 1 MB.' required: - image delete: summary: 'Remove the shop logo or the authorised signature.' operationId: removeTheShopLogoOrTheAuthorisedSignature description: "Documents already saved keep the image they were issued with; only future\ndocuments print without it.\n\nOnline only." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: invoice_template: classic logo_url: null signature_url: null properties: data: type: object properties: invoice_template: type: string example: classic logo_url: type: string example: null nullable: true signature_url: type: string example: null nullable: true tags: - 'Invoice branding' parameters: - in: path name: business_id description: 'Workspace ID.' example: 17 required: true schema: type: integer - in: path name: kind description: 'Either `logo` or `signature`.' example: logo required: true schema: type: string '/api/v1/businesses/{business_id}/invoices/{invoice_id}/preview': get: summary: 'Preview a real invoice under branding that has not been saved.' operationId: previewARealInvoiceUnderBrandingThatHasNotBeenSaved description: "Renders a saved invoice with the requested template and accent so the shop\ncan judge the choice against its own bill. Nothing is written: the invoice\nkeeps the branding it was saved with, and printing it still produces that.\n\nOnline only." parameters: - in: query name: template description: "Render this template instead of the invoice's own." example: compact required: false schema: type: string description: "Render this template instead of the invoice's own." example: compact - in: query name: accent description: "Render this accent instead of the invoice's own, as any seven-character hex." example: '#9A3412' required: false schema: type: string description: "Render this accent instead of the invoice's own, as any seven-character hex." example: '#9A3412' - in: query name: layout description: "Render these layout knobs instead of the invoice's own, as `layout[table]=zebra`. Merged over what the workspace has saved." example: [] required: false schema: type: object description: "Render these layout knobs instead of the invoice's own, as `layout[table]=zebra`. Merged over what the workspace has saved." example: [] properties: {} - in: query name: format description: 'Paper size: `a4` (default) or `80mm`.' example: 80mm required: false schema: type: string description: 'Paper size: `a4` (default) or `80mm`.' example: 80mm - in: query name: as description: 'Response body: `pdf` (default) or `html`.' example: html required: false schema: type: string description: 'Response body: `pdf` (default) or `html`.' example: html responses: 200: description: '' content: text/plain: schema: type: string example: 'A PDF or HTML document, not JSON.' 404: description: '' content: application/json: schema: type: object example: message: 'Not Found' properties: message: type: string example: 'Not Found' tags: - 'Invoice branding' parameters: - in: path name: business_id description: 'Workspace ID.' example: 17 required: true schema: type: integer - in: path name: invoice_id description: 'Invoice ID.' example: 482 required: true schema: type: integer '/api/v1/businesses/{business}/loan-accounts': get: summary: 'List loans and credit cards.' operationId: listLoansAndCreditCards description: '' parameters: - in: query name: status description: 'Filter by record status.' example: active required: false schema: type: string description: 'Filter by record status.' example: active enum: - active - closed - in: query name: type description: 'Filter by kind.' example: loan required: false schema: type: string description: 'Filter by kind.' example: loan enum: - loan - credit_card - in: query name: per_page description: 'Results per page, from 1 to 100.' example: 20 required: false schema: type: integer description: 'Results per page, from 1 to 100.' example: 20 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Loans and EMI' requestBody: required: false content: application/json: schema: type: object properties: status: type: string description: '' example: active enum: - active - closed nullable: true type: type: string description: '' example: loan enum: - loan - credit_card nullable: true per_page: type: integer description: 'Must be at least 1. Must not be greater than 100.' example: 1 nullable: true post: summary: 'Open a loan.' operationId: openALoan description: "Naming `disbursed_to_payment_account_id` says the money actually landed in that account, so\nthe opening journal is a disbursement — the bank balance rises and the liability rises with\nit. Leave it out for a loan taken before the shop kept books here: there is no receipt to\nrecord, so the borrowing opens against equity, the way a party's opening balance does.\n\nA `loan` with a `tenure_months` gets its amortisation worked out on the spot. A\n`credit_card` never does — it revolves, and an invented schedule would be a fiction." parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'This EMI does not cover the interest on the outstanding balance, so the loan would never be repaid. Raise the EMI or lower the interest rate.' errors: emi_amount: - 'This EMI does not cover the interest on the outstanding balance, so the loan would never be repaid. Raise the EMI or lower the interest rate.' properties: message: type: string example: 'This EMI does not cover the interest on the outstanding balance, so the loan would never be repaid. Raise the EMI or lower the interest rate.' errors: type: object properties: emi_amount: type: array example: - 'This EMI does not cover the interest on the outstanding balance, so the loan would never be repaid. Raise the EMI or lower the interest rate.' items: type: string tags: - 'Loans and EMI' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Must not be greater than 120 characters.' example: b type: type: string description: '' example: loan enum: - loan - credit_card nullable: true lender_name: type: string description: 'Must not be greater than 120 characters.' example: 'n' nullable: true account_last_four: type: string description: 'Must not be greater than 8 characters.' example: gzmiyvdl nullable: true principal: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 19 interest_rate: type: number description: 'Must be at least 0. Must not be greater than 100.' example: 17 nullable: true emi_amount: type: number description: 'Must not be greater than 999999999.' example: 5 nullable: true emi_day_of_month: type: integer description: 'Must be between 1 and 31.' example: 1 nullable: true tenure_months: type: integer description: 'Must be between 1 and 360.' example: 2 nullable: true started_on: type: string description: 'Must be a valid date.' example: '2026-01-15' first_emi_on: type: string description: 'Must be a valid date. Must be a date after or equal to started_on.' example: '2026-01-15' nullable: true disbursed_to_payment_account_id: type: integer description: '' example: 16 nullable: true notes: type: string description: 'Must not be greater than 1000 characters.' example: 'n' nullable: true required: - name - principal - started_on parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/loan-accounts/{loanAccount_id}': get: summary: 'Show one loan with its schedule and repayment history.' operationId: showOneLoanWithItsScheduleAndRepaymentHistory description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Loans and EMI' put: summary: "Correct a loan's description and terms." operationId: correctALoansDescriptionAndTerms description: "This is a **whole-record write**: a field left out is cleared, exactly as contact updates\nbehave. The borrowing itself is saved, so the principal and the start date are not\neditable here. Revising the terms rebuilds the instalments still to come; instalments\nalready paid are left exactly as they were paid." parameters: [] responses: {} tags: - 'Loans and EMI' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Must not be greater than 120 characters.' example: b lender_name: type: string description: 'Must not be greater than 120 characters.' example: 'n' nullable: true account_last_four: type: string description: 'Must not be greater than 8 characters.' example: gzmiyvdl nullable: true interest_rate: type: number description: 'Must be at least 0. Must not be greater than 100.' example: 19 nullable: true emi_amount: type: number description: 'Must not be greater than 999999999.' example: 17 nullable: true emi_day_of_month: type: integer description: 'Must be between 1 and 31.' example: 1 nullable: true tenure_months: type: integer description: 'Must be between 1 and 360.' example: 1 nullable: true first_emi_on: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true status: type: string description: '' example: active enum: - active - closed nullable: true notes: type: string description: 'Must not be greater than 1000 characters.' example: h nullable: true required: - name parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: loanAccount_id description: 'The ID of the loanAccount.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/loan-accounts/{loanAccount_id}/schedule': get: summary: 'The instalments worked out for this loan.' operationId: theInstalmentsWorkedOutForThisLoan description: '' parameters: - in: query name: status description: 'Filter by instalment status.' example: pending required: false schema: type: string description: 'Filter by instalment status.' example: pending enum: - pending - paid responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Loans and EMI' requestBody: required: false content: application/json: schema: type: object properties: status: type: string description: '' example: pending enum: - pending - paid nullable: true parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: loanAccount_id description: 'The ID of the loanAccount.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/loan-accounts/{loanAccount_id}/payments': get: summary: 'List repayments made against a loan.' operationId: listRepaymentsMadeAgainstALoan description: '' parameters: - in: query name: per_page description: 'Results per page, from 1 to 100.' example: 20 required: false schema: type: integer description: 'Results per page, from 1 to 100.' example: 20 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Loans and EMI' requestBody: required: false content: application/json: schema: type: object properties: per_page: type: integer description: 'Must be at least 1. Must not be greater than 100.' example: 1 nullable: true post: summary: 'Record a repayment.' operationId: recordARepayment description: "Name `loan_emi_schedule_id` and the lender's own split for that instalment is used, which is\nwhat a shop paying this month's EMI wants. State `principal` and `interest` instead for a\npart payment, a prepayment, or a credit-card payment with no schedule behind it. Sending\n`amount` as well is a check, not an override: parts that do not add up are refused.\n\nThe journal is split — principal off the liability, interest to expense, any lender charges\nto operating expenses — because a lump sum booked as one expense misstates both the balance\nsheet and the profit and loss." parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'Only ₹12,000.00 of principal is still outstanding on Vehicle loan. Reduce the principal portion, or record the excess as interest or charges.' errors: principal: - 'Only ₹12,000.00 of principal is still outstanding on Vehicle loan. Reduce the principal portion, or record the excess as interest or charges.' properties: message: type: string example: 'Only ₹12,000.00 of principal is still outstanding on Vehicle loan. Reduce the principal portion, or record the excess as interest or charges.' errors: type: object properties: principal: type: array example: - 'Only ₹12,000.00 of principal is still outstanding on Vehicle loan. Reduce the principal portion, or record the excess as interest or charges.' items: type: string tags: - 'Loans and EMI' requestBody: required: true content: application/json: schema: type: object properties: loan_emi_schedule_id: type: integer description: '' example: 16 nullable: true paid_on: type: string description: 'Must be a valid date.' example: '2026-01-15' amount: type: number description: 'Must not be greater than 999999999.' example: 22 nullable: true principal: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 7 nullable: true interest: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 16 nullable: true charges: type: number description: 'Must be at least 0. Must not be greater than 999999999.' example: 17 nullable: true payment_method: type: string description: '' example: cash enum: - cash - bank - upi - card - cheque - other payment_account_id: type: integer description: '' example: 16 nullable: true reference: type: string description: 'Must not be greater than 64 characters.' example: 'n' nullable: true note: type: string description: 'Must not be greater than 255 characters.' example: g nullable: true idempotency_key: type: string description: 'Stable UUID that makes a retry safe.' example: 6d61f406-f07d-482d-a284-3e06edfd7f55 nullable: true required: - paid_on - payment_method parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: loanAccount_id description: 'The ID of the loanAccount.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/loan-accounts/{loanAccount_id}/payments/{loanPayment_id}/void': post: summary: 'Void a repayment.' operationId: voidARepayment description: "Saved transactions keep their history. The journal is reversed, the instalment goes back to\npending, and the reason stays in the audit trail." parameters: [] responses: {} tags: - 'Loans and EMI' requestBody: required: true content: application/json: schema: type: object properties: reason: type: string description: 'Must be at least 3 characters. Must not be greater than 255 characters.' example: b required: - reason parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: loanAccount_id description: 'The ID of the loanAccount.' example: 1 required: true schema: type: integer - in: path name: loanPayment_id description: 'The ID of the loanPayment.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/other-incomes': get: summary: 'List other-income receipts.' operationId: listOtherIncomeReceipts description: '' parameters: - in: query name: q description: 'Case-insensitive search across category, payer, or description.' example: rent required: false schema: type: string description: 'Case-insensitive search across category, payer, or description.' example: rent - in: query name: category description: 'Filter by an exact category.' example: 'Rent received' required: false schema: type: string description: 'Filter by an exact category.' example: 'Rent received' - in: query name: payment_account_id description: 'Filter by the account the money landed in.' example: 9 required: false schema: type: integer description: 'Filter by the account the money landed in.' example: 9 - in: query name: from description: 'Include receipts on or after this date (`YYYY-MM-DD`).' example: '2026-08-01' required: false schema: type: string description: 'Include receipts on or after this date (`YYYY-MM-DD`).' example: '2026-08-01' - in: query name: to description: 'Include receipts on or before this date (`YYYY-MM-DD`).' example: '2026-08-31' required: false schema: type: string description: 'Include receipts on or before this date (`YYYY-MM-DD`).' example: '2026-08-31' - in: query name: status description: 'Filter by record status.' example: active required: false schema: type: string description: 'Filter by record status.' example: active enum: - active - voided - in: query name: per_page description: 'Results per page, from 1 to 100.' example: 20 required: false schema: type: integer description: 'Results per page, from 1 to 100.' example: 20 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Loans and EMI' requestBody: required: false content: application/json: schema: type: object properties: q: type: string description: 'Must not be greater than 100 characters.' example: b nullable: true category: type: string description: 'Must not be greater than 64 characters.' example: 'n' nullable: true payment_account_id: type: integer description: '' example: 16 nullable: true from: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true to: type: string description: 'Must be a valid date. Must be a date after or equal to from.' example: '2026-01-15' nullable: true status: type: string description: '' example: active enum: - active - voided nullable: true per_page: type: integer description: 'Must be at least 1. Must not be greater than 100.' example: 22 nullable: true post: summary: 'Record a receipt that is not a sale.' operationId: recordAReceiptThatIsNotASale description: "The receipt, its journal, the cash-drawer movement, and the audit record either all commit\nor all roll back." parameters: [] responses: {} tags: - 'Loans and EMI' requestBody: required: true content: application/json: schema: type: object properties: category: type: string description: 'Must not be greater than 64 characters.' example: b payer: type: string description: 'Must not be greater than 128 characters.' example: 'n' nullable: true amount: type: number description: 'Must not be greater than 999999999.' example: 7 occurred_on: type: string description: 'Must be a valid date.' example: '2026-01-15' payment_method: type: string description: '' example: cash enum: - cash - bank - upi - card - cheque - other payment_account_id: type: integer description: '' example: 16 nullable: true note: type: string description: 'Must not be greater than 1000 characters.' example: 'n' idempotency_key: type: string description: 'Stable UUID that makes a retry safe.' example: 6d61f406-f07d-482d-a284-3e06edfd7f55 nullable: true required: - category - amount - occurred_on - payment_method - note parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/other-incomes/{otherIncome_id}': get: summary: '' operationId: getApiV1BusinessesBusinessOtherIncomesOtherIncome_id description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Loans and EMI' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: otherIncome_id description: 'The ID of the otherIncome.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/other-incomes/{otherIncome_id}/void': post: summary: 'Void a receipt.' operationId: voidAReceipt description: "The void marker, the journal reversal, the cash-drawer reversal, and the audit record\neither all commit or all roll back." parameters: [] responses: {} tags: - 'Loans and EMI' requestBody: required: true content: application/json: schema: type: object properties: reason: type: string description: 'Must be at least 3 characters. Must not be greater than 255 characters.' example: b required: - reason parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: otherIncome_id description: 'The ID of the otherIncome.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/lookups': get: summary: 'Search lookup values.' operationId: searchLookupValues description: '' parameters: - in: query name: source description: 'Lookup source. The `items`, `contacts`, `customers`, and `suppliers` sources return only records that are still active, since a lookup is someone choosing what to put on a new document.' example: customers required: true schema: type: string description: 'Lookup source. The `items`, `contacts`, `customers`, and `suppliers` sources return only records that are still active, since a lookup is someone choosing what to put on a new document.' example: customers - in: query name: q description: 'Case-insensitive search text, up to 100 characters.' example: priya required: false schema: type: string description: 'Case-insensitive search text, up to 100 characters.' example: priya nullable: true - in: query name: method description: 'Payment method used to filter compatible payment accounts.' example: upi required: false schema: type: string description: 'Payment method used to filter compatible payment accounts.' example: upi nullable: true - in: query name: direction description: 'Document or account direction.' example: received required: false schema: type: string description: 'Document or account direction.' example: received nullable: true - in: query name: contact description: "Contact id. Narrows outstanding documents to a single customer or supplier, prices item results with that customer's rate card, and on contact sources keeps that party in the results even when it is no longer active, so a form can still show the party it already holds." example: 42 required: false schema: type: integer description: "Contact id. Narrows outstanding documents to a single customer or supplier, prices item results with that customer's rate card, and on contact sources keeps that party in the results even when it is no longer active, so a form can still show the party it already holds." example: 42 nullable: true - in: query name: item description: "Item id. Required for the `item_batches` source; returns that item's sellable lots, first-expiry-first-out." example: 12 required: false schema: type: integer description: "Item id. Required for the `item_batches` source; returns that item's sellable lots, first-expiry-first-out." example: 12 nullable: true - in: query name: supplier description: "Supplier contact id, or `preferred`. On the `items` source, adds supplier_price, supplier_price_source, and supplier_name: that supplier's agreed rate or, failing that, the rate on their last purchase invoice. `preferred` uses each item's preferred supplier. Only for callers who may see purchase prices." example: '17' required: false schema: type: string description: "Supplier contact id, or `preferred`. On the `items` source, adds supplier_price, supplier_price_source, and supplier_name: that supplier's agreed rate or, failing that, the rate on their last purchase invoice. `preferred` uses each item's preferred supplier. Only for callers who may see purchase prices." example: '17' nullable: true - in: query name: warehouse description: 'Warehouse id. On the `items` source, the pre-selected lot is the earliest-expiring one with stock in that warehouse; on `item_batches`, only lots with stock there are listed, with the quantity held there.' example: 1 required: false schema: type: integer description: 'Warehouse id. On the `items` source, the pre-selected lot is the earliest-expiring one with stock in that warehouse; on `item_batches`, only lots with stock there are listed, with the quantity held there.' example: 1 nullable: true - in: query name: ids description: 'Item ids to price or re-read, up to 200. Every named item is returned, including items that are no longer active, so a form can reprice the lines it already holds in one request.' example: - 12 - 18 required: false schema: type: array description: 'Item ids to price or re-read, up to 200. Every named item is returned, including items that are no longer active, so a form can reprice the lines it already holds in one request.' example: - 12 - 18 items: type: integer - in: query name: category_id description: 'Tenant-owned item category.' example: 1 required: false schema: type: integer description: 'Tenant-owned item category.' example: 1 - in: query name: subcategory_id description: 'Tenant-owned subcategory; combine with category_id.' example: 2 required: false schema: type: integer description: 'Tenant-owned subcategory; combine with category_id.' example: 2 - in: query name: brand_id description: 'Tenant-owned item brand.' example: 3 required: false schema: type: integer description: 'Tenant-owned item brand.' example: 3 - in: query name: manufacturer_id description: 'Tenant-owned manufacturer.' example: 4 required: false schema: type: integer description: 'Tenant-owned manufacturer.' example: 4 responses: 200: description: '' content: application/json: schema: type: object example: data: - value: '42' label: 'Priya Sharma' meta: 'Customer · 9876543210' attributes: type: customer phone: '9876543210' meta: source: customers query: priya has_more: false properties: data: type: array example: - value: '42' label: 'Priya Sharma' meta: 'Customer · 9876543210' attributes: type: customer phone: '9876543210' items: type: object properties: value: type: string example: '42' label: type: string example: 'Priya Sharma' meta: type: string example: 'Customer · 9876543210' attributes: type: object properties: type: type: string example: customer phone: type: string example: '9876543210' meta: type: object properties: source: type: string example: customers description: 'Lookup source used for the response.' query: type: string example: priya description: 'Normalized search query.' has_more: type: boolean example: false description: 'Whether more matching values exist beyond this response.' tags: - Lookups parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer /api/v1/partners/team: get: summary: 'Get my team performance.' operationId: getMyTeamPerformance description: "Aggregate funnel, per-member clicks/sign-ups/paid accounts, platform earnings,\nteam rewards and retained amount. No referred-shop identity or contact details." parameters: - in: query name: period description: '7d, 30d, 90d or all.' example: 30d required: false schema: type: string description: '7d, 30d, 90d or all.' example: 30d - in: header name: Cookie description: '' example: 'dukanam-session={PARTNER_SESSION}' schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: data: member_count: 1 funnel: clicks: 12 signups: 2 paid: 1 platform_earnings: lifetime_paise: 39900 team_rewards: lifetime_paise: 20000 retained_paise: 19900 members: - id: 8 name: 'Anita Rao' status: active team_role: associate phone: '+919123456789' email: null funnel: clicks: 12 signups: 2 paid: 1 earnings: lifetime_paise: 20000 properties: data: type: object properties: member_count: type: integer example: 1 funnel: type: object properties: clicks: type: integer example: 12 signups: type: integer example: 2 paid: type: integer example: 1 platform_earnings: type: object properties: lifetime_paise: type: integer example: 39900 team_rewards: type: object properties: lifetime_paise: type: integer example: 20000 retained_paise: type: integer example: 19900 members: type: array example: - id: 8 name: 'Anita Rao' status: active team_role: associate phone: '+919123456789' email: null funnel: clicks: 12 signups: 2 paid: 1 earnings: lifetime_paise: 20000 items: type: object properties: id: type: integer example: 8 name: type: string example: 'Anita Rao' status: type: string example: active team_role: type: string example: associate phone: type: string example: '+919123456789' email: type: string example: null nullable: true funnel: type: object properties: clicks: type: integer example: 12 signups: type: integer example: 2 paid: type: integer example: 1 earnings: type: object properties: lifetime_paise: type: integer example: 20000 403: description: '' content: application/json: schema: type: object example: message: Forbidden properties: message: type: string example: Forbidden tags: - 'Master partner teams' security: - partnerSession: [] /api/v1/partners/team/members: post: summary: 'Create my team member.' operationId: createMyTeamMember description: "Creates a new account belonging only to this master, with WhatsApp OTP login. It does\nnot send an invitation message; share /partners/login with the person. Existing\npartner numbers cannot be claimed. Members cannot create their own teams." parameters: - in: header name: Cookie description: '' example: 'dukanam-session={PARTNER_SESSION}' schema: type: string - name: X-CSRF-TOKEN in: header required: true description: 'Session CSRF token, or send the X-XSRF-TOKEN equivalent.' schema: type: string responses: 201: description: '' content: application/json: schema: type: object example: data: id: 8 name: 'Anita Rao' phone: '+919123456789' email: null status: active account_type: member team_role: associate master_partner_id: 3 properties: data: type: object properties: id: type: integer example: 8 name: type: string example: 'Anita Rao' phone: type: string example: '+919123456789' email: type: string example: null nullable: true status: type: string example: active account_type: type: string example: member team_role: type: string example: associate master_partner_id: type: integer example: 3 422: description: '' content: application/json: schema: type: object example: message: 'Another partner already uses this number.' errors: phone: - 'Another partner already uses this number.' properties: message: type: string example: 'Another partner already uses this number.' errors: type: object properties: phone: type: array example: - 'Another partner already uses this number.' items: type: string tags: - 'Master partner teams' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: '' example: 'Anita Rao' phone: type: string description: 'Indian WhatsApp mobile.' example: '9123456789' email: type: string description: '' example: anita@example.com nullable: true status: type: string description: 'active or suspended.' example: active team_role: type: string description: 'associate or employee.' example: associate account_type: type: string description: '' example: null master_partner_id: type: string description: '' example: null commission_type: type: string description: '' example: null commission_flat_amount: type: string description: '' example: null commission_percent: type: string description: '' example: null required: - name - phone - team_role security: - partnerSession: [] '/api/v1/partners/team/members/{member_id}': get: summary: 'Get my team member and link performance.' operationId: getMyTeamMemberAndLinkPerformance description: 'Includes eligible programs and reward balances, never platform admin notes or PAN.' parameters: - in: header name: Cookie description: '' example: 'dukanam-session={PARTNER_SESSION}' schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: data: member: id: 8 name: 'Anita Rao' master_partner_id: 3 funnel: clicks: 12 signups: 2 paid: 1 earnings: lifetime_paise: 20000 available_programs: [] links: - id: 4 code: ANITA4K program_id: 2 label: WhatsApp url: 'https://dukanam.com/p/ANITA4K' destination: register is_active: true stats: clicks: 12 signups: 2 paid: 1 properties: data: type: object properties: member: type: object properties: id: type: integer example: 8 name: type: string example: 'Anita Rao' master_partner_id: type: integer example: 3 funnel: type: object properties: clicks: type: integer example: 12 signups: type: integer example: 2 paid: type: integer example: 1 earnings: type: object properties: lifetime_paise: type: integer example: 20000 available_programs: type: array example: [] links: type: array example: - id: 4 code: ANITA4K program_id: 2 label: WhatsApp url: 'https://dukanam.com/p/ANITA4K' destination: register is_active: true stats: clicks: 12 signups: 2 paid: 1 items: type: object properties: id: type: integer example: 4 code: type: string example: ANITA4K program_id: type: integer example: 2 label: type: string example: WhatsApp url: type: string example: 'https://dukanam.com/p/ANITA4K' destination: type: string example: register is_active: type: boolean example: true stats: type: object properties: clicks: type: integer example: 12 signups: type: integer example: 2 paid: type: integer example: 1 404: description: '' content: application/json: schema: type: object example: message: 'Not found.' properties: message: type: string example: 'Not found.' tags: - 'Master partner teams' security: - partnerSession: [] put: summary: 'Update my team member.' operationId: updateMyTeamMember description: "Only profile, phone, status and team_role may change. Parent, account type and\nplatform commission fields are prohibited. Suspending stops login and new attribution." parameters: - in: header name: Cookie description: '' example: 'dukanam-session={PARTNER_SESSION}' schema: type: string - name: X-CSRF-TOKEN in: header required: true description: 'Session CSRF token, or send the X-XSRF-TOKEN equivalent.' schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: data: id: 8 name: 'Anita Rao' phone: '+919123456789' email: null status: suspended account_type: member team_role: employee master_partner_id: 3 properties: data: type: object properties: id: type: integer example: 8 name: type: string example: 'Anita Rao' phone: type: string example: '+919123456789' email: type: string example: null nullable: true status: type: string example: suspended account_type: type: string example: member team_role: type: string example: employee master_partner_id: type: integer example: 3 tags: - 'Master partner teams' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: '' example: 'Anita Rao' phone: type: string description: '' example: '9123456789' email: type: string description: '' example: anita@example.com nullable: true status: type: string description: 'active or suspended.' example: suspended team_role: type: string description: 'associate or employee.' example: employee account_type: type: string description: '' example: null master_partner_id: type: string description: '' example: null commission_type: type: string description: '' example: null commission_flat_amount: type: string description: '' example: null commission_percent: type: string description: '' example: null required: - team_role security: - partnerSession: [] parameters: - in: path name: member_id description: 'The ID of the member.' example: 1 required: true schema: type: integer '/api/v1/partners/team/members/{member_id}/links': post: summary: 'Create a link for my team member.' operationId: createALinkForMyTeamMember description: "A team program eligible for this member is required. Default platform programs\nand other masters' programs cannot be used. Shared link limit is 25 per member." parameters: - in: header name: Cookie description: '' example: 'dukanam-session={PARTNER_SESSION}' schema: type: string - name: X-CSRF-TOKEN in: header required: true description: 'Session CSRF token, or send the X-XSRF-TOKEN equivalent.' schema: type: string responses: 201: description: '' content: application/json: schema: type: object example: data: id: 4 code: ANITA4K program_id: 2 label: WhatsApp url: 'https://dukanam.com/p/ANITA4K' destination: register is_active: true created_at: '2026-10-06T12:00:00Z' properties: data: type: object properties: id: type: integer example: 4 code: type: string example: ANITA4K program_id: type: integer example: 2 label: type: string example: WhatsApp url: type: string example: 'https://dukanam.com/p/ANITA4K' destination: type: string example: register is_active: type: boolean example: true created_at: type: string example: '2026-10-06T12:00:00Z' tags: - 'Master partner teams' requestBody: required: true content: application/json: schema: type: object properties: program_id: type: integer description: '' example: 2 nullable: true label: type: string description: '' example: WhatsApp code: type: string description: 'Optional unique code, 4–16 letters/numbers.' example: ANITA4K nullable: true destination: type: string description: 'register, home or pricing.' example: register nullable: true required: - program_id - label security: - partnerSession: [] parameters: - in: path name: member_id description: 'The ID of the member.' example: 1 required: true schema: type: integer '/api/v1/partners/team/members/{member_id}/links/{link_id}': patch: summary: "Pause or resume my member's link." operationId: pauseOrResumeMyMembersLink description: "Both member and link must belong to this master's team. Paused links stop new attribution." parameters: - in: header name: Cookie description: '' example: 'dukanam-session={PARTNER_SESSION}' schema: type: string - name: X-CSRF-TOKEN in: header required: true description: 'Session CSRF token, or send the X-XSRF-TOKEN equivalent.' schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: data: id: 4 program_id: 2 code: ANITA4K is_active: false properties: data: type: object properties: id: type: integer example: 4 program_id: type: integer example: 2 code: type: string example: ANITA4K is_active: type: boolean example: false tags: - 'Master partner teams' requestBody: required: true content: application/json: schema: type: object properties: is_active: type: boolean description: '' example: false required: - is_active security: - partnerSession: [] parameters: - in: path name: member_id description: 'The ID of the member.' example: 1 required: true schema: type: integer - in: path name: link_id description: 'The ID of the link.' example: 1 required: true schema: type: integer /api/v1/partners/team/programs: get: summary: 'List my team programs.' operationId: listMyTeamPrograms description: "All/selected audiences apply only within this master's team. Includes paused and scheduled programs." parameters: - in: header name: Cookie description: '' example: 'dukanam-session={PARTNER_SESSION}' schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 2 owner_master_partner_id: 3 name: 'Shop referrals' audience: all partner_ids: [] is_active: true commission: type: flat flat_amount_paise: 20000 percent_basis_points: null custom: false description: '₹200.00 per paid account' hold_days: 7 properties: data: type: array example: - id: 2 owner_master_partner_id: 3 name: 'Shop referrals' audience: all partner_ids: [] is_active: true commission: type: flat flat_amount_paise: 20000 percent_basis_points: null custom: false description: '₹200.00 per paid account' hold_days: 7 items: type: object properties: id: type: integer example: 2 owner_master_partner_id: type: integer example: 3 name: type: string example: 'Shop referrals' audience: type: string example: all partner_ids: type: array example: [] is_active: type: boolean example: true commission: type: object properties: type: type: string example: flat flat_amount_paise: type: integer example: 20000 percent_basis_points: type: string example: null nullable: true custom: type: boolean example: false description: type: string example: '₹200.00 per paid account' hold_days: type: integer example: 7 tags: - 'Master partner teams' security: - partnerSession: [] post: summary: 'Create my team program.' operationId: createMyTeamProgram description: "Fixed rewards must be below the master's fixed platform rate. Percent rewards\nuse the master's platform commission, not the shop payment, up to 99.99%.\nA rounding safeguard retains at least one paise for the master per conversion.\nTeam hold is the longer of program and master platform holds. Terms are saved at sign-up." parameters: - in: header name: Cookie description: '' example: 'dukanam-session={PARTNER_SESSION}' schema: type: string - name: X-CSRF-TOKEN in: header required: true description: 'Session CSRF token, or send the X-XSRF-TOKEN equivalent.' schema: type: string responses: 201: description: '' content: application/json: schema: type: object example: data: id: 2 owner_master_partner_id: 3 name: 'Shop referrals' audience: all partner_ids: [] is_active: true commission: type: flat flat_amount_paise: 20000 percent_basis_points: null custom: false description: '₹200.00 per paid account' hold_days: 7 properties: data: type: object properties: id: type: integer example: 2 owner_master_partner_id: type: integer example: 3 name: type: string example: 'Shop referrals' audience: type: string example: all partner_ids: type: array example: [] is_active: type: boolean example: true commission: type: object properties: type: type: string example: flat flat_amount_paise: type: integer example: 20000 percent_basis_points: type: string example: null nullable: true custom: type: boolean example: false description: type: string example: '₹200.00 per paid account' hold_days: type: integer example: 7 422: description: '' content: application/json: schema: type: object example: message: 'Fixed team rewards must be lower than your fixed platform commission.' errors: flat_amount: - 'Fixed team rewards must be lower than your fixed platform commission.' properties: message: type: string example: 'Fixed team rewards must be lower than your fixed platform commission.' errors: type: object properties: flat_amount: type: array example: - 'Fixed team rewards must be lower than your fixed platform commission.' items: type: string tags: - 'Master partner teams' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: '' example: 'Shop referrals' description: type: string description: '' example: 'Refer shops and earn rewards.' is_active: type: boolean description: '' example: true audience: type: string description: 'all or selected within my team.' example: all partner_ids: type: array description: 'Required for selected; only my member IDs.' example: - 8 items: type: integer starts_at: type: string description: 'Nullable inclusive start.' example: null nullable: true ends_at: type: string description: 'Nullable exclusive end, after start if supplied.' example: null nullable: true commission_type: type: string description: 'flat or percent of master commission.' example: flat flat_amount: type: number description: 'Positive rupees, required for flat.' example: 200.0 nullable: true percent: type: number description: 'Positive percent below 100, required for percent.' example: 50.0 nullable: true hold_days: type: integer description: 0–180. example: 7 owner_master_partner_id: type: string description: '' example: null required: - name - description - is_active - audience - commission_type - hold_days security: - partnerSession: [] '/api/v1/partners/team/programs/{program_id}': put: summary: 'Replace my team program.' operationId: replaceMyTeamProgram description: "Uses the same full configuration and validation as creation. Prior referrals\nretain saved master and member terms. Omitted optional timestamps clear the window." parameters: - in: header name: Cookie description: '' example: 'dukanam-session={PARTNER_SESSION}' schema: type: string - name: X-CSRF-TOKEN in: header required: true description: 'Session CSRF token, or send the X-XSRF-TOKEN equivalent.' schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: data: id: 2 owner_master_partner_id: 3 name: 'Shop referrals' is_active: false audience: all partner_ids: [] commission: type: flat flat_amount_paise: 20000 percent_basis_points: null custom: false description: '₹200.00 per paid account' hold_days: 7 properties: data: type: object properties: id: type: integer example: 2 owner_master_partner_id: type: integer example: 3 name: type: string example: 'Shop referrals' is_active: type: boolean example: false audience: type: string example: all partner_ids: type: array example: [] commission: type: object properties: type: type: string example: flat flat_amount_paise: type: integer example: 20000 percent_basis_points: type: string example: null nullable: true custom: type: boolean example: false description: type: string example: '₹200.00 per paid account' hold_days: type: integer example: 7 tags: - 'Master partner teams' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Must not be greater than 80 characters.' example: b description: type: string description: 'Must not be greater than 2000 characters.' example: 'Et animi quos velit et fugiat.' is_active: type: boolean description: '' example: false audience: type: string description: '' example: all enum: - all - selected partner_ids: type: array description: 'Must match an existing stored value.' example: - 16 items: type: integer starts_at: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true ends_at: type: string description: 'Must be a valid date.' example: '2026-01-15' nullable: true commission_type: type: string description: '' example: flat enum: - flat - percent flat_amount: type: number description: 'This field is required when commission_type is flat. Must be at least 0.01. Must not be greater than 1000000.' example: 22 nullable: true percent: type: number description: 'This field is required when commission_type is percent. Must be at least 0.01. Must not be greater than 99.99.' example: 7 nullable: true hold_days: type: integer description: 'Must be at least 0. Must not be greater than 180.' example: 16 owner_master_partner_id: type: string description: '' example: null required: - name - description - is_active - audience - partner_ids - commission_type - hold_days security: - partnerSession: [] parameters: - in: path name: program_id description: 'The ID of the program.' example: 1 required: true schema: type: integer /api/v1/partners/team/settlements: get: summary: 'List my team settlements.' operationId: listMyTeamSettlements description: 'Processing, paid and cancelled manual transfers, separate from Dukanam payouts.' parameters: - in: header name: Cookie description: '' example: 'dukanam-session={PARTNER_SESSION}' schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 1 master_partner_id: 3 partner_id: 8 status: processing gross_paise: 20000 commissions_count: 1 account_label: 'UPI · an••@okhdfcbank' reference: null notes: null created_by_partner_id: 3 paid_by_partner_id: null created_at: '2026-10-06T12:00:00Z' paid_at: null cancelled_at: null properties: data: type: array example: - id: 1 master_partner_id: 3 partner_id: 8 status: processing gross_paise: 20000 commissions_count: 1 account_label: 'UPI · an••@okhdfcbank' reference: null notes: null created_by_partner_id: 3 paid_by_partner_id: null created_at: '2026-10-06T12:00:00Z' paid_at: null cancelled_at: null items: type: object properties: id: type: integer example: 1 master_partner_id: type: integer example: 3 partner_id: type: integer example: 8 status: type: string example: processing gross_paise: type: integer example: 20000 commissions_count: type: integer example: 1 account_label: type: string example: 'UPI · an••@okhdfcbank' reference: type: string example: null nullable: true notes: type: string example: null nullable: true created_by_partner_id: type: integer example: 3 paid_by_partner_id: type: string example: null nullable: true created_at: type: string example: '2026-10-06T12:00:00Z' paid_at: type: string example: null nullable: true cancelled_at: type: string example: null nullable: true tags: - 'Master partner teams' security: - partnerSession: [] '/api/v1/partners/team/settlements/{settlement_id}': get: summary: "Get my settlement's payment details." operationId: getMySettlementsPaymentDetails description: "Full saved bank/UPI destination, only for the owning master, to make the external\ntransfer. List responses are masked. Later member edits never change this destination." parameters: - in: header name: Cookie description: '' example: 'dukanam-session={PARTNER_SESSION}' schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: data: id: 1 status: processing gross_paise: 20000 account: method: upi account_holder_name: 'Anita Rao' upi_id: anita@okhdfcbank properties: data: type: object properties: id: type: integer example: 1 status: type: string example: processing gross_paise: type: integer example: 20000 account: type: object properties: method: type: string example: upi account_holder_name: type: string example: 'Anita Rao' upi_id: type: string example: anita@okhdfcbank tags: - 'Master partner teams' security: - partnerSession: [] parameters: - in: path name: settlement_id description: 'The ID of the settlement.' example: 1 required: true schema: type: integer '/api/v1/partners/team/members/{member_id}/settlements': post: summary: "Prepare my member's settlement." operationId: prepareMyMembersSettlement description: "Reserves all approved unsettled rewards for this member. Requires completed profile\nand primary payout account; keeps an encrypted destination snapshot. Duplicate\npreparation without new rewards returns 422. Does not transfer money or reserve\nany Dukanam payout. Total is the full reward amount with no automatic withholding." parameters: - in: header name: Cookie description: '' example: 'dukanam-session={PARTNER_SESSION}' schema: type: string - name: X-CSRF-TOKEN in: header required: true description: 'Session CSRF token, or send the X-XSRF-TOKEN equivalent.' schema: type: string responses: 201: description: '' content: application/json: schema: type: object example: data: id: 1 master_partner_id: 3 partner_id: 8 status: processing gross_paise: 20000 commissions_count: 1 account_label: 'UPI · an••@okhdfcbank' reference: null notes: null created_by_partner_id: 3 paid_by_partner_id: null created_at: '2026-10-06T12:00:00Z' paid_at: null cancelled_at: null properties: data: type: object properties: id: type: integer example: 1 master_partner_id: type: integer example: 3 partner_id: type: integer example: 8 status: type: string example: processing gross_paise: type: integer example: 20000 commissions_count: type: integer example: 1 account_label: type: string example: 'UPI · an••@okhdfcbank' reference: type: string example: null nullable: true notes: type: string example: null nullable: true created_by_partner_id: type: integer example: 3 paid_by_partner_id: type: string example: null nullable: true created_at: type: string example: '2026-10-06T12:00:00Z' paid_at: type: string example: null nullable: true cancelled_at: type: string example: null nullable: true 422: description: '' content: application/json: schema: type: object example: message: 'No eligible rewards or missing payment details.' errors: settlement: - 'No eligible rewards or missing payment details.' properties: message: type: string example: 'No eligible rewards or missing payment details.' errors: type: object properties: settlement: type: array example: - 'No eligible rewards or missing payment details.' items: type: string tags: - 'Master partner teams' security: - partnerSession: [] parameters: - in: path name: member_id description: 'The ID of the member.' example: 1 required: true schema: type: integer '/api/v1/partners/team/settlements/{settlement_id}/mark-paid': post: summary: 'Mark my settlement paid.' operationId: markMySettlementPaid description: "Only processing records. Stores reference, date and acting master; marks reserved\nmember rewards paid. Repeated payment returns 422. Transfer happens outside Dukanam." parameters: - in: header name: Cookie description: '' example: 'dukanam-session={PARTNER_SESSION}' schema: type: string - name: X-CSRF-TOKEN in: header required: true description: 'Session CSRF token, or send the X-XSRF-TOKEN equivalent.' schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: data: id: 1 master_partner_id: 3 partner_id: 8 status: paid gross_paise: 20000 reference: UTR123456 paid_by_partner_id: 3 paid_at: '2026-10-06T12:00:00Z' properties: data: type: object properties: id: type: integer example: 1 master_partner_id: type: integer example: 3 partner_id: type: integer example: 8 status: type: string example: paid gross_paise: type: integer example: 20000 reference: type: string example: UTR123456 paid_by_partner_id: type: integer example: 3 paid_at: type: string example: '2026-10-06T12:00:00Z' tags: - 'Master partner teams' requestBody: required: true content: application/json: schema: type: object properties: reference: type: string description: 'Bank/UPI transfer reference, max 100.' example: UTR123456 paid_at: type: string description: 'Optional payment date/time, cannot be future.' example: null notes: type: string description: 'Optional notes, max 2000.' example: 'Paid by UPI' required: - reference security: - partnerSession: [] parameters: - in: path name: settlement_id description: 'The ID of the settlement.' example: 1 required: true schema: type: integer '/api/v1/partners/team/settlements/{settlement_id}/cancel': post: summary: 'Cancel my processing settlement.' operationId: cancelMyProcessingSettlement description: 'Frees its rewards for a future settlement. A paid record cannot be cancelled.' parameters: - in: header name: Cookie description: '' example: 'dukanam-session={PARTNER_SESSION}' schema: type: string - name: X-CSRF-TOKEN in: header required: true description: 'Session CSRF token, or send the X-XSRF-TOKEN equivalent.' schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: data: id: 1 master_partner_id: 3 partner_id: 8 status: cancelled gross_paise: 20000 cancelled_at: '2026-10-06T12:00:00Z' properties: data: type: object properties: id: type: integer example: 1 master_partner_id: type: integer example: 3 partner_id: type: integer example: 8 status: type: string example: cancelled gross_paise: type: integer example: 20000 cancelled_at: type: string example: '2026-10-06T12:00:00Z' tags: - 'Master partner teams' requestBody: required: false content: application/json: schema: type: object properties: notes: type: string description: 'Optional reason, max 2000.' example: 'Wrong account; prepare again' security: - partnerSession: [] parameters: - in: path name: settlement_id description: 'The ID of the settlement.' example: 1 required: true schema: type: integer /api/v1/partners/me: get: summary: 'Get my partner dashboard.' operationId: getMyPartnerDashboard description: "Shop identities and internal notes are excluded. Earnings are lifetime; funnel\nuses the requested period. Team members never see other members' balances." parameters: - in: query name: period description: '7d, 30d, 90d or all.' example: 30d required: false schema: type: string description: '7d, 30d, 90d or all.' example: 30d - in: header name: Cookie description: '' example: 'dukanam-session={PARTNER_SESSION}' schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: data: partner: id: 8 name: 'Anita Rao' account_type: member team_role: associate master_partner_id: 3 available_programs: - id: 2 name: 'Shop referrals' commission: type: flat flat_amount_paise: 20000 funnel: clicks: 12 signups: 2 paid: 1 earnings: on_hold_paise: 0 available_paise: 20000 processing_paise: 0 paid_paise: 0 lifetime_paise: 20000 properties: data: type: object properties: partner: type: object properties: id: type: integer example: 8 name: type: string example: 'Anita Rao' account_type: type: string example: member team_role: type: string example: associate master_partner_id: type: integer example: 3 available_programs: type: array example: - id: 2 name: 'Shop referrals' commission: type: flat flat_amount_paise: 20000 items: type: object properties: id: type: integer example: 2 name: type: string example: 'Shop referrals' commission: type: object properties: type: type: string example: flat flat_amount_paise: type: integer example: 20000 funnel: type: object properties: clicks: type: integer example: 12 signups: type: integer example: 2 paid: type: integer example: 1 earnings: type: object properties: on_hold_paise: type: integer example: 0 available_paise: type: integer example: 20000 processing_paise: type: integer example: 0 paid_paise: type: integer example: 0 lifetime_paise: type: integer example: 20000 tags: - 'Partner portal' security: - partnerSession: [] /api/v1/partners/me/links: get: summary: 'List my links and performance.' operationId: listMyLinksAndPerformance description: '' parameters: - in: header name: Cookie description: '' example: 'dukanam-session={PARTNER_SESSION}' schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 4 program_id: 2 code: ANITA4K label: WhatsApp url: 'https://dukanam.com/p/ANITA4K' destination: register is_active: true stats: clicks: 12 signups: 2 paid: 1 share: code: ANITA4K link: 'https://dukanam.com/p/ANITA4K' message: 'Try Dukanam' whatsapp_url: 'https://wa.me/?text=Try%20Dukanam' properties: data: type: array example: - id: 4 program_id: 2 code: ANITA4K label: WhatsApp url: 'https://dukanam.com/p/ANITA4K' destination: register is_active: true stats: clicks: 12 signups: 2 paid: 1 share: code: ANITA4K link: 'https://dukanam.com/p/ANITA4K' message: 'Try Dukanam' whatsapp_url: 'https://wa.me/?text=Try%20Dukanam' items: type: object properties: id: type: integer example: 4 program_id: type: integer example: 2 code: type: string example: ANITA4K label: type: string example: WhatsApp url: type: string example: 'https://dukanam.com/p/ANITA4K' destination: type: string example: register is_active: type: boolean example: true stats: type: object properties: clicks: type: integer example: 12 signups: type: integer example: 2 paid: type: integer example: 1 share: type: object properties: code: type: string example: ANITA4K link: type: string example: 'https://dukanam.com/p/ANITA4K' message: type: string example: 'Try Dukanam' whatsapp_url: type: string example: 'https://wa.me/?text=Try%20Dukanam' tags: - 'Partner portal' security: - partnerSession: [] post: summary: 'Create my program link.' operationId: createMyProgramLink description: "Team members must choose an active eligible team program. Individual/master\npartners may omit program_id for their default platform deal. Maximum 25 links." parameters: - in: header name: Cookie description: '' example: 'dukanam-session={PARTNER_SESSION}' schema: type: string - name: X-CSRF-TOKEN in: header required: true description: 'Session CSRF token, or send the X-XSRF-TOKEN equivalent.' schema: type: string responses: 201: description: '' content: application/json: schema: type: object example: data: id: 4 program_id: 2 code: ANITA4K label: WhatsApp url: 'https://dukanam.com/p/ANITA4K' destination: register is_active: true properties: data: type: object properties: id: type: integer example: 4 program_id: type: integer example: 2 code: type: string example: ANITA4K label: type: string example: WhatsApp url: type: string example: 'https://dukanam.com/p/ANITA4K' destination: type: string example: register is_active: type: boolean example: true tags: - 'Partner portal' requestBody: required: true content: application/json: schema: type: object properties: program_id: type: integer description: 'Eligible program; required for team members.' example: 2 nullable: true label: type: string description: '' example: WhatsApp code: type: string description: 'Optional unique letters/numbers, 4–16 characters.' example: ANITA4K nullable: true destination: type: string description: 'register, home or pricing.' example: register nullable: true required: - label security: - partnerSession: [] '/api/v1/partners/me/links/{link_id}': patch: summary: 'Update my link.' operationId: updateMyLink description: "Only your links; code, program and ownership cannot be changed. A paused link\nstill redirects, but stops attribution of new clicks and sign-ups." parameters: - in: header name: Cookie description: '' example: 'dukanam-session={PARTNER_SESSION}' schema: type: string - name: X-CSRF-TOKEN in: header required: true description: 'Session CSRF token, or send the X-XSRF-TOKEN equivalent.' schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: data: id: 4 program_id: 2 code: ANITA4K label: Instagram destination: register is_active: false properties: data: type: object properties: id: type: integer example: 4 program_id: type: integer example: 2 code: type: string example: ANITA4K label: type: string example: Instagram destination: type: string example: register is_active: type: boolean example: false 404: description: '' content: application/json: schema: type: object example: message: 'Not found.' properties: message: type: string example: 'Not found.' tags: - 'Partner portal' requestBody: required: false content: application/json: schema: type: object properties: label: type: string description: 'Optional new label, max 80.' example: Instagram destination: type: string description: 'register, home or pricing.' example: register is_active: type: boolean description: 'Pause or resume.' example: false security: - partnerSession: [] parameters: - in: path name: link_id description: 'The ID of the link.' example: 1 required: true schema: type: integer /api/v1/partners/me/commissions: get: summary: 'List my earnings history.' operationId: listMyEarningsHistory description: "Paginated commissions, using member rewards for a team account. Excludes shop\nidentities and the master's gross platform commission. All amounts are paise.\npartner_referral_id is null if the referred account has been deleted." parameters: - in: header name: Cookie description: '' example: 'dukanam-session={PARTNER_SESSION}' schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 1 partner_referral_id: 9 amount_paise: 20000 status: approved program_name: 'Shop referrals' earned_at: '2026-10-06T12:00:00Z' hold_until: '2026-10-06T12:00:00Z' paid_at: null rejection_reason: null current_page: 1 per_page: 25 total: 1 properties: data: type: array example: - id: 1 partner_referral_id: 9 amount_paise: 20000 status: approved program_name: 'Shop referrals' earned_at: '2026-10-06T12:00:00Z' hold_until: '2026-10-06T12:00:00Z' paid_at: null rejection_reason: null items: type: object properties: id: type: integer example: 1 partner_referral_id: type: integer example: 9 amount_paise: type: integer example: 20000 status: type: string example: approved program_name: type: string example: 'Shop referrals' earned_at: type: string example: '2026-10-06T12:00:00Z' hold_until: type: string example: '2026-10-06T12:00:00Z' paid_at: type: string example: null nullable: true rejection_reason: type: string example: null nullable: true current_page: type: integer example: 1 per_page: type: integer example: 25 total: type: integer example: 1 tags: - 'Partner portal' security: - partnerSession: [] /api/v1/partners/me/payments: get: summary: 'List my payments.' operationId: listMyPayments description: "Team accounts receive master-funded manual settlement records; other partners\nreceive platform payout records. Saved payment destinations are masked." parameters: - in: header name: Cookie description: '' example: 'dukanam-session={PARTNER_SESSION}' schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 1 master_partner_id: 3 partner_id: 8 status: paid gross_paise: 20000 account_label: 'UPI · an••@ybl' reference: UTR123456 paid_at: '2026-10-06T12:00:00Z' properties: data: type: array example: - id: 1 master_partner_id: 3 partner_id: 8 status: paid gross_paise: 20000 account_label: 'UPI · an••@ybl' reference: UTR123456 paid_at: '2026-10-06T12:00:00Z' items: type: object properties: id: type: integer example: 1 master_partner_id: type: integer example: 3 partner_id: type: integer example: 8 status: type: string example: paid gross_paise: type: integer example: 20000 account_label: type: string example: 'UPI · an••@ybl' reference: type: string example: UTR123456 paid_at: type: string example: '2026-10-06T12:00:00Z' tags: - 'Partner portal' security: - partnerSession: [] /api/v1/admin/partner-programs: get: summary: 'List named partner programs.' operationId: listNamedPartnerPrograms description: "Includes platform-owned drafts, paused, future and expired programs for administration.\nMaster-owned programs are managed only through the owning master’s team API.\nThe default program is managed by /admin/partner-program and has no database ID." parameters: - in: query name: per_page description: 'Page size, from 1 to 50.' example: 25 required: false schema: type: integer description: 'Page size, from 1 to 50.' example: 25 responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 1 owner_master_partner_id: null name: 'Creator campaign' description: 'Help shops discover Dukanam.' is_active: true audience: selected partner_ids: - 3 starts_at: null ends_at: null commission: type: percent flat_amount_paise: null percent_basis_points: 2500 custom: false description: '25% of the first payment' hold_days: 15 created_at: '2026-10-06T12:00:00.000000Z' properties: data: type: array example: - id: 1 owner_master_partner_id: null name: 'Creator campaign' description: 'Help shops discover Dukanam.' is_active: true audience: selected partner_ids: - 3 starts_at: null ends_at: null commission: type: percent flat_amount_paise: null percent_basis_points: 2500 custom: false description: '25% of the first payment' hold_days: 15 created_at: '2026-10-06T12:00:00.000000Z' items: type: object properties: id: type: integer example: 1 owner_master_partner_id: type: string example: null nullable: true name: type: string example: 'Creator campaign' description: type: string example: 'Help shops discover Dukanam.' is_active: type: boolean example: true audience: type: string example: selected partner_ids: type: array example: - 3 items: type: integer starts_at: type: string example: null nullable: true ends_at: type: string example: null nullable: true commission: type: object properties: type: type: string example: percent flat_amount_paise: type: string example: null nullable: true percent_basis_points: type: integer example: 2500 custom: type: boolean example: false description: type: string example: '25% of the first payment' hold_days: type: integer example: 15 created_at: type: string example: '2026-10-06T12:00:00.000000Z' tags: - 'Partner program administration' requestBody: required: false content: application/json: schema: type: object properties: per_page: type: integer description: 'Must be at least 1. Must not be greater than 50.' example: 1 nullable: true post: summary: 'Create a named partner program.' operationId: createANamedPartnerProgram description: '' parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: id: 1 owner_master_partner_id: null name: 'Creator campaign' description: 'Help shops discover Dukanam.' is_active: true audience: selected partner_ids: - 3 starts_at: null ends_at: null commission: type: percent flat_amount_paise: null percent_basis_points: 2500 custom: false description: '25% of the first payment' hold_days: 15 created_at: '2026-10-06T12:00:00.000000Z' properties: data: type: object properties: id: type: integer example: 1 owner_master_partner_id: type: string example: null nullable: true name: type: string example: 'Creator campaign' description: type: string example: 'Help shops discover Dukanam.' is_active: type: boolean example: true audience: type: string example: selected partner_ids: type: array example: - 3 items: type: integer starts_at: type: string example: null nullable: true ends_at: type: string example: null nullable: true commission: type: object properties: type: type: string example: percent flat_amount_paise: type: string example: null nullable: true percent_basis_points: type: integer example: 2500 custom: type: boolean example: false description: type: string example: '25% of the first payment' hold_days: type: integer example: 15 created_at: type: string example: '2026-10-06T12:00:00.000000Z' 422: description: '' content: application/json: schema: type: object example: message: 'The selected audience is invalid.' errors: audience: - 'The selected audience is invalid.' properties: message: type: string example: 'The selected audience is invalid.' errors: type: object properties: audience: type: array example: - 'The selected audience is invalid.' items: type: string tags: - 'Partner program administration' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Program name (up to 80 characters).' example: 'Creator campaign' description: type: string description: 'Partner-facing details (up to 2000 characters).' example: 'Help shops discover Dukanam.' is_active: type: boolean description: 'Enable availability during the scheduled window.' example: true audience: type: string description: 'all or selected.' example: selected partner_ids: type: array description: 'Required and nonempty for selected; existing individual/master IDs, no team members or duplicates.' example: - 3 items: type: integer starts_at: type: string description: 'Optional start timestamp, inclusive; null for immediate availability.' example: null nullable: true ends_at: type: string description: 'Optional end timestamp, exclusive; must follow starts_at when both are supplied.' example: null nullable: true commission_type: type: string description: "flat, percent or first_month. Named program terms override the partner's personal deal for program links." example: percent flat_amount: type: number description: 'Required for flat, positive rupees up to 1000000.' example: 250.0 nullable: true percent: type: number description: 'Required for percent, positive percentage up to 100.' example: 25.0 nullable: true hold_days: type: integer description: 'Refund hold in days, from 0 to 180.' example: 15 required: - name - description - is_active - audience - commission_type - hold_days '/api/v1/admin/partner-programs/{program_id}': get: summary: 'Get a named partner program.' operationId: getANamedPartnerProgram description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 1 owner_master_partner_id: null name: 'Creator campaign' description: 'Help shops discover Dukanam.' is_active: true audience: selected partner_ids: - 3 starts_at: null ends_at: null commission: type: percent flat_amount_paise: null percent_basis_points: 2500 custom: false description: '25% of the first payment' hold_days: 15 created_at: '2026-10-06T12:00:00.000000Z' properties: data: type: object properties: id: type: integer example: 1 owner_master_partner_id: type: string example: null nullable: true name: type: string example: 'Creator campaign' description: type: string example: 'Help shops discover Dukanam.' is_active: type: boolean example: true audience: type: string example: selected partner_ids: type: array example: - 3 items: type: integer starts_at: type: string example: null nullable: true ends_at: type: string example: null nullable: true commission: type: object properties: type: type: string example: percent flat_amount_paise: type: string example: null nullable: true percent_basis_points: type: integer example: 2500 custom: type: boolean example: false description: type: string example: '25% of the first payment' hold_days: type: integer example: 15 created_at: type: string example: '2026-10-06T12:00:00.000000Z' 404: description: '' content: application/json: schema: type: object example: message: 'Not found.' properties: message: type: string example: 'Not found.' tags: - 'Partner program administration' put: summary: 'Replace a named partner program.' operationId: replaceANamedPartnerProgram description: "Send the full configuration. Pausing, expiry or removing an eligible partner\nstops new attribution. Already attributed referrals retain a snapshot of the\nprogram reward and hold period from sign-up, even if the program changes later.\nSuspending the partner still blocks commissions. Omitted timestamps clear the window." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 1 owner_master_partner_id: null name: 'Creator campaign' description: 'Help shops discover Dukanam.' is_active: true audience: selected partner_ids: - 3 starts_at: null ends_at: null commission: type: percent flat_amount_paise: null percent_basis_points: 2500 custom: false description: '25% of the first payment' hold_days: 15 created_at: '2026-10-06T12:00:00.000000Z' properties: data: type: object properties: id: type: integer example: 1 owner_master_partner_id: type: string example: null nullable: true name: type: string example: 'Creator campaign' description: type: string example: 'Help shops discover Dukanam.' is_active: type: boolean example: true audience: type: string example: selected partner_ids: type: array example: - 3 items: type: integer starts_at: type: string example: null nullable: true ends_at: type: string example: null nullable: true commission: type: object properties: type: type: string example: percent flat_amount_paise: type: string example: null nullable: true percent_basis_points: type: integer example: 2500 custom: type: boolean example: false description: type: string example: '25% of the first payment' hold_days: type: integer example: 15 created_at: type: string example: '2026-10-06T12:00:00.000000Z' 422: description: '' content: application/json: schema: type: object example: message: 'The ends at field must be a date after starts at.' errors: ends_at: - 'The ends at field must be a date after starts at.' properties: message: type: string example: 'The ends at field must be a date after starts at.' errors: type: object properties: ends_at: type: array example: - 'The ends at field must be a date after starts at.' items: type: string tags: - 'Partner program administration' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Program name (up to 80 characters).' example: 'Creator campaign' description: type: string description: 'Partner-facing details (up to 2000 characters).' example: 'Help shops discover Dukanam.' is_active: type: boolean description: 'Enable availability during the scheduled window.' example: true audience: type: string description: 'all or selected.' example: selected partner_ids: type: array description: 'Required and nonempty for selected; existing individual/master IDs, no team members or duplicates.' example: - 3 items: type: integer starts_at: type: string description: 'Optional start timestamp, inclusive; null for immediate availability.' example: null nullable: true ends_at: type: string description: 'Optional end timestamp, exclusive; must follow starts_at when both are supplied.' example: null nullable: true commission_type: type: string description: "flat, percent or first_month. Named program terms override the partner's personal deal for program links." example: percent flat_amount: type: number description: 'Required for flat, positive rupees up to 1000000.' example: 250.0 nullable: true percent: type: number description: 'Required for percent, positive percentage up to 100.' example: 25.0 nullable: true hold_days: type: integer description: 'Refund hold in days, from 0 to 180.' example: 15 required: - name - description - is_active - audience - commission_type - hold_days parameters: - in: path name: program_id description: 'The ID of the program.' example: 1 required: true schema: type: integer /api/v1/admin/partner-program: get: summary: 'Get partner program settings.' operationId: getPartnerProgramSettings description: "The default commission deal, the refund hold period, the minimum payout, the TDS\nrate applied to payouts and the share message, with program-wide totals." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: enabled: true commission_type: flat flat_amount_paise: 20000 percent_basis_points: 2000 hold_days: 30 minimum_payout_paise: 50000 tds_basis_points: 0 share_message: 'I use Dukanam for billing, stock and khata. Try it free: {link}' placeholders: - '{link}' - '{code}' - '{name}' totals: partners: 4 clicks: 1820 signups: 96 paid: 14 on_hold_paise: 120000 available_paise: 80000 processing_paise: 0 paid_out_net_paise: 160000 properties: data: type: object properties: enabled: type: boolean example: true commission_type: type: string example: flat flat_amount_paise: type: integer example: 20000 percent_basis_points: type: integer example: 2000 hold_days: type: integer example: 30 minimum_payout_paise: type: integer example: 50000 tds_basis_points: type: integer example: 0 share_message: type: string example: 'I use Dukanam for billing, stock and khata. Try it free: {link}' placeholders: type: array example: - '{link}' - '{code}' - '{name}' items: type: string totals: type: object properties: partners: type: integer example: 4 clicks: type: integer example: 1820 signups: type: integer example: 96 paid: type: integer example: 14 on_hold_paise: type: integer example: 120000 available_paise: type: integer example: 80000 processing_paise: type: integer example: 0 paid_out_net_paise: type: integer example: 160000 tags: - 'Partner program administration' put: summary: 'Update partner program settings.' operationId: updatePartnerProgramSettings description: "Amounts in rupees and rates in percent, as the admin console takes them. Applies to\ncommissions earned and payouts prepared from now on; each existing commission keeps\nthe rule it was earned under. Turning the program off stops new clicks and sign-ups\nbeing credited; accounts already credited still earn when they pay." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: enabled: true commission_type: percent flat_amount_paise: 20000 percent_basis_points: 2500 hold_days: 15 minimum_payout_paise: 50000 tds_basis_points: 200 share_message: 'Try Dukanam free: {link}' placeholders: - '{link}' - '{code}' - '{name}' totals: partners: 4 clicks: 1820 signups: 96 paid: 14 on_hold_paise: 120000 available_paise: 80000 processing_paise: 0 paid_out_net_paise: 160000 properties: data: type: object properties: enabled: type: boolean example: true commission_type: type: string example: percent flat_amount_paise: type: integer example: 20000 percent_basis_points: type: integer example: 2500 hold_days: type: integer example: 15 minimum_payout_paise: type: integer example: 50000 tds_basis_points: type: integer example: 200 share_message: type: string example: 'Try Dukanam free: {link}' placeholders: type: array example: - '{link}' - '{code}' - '{name}' items: type: string totals: type: object properties: partners: type: integer example: 4 clicks: type: integer example: 1820 signups: type: integer example: 96 paid: type: integer example: 14 on_hold_paise: type: integer example: 120000 available_paise: type: integer example: 80000 processing_paise: type: integer example: 0 paid_out_net_paise: type: integer example: 160000 422: description: '' content: application/json: schema: type: object example: message: 'The commission type field is required.' errors: commission_type: - 'The commission type field is required.' properties: message: type: string example: 'The commission type field is required.' errors: type: object properties: commission_type: type: array example: - 'The commission type field is required.' items: type: string tags: - 'Partner program administration' requestBody: required: true content: application/json: schema: type: object properties: enabled: type: boolean description: '' example: true commission_type: type: string description: "flat, percent or first_month (one month of the bought plan's current monthly price, however the shop pays)." example: flat flat_amount: type: number description: 'Rupees per paid account for flat deals.' example: 200.0 percent: type: number description: 'Percent of the first payment (before GST) for percent deals.' example: 20.0 hold_days: type: integer description: 'Days a commission is held before it can be paid, 0–180.' example: 30 minimum_payout: type: number description: 'Rupees a partner must have approved before a payout is prepared.' example: 500.0 tds_percent: type: number description: 'Tax deducted at source on each payout, 0–30.' example: 2.0 share_message: type: string description: 'Up to 1000 characters; {link}, {code} and {name} are filled in.' example: 'Try Dukanam free: {link}' required: - enabled - commission_type - flat_amount - percent - hold_days - minimum_payout - tds_percent - share_message /api/v1/admin/partners: get: summary: 'List partners.' operationId: listPartners description: "Every partner with their deal and a leaderboard row for the period: human clicks,\nsign-ups and paid accounts in the period, and lifetime money earned (not rejected),\nowed (on hold or approved, not yet paid) and paid out net of TDS." parameters: - in: query name: search description: 'Match the name, email or a link code.' example: ravi required: false schema: type: string description: 'Match the name, email or a link code.' example: ravi - in: query name: status description: 'active or suspended.' example: active required: false schema: type: string description: 'active or suspended.' example: active - in: query name: period description: '7d, 30d, 90d or all. Defaults to 30d.' example: 30d required: false schema: type: string description: '7d, 30d, 90d or all. Defaults to 30d.' example: 30d - in: query name: per_page description: 'Results per page, from 1 to 50.' example: 25 required: false schema: type: integer description: 'Results per page, from 1 to 50.' example: 25 responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 3 name: 'Ravi Kumar' email: ravi@example.com phone: '+919876543210' pan_on_file: true pan_masked: ••••••234F status: active signup_source: invited profile_completed_at: '2026-09-29T10:00:00.000000Z' commission: type: flat flat_amount_paise: 25000 percent_basis_points: null custom: true description: '₹250.00 per paid account' notes: 'YouTube, 80k subscribers' links_count: 3 has_payout_account: true last_login_at: '2026-09-28T10:00:00.000000Z' created_at: '2026-09-01T10:00:00.000000Z' stats: clicks: 640 signups: 31 paid: 6 earned_paise: 150000 owed_paise: 50000 paid_out_net_paise: 100000 account_type: individual master_partner_id: null team_role: null payment_responsibility: platform platform_payout_terms: hold_days: 30 minimum_payout_paise: 50000 tds_basis_points: 0 links: {} meta: current_page: 1 per_page: 25 total: 1 properties: data: type: array example: - id: 3 name: 'Ravi Kumar' email: ravi@example.com phone: '+919876543210' pan_on_file: true pan_masked: ••••••234F status: active signup_source: invited profile_completed_at: '2026-09-29T10:00:00.000000Z' commission: type: flat flat_amount_paise: 25000 percent_basis_points: null custom: true description: '₹250.00 per paid account' notes: 'YouTube, 80k subscribers' links_count: 3 has_payout_account: true last_login_at: '2026-09-28T10:00:00.000000Z' created_at: '2026-09-01T10:00:00.000000Z' stats: clicks: 640 signups: 31 paid: 6 earned_paise: 150000 owed_paise: 50000 paid_out_net_paise: 100000 account_type: individual master_partner_id: null team_role: null payment_responsibility: platform platform_payout_terms: hold_days: 30 minimum_payout_paise: 50000 tds_basis_points: 0 items: type: object properties: id: type: integer example: 3 name: type: string example: 'Ravi Kumar' email: type: string example: ravi@example.com phone: type: string example: '+919876543210' pan_on_file: type: boolean example: true pan_masked: type: string example: ••••••234F status: type: string example: active signup_source: type: string example: invited profile_completed_at: type: string example: '2026-09-29T10:00:00.000000Z' commission: type: object properties: type: type: string example: flat flat_amount_paise: type: integer example: 25000 percent_basis_points: type: string example: null nullable: true custom: type: boolean example: true description: type: string example: '₹250.00 per paid account' notes: type: string example: 'YouTube, 80k subscribers' links_count: type: integer example: 3 has_payout_account: type: boolean example: true last_login_at: type: string example: '2026-09-28T10:00:00.000000Z' created_at: type: string example: '2026-09-01T10:00:00.000000Z' stats: type: object properties: clicks: type: integer example: 640 signups: type: integer example: 31 paid: type: integer example: 6 earned_paise: type: integer example: 150000 owed_paise: type: integer example: 50000 paid_out_net_paise: type: integer example: 100000 account_type: type: string example: individual master_partner_id: type: string example: null nullable: true team_role: type: string example: null nullable: true payment_responsibility: type: string example: platform platform_payout_terms: type: object properties: hold_days: type: integer example: 30 minimum_payout_paise: type: integer example: 50000 tds_basis_points: type: integer example: 0 links: type: object properties: {} meta: type: object properties: current_page: type: integer example: 1 per_page: type: integer example: 25 total: type: integer example: 1 tags: - 'Partner program administration' requestBody: required: false content: application/json: schema: type: object properties: search: type: string description: 'Must not be greater than 100 characters.' example: b nullable: true status: type: string description: '' example: null nullable: true period: type: string description: '' example: null nullable: true per_page: type: integer description: 'Must be at least 1. Must not be greater than 50.' example: 22 nullable: true post: summary: 'Invite a partner.' operationId: inviteAPartner description: "Creates the partner and a first link. They can sign in at /partners/login straight\naway with a WhatsApp code to this number. Leave `commission_type` empty for the\nprogram default." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: id: 3 name: 'Ravi Kumar' email: ravi@example.com phone: '+919876543210' pan_on_file: false pan_masked: null status: active signup_source: invited profile_completed_at: '2026-09-29T10:00:00.000000Z' commission: type: flat flat_amount_paise: 25000 percent_basis_points: null custom: true description: '₹250.00 per paid account' notes: 'YouTube, 80k subscribers' last_login_at: null created_at: '2026-09-29T10:00:00.000000Z' links: - id: 7 code: RAVI7K label: 'Main link' program_id: null url: 'https://dukanam.com/p/RAVI7K' destination: register is_active: true created_at: '2026-09-29T10:00:00.000000Z' account_type: individual master_partner_id: null team_role: null payment_responsibility: platform platform_payout_terms: hold_days: 30 minimum_payout_paise: 50000 tds_basis_points: 0 properties: data: type: object properties: id: type: integer example: 3 name: type: string example: 'Ravi Kumar' email: type: string example: ravi@example.com phone: type: string example: '+919876543210' pan_on_file: type: boolean example: false pan_masked: type: string example: null nullable: true status: type: string example: active signup_source: type: string example: invited profile_completed_at: type: string example: '2026-09-29T10:00:00.000000Z' commission: type: object properties: type: type: string example: flat flat_amount_paise: type: integer example: 25000 percent_basis_points: type: string example: null nullable: true custom: type: boolean example: true description: type: string example: '₹250.00 per paid account' notes: type: string example: 'YouTube, 80k subscribers' last_login_at: type: string example: null nullable: true created_at: type: string example: '2026-09-29T10:00:00.000000Z' links: type: array example: - id: 7 code: RAVI7K label: 'Main link' program_id: null url: 'https://dukanam.com/p/RAVI7K' destination: register is_active: true created_at: '2026-09-29T10:00:00.000000Z' items: type: object properties: id: type: integer example: 7 code: type: string example: RAVI7K label: type: string example: 'Main link' program_id: type: string example: null nullable: true url: type: string example: 'https://dukanam.com/p/RAVI7K' destination: type: string example: register is_active: type: boolean example: true created_at: type: string example: '2026-09-29T10:00:00.000000Z' account_type: type: string example: individual master_partner_id: type: string example: null nullable: true team_role: type: string example: null nullable: true payment_responsibility: type: string example: platform platform_payout_terms: type: object properties: hold_days: type: integer example: 30 minimum_payout_paise: type: integer example: 50000 tds_basis_points: type: integer example: 0 422: description: '' content: application/json: schema: type: object example: message: 'Another partner already uses this number.' errors: phone: - 'Another partner already uses this number.' properties: message: type: string example: 'Another partner already uses this number.' errors: type: object properties: phone: type: array example: - 'Another partner already uses this number.' items: type: string tags: - 'Partner program administration' requestBody: required: true content: application/json: schema: type: object properties: account_type: type: string description: 'individual or master. Masters need a separate commission deal.' example: master master_partner_id: type: string description: '' example: null commission_hold_days: type: integer description: 'Nullable master hold override, 0–180.' example: 7 nullable: true payout_minimum_amount: type: number description: 'Nullable master minimum payout in rupees, 0–1000000.' example: 1000.0 nullable: true payout_tds_percent: type: number description: 'Nullable master TDS override, 0–30.' example: 2.0 nullable: true name: type: string description: '' example: 'Ravi Kumar' phone: type: string description: 'Indian mobile number they sign in with.' example: '9876543210' email: type: string description: '' example: ravi@example.com nullable: true status: type: string description: '' example: active enum: - active - suspended commission_type: type: string description: 'Their own deal: flat, percent or first_month. Omit or null for the program default.' example: flat nullable: true commission_flat_amount: type: number description: 'Rupees per paid account, required for flat.' example: 250.0 nullable: true commission_percent: type: number description: 'Percent of the first payment, required for percent.' example: 25.0 nullable: true notes: type: string description: 'Internal notes.' example: 'YouTube, 80k subscribers' nullable: true required: - name - phone '/api/v1/admin/partners/{id}': get: summary: "Get a partner's performance." operationId: getAPartnersPerformance description: "The partner, their funnel for the period, lifetime earnings, every link with its own\nfunnel, click sources and a zero-filled 30-day daily series. `available_programs`\nuses the same active, scheduled and audience filtering as the partner portal;\nid null denotes the default program with the partner’s personal deal applied.\nSuspended partners and global program pause return an empty available_programs list.\nActive masters include team_overview with team funnel, per-member performance,\nplatform earnings, team obligations and retained amount. Member available_programs\ncontains only their master's eligible programs; earnings use the team reward ledger." parameters: - in: query name: period description: '7d, 30d, 90d or all. Defaults to 30d.' example: 30d required: false schema: type: string description: '7d, 30d, 90d or all. Defaults to 30d.' example: 30d responses: 200: description: '' content: application/json: schema: type: object example: data: partner: id: 3 name: 'Ravi Kumar' email: ravi@example.com phone: '+919876543210' pan_on_file: true pan_masked: ••••••234F status: active signup_source: invited profile_completed_at: '2026-09-29T10:00:00.000000Z' commission: type: flat flat_amount_paise: 25000 percent_basis_points: null custom: true description: '₹250.00 per paid account' notes: null has_payout_account: true last_login_at: null created_at: '2026-09-01T10:00:00.000000Z' account_type: individual master_partner_id: null team_role: null payment_responsibility: platform platform_payout_terms: hold_days: 30 minimum_payout_paise: 50000 tds_basis_points: 0 funnel: clicks: 640 unique_visitors: 512 signups: 31 checkouts_started: 9 paid: 6 click_to_signup_rate: 4.8 signup_to_paid_rate: 19.4 earnings: on_hold_paise: 50000 available_paise: 0 processing_paise: 0 paid_paise: 100000 lifetime_paise: 150000 paid_out_net_paise: 100000 tds_deducted_paise: 0 links: - id: 7 code: RAVI7K label: 'Main link' program_id: null url: 'https://dukanam.com/p/RAVI7K' destination: register is_active: true created_at: '2026-09-01T10:00:00.000000Z' stats: clicks: 640 unique_visitors: 512 signups: 31 checkouts_started: 9 paid: 6 click_to_signup_rate: 4.8 signup_to_paid_rate: 19.4 sources: youtube: 420 whatsapp: 150 direct: 70 daily: - date: '2026-09-29' clicks: 24 signups: 1 paid: 0 available_programs: - id: null name: 'Shop referral program' description: 'Help shops get started with Dukanam and earn on their first paid plan payment.' is_default: true commission: type: flat flat_amount_paise: 25000 percent_basis_points: null custom: true description: '₹250.00 per paid account' hold_days: 30 minimum_payout_paise: 50000 tds_basis_points: 0 starts_at: null ends_at: null team_overview: null properties: data: type: object properties: partner: type: object properties: id: type: integer example: 3 name: type: string example: 'Ravi Kumar' email: type: string example: ravi@example.com phone: type: string example: '+919876543210' pan_on_file: type: boolean example: true pan_masked: type: string example: ••••••234F status: type: string example: active signup_source: type: string example: invited profile_completed_at: type: string example: '2026-09-29T10:00:00.000000Z' commission: type: object properties: type: type: string example: flat flat_amount_paise: type: integer example: 25000 percent_basis_points: type: string example: null nullable: true custom: type: boolean example: true description: type: string example: '₹250.00 per paid account' notes: type: string example: null nullable: true has_payout_account: type: boolean example: true last_login_at: type: string example: null nullable: true created_at: type: string example: '2026-09-01T10:00:00.000000Z' account_type: type: string example: individual master_partner_id: type: string example: null nullable: true team_role: type: string example: null nullable: true payment_responsibility: type: string example: platform platform_payout_terms: type: object properties: hold_days: type: integer example: 30 minimum_payout_paise: type: integer example: 50000 tds_basis_points: type: integer example: 0 funnel: type: object properties: clicks: type: integer example: 640 unique_visitors: type: integer example: 512 signups: type: integer example: 31 checkouts_started: type: integer example: 9 paid: type: integer example: 6 click_to_signup_rate: type: number example: 4.8 signup_to_paid_rate: type: number example: 19.4 earnings: type: object properties: on_hold_paise: type: integer example: 50000 available_paise: type: integer example: 0 processing_paise: type: integer example: 0 paid_paise: type: integer example: 100000 lifetime_paise: type: integer example: 150000 paid_out_net_paise: type: integer example: 100000 tds_deducted_paise: type: integer example: 0 links: type: array example: - id: 7 code: RAVI7K label: 'Main link' program_id: null url: 'https://dukanam.com/p/RAVI7K' destination: register is_active: true created_at: '2026-09-01T10:00:00.000000Z' stats: clicks: 640 unique_visitors: 512 signups: 31 checkouts_started: 9 paid: 6 click_to_signup_rate: 4.8 signup_to_paid_rate: 19.4 items: type: object properties: id: type: integer example: 7 code: type: string example: RAVI7K label: type: string example: 'Main link' program_id: type: string example: null nullable: true url: type: string example: 'https://dukanam.com/p/RAVI7K' destination: type: string example: register is_active: type: boolean example: true created_at: type: string example: '2026-09-01T10:00:00.000000Z' stats: type: object properties: clicks: type: integer example: 640 unique_visitors: type: integer example: 512 signups: type: integer example: 31 checkouts_started: type: integer example: 9 paid: type: integer example: 6 click_to_signup_rate: type: number example: 4.8 signup_to_paid_rate: type: number example: 19.4 sources: type: object properties: youtube: type: integer example: 420 whatsapp: type: integer example: 150 direct: type: integer example: 70 daily: type: array example: - date: '2026-09-29' clicks: 24 signups: 1 paid: 0 items: type: object properties: date: type: string example: '2026-09-29' clicks: type: integer example: 24 signups: type: integer example: 1 paid: type: integer example: 0 available_programs: type: array example: - id: null name: 'Shop referral program' description: 'Help shops get started with Dukanam and earn on their first paid plan payment.' is_default: true commission: type: flat flat_amount_paise: 25000 percent_basis_points: null custom: true description: '₹250.00 per paid account' hold_days: 30 minimum_payout_paise: 50000 tds_basis_points: 0 starts_at: null ends_at: null items: type: object properties: id: type: string example: null nullable: true name: type: string example: 'Shop referral program' description: type: string example: 'Help shops get started with Dukanam and earn on their first paid plan payment.' is_default: type: boolean example: true commission: type: object properties: type: type: string example: flat flat_amount_paise: type: integer example: 25000 percent_basis_points: type: string example: null nullable: true custom: type: boolean example: true description: type: string example: '₹250.00 per paid account' hold_days: type: integer example: 30 minimum_payout_paise: type: integer example: 50000 tds_basis_points: type: integer example: 0 starts_at: type: string example: null nullable: true ends_at: type: string example: null nullable: true team_overview: type: string example: null nullable: true tags: - 'Partner program administration' requestBody: required: false content: application/json: schema: type: object properties: period: type: string description: '' example: null nullable: true patch: summary: 'Update a partner.' operationId: updateAPartner description: "Send only what changes. Suspending a partner signs them out, stops their links\ncrediting new sign-ups and stops new commissions. Send `commission_type` as null to\nmove individuals back to the program default. Members cannot change hierarchy or\nplatform deals. Master fixed deals must remain above active fixed team rewards.\nNull payout overrides restore global defaults. Masters with members cannot be downgraded." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 3 name: 'Ravi Kumar' email: ravi@example.com phone: '+919876543210' pan_on_file: false pan_masked: null status: suspended commission: type: percent flat_amount_paise: null percent_basis_points: 2500 custom: true description: '25% of the first payment' notes: 'Paused while the campaign is reviewed' last_login_at: null created_at: '2026-09-01T10:00:00.000000Z' account_type: individual master_partner_id: null team_role: null payment_responsibility: platform platform_payout_terms: hold_days: 30 minimum_payout_paise: 50000 tds_basis_points: 0 properties: data: type: object properties: id: type: integer example: 3 name: type: string example: 'Ravi Kumar' email: type: string example: ravi@example.com phone: type: string example: '+919876543210' pan_on_file: type: boolean example: false pan_masked: type: string example: null nullable: true status: type: string example: suspended commission: type: object properties: type: type: string example: percent flat_amount_paise: type: string example: null nullable: true percent_basis_points: type: integer example: 2500 custom: type: boolean example: true description: type: string example: '25% of the first payment' notes: type: string example: 'Paused while the campaign is reviewed' last_login_at: type: string example: null nullable: true created_at: type: string example: '2026-09-01T10:00:00.000000Z' account_type: type: string example: individual master_partner_id: type: string example: null nullable: true team_role: type: string example: null nullable: true payment_responsibility: type: string example: platform platform_payout_terms: type: object properties: hold_days: type: integer example: 30 minimum_payout_paise: type: integer example: 50000 tds_basis_points: type: integer example: 0 tags: - 'Partner program administration' requestBody: required: false content: application/json: schema: type: object properties: account_type: type: string description: 'individual or master; no downgrading masters with members or changing member roles.' example: master master_partner_id: type: string description: '' example: null commission_hold_days: type: integer description: 'Nullable master hold override, 0–180.' example: 7 nullable: true payout_minimum_amount: type: number description: 'Nullable master minimum payout in rupees.' example: 1000.0 nullable: true payout_tds_percent: type: number description: 'Nullable master TDS override, 0–30.' example: 2.0 nullable: true name: type: string description: '' example: 'Ravi Kumar' phone: type: string description: '' example: '9876543210' email: type: string description: '' example: ravi@example.com nullable: true status: type: string description: 'active or suspended.' example: suspended commission_type: type: string description: 'flat, percent, first_month or null.' example: percent nullable: true commission_flat_amount: type: number description: '' example: 250.0 nullable: true commission_percent: type: number description: '' example: 25.0 nullable: true notes: type: string description: '' example: 'Paused while the campaign is reviewed' nullable: true parameters: - in: path name: id description: 'The ID of the partner.' example: 1 required: true schema: type: integer '/api/v1/admin/partners/{partner_id}/links': post: summary: 'Add a link for a partner.' operationId: addALinkForAPartner description: "Codes are letters and numbers, 4–16 characters, unique across partner links and\ncustomer referral codes. Omit `code` to generate one from the partner's name." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: id: 8 code: RAVIINSTA label: 'Instagram bio' program_id: null url: 'https://dukanam.com/p/RAVIINSTA' destination: pricing is_active: true created_at: '2026-09-29T10:00:00.000000Z' properties: data: type: object properties: id: type: integer example: 8 code: type: string example: RAVIINSTA label: type: string example: 'Instagram bio' program_id: type: string example: null nullable: true url: type: string example: 'https://dukanam.com/p/RAVIINSTA' destination: type: string example: pricing is_active: type: boolean example: true created_at: type: string example: '2026-09-29T10:00:00.000000Z' 422: description: '' content: application/json: schema: type: object example: message: 'This code is already taken. Try another.' errors: code: - 'This code is already taken. Try another.' properties: message: type: string example: 'This code is already taken. Try another.' errors: type: object properties: code: type: array example: - 'This code is already taken. Try another.' items: type: string tags: - 'Partner program administration' requestBody: required: true content: application/json: schema: type: object properties: program_id: type: integer description: 'Optional eligible named program ID; omit or null for the default program. Unavailable programs return 422.' example: null nullable: true label: type: string description: '' example: 'Instagram bio' code: type: string description: '' example: RAVIINSTA nullable: true destination: type: string description: 'register, home or pricing. Defaults to register.' example: pricing nullable: true required: - label parameters: - in: path name: partner_id description: 'The ID of the partner.' example: 1 required: true schema: type: integer '/api/v1/admin/partners/{partner_id}/links/{id}': patch: summary: 'Pause or resume a partner link.' operationId: pauseOrResumeAPartnerLink description: "A paused link still sends visitors to sign-up but no longer counts clicks or credits\nsign-ups to the partner." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 8 code: RAVIINSTA label: 'Instagram bio' program_id: null url: 'https://dukanam.com/p/RAVIINSTA' destination: pricing is_active: false created_at: '2026-09-29T10:00:00.000000Z' properties: data: type: object properties: id: type: integer example: 8 code: type: string example: RAVIINSTA label: type: string example: 'Instagram bio' program_id: type: string example: null nullable: true url: type: string example: 'https://dukanam.com/p/RAVIINSTA' destination: type: string example: pricing is_active: type: boolean example: false created_at: type: string example: '2026-09-29T10:00:00.000000Z' tags: - 'Partner program administration' requestBody: required: true content: application/json: schema: type: object properties: is_active: type: boolean description: '' example: false required: - is_active parameters: - in: path name: partner_id description: 'The ID of the partner.' example: 1 required: true schema: type: integer - in: path name: id description: 'The ID of the link.' example: 1 required: true schema: type: integer '/api/v1/admin/partners/{partner_id}/referrals': get: summary: "List a partner's sign-ups." operationId: listAPartnersSignUps description: "Every account credited to the partner, with the account and workspace. Partners\nthemselves never see these names." parameters: - in: query name: status description: 'registered, subscription_pending or paid.' example: paid required: false schema: type: string description: 'registered, subscription_pending or paid.' example: paid - in: query name: per_page description: 'Results per page, from 1 to 50.' example: 25 required: false schema: type: integer description: 'Results per page, from 1 to 50.' example: 25 responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 41 code: RAVI7K link: id: 7 label: 'Main link' source: web status: paid status_label: Paid user: id: 88 name: 'Meena Shah' email: meena@example.com business: id: 90 name: 'Meena Stores' payment_source: razorpay payment_amount_paise: 84661 commission_blocked_reason: null registered_at: '2026-09-10T08:00:00.000000Z' subscription_pending_at: '2026-09-12T08:00:00.000000Z' paid_at: '2026-09-12T08:05:00.000000Z' links: {} meta: current_page: 1 per_page: 25 total: 1 properties: data: type: array example: - id: 41 code: RAVI7K link: id: 7 label: 'Main link' source: web status: paid status_label: Paid user: id: 88 name: 'Meena Shah' email: meena@example.com business: id: 90 name: 'Meena Stores' payment_source: razorpay payment_amount_paise: 84661 commission_blocked_reason: null registered_at: '2026-09-10T08:00:00.000000Z' subscription_pending_at: '2026-09-12T08:00:00.000000Z' paid_at: '2026-09-12T08:05:00.000000Z' items: type: object properties: id: type: integer example: 41 code: type: string example: RAVI7K link: type: object properties: id: type: integer example: 7 label: type: string example: 'Main link' source: type: string example: web status: type: string example: paid status_label: type: string example: Paid user: type: object properties: id: type: integer example: 88 name: type: string example: 'Meena Shah' email: type: string example: meena@example.com business: type: object properties: id: type: integer example: 90 name: type: string example: 'Meena Stores' payment_source: type: string example: razorpay payment_amount_paise: type: integer example: 84661 commission_blocked_reason: type: string example: null nullable: true registered_at: type: string example: '2026-09-10T08:00:00.000000Z' subscription_pending_at: type: string example: '2026-09-12T08:00:00.000000Z' paid_at: type: string example: '2026-09-12T08:05:00.000000Z' links: type: object properties: {} meta: type: object properties: current_page: type: integer example: 1 per_page: type: integer example: 25 total: type: integer example: 1 tags: - 'Partner program administration' requestBody: required: false content: application/json: schema: type: object properties: status: type: string description: '' example: null nullable: true per_page: type: integer description: 'Must be at least 1. Must not be greater than 50.' example: 1 nullable: true parameters: - in: path name: partner_id description: 'The ID of the partner.' example: 1 required: true schema: type: integer '/api/v1/admin/partners/{partner_id}/adjustments': post: summary: 'Add a bonus or adjustment.' operationId: addABonusOrAdjustment description: "A campaign fee or bonus (positive) or a claw-back (negative), in rupees. Approved at\nonce and included in the partner's next payout." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: id: 55 partner_referral_id: null kind: adjustment amount_paise: 150000 basis: null description: 'Diwali campaign fee' status: approved status_label: Approved earned_at: '2026-09-29T10:00:00.000000Z' hold_until: null approved_at: '2026-09-29T10:00:00.000000Z' rejected_at: null rejection_reason: null partner_payout_id: null paid_at: null properties: data: type: object properties: id: type: integer example: 55 partner_referral_id: type: string example: null nullable: true kind: type: string example: adjustment amount_paise: type: integer example: 150000 basis: type: string example: null nullable: true description: type: string example: 'Diwali campaign fee' status: type: string example: approved status_label: type: string example: Approved earned_at: type: string example: '2026-09-29T10:00:00.000000Z' hold_until: type: string example: null nullable: true approved_at: type: string example: '2026-09-29T10:00:00.000000Z' rejected_at: type: string example: null nullable: true rejection_reason: type: string example: null nullable: true partner_payout_id: type: string example: null nullable: true paid_at: type: string example: null nullable: true tags: - 'Partner program administration' requestBody: required: true content: application/json: schema: type: object properties: amount: type: number description: 'Rupees; negative to claw back.' example: 1500.0 description: type: string description: 'Shown to the partner.' example: 'Diwali campaign fee' required: - amount - description parameters: - in: path name: partner_id description: 'The ID of the partner.' example: 1 required: true schema: type: integer /api/v1/admin/partner-commissions: get: summary: 'List partner commissions.' operationId: listPartnerCommissions description: "A conversion commission is earned when a credited account's first paid charge is\ncaptured; it is `pending` (on hold) until `hold_until`, then `approved`. An approved\ncommission with a `partner_payout_id` is in a payout being sent. `basis` records the\nrule it was earned under. For named-program links it also includes `program_id`,\n`program_name` and the reward/hold terms saved at sign-up." parameters: - in: query name: partner_id description: 'Only this partner.' example: 3 required: false schema: type: integer description: 'Only this partner.' example: 3 - in: query name: status description: 'pending, approved, paid or rejected.' example: pending required: false schema: type: string description: 'pending, approved, paid or rejected.' example: pending - in: query name: per_page description: 'Results per page, from 1 to 50.' example: 25 required: false schema: type: integer description: 'Results per page, from 1 to 50.' example: 25 responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 54 partner: id: 3 name: 'Ravi Kumar' partner_referral_id: 41 kind: conversion amount_paise: 25000 basis: type: flat flat_paise: 25000 percent_basis_points: null custom: true base_paise: 84661 hold_days: 30 program_id: 1 program_name: 'Creator campaign' description: 'Paid account via RAVI7K' status: pending status_label: 'On hold' earned_at: '2026-09-12T08:05:00.000000Z' hold_until: '2026-10-12T08:05:00.000000Z' approved_at: null rejected_at: null rejection_reason: null partner_payout_id: null paid_at: null links: {} meta: current_page: 1 per_page: 25 total: 1 properties: data: type: array example: - id: 54 partner: id: 3 name: 'Ravi Kumar' partner_referral_id: 41 kind: conversion amount_paise: 25000 basis: type: flat flat_paise: 25000 percent_basis_points: null custom: true base_paise: 84661 hold_days: 30 program_id: 1 program_name: 'Creator campaign' description: 'Paid account via RAVI7K' status: pending status_label: 'On hold' earned_at: '2026-09-12T08:05:00.000000Z' hold_until: '2026-10-12T08:05:00.000000Z' approved_at: null rejected_at: null rejection_reason: null partner_payout_id: null paid_at: null items: type: object properties: id: type: integer example: 54 partner: type: object properties: id: type: integer example: 3 name: type: string example: 'Ravi Kumar' partner_referral_id: type: integer example: 41 kind: type: string example: conversion amount_paise: type: integer example: 25000 basis: type: object properties: type: type: string example: flat flat_paise: type: integer example: 25000 percent_basis_points: type: string example: null nullable: true custom: type: boolean example: true base_paise: type: integer example: 84661 hold_days: type: integer example: 30 program_id: type: integer example: 1 program_name: type: string example: 'Creator campaign' description: type: string example: 'Paid account via RAVI7K' status: type: string example: pending status_label: type: string example: 'On hold' earned_at: type: string example: '2026-09-12T08:05:00.000000Z' hold_until: type: string example: '2026-10-12T08:05:00.000000Z' approved_at: type: string example: null nullable: true rejected_at: type: string example: null nullable: true rejection_reason: type: string example: null nullable: true partner_payout_id: type: string example: null nullable: true paid_at: type: string example: null nullable: true links: type: object properties: {} meta: type: object properties: current_page: type: integer example: 1 per_page: type: integer example: 25 total: type: integer example: 1 tags: - 'Partner program administration' requestBody: required: false content: application/json: schema: type: object properties: partner_id: type: integer description: '' example: 16 nullable: true status: type: string description: '' example: null nullable: true per_page: type: integer description: 'Must be at least 1. Must not be greater than 50.' example: 22 nullable: true '/api/v1/admin/partner-commissions/{commission_id}/approve': post: summary: 'Approve a commission now.' operationId: approveACommissionNow description: 'Skips the rest of the hold period, so the commission goes into the next payout.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 54 partner_referral_id: 41 kind: conversion amount_paise: 25000 basis: type: flat flat_paise: 25000 percent_basis_points: null custom: true base_paise: 84661 hold_days: 30 description: 'Paid account via RAVI7K' status: approved status_label: Approved earned_at: '2026-09-12T08:05:00.000000Z' hold_until: '2026-10-12T08:05:00.000000Z' approved_at: '2026-09-29T10:00:00.000000Z' rejected_at: null rejection_reason: null partner_payout_id: null paid_at: null properties: data: type: object properties: id: type: integer example: 54 partner_referral_id: type: integer example: 41 kind: type: string example: conversion amount_paise: type: integer example: 25000 basis: type: object properties: type: type: string example: flat flat_paise: type: integer example: 25000 percent_basis_points: type: string example: null nullable: true custom: type: boolean example: true base_paise: type: integer example: 84661 hold_days: type: integer example: 30 description: type: string example: 'Paid account via RAVI7K' status: type: string example: approved status_label: type: string example: Approved earned_at: type: string example: '2026-09-12T08:05:00.000000Z' hold_until: type: string example: '2026-10-12T08:05:00.000000Z' approved_at: type: string example: '2026-09-29T10:00:00.000000Z' rejected_at: type: string example: null nullable: true rejection_reason: type: string example: null nullable: true partner_payout_id: type: string example: null nullable: true paid_at: type: string example: null nullable: true 422: description: '' content: application/json: schema: type: object example: message: 'Only a commission on hold can be approved.' errors: commission: - 'Only a commission on hold can be approved.' properties: message: type: string example: 'Only a commission on hold can be approved.' errors: type: object properties: commission: type: array example: - 'Only a commission on hold can be approved.' items: type: string tags: - 'Partner program administration' parameters: - in: path name: commission_id description: 'The ID of the commission.' example: 1 required: true schema: type: integer '/api/v1/admin/partner-commissions/{commission_id}/reject': post: summary: 'Reject a commission.' operationId: rejectACommission description: "For a refund, a fake sign-up or a policy breach. Only an unpaid commission that is not\nin a payout can be rejected; cancel the payout first. The partner sees the reason.\nIts unpaid team reward is also rejected; a reserved team settlement is cancelled,\nreleasing other rewards. Already recorded external team payments remain paid." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 54 partner_referral_id: 41 kind: conversion amount_paise: 25000 basis: null description: 'Paid account via RAVI7K' status: rejected status_label: Rejected earned_at: '2026-09-12T08:05:00.000000Z' hold_until: '2026-10-12T08:05:00.000000Z' approved_at: null rejected_at: '2026-09-29T10:00:00.000000Z' rejection_reason: 'Subscription refunded' partner_payout_id: null paid_at: null properties: data: type: object properties: id: type: integer example: 54 partner_referral_id: type: integer example: 41 kind: type: string example: conversion amount_paise: type: integer example: 25000 basis: type: string example: null nullable: true description: type: string example: 'Paid account via RAVI7K' status: type: string example: rejected status_label: type: string example: Rejected earned_at: type: string example: '2026-09-12T08:05:00.000000Z' hold_until: type: string example: '2026-10-12T08:05:00.000000Z' approved_at: type: string example: null nullable: true rejected_at: type: string example: '2026-09-29T10:00:00.000000Z' rejection_reason: type: string example: 'Subscription refunded' partner_payout_id: type: string example: null nullable: true paid_at: type: string example: null nullable: true tags: - 'Partner program administration' requestBody: required: true content: application/json: schema: type: object properties: reason: type: string description: '' example: 'Subscription refunded' required: - reason parameters: - in: path name: commission_id description: 'The ID of the commission.' example: 1 required: true schema: type: integer /api/v1/admin/partner-payouts: get: summary: 'List payouts.' operationId: listPayouts description: 'Payouts are listed without full account numbers; download the payment sheet for those.' parameters: - in: query name: partner_id description: 'Only this partner.' example: 3 required: false schema: type: integer description: 'Only this partner.' example: 3 - in: query name: status description: 'processing, paid or cancelled.' example: processing required: false schema: type: string description: 'processing, paid or cancelled.' example: processing - in: query name: per_page description: 'Results per page, from 1 to 50.' example: 25 required: false schema: type: integer description: 'Results per page, from 1 to 50.' example: 25 responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 12 partner: id: 3 name: 'Ravi Kumar' status: processing status_label: Processing gross_paise: 100000 tds_basis_points: 200 tds_paise: 2000 net_paise: 98000 commissions_count: 4 account: method: bank label: 'HDFC Bank ••6789' account_holder_name: 'Ravi Kumar' ifsc: HDFC0001234 reference: null notes: null created_at: '2026-09-29T10:00:00.000000Z' paid_at: null cancelled_at: null links: {} meta: current_page: 1 per_page: 25 total: 1 properties: data: type: array example: - id: 12 partner: id: 3 name: 'Ravi Kumar' status: processing status_label: Processing gross_paise: 100000 tds_basis_points: 200 tds_paise: 2000 net_paise: 98000 commissions_count: 4 account: method: bank label: 'HDFC Bank ••6789' account_holder_name: 'Ravi Kumar' ifsc: HDFC0001234 reference: null notes: null created_at: '2026-09-29T10:00:00.000000Z' paid_at: null cancelled_at: null items: type: object properties: id: type: integer example: 12 partner: type: object properties: id: type: integer example: 3 name: type: string example: 'Ravi Kumar' status: type: string example: processing status_label: type: string example: Processing gross_paise: type: integer example: 100000 tds_basis_points: type: integer example: 200 tds_paise: type: integer example: 2000 net_paise: type: integer example: 98000 commissions_count: type: integer example: 4 account: type: object properties: method: type: string example: bank label: type: string example: 'HDFC Bank ••6789' account_holder_name: type: string example: 'Ravi Kumar' ifsc: type: string example: HDFC0001234 reference: type: string example: null nullable: true notes: type: string example: null nullable: true created_at: type: string example: '2026-09-29T10:00:00.000000Z' paid_at: type: string example: null nullable: true cancelled_at: type: string example: null nullable: true links: type: object properties: {} meta: type: object properties: current_page: type: integer example: 1 per_page: type: integer example: 25 total: type: integer example: 1 tags: - 'Partner program administration' requestBody: required: false content: application/json: schema: type: object properties: partner_id: type: integer description: '' example: 16 nullable: true status: type: string description: '' example: null nullable: true per_page: type: integer description: 'Must be at least 1. Must not be greater than 50.' example: 22 nullable: true /api/v1/admin/partner-payouts/prepare: post: summary: 'Prepare payouts.' operationId: preparePayouts description: "Releases commissions whose hold has ended, then groups every approved commission not\nyet in a payout into one `processing` payout per partner, sent to their primary payout\naccount with their effective TDS rate deducted. Master payout overrides apply.\nTeam members are paid separately by their masters and never receive platform payouts.\nPartners under their effective minimum payout, or\nwithout a payout account, are listed and skipped." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: created: - id: 12 partner: id: 3 name: 'Ravi Kumar' status: processing status_label: Processing gross_paise: 100000 tds_basis_points: 200 tds_paise: 2000 net_paise: 98000 commissions_count: 4 account: method: bank label: 'HDFC Bank ••6789' account_holder_name: 'Ravi Kumar' ifsc: HDFC0001234 reference: null notes: null created_at: '2026-09-29T10:00:00.000000Z' paid_at: null cancelled_at: null missing_account: - id: 5 name: 'Anita Rao' below_minimum: [] properties: data: type: object properties: created: type: array example: - id: 12 partner: id: 3 name: 'Ravi Kumar' status: processing status_label: Processing gross_paise: 100000 tds_basis_points: 200 tds_paise: 2000 net_paise: 98000 commissions_count: 4 account: method: bank label: 'HDFC Bank ••6789' account_holder_name: 'Ravi Kumar' ifsc: HDFC0001234 reference: null notes: null created_at: '2026-09-29T10:00:00.000000Z' paid_at: null cancelled_at: null items: type: object properties: id: type: integer example: 12 partner: type: object properties: id: type: integer example: 3 name: type: string example: 'Ravi Kumar' status: type: string example: processing status_label: type: string example: Processing gross_paise: type: integer example: 100000 tds_basis_points: type: integer example: 200 tds_paise: type: integer example: 2000 net_paise: type: integer example: 98000 commissions_count: type: integer example: 4 account: type: object properties: method: type: string example: bank label: type: string example: 'HDFC Bank ••6789' account_holder_name: type: string example: 'Ravi Kumar' ifsc: type: string example: HDFC0001234 reference: type: string example: null nullable: true notes: type: string example: null nullable: true created_at: type: string example: '2026-09-29T10:00:00.000000Z' paid_at: type: string example: null nullable: true cancelled_at: type: string example: null nullable: true missing_account: type: array example: - id: 5 name: 'Anita Rao' items: type: object properties: id: type: integer example: 5 name: type: string example: 'Anita Rao' below_minimum: type: array example: [] tags: - 'Partner program administration' requestBody: required: false content: application/json: schema: type: object properties: partner_ids: type: array description: 'Only these partners. Omit for everyone.' example: - 3 items: type: integer /api/v1/admin/partner-payouts/export: get: summary: 'Download the payment sheet.' operationId: downloadThePaymentSheet description: "A CSV with one row per payout and the full account details to transfer to: partner\nname, phone, email, PAN, method, account holder, account number, IFSC, bank, UPI ID,\ngross, TDS and net amounts in rupees, and the reference and date once paid." parameters: - in: query name: status description: 'processing (default), paid or cancelled.' example: processing required: false schema: type: string description: 'processing (default), paid or cancelled.' example: processing responses: 200: description: 'CSV file' content: text/plain: schema: type: string example: 'payout_id,partner_id,partner_name,phone,email,pan,method,account_holder_name,account_number,ifsc,bank_name,upi_id,gross_amount,tds_amount,net_amount,commissions,status,prepared_at,reference,paid_at' tags: - 'Partner program administration' requestBody: required: false content: application/json: schema: type: object properties: status: type: string description: '' example: null nullable: true '/api/v1/admin/partner-payouts/{payout_id}/mark-paid': post: summary: 'Mark a payout paid.' operationId: markAPayoutPaid description: "Record the bank reference (UTR) and date. The payout and its commissions become\n`paid`, and the partner sees it in their portal." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 12 status: paid status_label: Paid gross_paise: 100000 tds_basis_points: 200 tds_paise: 2000 net_paise: 98000 commissions_count: 4 account: method: bank label: 'HDFC Bank ••6789' account_holder_name: 'Ravi Kumar' ifsc: HDFC0001234 reference: UTR123456789 notes: 'Paid by NEFT' created_at: '2026-09-29T10:00:00.000000Z' paid_at: '2026-09-29T12:00:00.000000Z' cancelled_at: null properties: data: type: object properties: id: type: integer example: 12 status: type: string example: paid status_label: type: string example: Paid gross_paise: type: integer example: 100000 tds_basis_points: type: integer example: 200 tds_paise: type: integer example: 2000 net_paise: type: integer example: 98000 commissions_count: type: integer example: 4 account: type: object properties: method: type: string example: bank label: type: string example: 'HDFC Bank ••6789' account_holder_name: type: string example: 'Ravi Kumar' ifsc: type: string example: HDFC0001234 reference: type: string example: UTR123456789 notes: type: string example: 'Paid by NEFT' created_at: type: string example: '2026-09-29T10:00:00.000000Z' paid_at: type: string example: '2026-09-29T12:00:00.000000Z' cancelled_at: type: string example: null nullable: true 422: description: '' content: application/json: schema: type: object example: message: 'Only a payout that is processing can be marked paid.' errors: payout: - 'Only a payout that is processing can be marked paid.' properties: message: type: string example: 'Only a payout that is processing can be marked paid.' errors: type: object properties: payout: type: array example: - 'Only a payout that is processing can be marked paid.' items: type: string tags: - 'Partner program administration' requestBody: required: false content: application/json: schema: type: object properties: reference: type: string description: 'The bank or UPI reference.' example: UTR123456789 nullable: true paid_on: type: date description: 'Defaults to today; not in the future.' example: '2026-09-29' nullable: true notes: type: string description: '' example: 'Paid by NEFT' nullable: true parameters: - in: path name: payout_id description: 'The ID of the payout.' example: 1 required: true schema: type: integer '/api/v1/admin/partner-payouts/{payout_id}/cancel': post: summary: 'Cancel a payout.' operationId: cancelAPayout description: "For a transfer that bounced or will not be made. Its commissions return to approved and\ngo into the next payout." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 12 status: cancelled status_label: Cancelled gross_paise: 100000 tds_basis_points: 200 tds_paise: 2000 net_paise: 98000 commissions_count: 4 account: method: bank label: 'HDFC Bank ••6789' account_holder_name: 'Ravi Kumar' ifsc: HDFC0001234 reference: null notes: 'Account closed; partner is adding a new one' created_at: '2026-09-29T10:00:00.000000Z' paid_at: null cancelled_at: '2026-09-29T12:00:00.000000Z' properties: data: type: object properties: id: type: integer example: 12 status: type: string example: cancelled status_label: type: string example: Cancelled gross_paise: type: integer example: 100000 tds_basis_points: type: integer example: 200 tds_paise: type: integer example: 2000 net_paise: type: integer example: 98000 commissions_count: type: integer example: 4 account: type: object properties: method: type: string example: bank label: type: string example: 'HDFC Bank ••6789' account_holder_name: type: string example: 'Ravi Kumar' ifsc: type: string example: HDFC0001234 reference: type: string example: null nullable: true notes: type: string example: 'Account closed; partner is adding a new one' created_at: type: string example: '2026-09-29T10:00:00.000000Z' paid_at: type: string example: null nullable: true cancelled_at: type: string example: '2026-09-29T12:00:00.000000Z' tags: - 'Partner program administration' requestBody: required: false content: application/json: schema: type: object properties: notes: type: string description: '' example: 'Account closed; partner is adding a new one' nullable: true parameters: - in: path name: payout_id description: 'The ID of the payout.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/contacts/{contact}/profitability/export': get: summary: 'Export party profit and loss.' operationId: exportPartyProfitAndLoss description: '' parameters: - in: query name: format description: '' example: pdf required: true schema: type: string description: '' example: pdf enum: - pdf - xlsx - csv - in: query name: period description: '' example: this-financial-year required: false schema: type: string description: '' example: this-financial-year enum: - today - this-month - last-month - this-financial-year - last-financial-year - custom - in: query name: from description: 'Custom start Y-m-d.' example: null required: false schema: type: string description: 'Custom start Y-m-d.' example: null - in: query name: to description: 'Inclusive custom end Y-m-d.' example: null required: false schema: type: string description: 'Inclusive custom end Y-m-d.' example: null responses: 200: description: 'All invoice contribution rows and report total.' content: application/octet-stream: schema: type: string format: binary tags: - 'Party and referrer profitability' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: contact description: 'The contact.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/workers/{worker}/profitability/export': get: summary: 'Export referrer profit and loss.' operationId: exportReferrerProfitAndLoss description: '' parameters: - in: query name: format description: '' example: xlsx required: true schema: type: string description: '' example: xlsx enum: - pdf - xlsx - csv - in: query name: period description: '' example: this-financial-year required: false schema: type: string description: '' example: this-financial-year enum: - today - this-month - last-month - this-financial-year - last-financial-year - custom - in: query name: from description: 'Custom start Y-m-d.' example: null required: false schema: type: string description: 'Custom start Y-m-d.' example: null - in: query name: to description: 'Inclusive custom end Y-m-d.' example: null required: false schema: type: string description: 'Inclusive custom end Y-m-d.' example: null responses: 200: description: 'All owner-attributed invoice contribution rows and report total.' content: application/octet-stream: schema: type: string format: binary tags: - 'Party and referrer profitability' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: worker description: 'The worker.' example: architecto required: true schema: type: string '/api/v1/businesses/{business}/workers/{worker}/statement/export': get: summary: 'Export the referrer commission statement.' operationId: exportTheReferrerCommissionStatement description: "Opening balance precedes the selected period. Every dated earning, return debit note,\npayout and correction is retained with a running balance. Negative balances are\nrecoverable; customer dues are not offset. Payments do not create commission expenses." parameters: - in: query name: format description: '' example: csv required: true schema: type: string description: '' example: csv enum: - pdf - xlsx - csv - in: query name: period description: '' example: this-financial-year required: false schema: type: string description: '' example: this-financial-year enum: - today - this-month - last-month - this-financial-year - last-financial-year - custom - in: query name: from description: 'Custom start Y-m-d.' example: null required: false schema: type: string description: 'Custom start Y-m-d.' example: null - in: query name: to description: 'Inclusive custom end Y-m-d.' example: null required: false schema: type: string description: 'Inclusive custom end Y-m-d.' example: null responses: 200: description: 'Complete dated statement with opening, running and closing balances.' content: application/octet-stream: schema: type: string format: binary tags: - 'Party and referrer profitability' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: worker description: 'The worker.' example: architecto required: true schema: type: string '/api/v1/businesses/{business}/reports/party-profitability': get: summary: 'Compare party profit and loss.' operationId: comparePartyProfitAndLoss description: "Requires reports.party_profitability and accounting. Includes customer/both parties,\narchived parties and those with no activity. Same dated facts as profile P&L. Summary\ncovers every party matching search/status, independently of pagination. Invoice count\nuses current non-voided invoices issued in the period; current invoice dues exclude\nopening khata balances. Untracked goods have no recorded inventory cost." parameters: - in: query name: period description: '' example: this-financial-year required: false schema: type: string description: '' example: this-financial-year enum: - today - this-month - last-month - this-financial-year - last-financial-year - custom - in: query name: from description: 'Custom start Y-m-d.' example: null required: false schema: type: string description: 'Custom start Y-m-d.' example: null - in: query name: to description: 'Inclusive custom end Y-m-d.' example: null required: false schema: type: string description: 'Inclusive custom end Y-m-d.' example: null - in: query name: q description: 'Party name, up to 100 characters.' example: null required: false schema: type: string description: 'Party name, up to 100 characters.' example: null - in: query name: status description: '' example: all required: false schema: type: string description: '' example: all enum: - active - archived - 'all. Default all' - in: query name: sort description: '' example: profit required: false schema: type: string description: '' example: profit enum: - profit - sales - 'name. Default profit' - in: query name: per_page description: 'From 1 to 100.' example: 20 required: false schema: type: integer description: 'From 1 to 100.' example: 20 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Party and referrer profitability' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/reports/party-profitability/export': get: summary: 'Export the party profitability comparison.' operationId: exportThePartyProfitabilityComparison description: "Same search/status/date filters as comparison. Every matching party is included,\nindependently of pagination. Inventory costs containing estimates are labelled." parameters: - in: query name: format description: '' example: xlsx required: true schema: type: string description: '' example: xlsx enum: - pdf - xlsx - csv - in: query name: period description: '' example: this-financial-year required: false schema: type: string description: '' example: this-financial-year enum: - today - this-month - last-month - this-financial-year - last-financial-year - custom - in: query name: from description: 'Custom start Y-m-d.' example: null required: false schema: type: string description: 'Custom start Y-m-d.' example: null - in: query name: to description: 'Inclusive custom end Y-m-d.' example: null required: false schema: type: string description: 'Inclusive custom end Y-m-d.' example: null - in: query name: q description: 'Party name, up to 100 characters.' example: null required: false schema: type: string description: 'Party name, up to 100 characters.' example: null - in: query name: status description: '' example: all required: false schema: type: string description: '' example: all enum: - active - archived - 'all. Default all' - in: query name: sort description: '' example: profit required: false schema: type: string description: '' example: profit enum: - profit - sales - 'name. Default profit' responses: 200: description: 'All matching parties, current invoice dues and contribution total.' content: application/octet-stream: schema: type: string format: binary tags: - 'Party and referrer profitability' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/contacts/{contact}/profitability': get: summary: 'Party profit and loss.' operationId: partyProfitAndLoss description: "Feature `reports.party_profitability`. Reports customer sales contribution, excluding\ngeneral shop overhead. Supplier spend is not treated as supplier profit/loss." parameters: - in: query name: period description: '' example: this-financial-year required: false schema: type: string description: '' example: this-financial-year enum: - today - this-month - last-month - this-financial-year - last-financial-year - custom - in: query name: from description: 'Custom start Y-m-d; required with custom period.' example: null required: false schema: type: string description: 'Custom start Y-m-d; required with custom period.' example: null - in: query name: to description: 'Inclusive custom end Y-m-d; required with custom period.' example: null required: false schema: type: string description: 'Inclusive custom end Y-m-d; required with custom period.' example: null - in: query name: per_page description: 'From 1 to 100.' example: 20 required: false schema: type: integer description: 'From 1 to 100.' example: 20 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Party and referrer profitability' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: contact description: 'The contact.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/workers/performance': get: summary: 'Compare referrer performance.' operationId: compareReferrerPerformance description: "Requires referral_workers and reports.worker_profitability. Includes referrers with no\nreferrals. Sales/costs use owner-entered shares, while each referrer's commission remains\ntheir own amount. Invoice/customer counts count current non-voided referrals issued in\nthe period; dated returns from earlier invoices affect profit independently. Dues and\nbalances are current, and shared customer dues appear under each referrer and\nmust not be added together. Last referral is the latest current non-voided invoice date." parameters: - in: query name: period description: '' example: this-financial-year required: false schema: type: string description: '' example: this-financial-year enum: - today - this-month - last-month - this-financial-year - last-financial-year - custom - in: query name: from description: 'Custom start Y-m-d.' example: null required: false schema: type: string description: 'Custom start Y-m-d.' example: null - in: query name: to description: 'Inclusive custom end Y-m-d.' example: null required: false schema: type: string description: 'Inclusive custom end Y-m-d.' example: null - in: query name: q description: 'Referrer name or trade, up to 100 characters.' example: null required: false schema: type: string description: 'Referrer name or trade, up to 100 characters.' example: null - in: query name: status description: '' example: all required: false schema: type: string description: '' example: all enum: - active - archived - 'all. Default all' - in: query name: worker_type description: '' example: referral required: false schema: type: string description: '' example: referral enum: - referral - cook - carpenter - painter - electrician - plumber - doctor - rmp - contractor - sales_agent - other - in: query name: sort description: '' example: profit required: false schema: type: string description: '' example: profit enum: - profit - sales - referrals - 'name. Default profit' - in: query name: per_page description: 'From 1 to 100.' example: 20 required: false schema: type: integer description: 'From 1 to 100.' example: 20 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Party and referrer profitability' requestBody: required: false content: application/json: schema: type: object properties: q: type: string description: 'Must not be greater than 100 characters.' example: b nullable: true status: type: string description: '' example: active enum: - active - archived - all nullable: true worker_type: type: string description: '' example: architecto nullable: true sort: type: string description: '' example: profit enum: - profit - sales - referrals - name nullable: true per_page: type: integer description: 'Must be between 1 and 100.' example: 2 nullable: true page: type: integer description: 'Must be at least 1.' example: 67 nullable: true parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/workers/{worker}/profitability': get: summary: 'Referrer profit and loss.' operationId: referrerProfitAndLoss description: "Feature `reports.worker_profitability`, alongside `referral_workers`. Reports the shop's\ncontribution from this referrer's referred invoices. The referrer profile commission\nstatement separately reports the referrer's own earned/paid/due/recoverable amounts.\nThe `costing` object has the same inventory confidence fields as party profitability." parameters: - in: query name: period description: '' example: this-financial-year required: false schema: type: string description: '' example: this-financial-year enum: - today - this-month - last-month - this-financial-year - last-financial-year - custom - in: query name: from description: 'Custom start Y-m-d.' example: null required: false schema: type: string description: 'Custom start Y-m-d.' example: null - in: query name: to description: 'Inclusive custom end Y-m-d.' example: null required: false schema: type: string description: 'Inclusive custom end Y-m-d.' example: null - in: query name: per_page description: 'From 1 to 100.' example: 20 required: false schema: type: integer description: 'From 1 to 100.' example: 20 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Party and referrer profitability' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: worker description: 'The worker.' example: architecto required: true schema: type: string '/api/v1/businesses/{business}/price-lists': get: summary: 'List the rate cards of the business.' operationId: listTheRateCardsOfTheBusiness description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Party pricing' requestBody: required: false content: application/json: schema: type: object properties: per_page: type: integer description: 'Must be at least 1. Must not be greater than 100.' example: 1 nullable: true post: summary: 'Create a rate card.' operationId: createARateCard description: "Every row is either a fixed rate or a percentage off the item master, and applies from its\n`min_quantity` upwards, so one item can carry several quantity breaks on the same card." parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'Enter either a fixed rate or a discount for this item, not both.' errors: items.0.sale_price: - 'Enter either a fixed rate or a discount for this item, not both.' properties: message: type: string example: 'Enter either a fixed rate or a discount for this item, not both.' errors: type: object properties: items.0.sale_price: type: array example: - 'Enter either a fixed rate or a discount for this item, not both.' items: type: string tags: - 'Party pricing' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Name of the rate card, unique within the business. Must not be greater than 96 characters.' example: Wholesale description: type: string description: 'Must not be greater than 255 characters.' example: 'Eius et animi quos velit et.' nullable: true price_includes_tax: type: boolean description: 'Whether the fixed rates on this card already include GST. Applies only to rows carrying a fixed rate; a percentage row follows the item master.' example: false is_active: type: boolean description: '' example: false valid_from: type: string description: 'First day the card prices anything. Leave empty for no start date. Must be a valid date.' example: '2026-04-01' nullable: true valid_to: type: string description: 'Last day the card prices anything. Outside the window the item master applies. Must be a valid date. Must be a date after or equal to valid_from.' example: '2027-03-31' nullable: true items: type: array description: 'The rows of the card. Sending this key replaces every existing row; omitting it leaves the rows untouched. Must not have more than 500 items.' example: null items: type: object properties: item_id: type: integer description: '' example: 16 sale_price: type: number description: 'This field is required when items.*.discount_percent is not present. Must be at least 0. Must not be greater than 999999999.' example: 455 nullable: true discount_percent: type: number description: 'Must be at least 0. Must not be greater than 100.' example: 5 nullable: true min_quantity: type: number description: 'Must be at least 0. Must not be greater than 999999.' example: 25 nullable: true required: - item_id required: - name parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/price-lists/{id}': get: summary: '' operationId: getApiV1BusinessesBusinessPriceListsId description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Party pricing' put: summary: 'Replace a rate card.' operationId: replaceARateCard description: "Sending `items` replaces every row on the card; leaving it out edits the card header and\nkeeps the rows. Saved documents keep the rate they were raised at either way." parameters: [] responses: {} tags: - 'Party pricing' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Name of the rate card, unique within the business. Must not be greater than 96 characters.' example: Wholesale description: type: string description: 'Must not be greater than 255 characters.' example: 'Eius et animi quos velit et.' nullable: true price_includes_tax: type: boolean description: 'Whether the fixed rates on this card already include GST. Applies only to rows carrying a fixed rate; a percentage row follows the item master.' example: false is_active: type: boolean description: '' example: false valid_from: type: string description: 'First day the card prices anything. Leave empty for no start date. Must be a valid date.' example: '2026-04-01' nullable: true valid_to: type: string description: 'Last day the card prices anything. Outside the window the item master applies. Must be a valid date. Must be a date after or equal to valid_from.' example: '2027-03-31' nullable: true items: type: array description: 'The rows of the card. Sending this key replaces every existing row; omitting it leaves the rows untouched. Must not have more than 500 items.' example: null items: type: object properties: item_id: type: integer description: '' example: 16 sale_price: type: number description: 'This field is required when items.*.discount_percent is not present. Must be at least 0. Must not be greater than 999999999.' example: 455 nullable: true discount_percent: type: number description: 'Must be at least 0. Must not be greater than 100.' example: 5 nullable: true min_quantity: type: number description: 'Must be at least 0. Must not be greater than 999999.' example: 25 nullable: true required: - item_id required: - name delete: summary: 'Delete a rate card that no party carries.' operationId: deleteARateCardThatNoPartyCarries description: '' parameters: [] responses: 422: description: '' content: application/json: schema: type: object example: message: 'Move the parties on this rate card to another one before deleting it.' errors: price_list: - 'Move the parties on this rate card to another one before deleting it.' properties: message: type: string example: 'Move the parties on this rate card to another one before deleting it.' errors: type: object properties: price_list: type: array example: - 'Move the parties on this rate card to another one before deleting it.' items: type: string tags: - 'Party pricing' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: id description: 'The ID of the price list.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/payment-accounts': get: summary: '' operationId: getApiV1BusinessesBusinessPaymentAccounts description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Payment accounts' post: summary: '' operationId: postApiV1BusinessesBusinessPaymentAccounts description: '' parameters: [] responses: {} tags: - 'Payment accounts' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/payment-accounts/{paymentAccount_id}': patch: summary: '' operationId: patchApiV1BusinessesBusinessPaymentAccountsPaymentAccount_id description: '' parameters: [] responses: {} tags: - 'Payment accounts' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: paymentAccount_id description: 'The ID of the paymentAccount.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/payment-accounts/transfer': post: summary: '' operationId: postApiV1BusinessesBusinessPaymentAccountsTransfer description: '' parameters: [] responses: {} tags: - 'Payment accounts' requestBody: required: true content: application/json: schema: type: object properties: from_payment_account_id: type: integer description: '' example: 16 to_payment_account_id: type: integer description: 'The value and from_payment_account_id must be different.' example: 16 amount: type: number description: '' example: 4326.41688 transferred_on: type: string description: 'Must be a valid date.' example: '2026-01-15' reference: type: string description: 'Must not be greater than 64 characters.' example: m nullable: true notes: type: string description: 'Must not be greater than 255 characters.' example: i nullable: true required: - from_payment_account_id - to_payment_account_id - amount - transferred_on parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/payment-accounts/{paymentAccount_id}/reconciliation': get: summary: 'Show eligible transactions and statement-reconciliation history.' operationId: showEligibleTransactionsAndStatementReconciliationHistory description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Payment accounts' requestBody: required: false content: application/json: schema: type: object properties: statement_date: type: string description: 'Must be a valid date. Must be a date before or equal to today.' example: '2026-01-15' nullable: true post: summary: 'Complete and lock a statement reconciliation.' operationId: completeAndLockAStatementReconciliation description: '' parameters: [] responses: {} tags: - 'Payment accounts' requestBody: required: true content: application/json: schema: type: object properties: statement_date: type: string description: 'Must be a valid date. Must be a date before or equal to today.' example: '2026-01-15' statement_balance: type: number description: 'Must be between -999999999 and 999999999.' example: -999999998 transaction_ids: type: array description: '' example: - 16 items: type: integer notes: type: string description: 'Must not be greater than 255 characters.' example: 'n' nullable: true required: - statement_date - statement_balance parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: paymentAccount_id description: 'The ID of the paymentAccount.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/payment-accounts/{paymentAccount_id}/reconcile/{journalLine_id}': post: summary: '' operationId: postApiV1BusinessesBusinessPaymentAccountsPaymentAccount_idReconcileJournalLine_id description: '' parameters: [] responses: {} tags: - 'Payment accounts' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: paymentAccount_id description: 'The ID of the paymentAccount.' example: 1 required: true schema: type: integer - in: path name: journalLine_id description: 'The ID of the journalLine.' example: 1 required: true schema: type: integer /api/v1/admin/notifications: get: summary: 'List sent broadcasts.' operationId: listSentBroadcasts description: 'Newest first, with the filters each was sent to and how many devices Firebase accepted.' parameters: - in: query name: per_page description: 'Results per page, from 1 to 50.' example: 24 required: false schema: type: integer description: 'Results per page, from 1 to 50.' example: 24 responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 9 title: 'GST filing window opens tomorrow' body: 'GSTR-3B for this month can be prepared from the GST workspace starting tomorrow.' image_url: 'https://app.dukanam.test/storage/broadcast-images/gst.png' link_url: 'https://app.dukanam.test/gst' audience: state_codes: - '33' city: null store_types: [] plan_ids: - 2 subscription_statuses: - active workspace_status: active recipients: owners audience_summary: 'Tamil Nadu · Growth plan · Active · Active workspaces · Business owners only' businesses_count: 128 recipients_count: 96 sent_count: 94 failed_count: 2 status: sent failure_message: null created_by: id: 1 name: 'Platform Admin' created_at: '2026-09-20T09:00:00.000000Z' sent_at: '2026-09-20T09:00:12.000000Z' links: {} meta: current_page: 1 per_page: 24 total: 1 properties: data: type: array example: - id: 9 title: 'GST filing window opens tomorrow' body: 'GSTR-3B for this month can be prepared from the GST workspace starting tomorrow.' image_url: 'https://app.dukanam.test/storage/broadcast-images/gst.png' link_url: 'https://app.dukanam.test/gst' audience: state_codes: - '33' city: null store_types: [] plan_ids: - 2 subscription_statuses: - active workspace_status: active recipients: owners audience_summary: 'Tamil Nadu · Growth plan · Active · Active workspaces · Business owners only' businesses_count: 128 recipients_count: 96 sent_count: 94 failed_count: 2 status: sent failure_message: null created_by: id: 1 name: 'Platform Admin' created_at: '2026-09-20T09:00:00.000000Z' sent_at: '2026-09-20T09:00:12.000000Z' items: type: object properties: id: type: integer example: 9 title: type: string example: 'GST filing window opens tomorrow' body: type: string example: 'GSTR-3B for this month can be prepared from the GST workspace starting tomorrow.' image_url: type: string example: 'https://app.dukanam.test/storage/broadcast-images/gst.png' link_url: type: string example: 'https://app.dukanam.test/gst' audience: type: object properties: state_codes: type: array example: - '33' items: type: string city: type: string example: null nullable: true store_types: type: array example: [] plan_ids: type: array example: - 2 items: type: integer subscription_statuses: type: array example: - active items: type: string workspace_status: type: string example: active recipients: type: string example: owners audience_summary: type: string example: 'Tamil Nadu · Growth plan · Active · Active workspaces · Business owners only' businesses_count: type: integer example: 128 recipients_count: type: integer example: 96 sent_count: type: integer example: 94 failed_count: type: integer example: 2 status: type: string example: sent failure_message: type: string example: null nullable: true created_by: type: object properties: id: type: integer example: 1 name: type: string example: 'Platform Admin' created_at: type: string example: '2026-09-20T09:00:00.000000Z' sent_at: type: string example: '2026-09-20T09:00:12.000000Z' links: type: object properties: {} meta: type: object properties: current_page: type: integer example: 1 per_page: type: integer example: 24 total: type: integer example: 1 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 403: description: '' content: application/json: schema: type: object example: message: 'This action is unauthorized.' properties: message: type: string example: 'This action is unauthorized.' tags: - 'Platform broadcasts' requestBody: required: false content: application/json: schema: type: object properties: per_page: type: integer description: 'Must be at least 1. Must not be greater than 50.' example: 1 nullable: true post: summary: 'Send a broadcast.' operationId: sendABroadcast description: "The broadcast is recorded and then pushed on the queue, so the response returns with\nstatus `queued` and `recipients_count` already resolved. Send as `multipart/form-data`\nwhen attaching an image; array filters use `state_codes[]` style keys. A filter set that\nreaches no registered device is rejected with `422` and nothing is pushed." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: id: 9 title: 'GST filing window opens tomorrow' body: 'GSTR-3B for this month can be prepared from the GST workspace starting tomorrow.' image_url: 'https://app.dukanam.test/storage/broadcast-images/gst.png' link_url: 'https://app.dukanam.test/gst' audience: state_codes: - '33' city: null store_types: [] plan_ids: - 2 subscription_statuses: - active workspace_status: active recipients: owners audience_summary: 'Tamil Nadu · Growth plan · Active · Active workspaces · Business owners only' businesses_count: 128 recipients_count: 96 sent_count: 0 failed_count: 0 status: queued failure_message: null created_by: id: 1 name: 'Platform Admin' created_at: '2026-09-20T09:00:00.000000Z' sent_at: null properties: data: type: object properties: id: type: integer example: 9 title: type: string example: 'GST filing window opens tomorrow' body: type: string example: 'GSTR-3B for this month can be prepared from the GST workspace starting tomorrow.' image_url: type: string example: 'https://app.dukanam.test/storage/broadcast-images/gst.png' link_url: type: string example: 'https://app.dukanam.test/gst' audience: type: object properties: state_codes: type: array example: - '33' items: type: string city: type: string example: null nullable: true store_types: type: array example: [] plan_ids: type: array example: - 2 items: type: integer subscription_statuses: type: array example: - active items: type: string workspace_status: type: string example: active recipients: type: string example: owners audience_summary: type: string example: 'Tamil Nadu · Growth plan · Active · Active workspaces · Business owners only' businesses_count: type: integer example: 128 recipients_count: type: integer example: 96 sent_count: type: integer example: 0 failed_count: type: integer example: 0 status: type: string example: queued failure_message: type: string example: null nullable: true created_by: type: object properties: id: type: integer example: 1 name: type: string example: 'Platform Admin' created_at: type: string example: '2026-09-20T09:00:00.000000Z' sent_at: type: string example: null nullable: true 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 403: description: '' content: application/json: schema: type: object example: message: 'This action is unauthorized.' properties: message: type: string example: 'This action is unauthorized.' 422: description: '' content: application/json: schema: type: object example: message: 'No workspace matching these filters has a registered device.' errors: recipients: - 'No workspace matching these filters has a registered device.' properties: message: type: string example: 'No workspace matching these filters has a registered device.' errors: type: object properties: recipients: type: array example: - 'No workspace matching these filters has a registered device.' items: type: string tags: - 'Platform broadcasts' requestBody: required: true content: multipart/form-data: schema: type: object properties: state_codes: type: array description: 'GST state codes to target, for example 33 for Tamil Nadu. Omit to reach every state.' example: - '33' - '29' items: type: string city: type: string description: 'City to target, matched case-insensitively against the workspace address. Must not be greater than 120 characters.' example: Coimbatore nullable: true store_types: type: array description: 'Store type slugs to target. Omit to reach every store type.' example: - pharmacy items: type: string plan_ids: type: array description: "Plans whose subscribers should be targeted, matched on each workspace's newest subscription." example: - 2 items: type: integer subscription_statuses: type: array description: 'Subscription statuses to target: active, trialing, past_due, paused, ended, or none for workspaces that never subscribed.' example: - active - trialing items: type: string workspace_status: type: string description: 'active, suspended or any. Defaults to active.' example: active enum: - any - active - suspended nullable: true recipients: type: string description: 'owners to reach only the business owner, members to reach every active team member. Defaults to owners.' example: owners enum: - owners - members nullable: true title: type: string description: 'Notification title, up to 120 characters. Must not be greater than 120 characters.' example: 'GST filing window opens tomorrow' body: type: string description: 'Notification message, up to 1,000 characters. Must not be greater than 1000 characters.' example: 'GSTR-3B for this month can be prepared from the GST workspace starting tomorrow.' image: type: string format: binary description: 'Optional JPEG, PNG or WebP banner, 200x200 to 4000x4000 pixels, maximum 1 MB. Sent as multipart/form-data. Must be an image. Must not be greater than 1024 kilobytes.' nullable: true image_url: type: string description: 'Optional HTTPS URL of an already-hosted banner. Cannot be combined with image. Must be a valid URL. Must not be greater than 2048 characters.' example: 'https://cdn.example.com/banners/gst.png' nullable: true link_url: type: string description: 'Optional URL delivered as data.url for the client to open when the push is tapped. Must be a valid URL. Must not be greater than 2048 characters.' example: 'https://app.dukanam.test/gst' nullable: true required: - title - body /api/v1/admin/notifications/audience: get: summary: 'Preview how many shops and devices a filter set reaches.' operationId: previewHowManyShopsAndDevicesAFilterSetReaches description: "Nothing is sent or stored. `reachable` is the number of people with a registered device,\nwhich is what a send would actually push to, and `push_enabled` reports whether Firebase\npush is configured on this deployment at all.\n\nThe list filters are spelled out here because Scribe reduces an `in`-constrained list to a\nsingle value when it reads them off the form request." parameters: - in: query name: state_codes description: 'GST state codes to target, for example 33 for Tamil Nadu. Omit to reach every state.' example: - '33' - '29' required: false schema: type: array description: 'GST state codes to target, for example 33 for Tamil Nadu. Omit to reach every state.' example: - '33' - '29' items: type: string - in: query name: city description: 'City to target, matched case-insensitively against the workspace address. Must not be greater than 120 characters.' example: Coimbatore required: false schema: type: string description: 'City to target, matched case-insensitively against the workspace address. Must not be greater than 120 characters.' example: Coimbatore nullable: true - in: query name: store_types description: 'Store type slugs to target. Omit to reach every store type.' example: - pharmacy required: false schema: type: array description: 'Store type slugs to target. Omit to reach every store type.' example: - pharmacy items: type: string - in: query name: plan_ids description: "Plans whose subscribers should be targeted, matched on each workspace's newest subscription." example: - 2 required: false schema: type: array description: "Plans whose subscribers should be targeted, matched on each workspace's newest subscription." example: - 2 items: type: integer - in: query name: subscription_statuses description: 'Subscription statuses to target: active, trialing, past_due, paused, ended, or none for workspaces that never subscribed.' example: - active - trialing required: false schema: type: array description: 'Subscription statuses to target: active, trialing, past_due, paused, ended, or none for workspaces that never subscribed.' example: - active - trialing items: type: string - in: query name: workspace_status description: 'active, suspended or any. Defaults to active.' example: any required: false schema: type: string description: 'active, suspended or any. Defaults to active.' example: any enum: - any - active - suspended nullable: true - in: query name: recipients description: 'owners to reach only the business owner, members to reach every active team member. Defaults to owners.' example: owners required: false schema: type: string description: 'owners to reach only the business owner, members to reach every active team member. Defaults to owners.' example: owners enum: - owners - members nullable: true responses: 200: description: '' content: application/json: schema: type: object example: data: businesses: 128 recipients: 141 reachable: 96 push_enabled: true audience: state_codes: - '33' city: null store_types: [] plan_ids: - 2 subscription_statuses: - active workspace_status: active recipients: owners audience_summary: 'Tamil Nadu · Growth plan · Active · Active workspaces · Business owners only' properties: data: type: object properties: businesses: type: integer example: 128 recipients: type: integer example: 141 reachable: type: integer example: 96 push_enabled: type: boolean example: true audience: type: object properties: state_codes: type: array example: - '33' items: type: string city: type: string example: null nullable: true store_types: type: array example: [] plan_ids: type: array example: - 2 items: type: integer subscription_statuses: type: array example: - active items: type: string workspace_status: type: string example: active recipients: type: string example: owners audience_summary: type: string example: 'Tamil Nadu · Growth plan · Active · Active workspaces · Business owners only' 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 403: description: '' content: application/json: schema: type: object example: message: 'This action is unauthorized.' properties: message: type: string example: 'This action is unauthorized.' 422: description: '' content: application/json: schema: type: object example: message: 'The selected recipients is invalid.' errors: recipients: - 'The selected recipients is invalid.' properties: message: type: string example: 'The selected recipients is invalid.' errors: type: object properties: recipients: type: array example: - 'The selected recipients is invalid.' items: type: string tags: - 'Platform broadcasts' '/api/v1/admin/businesses/{business_id}/device-usage': get: summary: 'Inspect aggregate phone usage.' operationId: inspectAggregatePhoneUsage description: "Super-admin only, with no-store caching. Dates use Asia/Kolkata. Defaults to the last\n30 calendar days including today; maximum 366 days. Latest is the all-time latest snapshot;\nsnapshots and event totals are limited to the requested range. No installation IDs or raw reports.\nSource is device only while the latest reported mode is local; otherwise use server records." parameters: - in: query name: from description: 'Start date, YYYY-MM-DD.' example: '2026-09-01' required: false schema: type: string description: 'Start date, YYYY-MM-DD.' example: '2026-09-01' - in: query name: to description: 'End date, YYYY-MM-DD.' example: '2026-09-27' required: false schema: type: string description: 'End date, YYYY-MM-DD.' example: '2026-09-27' responses: 200: description: '' content: application/json: schema: type: object example: data: from: '2026-09-01' to: '2026-09-27' timezone: Asia/Kolkata source: device mode: local app_version: 1.1.1 last_reported_at: '2026-09-27T03:44:05.000000Z' latest: date: '2026-09-27' mode: local totals: invoices: 5 money: billed_value_paise: 120000 reported_at: '2026-09-27T03:44:05.000000Z' snapshots: [] events: - key: invoice_shared_whatsapp label: 'Invoice shared whatsapp' count: 2 milestones: first_invoice_at: '2026-09-27T03:40:00.000000Z' properties: data: type: object properties: from: type: string example: '2026-09-01' to: type: string example: '2026-09-27' timezone: type: string example: Asia/Kolkata source: type: string example: device mode: type: string example: local app_version: type: string example: 1.1.1 last_reported_at: type: string example: '2026-09-27T03:44:05.000000Z' latest: type: object properties: date: type: string example: '2026-09-27' mode: type: string example: local totals: type: object properties: invoices: type: integer example: 5 money: type: object properties: billed_value_paise: type: integer example: 120000 reported_at: type: string example: '2026-09-27T03:44:05.000000Z' snapshots: type: array example: [] events: type: array example: - key: invoice_shared_whatsapp label: 'Invoice shared whatsapp' count: 2 items: type: object properties: key: type: string example: invoice_shared_whatsapp label: type: string example: 'Invoice shared whatsapp' count: type: integer example: 2 milestones: type: object properties: first_invoice_at: type: string example: '2026-09-27T03:40:00.000000Z' 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 403: description: '' content: application/json: schema: type: object example: message: 'This action is unauthorized.' properties: message: type: string example: 'This action is unauthorized.' 404: description: '' content: application/json: schema: type: object example: message: 'Not found.' properties: message: type: string example: 'Not found.' 422: description: '' content: application/json: schema: type: object example: message: 'The to field must be a date after or equal to from.' errors: to: - 'The to field must be a date after or equal to from.' properties: message: type: string example: 'The to field must be a date after or equal to from.' errors: type: object properties: to: type: array example: - 'The to field must be a date after or equal to from.' items: type: string tags: - 'Platform business insights' requestBody: required: false content: application/json: schema: type: object properties: from: type: string description: 'Must be a valid date in the format Y-m-d.' example: '2026-01-15' to: type: string description: 'Must be a valid date in the format Y-m-d.' example: '2026-01-15' parameters: - in: path name: business_id description: 'Business ID.' example: 1 required: true schema: type: integer '/api/v1/admin/businesses/{id}': get: summary: 'Inspect a business and its daily usage.' operationId: inspectABusinessAndItsDailyUsage description: "Requires a super-admin token; ordinary workspace members cannot access this endpoint.\nDaily usage counts records by creation time in the business timezone, including today\n(partial), with zero-filled dates in ascending order. It does not measure sessions or time spent.\nInvoices include every status. Customer contacts include type customer and both;\nsupplier contacts include type supplier and both, so a contact marked as both is counted in each.\nAdded counts include soft-deleted items, customers and suppliers; current totals exclude them.\nContact classification reflects the contact's current type. Permanently deleted records\ncannot be counted. Last record added is the latest creation across these four categories,\nregardless of the selected period. All timestamps are ISO 8601; calendar dates are YYYY-MM-DD.\n\nOwner insights include mobile activity (active within 30 days, inactive, seen, unknown),\nISO 8601 first/last seen timestamps, push registration, first observed login, and acquisition.\nInstallation status is always unknown. Mobile activity reflects mobile-scoped API tokens,\nnot proof of a current installation. First observed login starts when tracking is enabled.\nAcquisition source is referral, campaign, referring_site, direct_or_unknown, or unknown." parameters: - in: query name: days description: 'Calendar days including today: 7, 30 or 90. Defaults to 30.' example: 7 required: false schema: type: integer description: 'Calendar days including today: 7, 30 or 90. Defaults to 30.' example: 7 responses: 200: description: '' content: application/json: schema: type: object example: data: business: id: 17 name: 'Sampada Stores' legal_name: 'Sampada Retail LLP' store_type: grocery is_active: true email: shop@example.com phone: '+919876543210' owner: id: 42 name: 'Kavitha Reddy' email: kavitha@example.com phone: null gstin: null gst_registration_type: unregistered address: line_1: '14 Market Road' line_2: 'First floor' city: Pune state_code: '27' state_name: Maharashtra pincode: '411001' currency: INR timezone: Asia/Kolkata default_locale: en created_at: '2026-09-01T04:30:00.000000Z' usage: days: 7 from: '2026-09-08' to: '2026-09-14' timezone: Asia/Kolkata generated_at: '2026-09-14T08:00:00.000000Z' totals: invoices: 23 items: 12 customers: 31 suppliers: 4 added: invoices: 3 items: 2 customers: 4 suppliers: 1 today: date: '2026-09-14' invoices: 3 items: 2 customers: 4 suppliers: 1 total: 10 active_days: 1 last_record_added_at: '2026-09-14T07:00:00.000000Z' daily: - date: '2026-09-08' invoices: 0 items: 0 customers: 0 suppliers: 0 total: 0 - date: '2026-09-09' invoices: 0 items: 0 customers: 0 suppliers: 0 total: 0 - date: '2026-09-10' invoices: 0 items: 0 customers: 0 suppliers: 0 total: 0 - date: '2026-09-11' invoices: 0 items: 0 customers: 0 suppliers: 0 total: 0 - date: '2026-09-12' invoices: 0 items: 0 customers: 0 suppliers: 0 total: 0 - date: '2026-09-13' invoices: 0 items: 0 customers: 0 suppliers: 0 total: 0 - date: '2026-09-14' invoices: 3 items: 2 customers: 4 suppliers: 1 total: 10 owner_insights: mobile: status: unknown label: 'No app activity recorded' installation_status: unknown first_seen_at: null last_seen_at: null push_registered: false first_observed_login: at: null method: null channel: null acquisition: source: unknown properties: data: type: object properties: business: type: object properties: id: type: integer example: 17 name: type: string example: 'Sampada Stores' legal_name: type: string example: 'Sampada Retail LLP' store_type: type: string example: grocery is_active: type: boolean example: true email: type: string example: shop@example.com phone: type: string example: '+919876543210' owner: type: object properties: id: type: integer example: 42 name: type: string example: 'Kavitha Reddy' email: type: string example: kavitha@example.com phone: type: string example: null nullable: true gstin: type: string example: null nullable: true gst_registration_type: type: string example: unregistered address: type: object properties: line_1: type: string example: '14 Market Road' line_2: type: string example: 'First floor' city: type: string example: Pune state_code: type: string example: '27' state_name: type: string example: Maharashtra pincode: type: string example: '411001' currency: type: string example: INR timezone: type: string example: Asia/Kolkata default_locale: type: string example: en created_at: type: string example: '2026-09-01T04:30:00.000000Z' usage: type: object properties: days: type: integer example: 7 from: type: string example: '2026-09-08' to: type: string example: '2026-09-14' timezone: type: string example: Asia/Kolkata generated_at: type: string example: '2026-09-14T08:00:00.000000Z' totals: type: object properties: invoices: type: integer example: 23 items: type: integer example: 12 customers: type: integer example: 31 suppliers: type: integer example: 4 added: type: object properties: invoices: type: integer example: 3 items: type: integer example: 2 customers: type: integer example: 4 suppliers: type: integer example: 1 today: type: object properties: date: type: string example: '2026-09-14' invoices: type: integer example: 3 items: type: integer example: 2 customers: type: integer example: 4 suppliers: type: integer example: 1 total: type: integer example: 10 active_days: type: integer example: 1 last_record_added_at: type: string example: '2026-09-14T07:00:00.000000Z' daily: type: array example: - date: '2026-09-08' invoices: 0 items: 0 customers: 0 suppliers: 0 total: 0 - date: '2026-09-09' invoices: 0 items: 0 customers: 0 suppliers: 0 total: 0 - date: '2026-09-10' invoices: 0 items: 0 customers: 0 suppliers: 0 total: 0 - date: '2026-09-11' invoices: 0 items: 0 customers: 0 suppliers: 0 total: 0 - date: '2026-09-12' invoices: 0 items: 0 customers: 0 suppliers: 0 total: 0 - date: '2026-09-13' invoices: 0 items: 0 customers: 0 suppliers: 0 total: 0 - date: '2026-09-14' invoices: 3 items: 2 customers: 4 suppliers: 1 total: 10 items: type: object properties: date: type: string example: '2026-09-08' invoices: type: integer example: 0 items: type: integer example: 0 customers: type: integer example: 0 suppliers: type: integer example: 0 total: type: integer example: 0 owner_insights: type: object properties: mobile: type: object properties: status: type: string example: unknown label: type: string example: 'No app activity recorded' installation_status: type: string example: unknown first_seen_at: type: string example: null nullable: true last_seen_at: type: string example: null nullable: true push_registered: type: boolean example: false first_observed_login: type: object properties: at: type: string example: null nullable: true method: type: string example: null nullable: true channel: type: string example: null nullable: true acquisition: type: object properties: source: type: string example: unknown 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 403: description: '' content: application/json: schema: type: object example: message: 'This action is unauthorized.' properties: message: type: string example: 'This action is unauthorized.' 404: description: '' content: application/json: schema: type: object example: message: 'Not found.' properties: message: type: string example: 'Not found.' 422: description: '' content: application/json: schema: type: object example: message: 'The selected days is invalid.' errors: days: - 'The selected days is invalid.' properties: message: type: string example: 'The selected days is invalid.' errors: type: object properties: days: type: array example: - 'The selected days is invalid.' items: type: string tags: - 'Platform business insights' parameters: - in: path name: id description: 'Business ID to inspect.' example: 17 required: true schema: type: integer '/api/v1/admin/insights/{section}': get: summary: 'Get one overview section.' operationId: getOneOverviewSection description: "`today` compares today with yesterday and returns seven-day series (oldest first) for each\nheadline number, invoices per hour (hours still to come are null), active businesses by\nchannel, counts that need attention, and today's milestones. `growth` returns signups,\nactivation (5+ invoices) and signup-to-paid rates against the previous period of equal\nlength, a daily series, the activation funnel for people who signed up in the range, signup\nsources, and weekly retention for up to eight weekly cohorts of new businesses. `engagement`\nreturns active businesses today and over 7 and 30 days ending at `to`, stickiness (average\ndaily actives over the last 7 days ÷ 30-day actives), a daily series split by channel with\ninvoices and billed value, the spread of businesses by invoice count, feature adoption among\nactive businesses, the top 10 businesses by invoices, and up to 10 businesses quiet for 14+\ndays. `revenue` returns paid MRR and ARR (paying subscriptions only: no trials, no\ncomplimentary plans), money collected in the range and the previous one, GST collected,\ntrial-to-paid conversion for trials that ended in the range, six months of new and churned\nMRR, money collected per day by Razorpay and Apple, the current plan mix and a watch list." parameters: - in: query name: from description: 'First calendar day, YYYY-MM-DD. Defaults to 29 days before `to`. Ignored by the today section. Must be a valid date in the format Y-m-d.' example: '2026-01-15' required: false schema: type: string description: 'First calendar day, YYYY-MM-DD. Defaults to 29 days before `to`. Ignored by the today section. Must be a valid date in the format Y-m-d.' example: '2026-01-15' nullable: true - in: query name: to description: 'Last calendar day, YYYY-MM-DD. Defaults to today. The range may cover at most 366 days. Must be a valid date in the format Y-m-d.' example: '2026-01-15' required: false schema: type: string description: 'Last calendar day, YYYY-MM-DD. Defaults to today. The range may cover at most 366 days. Must be a valid date in the format Y-m-d.' example: '2026-01-15' nullable: true responses: 200: description: '' content: application/json: schema: oneOf: - description: '' type: object example: data: date: '2026-09-26' kpis: new_signups: today: 0 yesterday: 1 last_7_days: - 1 - 0 - 0 - 0 - 0 - 1 - 0 new_businesses: today: 0 yesterday: 1 last_7_days: - 1 - 0 - 0 - 0 - 0 - 1 - 0 active_businesses: today: 1 yesterday: 2 last_7_days: - 0 - 0 - 0 - 0 - 0 - 2 - 1 share_of_businesses: 33.3 total_businesses: 3 web: 1 mobile: 1 invoices_created: today: 2 yesterday: 1 last_7_days: - 5 - 0 - 0 - 0 - 0 - 1 - 2 per_active_business: 2 billed_value_paise: today: 240000 yesterday: 120000 last_7_days: - 600000 - 0 - 0 - 0 - 0 - 120000 - 240000 average_invoice_paise: 120000 subscription_revenue_paise: today: 33814 yesterday: 0 last_7_days: - 0 - 0 - 0 - 0 - 0 - 0 - 33814 payments: 1 gross_paise: 39900 new_paying_businesses: today: 1 yesterday: 0 last_7_days: - 0 - 0 - 0 - 0 - 0 - 0 - 1 paying_businesses: 1 app_link_opens: today: 0 yesterday: 0 last_7_days: - 0 - 0 - 0 - 0 - 0 - 0 - 0 store_redirects: 0 link_previews: 0 invoices_by_hour: today: - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 2 - 0 - 0 - 0 - 0 - null - null - null - null - null - null - null - null yesterday: - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 1 - 0 - 0 - 0 - 0 - 0 active_by_channel: mobile_only: 0 web_only: 0 both: 1 records_only: 0 needs_attention: past_due: 0 trials_ending_7_days: 1 unpaid_checkouts_7_days: 0 new_without_invoice: 0 feedback_pending: 0 testimonials_pending: 0 milestones: - at: '2026-09-26T15:00:00+05:30' type: trial business: 'Ravi Hardware' text: 'started a Smart Books trial' - at: '2026-09-26T09:30:00+05:30' type: payment business: 'Kavya Fashions' text: 'paid for Business · ₹399 incl. GST' properties: data: type: object properties: date: type: string example: '2026-09-26' kpis: type: object properties: new_signups: type: object properties: today: type: integer example: 0 yesterday: type: integer example: 1 last_7_days: type: array example: - 1 - 0 - 0 - 0 - 0 - 1 - 0 items: type: integer new_businesses: type: object properties: today: type: integer example: 0 yesterday: type: integer example: 1 last_7_days: type: array example: - 1 - 0 - 0 - 0 - 0 - 1 - 0 items: type: integer active_businesses: type: object properties: today: type: integer example: 1 yesterday: type: integer example: 2 last_7_days: type: array example: - 0 - 0 - 0 - 0 - 0 - 2 - 1 items: type: integer share_of_businesses: type: number example: 33.3 total_businesses: type: integer example: 3 web: type: integer example: 1 mobile: type: integer example: 1 invoices_created: type: object properties: today: type: integer example: 2 yesterday: type: integer example: 1 last_7_days: type: array example: - 5 - 0 - 0 - 0 - 0 - 1 - 2 items: type: integer per_active_business: type: integer example: 2 billed_value_paise: type: object properties: today: type: integer example: 240000 yesterday: type: integer example: 120000 last_7_days: type: array example: - 600000 - 0 - 0 - 0 - 0 - 120000 - 240000 items: type: integer average_invoice_paise: type: integer example: 120000 subscription_revenue_paise: type: object properties: today: type: integer example: 33814 yesterday: type: integer example: 0 last_7_days: type: array example: - 0 - 0 - 0 - 0 - 0 - 0 - 33814 items: type: integer payments: type: integer example: 1 gross_paise: type: integer example: 39900 new_paying_businesses: type: object properties: today: type: integer example: 1 yesterday: type: integer example: 0 last_7_days: type: array example: - 0 - 0 - 0 - 0 - 0 - 0 - 1 items: type: integer paying_businesses: type: integer example: 1 app_link_opens: type: object properties: today: type: integer example: 0 yesterday: type: integer example: 0 last_7_days: type: array example: - 0 - 0 - 0 - 0 - 0 - 0 - 0 items: type: integer store_redirects: type: integer example: 0 link_previews: type: integer example: 0 invoices_by_hour: type: object properties: today: type: array example: - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 2 - 0 - 0 - 0 - 0 - null - null - null - null - null - null - null - null items: type: integer yesterday: type: array example: - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 0 - 1 - 0 - 0 - 0 - 0 - 0 items: type: integer active_by_channel: type: object properties: mobile_only: type: integer example: 0 web_only: type: integer example: 0 both: type: integer example: 1 records_only: type: integer example: 0 needs_attention: type: object properties: past_due: type: integer example: 0 trials_ending_7_days: type: integer example: 1 unpaid_checkouts_7_days: type: integer example: 0 new_without_invoice: type: integer example: 0 feedback_pending: type: integer example: 0 testimonials_pending: type: integer example: 0 milestones: type: array example: - at: '2026-09-26T15:00:00+05:30' type: trial business: 'Ravi Hardware' text: 'started a Smart Books trial' - at: '2026-09-26T09:30:00+05:30' type: payment business: 'Kavya Fashions' text: 'paid for Business · ₹399 incl. GST' items: type: object properties: at: type: string example: '2026-09-26T15:00:00+05:30' type: type: string example: trial business: type: string example: 'Ravi Hardware' text: type: string example: 'started a Smart Books trial' - description: engagement type: object example: data: range: from: '2026-09-20' to: '2026-09-26' days: 7 kpis: active_today: 1 active_7_days: 2 active_30_days: 3 active_businesses_total: 3 stickiness: 14.3 daily: - date: '2026-09-20' mobile_only: 0 web_only: 0 both: 0 records_only: 0 invoices: 5 billed_value_paise: 600000 - date: '2026-09-21' mobile_only: 0 web_only: 0 both: 0 records_only: 0 invoices: 0 billed_value_paise: 0 - date: '2026-09-22' mobile_only: 0 web_only: 0 both: 0 records_only: 0 invoices: 0 billed_value_paise: 0 - date: '2026-09-23' mobile_only: 0 web_only: 0 both: 0 records_only: 0 invoices: 0 billed_value_paise: 0 - date: '2026-09-24' mobile_only: 0 web_only: 0 both: 0 records_only: 0 invoices: 0 billed_value_paise: 0 - date: '2026-09-25' mobile_only: 1 web_only: 1 both: 0 records_only: 0 invoices: 1 billed_value_paise: 120000 - date: '2026-09-26' mobile_only: 0 web_only: 0 both: 1 records_only: 0 invoices: 2 billed_value_paise: 240000 invoice_distribution: - label: None businesses: 2 - label: 1–10 businesses: 1 - label: 11–50 businesses: 0 - label: 51–200 businesses: 0 - label: 200+ businesses: 0 feature_adoption: - key: sales_invoices label: 'Sales invoices' businesses: 1 share: 50 - key: pos label: 'POS counter' businesses: 1 share: 50 - key: payments label: 'Payments received' businesses: 0 share: 0 - key: stock label: 'Stock tracking' businesses: 0 share: 0 - key: purchases label: 'Purchase bills' businesses: 0 share: 0 - key: expenses label: Expenses businesses: 0 share: 0 - key: quotes_challans label: 'Quotes and challans' businesses: 0 share: 0 - key: email label: 'Emailed a document' businesses: 0 share: 0 - key: gst label: 'GST returns' businesses: 0 share: 0 top_businesses: - id: 1 name: 'Kavya Fashions' plan: Business invoices: 8 billed_value_paise: 960000 last_active_on: '2026-09-26' last_channel: both going_quiet: - id: 3 name: 'Durga Textiles' last_active_on: '2026-09-05' quiet_days: 21 invoices_ever: 3 phone: '9848012345' properties: data: type: object properties: range: type: object properties: from: type: string example: '2026-09-20' to: type: string example: '2026-09-26' days: type: integer example: 7 kpis: type: object properties: active_today: type: integer example: 1 active_7_days: type: integer example: 2 active_30_days: type: integer example: 3 active_businesses_total: type: integer example: 3 stickiness: type: number example: 14.3 daily: type: array example: - date: '2026-09-20' mobile_only: 0 web_only: 0 both: 0 records_only: 0 invoices: 5 billed_value_paise: 600000 - date: '2026-09-21' mobile_only: 0 web_only: 0 both: 0 records_only: 0 invoices: 0 billed_value_paise: 0 - date: '2026-09-22' mobile_only: 0 web_only: 0 both: 0 records_only: 0 invoices: 0 billed_value_paise: 0 - date: '2026-09-23' mobile_only: 0 web_only: 0 both: 0 records_only: 0 invoices: 0 billed_value_paise: 0 - date: '2026-09-24' mobile_only: 0 web_only: 0 both: 0 records_only: 0 invoices: 0 billed_value_paise: 0 - date: '2026-09-25' mobile_only: 1 web_only: 1 both: 0 records_only: 0 invoices: 1 billed_value_paise: 120000 - date: '2026-09-26' mobile_only: 0 web_only: 0 both: 1 records_only: 0 invoices: 2 billed_value_paise: 240000 items: type: object properties: date: type: string example: '2026-09-20' mobile_only: type: integer example: 0 web_only: type: integer example: 0 both: type: integer example: 0 records_only: type: integer example: 0 invoices: type: integer example: 5 billed_value_paise: type: integer example: 600000 invoice_distribution: type: array example: - label: None businesses: 2 - label: 1–10 businesses: 1 - label: 11–50 businesses: 0 - label: 51–200 businesses: 0 - label: 200+ businesses: 0 items: type: object properties: label: type: string example: None businesses: type: integer example: 2 feature_adoption: type: array example: - key: sales_invoices label: 'Sales invoices' businesses: 1 share: 50 - key: pos label: 'POS counter' businesses: 1 share: 50 - key: payments label: 'Payments received' businesses: 0 share: 0 - key: stock label: 'Stock tracking' businesses: 0 share: 0 - key: purchases label: 'Purchase bills' businesses: 0 share: 0 - key: expenses label: Expenses businesses: 0 share: 0 - key: quotes_challans label: 'Quotes and challans' businesses: 0 share: 0 - key: email label: 'Emailed a document' businesses: 0 share: 0 - key: gst label: 'GST returns' businesses: 0 share: 0 items: type: object properties: key: type: string example: sales_invoices label: type: string example: 'Sales invoices' businesses: type: integer example: 1 share: type: integer example: 50 top_businesses: type: array example: - id: 1 name: 'Kavya Fashions' plan: Business invoices: 8 billed_value_paise: 960000 last_active_on: '2026-09-26' last_channel: both items: type: object properties: id: type: integer example: 1 name: type: string example: 'Kavya Fashions' plan: type: string example: Business invoices: type: integer example: 8 billed_value_paise: type: integer example: 960000 last_active_on: type: string example: '2026-09-26' last_channel: type: string example: both going_quiet: type: array example: - id: 3 name: 'Durga Textiles' last_active_on: '2026-09-05' quiet_days: 21 invoices_ever: 3 phone: '9848012345' items: type: object properties: id: type: integer example: 3 name: type: string example: 'Durga Textiles' last_active_on: type: string example: '2026-09-05' quiet_days: type: integer example: 21 invoices_ever: type: integer example: 3 phone: type: string example: '9848012345' 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 403: description: '' content: application/json: schema: type: object example: message: 'This action is unauthorized.' properties: message: type: string example: 'This action is unauthorized.' 404: description: '' content: application/json: schema: type: object example: message: 'Not found.' properties: message: type: string example: 'Not found.' 422: description: '' content: application/json: schema: type: object example: message: 'Choose a range of at most 366 days.' errors: from: - 'Choose a range of at most 366 days.' properties: message: type: string example: 'Choose a range of at most 366 days.' errors: type: object properties: from: type: array example: - 'Choose a range of at most 366 days.' items: type: string tags: - 'Platform overview' parameters: - in: path name: section description: 'today, growth, engagement or revenue.' example: engagement required: true schema: type: string /api/v1/fcm-test: post: summary: '' operationId: postApiV1FcmTest description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: sent: true token: eY2x9_e…n_token title: 'Stock alert' body: 'Rice is running low.' data: type: low_stock item_id: '7' properties: data: type: object properties: sent: type: boolean example: true token: type: string example: eY2x9_e…n_token title: type: string example: 'Stock alert' body: type: string example: 'Rice is running low.' data: type: object properties: type: type: string example: low_stock item_id: type: string example: '7' 404: description: '' content: application/json: schema: type: object example: message: 'Not Found.' properties: message: type: string example: 'Not Found.' 422: description: '' content: application/json: schema: type: object example: message: 'The data payload must be a flat object.' errors: data: - 'The data payload must be a flat object.' properties: message: type: string example: 'The data payload must be a flat object.' errors: type: object properties: data: type: array example: - 'The data payload must be a flat object.' items: type: string 503: description: '' content: application/json: schema: type: object example: message: 'Firebase push is disabled. Set FIREBASE_PUSH_ENABLED=true and provide a readable service-account credentials file.' properties: message: type: string example: 'Firebase push is disabled. Set FIREBASE_PUSH_ENABLED=true and provide a readable service-account credentials file.' tags: - 'Push testing' requestBody: required: true content: application/json: schema: type: object properties: fcm_token: type: string description: 'The device registration token to push to.' example: eY2x9_example_fcm_registration_token title: type: string description: 'Notification title, max 120 characters.' example: 'Stock alert' body: type: string description: 'Notification message, max 1000 characters.' example: 'Rice is running low.' data: type: object description: 'optional Flat key/value data payload. Values are delivered as strings.' example: type: low_stock item_id: '7' properties: {} nullable: true required: - fcm_token - title - body security: [] '/api/v1/businesses/{business}/invoices/{invoice}/workers': get: summary: "Read the invoice's current referrers." operationId: readTheInvoicesCurrentReferrers description: "Finance-only assignments are separate from customer-facing invoice payloads.\nCommission amounts are original earnings before return adjustments; use the referrer\nstatement for debit notes and current balances. Voided invoices have no active awards." parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Referrers and referral commissions' put: summary: 'Set all invoice referrers and commission rules.' operationId: setAllInvoiceReferrersAndCommissionRules description: "Replaces the complete assignment set atomically (maximum 10, no duplicate referrers).\nEvery performance share is required and shares must total 10000 (100%). Shares allocate\nsales, goods cost and shared invoice expenses exactly once; each commission is calculated\nindependently. An empty array removes all assignments through compensating corrections.\nFixed commissions reduce proportionally on returns, with cumulative paise rounding.\nChanging assignment rules keeps every previous earning, debit note and payout in history." parameters: [] responses: {} tags: - 'Referrers and referral commissions' requestBody: required: true content: application/json: schema: type: object properties: workers: type: array description: 'Complete assignment set, or [] to remove all.' example: - worker_id: 1 commission_kind: percentage rate_basis_points: 1000 attribution_basis_points: 6000 - worker_id: 2 commission_kind: fixed fixed_amount_paise: 20000 attribution_basis_points: 4000 items: type: object properties: worker_id: type: integer description: 'Active referrer in this business.' example: 1 commission_kind: type: string description: '' example: percentage enum: - percentage - fixed rate_basis_points: type: integer description: 'Percentage from 0 to 10000; omitted retains the saved rate or uses the referrer default for a new assignment.' example: 1000 fixed_amount_paise: type: integer description: 'Required for fixed commission, 0 to 99999999900.' example: null attribution_basis_points: type: integer description: 'Owner-entered share from 1 to 10000; shares total 10000.' example: 6000 required: - worker_id - commission_kind - attribution_basis_points required: - workers parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: invoice description: 'The invoice.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/workers': get: summary: 'List referrers.' operationId: listReferrers description: '' parameters: - in: query name: q description: 'Search name, trade or phone.' example: carpenter required: false schema: type: string description: 'Search name, trade or phone.' example: carpenter - in: query name: status description: '' example: active required: false schema: type: string description: '' example: active enum: - active - archived - all - in: query name: worker_type description: '' example: referral required: false schema: type: string description: '' example: referral enum: - referral - cook - carpenter - painter - electrician - plumber - doctor - rmp - contractor - sales_agent - other - in: query name: per_page description: 'From 1 to 100.' example: 20 required: false schema: type: integer description: 'From 1 to 100.' example: 20 responses: 200: description: '' content: application/json: schema: type: object example: data: [] current_page: 1 per_page: 20 total: 0 properties: data: type: array example: [] current_page: type: integer example: 1 per_page: type: integer example: 20 total: type: integer example: 0 tags: - 'Referrers and referral commissions' requestBody: required: false content: application/json: schema: type: object properties: q: type: string description: 'Must not be greater than 100 characters.' example: b nullable: true status: type: string description: '' example: active enum: - active - archived - all nullable: true worker_type: type: string description: '' example: architecto nullable: true per_page: type: integer description: 'Must be between 1 and 100.' example: 2 nullable: true post: summary: 'Add a referrer.' operationId: addAReferrer description: "Aadhaar is optional, encrypted at rest, and returned only as its masked last four digits.\nPAN and bank details are encrypted, excluded from audit metadata and raw model serialization.\nLists and write responses mask bank numbers; only the owner/administrator detail response includes the full number.\nPhotos are private and require the same workspace, plan and role checks as a profile." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: id: 1 name: Ravi worker_type: carpenter worker_type_label: Carpenter commission_rate_basis_points: 1000 is_active: true balance_paise: 0 commission_due_paise: 0 recoverable_paise: 0 aadhaar_masked: null pan_masked: null bank_account: null properties: data: type: object properties: id: type: integer example: 1 name: type: string example: Ravi worker_type: type: string example: carpenter worker_type_label: type: string example: Carpenter commission_rate_basis_points: type: integer example: 1000 is_active: type: boolean example: true balance_paise: type: integer example: 0 commission_due_paise: type: integer example: 0 recoverable_paise: type: integer example: 0 aadhaar_masked: type: string example: null nullable: true pan_masked: type: string example: null nullable: true bank_account: type: string example: null nullable: true tags: - 'Referrers and referral commissions' requestBody: required: true content: multipart/form-data: schema: type: object properties: name: type: string description: "Referrer's name." example: Ravi worker_type: type: string description: 'Defaults to referral when omitted.' example: carpenter enum: - referral - cook - carpenter - painter - electrician - plumber - doctor - rmp - contractor - sales_agent - other trade: type: string description: 'Trade or referral role.' example: Carpenter phone: type: string description: 'Phone number.' example: '9876543210' email: type: string description: 'Email address.' example: ravi@example.com aadhaar_number: type: string description: 'Optional 12 digits.' example: null pan_number: type: string description: 'Optional PAN, 5 letters + 4 digits + 1 letter; trimmed and uppercased. Blank retains the saved value.' example: ABCDE1234F clear_pan: type: boolean description: 'Remove the saved PAN.' example: false bank_account: type: object description: 'Optional payout bank details. Omit or leave all fields blank to retain; partial updates merge with saved details.' example: bank_name: 'Example Bank' account_holder: 'Ravi Kumar' account_number: '001234567890' ifsc: HDFC0000123 branch: Hyderabad properties: bank_name: type: string description: 'Bank name, up to 128 characters; required for a new bank account.' example: null account_holder: type: string description: 'Account holder, up to 128 characters; required for a new bank account.' example: null account_number: type: string description: '6–34 digits as a string, preserving leading zeroes; required for a new bank account.' example: null ifsc: type: string description: 'Valid 11-character IFSC, trimmed and uppercased; required for a new bank account.' example: null branch: type: string description: 'Optional branch, up to 128 characters; blank clears the branch when other bank fields are provided.' example: null clear_bank_account: type: boolean description: 'Remove all saved bank details.' example: false contact_id: type: integer description: "Customer account for the referrer's own purchases." example: null commission_rate_basis_points: type: integer description: 'Default percentage, 0 to 10000.' example: 1000 notes: type: string description: 'Internal notes, up to 2000 characters.' example: null photo: type: string format: binary description: 'Private JPEG, PNG or WebP, at most 2 MB and 8192 pixels per side. Omit to retain.' remove_photo: type: boolean description: 'Set true to remove the saved photo; cannot be combined with an upload.' example: false required: - name parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business}/workers/{worker}': get: summary: 'Referrer profile and commission statement.' operationId: referrerProfileAndCommissionStatement description: "Earnings are independent of collection. The statement retains every payout, debit note\nand compensating correction. A negative balance is recoverable from the referrer. Referred\ninvoice dues and the referrer's own customer-account dues are separate current balances.\n\nFull bank account numbers are returned only here for owners and administrators; fetch fresh and do not cache offline.\nAadhaar and PAN remain masked. The response is served with Cache-Control: no-store, private." parameters: - in: query name: period description: '' example: this-financial-year required: false schema: type: string description: '' example: this-financial-year enum: - today - this-month - last-month - this-financial-year - last-financial-year - custom - in: query name: from description: 'Custom period start, Y-m-d; both dates required.' example: null required: false schema: type: string description: 'Custom period start, Y-m-d; both dates required.' example: null - in: query name: to description: 'Inclusive custom end, Y-m-d.' example: null required: false schema: type: string description: 'Inclusive custom end, Y-m-d.' example: null - in: query name: page description: 'Statement page.' example: 1 required: false schema: type: integer description: 'Statement page.' example: 1 - in: query name: per_page description: 'From 1 to 100.' example: 20 required: false schema: type: integer description: 'From 1 to 100.' example: 20 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Referrers and referral commissions' requestBody: required: false content: application/json: schema: type: object properties: per_page: type: integer description: 'Must be between 1 and 100.' example: 2 nullable: true page: type: integer description: 'Must be at least 1.' example: 22 nullable: true put: summary: 'Update or archive a referrer.' operationId: updateOrArchiveAReferrer description: "Archiving preserves financial history and prevents new assignments. Changing a default\npercentage affects future assignments only. Leave Aadhaar blank to retain it; pass\n`clear_aadhaar=true` to remove it. Existing payouts and invoice rates are retained." parameters: [] responses: {} tags: - 'Referrers and referral commissions' requestBody: required: true content: multipart/form-data: schema: type: object properties: name: type: string description: "Referrer's name." example: Ravi worker_type: type: string description: 'Omit to retain the saved type.' example: carpenter enum: - referral - cook - carpenter - painter - electrician - plumber - doctor - rmp - contractor - sales_agent - other trade: type: string description: 'Trade or referral role.' example: Carpenter phone: type: string description: 'Phone number.' example: '9876543210' email: type: string description: 'Email address.' example: ravi@example.com aadhaar_number: type: string description: 'Optional 12 digits.' example: null pan_number: type: string description: 'Optional PAN, 5 letters + 4 digits + 1 letter; trimmed and uppercased. Blank retains the saved value.' example: ABCDE1234F clear_pan: type: boolean description: 'Remove the saved PAN.' example: false bank_account: type: object description: 'Optional payout bank details. Omit or leave all fields blank to retain; partial updates merge with saved details.' example: bank_name: 'Example Bank' account_holder: 'Ravi Kumar' account_number: '001234567890' ifsc: HDFC0000123 branch: Hyderabad properties: bank_name: type: string description: 'Bank name, up to 128 characters; required for a new bank account.' example: null account_holder: type: string description: 'Account holder, up to 128 characters; required for a new bank account.' example: null account_number: type: string description: '6–34 digits as a string, preserving leading zeroes; required for a new bank account.' example: null ifsc: type: string description: 'Valid 11-character IFSC, trimmed and uppercased; required for a new bank account.' example: null branch: type: string description: 'Optional branch, up to 128 characters; blank clears the branch when other bank fields are provided.' example: null clear_bank_account: type: boolean description: 'Remove all saved bank details.' example: false clear_aadhaar: type: boolean description: 'Remove the stored identifier.' example: false contact_id: type: integer description: 'Linked own customer account.' example: null commission_rate_basis_points: type: integer description: 'Default rate, 0 to 10000.' example: 1000 is_active: type: boolean description: 'Whether new referrals can be assigned.' example: true notes: type: string description: 'Internal notes.' example: null photo: type: string format: binary description: 'Private JPEG, PNG or WebP, at most 2 MB and 8192 pixels per side. Omit to retain.' remove_photo: type: boolean description: 'Set true to remove the saved photo; cannot be combined with an upload.' example: false required: - name parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: worker description: 'The worker.' example: architecto required: true schema: type: string '/api/v1/businesses/{business}/workers/{worker}/photo': get: summary: "View the referrer's private photo." operationId: viewTheReferrersPrivatePhoto description: "Production photos use private S3; legacy photos use their recorded disk.\nThis authenticated route never exposes an object key or public bucket URL.\n\nReturns an authenticated private image, never a public storage URL." parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 404: description: '' content: application/json: schema: type: object example: message: 'Not Found' properties: message: type: string example: 'Not Found' tags: - 'Referrers and referral commissions' parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: worker description: 'The worker.' example: architecto required: true schema: type: string '/api/v1/businesses/{business}/workers/{worker}/payouts': post: summary: 'Pay earned commission.' operationId: payEarnedCommission description: "Partial/full payment is the owner's decision, independent of customer receipts. Paying\nreduces commission payable and the selected cash/bank account, without adding another\nexpense. Cash requires an open register. Overpayments are rejected; retries require the\nsame UUID and identical amount/date/method/account/reference, otherwise 422." parameters: [] responses: {} tags: - 'Referrers and referral commissions' requestBody: required: true content: application/json: schema: type: object properties: amount_paise: type: integer description: 'Positive paise, no more than current commission due.' example: 30000 occurred_on: type: string description: 'Payment date, Y-m-d.' example: '2026-10-04' payment_method: type: string description: '' example: bank enum: - cash - bank - upi - card - other payment_account_id: type: integer description: 'Active compatible payment account in this business.' example: null nullable: true reference: type: string description: 'Transfer reference, up to 128 characters.' example: null nullable: true idempotency_key: type: string description: 'Stable UUID for retries.' example: e0e7b8fb-96bb-44bd-95f8-aad1eb6cf473 required: - amount_paise - occurred_on - payment_method - idempotency_key parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: worker description: 'The worker.' example: architecto required: true schema: type: string '/api/v1/businesses/{business}/workers/{worker}/payouts/{entry}/void': post: summary: 'Void a referrer payout.' operationId: voidAReferrerPayout description: "Adds a current-date compensating event and reverses the cash/bank posting. Original\nearning, payout and return records remain. Repeated void requests do not duplicate it." parameters: [] responses: {} tags: - 'Referrers and referral commissions' requestBody: required: true content: application/json: schema: type: object properties: reason: type: string description: 'Why the payment is being corrected, up to 1000 characters.' example: 'Incorrect bank transfer' required: - reason parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: worker description: 'The worker.' example: architecto required: true schema: type: string - in: path name: entry description: '' example: architecto required: true schema: type: string '/api/v1/businesses/{business}/invoices/{invoice}/worker': post: summary: 'Assign the invoice referrer and earn commission.' operationId: assignTheInvoiceReferrerAndEarnCommission description: "Assignment is idempotent for an unchanged referrer/rate/invoice. Overrides require an\nowner or administrator. Reassignment appends financial corrections on the original dates;\nprior payouts remain in the previous referrer's account. Existing returns receive debit\nnotes using cumulative rounding. Customer invoice print/shared payloads omit commissions." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 1 worker_id: 1 invoice_id: 1 rate_basis_points: 1000 basis_paise: 300000 earned_paise: 30000 earned_on: '2026-01-10' properties: data: type: object properties: id: type: integer example: 1 worker_id: type: integer example: 1 invoice_id: type: integer example: 1 rate_basis_points: type: integer example: 1000 basis_paise: type: integer example: 300000 earned_paise: type: integer example: 30000 earned_on: type: string example: '2026-01-10' tags: - 'Referrers and referral commissions' requestBody: required: true content: application/json: schema: type: object properties: attribution_basis_points: type: integer description: 'Owner-entered single-referrer share; must be 10000.' example: 10000 worker_id: type: integer description: 'Active referrer in this business.' example: 1 rate_basis_points: type: integer description: 'Optional override from 0 to 10000; omitted uses the referrer default.' example: 1000 nullable: true required: - attribution_basis_points - worker_id parameters: - in: path name: business description: 'The business.' example: 1 required: true schema: type: integer - in: path name: invoice description: 'The invoice.' example: 1 required: true schema: type: integer '/api/v1/businesses/{business_id}/usage-report': post: summary: 'Report aggregate phone usage.' operationId: reportAggregatePhoneUsage description: "Available to every active member, including free workspaces with unfinished onboarding.\nMaximum 64 KB and 30 requests per hour per user, independent of the general API limit.\nReusing a report_id returns 204 without applying event deltas again (90-day deduplication window).\nUnknown numeric metrics are retained; unknown text fields are discarded, never stored.\nMetric keys must be snake_case, at most 64 characters. Counts must be JSON integers,\nnot numeric strings. Server dates use Asia/Kolkata; phone timestamps are metadata.\nTotals replace the day's snapshot; events accumulate. Competing local installs keep the\nhigher invoice count (ties retain the existing snapshot). Server mode may omit totals/money." parameters: [] responses: 204: description: '' content: application/json: schema: type: object example: {} properties: {} 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 404: description: '' content: application/json: schema: type: object example: message: 'Not found.' properties: message: type: string example: 'Not found.' 413: description: '' content: application/json: schema: type: object example: message: 'Usage reports must not exceed 64 KB.' properties: message: type: string example: 'Usage reports must not exceed 64 KB.' 422: description: '' content: application/json: schema: type: object example: message: 'The report id field is required.' errors: report_id: - 'The report id field is required.' properties: message: type: string example: 'The report id field is required.' errors: type: object properties: report_id: type: array example: - 'The report id field is required.' items: type: string 429: description: '' content: application/json: schema: type: object example: message: 'Too Many Attempts.' properties: message: type: string example: 'Too Many Attempts.' tags: - 'Usage reports' requestBody: required: true content: application/json: schema: type: object properties: report_id: type: string description: 'Unique report UUID.' example: 9b2f6c1e-4a7d-4f0e-9a51-3d2c8e7b6a10 installation_id: type: string description: 'Random installation UUID, never a hardware ID.' example: 5d8a1f3c-7e20-4b6a-8c11-0f9e2d4b7a33 reported_at: type: string description: 'ISO 8601 timestamp with offset.' example: '2026-09-27T09:14:05+05:30' period_start: type: string description: 'Nullable ISO 8601 timestamp with offset.' example: '2026-09-26T08:02:11+05:30' nullable: true mode: type: string description: 'local or server.' example: local app: type: object description: 'Application metadata.' example: [] properties: version: type: string description: 'Version, maximum 32 characters.' example: 1.1.1 build: type: integer description: 'Nonnegative build number.' example: 20 platform: type: string description: 'android or ios.' example: android os_version: type: string description: 'Must match the regex /^[a-zA-Z0-9._+-]+$/. Must not be greater than 32 characters.' example: g locale: type: string description: 'Must match the regex /^[a-zA-Z_-]+$/. Must not be greater than 32 characters.' example: en_MT theme: type: string description: 'Must match the regex /^[a-z_]+$/. Must not be greater than 32 characters.' example: m required: - version - build - platform totals: type: object description: 'Running record counts, each integer 0–10000000.' example: invoices: 5 customers: 2 properties: {} money: type: object description: 'Running sums in paise, each integer -100000000000000–100000000000000.' example: billed_value_paise: 120000 properties: {} milestones: type: object description: 'Nullable ISO timestamps: earliest first_* and onboarding_completed_at, latest last_*.' example: first_invoice_at: '2026-09-27T09:10:00+05:30' properties: first_invoice_at: type: string description: 'Must be a valid date. Must match the regex /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/D.' example: '2026-01-15' nullable: true last_invoice_at: type: string description: 'Must be a valid date. Must match the regex /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/D.' example: '2026-01-15' nullable: true first_payment_at: type: string description: 'Must be a valid date. Must match the regex /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/D.' example: '2026-01-15' nullable: true last_payment_at: type: string description: 'Must be a valid date. Must match the regex /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/D.' example: '2026-01-15' nullable: true first_record_at: type: string description: 'Must be a valid date. Must match the regex /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/D.' example: '2026-01-15' nullable: true last_record_at: type: string description: 'Must be a valid date. Must match the regex /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/D.' example: '2026-01-15' nullable: true onboarding_completed_at: type: string description: 'Must be a valid date. Must match the regex /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/D.' example: '2026-01-15' nullable: true first_share_at: type: string description: 'Must be a valid date. Must match the regex /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/D.' example: '2026-01-15' nullable: true events: type: object description: 'At most 300 action deltas, each integer 0–100000.' example: invoice_created: 5 invoice_shared_whatsapp: 2 properties: {} required: - report_id - installation_id - reported_at - mode - app parameters: - in: path name: business_id description: 'Business ID.' example: 1 required: true schema: type: integer