Endpoints
GET/api/v1/inventoryinventory:read
List lots in stock
Every lot with kg remaining > 0, newest received first, with its SKU, godown and cost.
godown_code — Only lots in this godown.yarn_type — Only SKUs of this yarn type.limit — Rows per page, 1–1000 (default 200).offset — Rows to skip, 0–100000. Use next_offset from the previous page.
GET/api/v1/salessales:read
List sales
Sales in a date window (India business days, IST), newest first. Cancelled sales are excluded. Defaults to the last 30 days.
from — First business day (YYYY-MM-DD). Default: 30 days ago.to — Last business day (YYYY-MM-DD), inclusive. Default: today.limit — Rows per page, 1–1000 (default 200).offset — Rows to skip, 0–100000. Use next_offset from the previous page.
GET/api/v1/export/customersexports:read
Customers CSV
Every customer with credit limit and totals. `outstanding` has credit notes netted off.
Returns `text/csv` with a header row: `id`, `name`, `contact`, `gstin`, `address`, `credit_limit`, `total_billed`, `total_paid`, `outstanding`. Not paginated — the whole export in one file (each source table is capped at 10,000 rows). Errors are JSON, in the standard envelope.
GET/api/v1/export/expensesexports:read
Expenses CSV
Expenses, newest first, with who recorded each.
Returns `text/csv` with a header row: `date`, `category`, `amount`, `notes`, `created_by`. Not paginated — the whole export in one file (each source table is capped at 10,000 rows). Errors are JSON, in the standard envelope.
GET/api/v1/export/mill-paymentsexports:read
Mill invoices and payments CSV
One row per mill invoice (`type=invoice`) and per payment (`type=payment`), newest first.
Returns `text/csv` with a header row: `supplier_name`, `type`, `date`, `amount`, `reference`, `method`. Not paginated — the whole export in one file (each source table is capped at 10,000 rows). Errors are JSON, in the standard envelope.
GET/api/v1/openapi.jsonpublic
This document
The OpenAPI 3.1 description of the API. Public, no key needed, cacheable.
Rate limits
Per key, per minute, by the organisation's plan. The REST API itself needs Business or above — the lower plans are listed because the limiter knows them, but their requests are refused with plan_required before any limit applies.
| Plan | Requests / minute |
|---|
| starter | 60 |
| growth | 300 |
| business | 1,200 |
| scale | 3,000 |
| enterprise | 6,000 |
Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix seconds). A 429 adds Retry-After.
Webhook signatures
Each delivery is a POST with four headers: X-YarnTally-Signature, X-YarnTally-Timestamp, X-YarnTally-Event and X-YarnTally-Delivery. The signature is the lower-case hex HMAC-SHA256 of <timestamp>.<raw body> using your endpoint's signing secret. Reject anything older than 300 seconds, compare in constant time, and verify the raw bytes before parsing JSON. Events: sale.created, sale.cancelled, sale.dispatched, invoice.paid, customer.created, stock.low, compliance.due.
// Verify a YarnTally webhook (Node.js)
import crypto from "crypto";
const SECRET = process.env.YARNTALLY_WEBHOOK_SECRET; // shown once, at creation
export function verify(rawBody, headers) {
const signature = headers["x-yarntally-signature"];
const timestamp = headers["x-yarntally-timestamp"]; // unix seconds
if (!signature || !timestamp) return false;
// Reject replays. We re-send the ORIGINAL timestamp on retries, so allow
// 300 seconds.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
// Sign the timestamp, a dot, then the raw body — bytes exactly as received,
// before any JSON parsing or re-serialising.
const expected = crypto
.createHmac("sha256", SECRET)
.update(timestamp + "." + rawBody)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
# Verify a YarnTally webhook (Python 3)
import hmac, hashlib, time
SECRET = b"whsec_..." # shown once, when the endpoint was created
def verify(raw_body: bytes, headers) -> bool:
signature = headers.get("X-YarnTally-Signature")
timestamp = headers.get("X-YarnTally-Timestamp") # unix seconds
if not signature or not timestamp:
return False
if abs(time.time() - int(timestamp)) > 300:
return False # replay
expected = hmac.new(SECRET, timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)