Operations
Every action available on the API, read from the running server — so it is always what the software actually supports.
Calling an operation
POST http://localhost:4173/api/v1/tally/receipts.create
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
Idempotency-Key: payment-pay_9KfX2
{ "date": "2026-04-02", "received_into": "HDFC Bank", "received_from": "Aurora Traders", "amount": 5000 }Send the same Idempotency-Key when you retry a write and it lands on the same record instead of creating a second one. Without the header, an identical body is already recognised as a retry.
Ledgers and stock items
What should happen to a record that already exists is chosen by the operation you call, not by a setting:
| Operation | When the name already exists |
|---|---|
ledgers.create | Fails with ledger_exists (409). Nothing is changed. |
ledgers.find_or_create | Returns the existing ledger untouched, with meta.outcome: found. |
ledgers.create_or_update | Updates it from the fields you sent. |
ledgers.update | Updates an existing ledger, and can rename it; a missing one is ledger_not_found. |
Stock items follow the same four. Units, stock groups and godowns only have find_or_create: an existing one is always the one you want. Every variant keeps TallyPrime's own safety checks — a same-named ledger under another group, or with another party's GSTIN, is refused rather than reused.
The catalogue
The catalogue could not be loaded
Triggers
A trigger starts a workflow when something is entered directly in TallyPrime: a sales invoice, a receipt, a new ledger. Each one is a GET list your automation platform polls on its own schedule, the way Zapier, Make, n8n, Pipedream and Pabbly Connect build polling triggers:
The newest record comes first, and every record has a stable
idto tell new from seen.sequenceonly grows, for platforms that compare numbers;after_sequencereturns only newer ones.detected_atis when Balancely first saw the entry.dateis the voucher's own date, which may be in the past.The first call already has the latest entries, so a new trigger has samples. Nothing older is ever sent as new.
Entries Balancely itself posted are left out, so a workflow is never started by its own entry.
Cancelled, optional and post-dated vouchers are left out too.
When the computer is off
200 with what is already known, and meta.up_to_date is false with a reason such as connector_offline. When the computer is back, the next poll brings everything entered in between.Settling bills
A receipt or payment settles open bills with settle_bills, each line naming the bill by bill_name. GET /api/v1/tally/lists/outstanding_bills?party=… lists the bills still open for a party. A line with an amount but no bill_name is refused rather than posted On Account; whatever no bill takes is On Account.
{
"date": "2026-04-02",
"received_into": "HDFC Bank",
"received_from": "Aurora Traders",
"amount": 5000,
"settle_bills": [ { "bill_name": "INV-1042", "amount": 5000 } ]
}vouchers.get reads a voucher back in full. Its amount is the plain total; on ledger_lines a negative amount is a debit and a positive one a credit. On a receipt or payment, party is the customer or supplier, not the bank or cash account.
Sending many at once
Up to fifty operations in one call, run in order, each answered on its own — a failing row does not stop the rest. Each write that succeeds counts as one operation; reads in a batch are free.
{
"operations": [
{ "operation": "receipts.create", "input": { "date": "2026-04-02", "received_into": "Cash", "received_from": "Aurora Traders", "amount": 5000 } },
{ "operation": "receipts.create", "input": { "date": "2026-04-02", "received_into": "Cash", "received_from": "Nimbus Retail", "amount": 800 } },
{ "operation": "ledgers.get", "input": { "name": "Aurora Traders" } }
]
}
→ {
"data": [
{ "index": 0, "operation": "receipts.create", "status": 201, "data": { "object": "receipt", … }, "meta": { "outcome": "created" } },
{ "index": 1, "operation": "receipts.create", "status": 422, "error": { "code": "ledger_not_found", … } },
{ "index": 2, "operation": "ledgers.get", "status": 200, "data": { "object": "ledger", … } }
],
"meta": { "request_id": "req_…", "operation": "batch", "count": 3, "succeeded": 2, "failed": 1 }
}Example: a sales invoice
{
"date": "2026-04-12",
"customer": "Aurora Traders",
"sales_ledger": "Sales - GST 18%",
"invoice_number": "INV-1042",
"items": [
{
"item": "Widget A",
"quantity": 10,
"rate": 250,
"unit": "Nos",
"godown": "Main Location"
}
],
"taxes": [
{ "tax_ledger": "CGST", "amount": 225 },
{ "tax_ledger": "SGST", "amount": 225 }
],
"charges": [ { "ledger": "Freight", "amount": 100 } ],
"round_off": { "ledger": "Round Off", "amount": -0.5 },
"bill_reference": { "name": "INV-1042", "due_date": "2026-05-12" },
"narration": "Order #4471"
}godown is only required when the company uses more than one; with a single godown it is filled in automatically. unit falls back to the stock item's own base unit, and a line's amount defaults to quantity multiplied by rate.
Something missing on this page? Tell us and we will fix it.