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:

OperationWhen the name already exists
ledgers.createFails with ledger_exists (409). Nothing is changed.
ledgers.find_or_createReturns the existing ledger untouched, with meta.outcome: found.
ledgers.create_or_updateUpdates it from the fields you sent.
ledgers.updateUpdates 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
prerender

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 id to tell new from seen.

  • sequence only grows, for platforms that compare numbers; after_sequence returns only newer ones.

  • detected_at is when Balancely first saw the entry. date is 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
A trigger still answers 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.

POST /api/v1/tally/receipts.create
{
  "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.

POST /api/v1/tally/batch
{
  "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

POST /api/v1/tally/sales_invoices.create
{
  "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.