Masters
Masters are the reference data a voucher points at: ledgers, stock items, groups, units, godowns, voucher types. They are created by the customer, in their company, and named however they chose.
Why they cause most first-integration failures
A voucher payload references masters by name:
{
"party_ledger": "Example Customer",
"inventory_entries": [
{ "stock_item": "Example Item", "unit": "Nos", "godown": "Main Location" }
],
"ledger_entries": [
{ "ledger_name": "Sales" }
]
}Every one of those strings must match something that exists in the target company, exactly.
The failure is unhelpfully shaped: the request is well-formed, the API accepts it, success: true comes back — and the job fails later at the Tally boundary. If you are not checking transaction status, it looks like the write silently vanished.
Two strategies
Pre-create masters. Fine while developing, and fine for a small number of customers you onboard by hand.
Create them through the API. Necessary at scale, because you cannot ask 400 customers to create ledgers by hand and get it right.
POST /api/v1/masters/{type}
Authorization: Bearer {key_id}:{secret}
Content-Type: application/jsonMaster creation is asynchronous, like everything else touching Tally:
GET /api/v1/masters/{type}?company_id={company_id} # list jobs
GET /api/v1/masters/{type}/{job} # one job's statusA safe write sequence
Wait for master creation to complete before submitting the voucher that depends on it. Submitting both at once and hoping the ordering works out is a race, and it will resolve differently under load.
Reading masters back from Tally
GET /api/v1/masters/{type} reports your pushes. To answer "does this master exist, and how is it configured?" you want the other direction — the masters the Connector has read out of the customer's company:
GET /api/v1/pulled-masters/{type}?company_id={company_id}
Authorization: Bearer {key_id}:{secret}company_id is required and is one of your own company ids. Page with limit (max 200) and offset. This endpoint requires the pull.master capability on your key and the masters pull enabled for that company; without both, you will see an empty list rather than an error.
Each row carries the master's fields under payload:
{
"success": true,
"master_type": "voucher_type",
"total": 24,
"count": 24,
"masters": [
{
"master_type": "voucher_type",
"master_name": "Sales Bizmitra",
"tally_master_guid": "…",
"request_type": "pull",
"payload": {
"kind": "master",
"master_type": "voucher_type",
"name": "Sales Bizmitra",
"fields": {
"aliases": [],
"parent": "Sales",
"reserved_name": "Sales",
"numbering_method": "Manual",
"numbering_sub_method": "Auto Renumber",
"numbering_series": [
{
"name": "Default",
"numbering_method": "Manual",
"numbering_sub_method": "Auto Renumber",
"prevent_duplicates": "No",
"is_default": "No"
}
],
"is_active": "Yes",
"affects_stock": "No"
},
"tally": { "guid": "…", "master_id": "…", "alter_id": "914" }
}
}
]
}What each type exposes
payload.fields varies by type. Every type also carries name, aliases (see Aliases) and tally.guid / master_id / alter_id.
| Type | Fields |
|---|---|
group | parent, is_subledger, is_deemed_positive, is_revenue, is_billwise |
ledger | parent, currency, opening_balance, gstin, gst_registration_type, mobile, email, is_billwise |
currency | mailing_name, original_name, expanded_symbol, iso_code, decimal_places |
unit | is_simple_unit, decimal_places, base_units, additional_units |
godown | parent |
stock_group | parent, base_units, is_batchwise, closing_balance, closing_value |
stock_category | parent |
stock_item | parent, category, base_units, opening_balance, closing_balance, closing_value, gst_applicable |
voucher_type | parent, reserved_name, numbering_method, numbering_sub_method, numbering_series, is_active, affects_stock |
Values are Tally's, verbatim
Fields are passed through unmodified — no casing, type coercion, or normalization. Booleans arrive as Tally's "Yes" / "No" strings, not JSON true / false, and text like numbering_method arrives as Tally spells it. Compare case-insensitively rather than against an exact literal.
Whether a ledger keeps bills
is_billwise is Tally's Maintain balances bill-by-bill setting on that ledger, as "Yes" or "No". It matters more than its size suggests, because it decides whether bill references you send are kept at all.
is_billwise | Sending bill_allocations to this ledger |
|---|---|
Yes | The reference is kept. An invoice opens a bill; a receipt or payment can settle against it |
No | Tally accepts the voucher and keeps no bill reference. Nothing errors, and nothing can be matched to it later |
So the useful places to read it are both ends of the integration:
- At setup, warn when a party ledger you are about to transact against is not configured bill-by-bill. It is a one-line fix in Tally for the customer, and a permanently unmatchable ledger if nobody notices.
- Before sending, send
bill_allocationsonly to ledgers that will keep them.
const { masters } = await api.get('/api/v1/pulled-masters/ledger', {
params: { company_id: companyId },
})
const party = masters.find(m => m.master_name === partyLedger)
// Tally's strings, compared case-insensitively — see the warning above.
if ((party?.payload?.fields?.is_billwise ?? '').toLowerCase() !== 'yes') {
warn(`${partyLedger} does not keep balances bill-by-bill — bill references sent to it are dropped`)
}Tally omits the underlying tag entirely on some builds and returns No on others; Bizmitra normalizes both to a plain "No", so you never have to read a missing value as "off".
Bill references themselves are covered in Pulling vouchers and Pushing a receipt.
Verifying customer setup
This is the endpoint to build an admin or preflight screen on, instead of relying on someone remembering what they configured during onboarding.
The common check is voucher-type numbering. If your integration posts vouchers with numbers you generate, you need to know whether Tally will keep your number or assign its own. numbering_method is one of Tally's five settings:
numbering_method | Your voucher_number |
|---|---|
Manual | kept |
Automatic (Manual Override) | kept — this is Tally's default on a new voucher type |
Automatic | discarded; Tally assigns its own |
Multi-user Auto | discarded; Tally assigns its own |
None | the voucher carries no number at all |
const { masters } = await api.get('/api/v1/pulled-masters/voucher_type', {
params: { company_id: companyId },
})
// Allowlist what keeps your number, so an unrecognised value warns rather than
// passing silently — a missed warning means a wrong number on a real invoice.
const KEEPS_YOUR_NUMBER = ['manual', 'automatic (manual override)']
const type = masters.find(m => m.master_name === postingVoucherType)
const method = (type?.payload?.fields?.numbering_method ?? '').toLowerCase()
if (!KEEPS_YOUR_NUMBER.includes(method)) {
warn(`${postingVoucherType} is set to "${method}" — Tally will assign its own number`)
}Where this value comes from
TallyPrime keeps a voucher type's numbering on its numbering series, not on the voucher type itself. The voucher type carries a field of the same name, but Tally stamps it once at creation and never updates it — so it commonly reads Automatic (Manual Override), or None on types Tally derives itself, no matter what the voucher-type screen shows. numbering_method here is read from the series, so it matches the screen.
numbering_sub_method is the "numbering behaviour on insertion/deletion" setting: Auto Renumber (Renumber Vouchers), Auto Retain (Retain Original Voucher No.), or Default when it was never changed.
A voucher type can run several numbering series, in which case no single value describes it — numbering_method reflects the default series, and numbering_series lists them all.
Two fields make this check more robust than name matching alone:
reserved_name identifies which built-in type a custom or renamed type derives from. Tally sets it only on its own types (Sales, Purchase, Receipt, …) and keeps it through a rename, so a type the customer renamed Branch Billing still reports reserved_name: "Sales". Classify on this, not on the display name.
tally.alter_id increments whenever the master is altered in Tally. Store it at setup and compare on later reads to flag "someone changed this since we verified it" — rather than only ever seeing current state.
Pair your own records on tally_master_guid, as with pushes: it survives renames and re-imports, where the numeric master_id is per-company and the name can change.
Freshness
Pulled masters refresh on the Connector's normal sync cycle, not on demand. A master created in Tally seconds ago may not be in this list yet, so treat a miss as "not seen yet" rather than "does not exist" when you are racing a customer's data entry.
Naming
Master names are the customer's data. Bizmitra does not normalize, trim, or case-fold them, and should not — quietly matching example customer to Example Customer would eventually post a transaction into the wrong ledger.
Practical consequences:
- Normalize on your side before sending, and be consistent about it.
- Watch for trailing whitespace. It is invisible, survives copy-paste, and is a real cause of mismatches.
- Store the resolved master name against your own record once it works, rather than reconstructing it each time.
Aliases
A Tally master can answer to more than one name. The customer sets an alias so they can type EC instead of Example Customer during voucher entry, and it is genuinely how many of them work day to day — which means the name your user gives you is quite often an alias, not the master name.
aliases comes back on every pulled master:
{
"master_type": "ledger",
"master_name": "Example Customer",
"payload": {
"kind": "master",
"master_type": "ledger",
"name": "Example Customer",
"fields": {
"aliases": ["EC", "EXC"],
"parent": "Sundry Debtors",
"gstin": "24AAAAA0000A1Z5"
},
"tally": { "guid": "…", "master_id": "…", "alter_id": "917" }
}
}It is a list, and a master can hold several. The ledger above answers to Example Customer, EC and EXC, and all three work in Tally's voucher entry. Code that reads aliases[0] and stops will match one customer's shorthand and miss another's.
Every master type carries it, and it is [] — never null and never absent — when the master has no alias. Tally's own built-in groups ship with aliases already set (Indirect Expenses also answers to Expenses (Indirect)), so you will see populated lists in a company nobody has customized.
The practical use is resolution, not sending: match what your user typed against name and aliases, then send the name. Store the resolved name against your record, as with any other master — it is the value the rest of the API reports and the one your mapping should hold.
// Resolve a user-supplied name to the master it means.
const { masters } = await api.get('/api/v1/pulled-masters/ledger', {
params: { company_id: companyId },
})
const wanted = input.trim().toLowerCase()
const match = masters.find(m => {
const fields = m.payload?.fields ?? {}
return [m.payload?.name, ...(fields.aliases ?? [])]
.some(n => n?.trim().toLowerCase() === wanted)
})
// Send the master's own name, not what the user typed.
const partyLedger = match?.payload?.nameRequires connector v0.0.44
Older connectors return no aliases key at all. Read it defensively (fields.aliases ?? []) so a company on an older build degrades to name-only matching rather than throwing.
Aliases change without the name changing
Adding an alias is an alter, so tally.alter_id moves and the master re-syncs — but name is unchanged, and a check that compares only names will report "nothing changed". If you cache a resolution table, rebuild it on alter_id, not on the name.
Do not create masters casually
Every ledger and stock item you create appears in the customer's books, in their reports, and in front of their accountant. A stray Customer Name (2) from a retry is a real annoyance for them.
- Check before creating — see Reading masters back from Tally.
- Make creation idempotent on your side.
- Do not auto-create from unvalidated user input. A typo in your product becomes a permanent ledger in their accounts.
Group and hierarchy data
Stock groups can be read as a tree:
GET /api/v1/stock-treeUseful for showing the customer their own catalogue structure when mapping your product's items to theirs — which is generally a better onboarding experience than asking them to type names that must match exactly.