TallyPip API

Connect an online store, a booking system, an automation tool or your own code to a business's books in TallyPip: add customers, invoices, payments, bills and expenses, and read what's there. Everything an app adds goes through the same checks as on screen and lands in balanced books.

The guides below cover how it works; the API reference in the menu has every object, its fields, and each call with examples you can try. Running a business on TallyPip and just want to connect an app? See Connect other apps in the Help Center.

Getting started

The API lives at https://tallypip.com/api/v1. It takes and gives JSON over HTTPS. Building an app? Make a free sandbox business with sample books to try it against. For a real business, ask its owner or an admin to make you a key in TallyPip under Settings › Connected apps, then check it works:

curl https://tallypip.com/api/v1/me \
  -H "Authorization: Bearer tpk_your_key"

It answers with the business, the key and the person it acts for:

{
  "object": "connection",
  "business": { "id": "…", "name": "Oak Street Dental", "country": "US", "currency": "USD" },
  "key": { "id": "…", "name": "Zapier", "starts_with": "tpk_Ab12Cd", "access": "write" },
  "acting_as": { "name": "Jordan Lee", "role": "owner" }
}

Keys and what they can do

  • A key belongs to one business and acts as the person who made it, with their role there (Owner, Admin, Accountant or Standard). If they leave the team, the key stops working.
  • Read only keys can only read. Read and change keys can also add things.
  • Send it in the Authorization header as Bearer tpk_…. Keep it on a server: the API doesn't answer browsers on other sites, so a key never needs to be in a web page or an app people download.
  • The key is shown once when it's made. TallyPip keeps only a fingerprint of it, so a lost key can't be shown again: revoke it and make a new one.
  • A locked business (its trial or plan ended) or a closed one can still be read, not added to.
  • Building an app many businesses will use? Skip the copying: let them connect it in one click with Connect to TallyPip (OAuth 2.0). Its access tokens (tpo_…) work everywhere a key does.

Money, dates and ids

  • Money is whole cents in the business's currency: "total_cents": 12550 is 125.50. Fields that are money end in _cents.
  • Dates are YYYY-MM-DD; times are ISO 8601 in UTC (created_at).
  • Tax rates are percentages (8.875); quantities are numbers with up to 3 decimals (2.5).
  • Ids are UUIDs. Every object says what it is: "object": "invoice".
  • An invoice's status is draft (not in the books yet), sent (in the books and owing), paid or void. A bill's is open, paid or void.
  • Names you don't send are left out or take their usual default; names the endpoint doesn't know are refused, so a typo is never silently ignored.

All endpoints

All paths are under https://tallypip.com/api/v1.

RequestKeyWhat it does
GET /meAnyWhich business and person the key acts for. Use it to check a key works.
GET /customersAnyCustomers, newest first, with what each owes on sent invoices.Filters: email, name (exact, ignoring capitals: to find one before adding it)
POST /customersRead and changeAdd a customer.
GET /customers/{id}AnyOne customer.
GET /projectsAnyProjects (a job for a customer), newest first, each with its income, costs, labour and profit so far.Filters: customer_id, status (not_started, in_progress, completed, canceled)
POST /projectsRead and changeStart a project for a customer.
GET /projects/{id}AnyOne project with its numbers.
PATCH /projects/{id}Read and changeChange a project: send only what changes (its status, to complete or cancel it; its dates; its name).
GET /classesAnyClasses (Settings › Advanced), newest first, each with its full name.
POST /classesRead and changeAdd a class (or a sub-class, with parent_id).
GET /classes/{id}AnyOne class.
PATCH /classes/{id}Read and changeChange a class: its name, what it sits under, or make it inactive.
GET /locationsAnyLocations (or what the business calls them: stores, properties…), newest first.
POST /locationsRead and changeAdd a location (or a sub-location, with parent_id).
GET /locations/{id}AnyOne location.
PATCH /locations/{id}Read and changeChange a location: its name, what it sits under, or make it inactive.
GET /vendorsAnyVendors, newest first.Filters: email, name (exact, ignoring capitals)
POST /vendorsRead and changeAdd a vendor.
GET /vendors/{id}AnyOne vendor.
GET /productsAnyProducts and services, newest first.
POST /productsRead and changeAdd a service or non-inventory item.
GET /products/{id}AnyOne product or service.
GET /invoicesAnyInvoices, newest first (without their lines).Filters: customer_id, status (draft, sent, paid, void), number
POST /invoicesRead and changeMake an invoice: a draft, or sent (in the books and owing). It isn't emailed.
GET /invoices/{id}AnyOne invoice with its lines.
GET /paymentsAnyPayments received on invoices, newest first.Filters: invoice_id, customer_id
POST /paymentsRead and changeRecord a payment on a sent invoice.
GET /payments/{id}AnyOne payment.
GET /billsAnyBills, newest first (without their lines).Filters: vendor_id, status (open, paid, void)
POST /billsRead and changeEnter a bill to pay later.
GET /bills/{id}AnyOne bill with its lines.
GET /expensesAnyExpenses (paid straight away), newest first.Filters: vendor_id
POST /expensesRead and changeRecord an expense paid from a bank, card or cash account.
GET /expenses/{id}AnyOne expense with its lines.
GET /accountsAnyThe chart of accounts, each saying what it can be chosen for (pay_from, deposit_to, expense_category, income).
GET /tax-ratesAnySaved sales tax rates, to put on invoice lines.
GET /webhooksAnyThe webhook endpoints this key added.
POST /webhooksAnySubscribe: send these events to this https address. The answer has its signing secret, once.
DELETE /webhooks/{id}AnyUnsubscribe (an endpoint this key added).

Things are added through the API, not changed or deleted: corrections are made in TallyPip, where the books keep their history.

Adding things

For example, an invoice for two cleanings at 50.00 each, in the books straight away:

curl https://tallypip.com/api/v1/invoices \
  -H "Authorization: Bearer tpk_your_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1d2c9e-order-1042" \
  -d '{
    "customer_id": "3f6c1e2a-8b4d-4c1e-9a7b-2d5e6f708192",
    "status": "sent",
    "lines": [{ "description": "Cleaning", "quantity": 2, "rate_cents": 5000, "taxable": true }]
  }'

The answer is 201 with the invoice, its number, due date, tax and lines. What each kind of thing takes, and what comes back, is on its page in the API reference:

Lists, page by page

Lists come newest first, 25 at a time (?limit= up to 100). When has_more is true, ask for the next page with ?starting_after= and the last id you got:

GET /invoices?limit=50&status=sent
{ "object": "list", "url": "/api/v1/invoices", "has_more": true, "data": [ … ] }

GET /invoices?limit=50&status=sent&starting_after=<last id>

To pick up new invoices, payments or customers, ask for the first page now and then and keep the ones you haven't seen.

Retries without doubles

Networks drop. To make sure a retried request can't make an invoice or payment twice, send an Idempotency-Key header with each change: any text up to 255 characters, new for each change (a UUID, or your own order number).

  • Sent again within 24 hours with the same key and body, the request isn't repeated: you get the first answer, with Idempotent-Replayed: true.
  • The same key with a different body is refused with 409.
  • A refused request (400) changes nothing and isn't remembered, so fix it and send it again with the same key.

Webhooks: news as it happens

Instead of asking for new invoices every few minutes, an app can be told the moment something happens. A business adds an endpoint in Settings › Connected apps › Webhooks, or your app subscribes with its key:

curl https://tallypip.com/api/v1/webhooks \
  -H "Authorization: Bearer tpk_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://hooks.example.com/tallypip", "events": ["invoice.paid", "payment.received"] }'
urlstringRequired. An https address on the public internet (not a private or internal one).
eventslist or "all"Which events, from the list below. Default: all of them.
descriptionstringOptional. Shown to the business in Settings › Connected apps.

The answer includes the endpoint's secret (whsec_…), once: keep it to check messages. Endpoints an app adds stop when its key is revoked; unsubscribe with DELETE /webhooks/{id}.

The events

customer.createdCustomer added
vendor.createdVendor added
product.createdProduct or service added
invoice.createdInvoice made (draft or sent)
invoice.sentInvoice sent (in the books and owing)
invoice.paidInvoice paid in full
invoice.voidedInvoice voided
payment.receivedPayment received on an invoice
bill.createdBill entered
expense.createdExpense recorded

Things brought over from another app when a business switches (past transactions, starting balances) don't send events, and neither does writing off an invoice as a bad debt (no payment.received or invoice.paid: no money came in). Send a test in Settings sends a ping.

What arrives

A POST with a JSON body. data.object is the thing as the API shows it when the message is first sent (the same every time it's retried):

POST https://hooks.example.com/tallypip
TallyPip-Signature: t=1791288000,v1=5d41402abc4b2a76b9719d911017c592...
TallyPip-Event-Id: 0b48…
TallyPip-Event-Type: invoice.paid

{
  "id": "0b48…",
  "object": "event",
  "type": "invoice.paid",
  "created_at": "2026-10-06T14:30:00.000Z",
  "business_id": "…",
  "data": { "object": { "object": "invoice", "id": "…", "number": "INV-1042", "status": "paid", … } }
}

Check it came from TallyPip

  1. From TallyPip-Signature, take t (the time it was signed) and v1.
  2. Work out HMAC-SHA256 of t, a dot and the raw body (exactly as received), with your whsec_… secret as the key, in hex.
  3. It must equal v1 (compare in constant time), and t must be within 5 minutes of now, so an old message can't be replayed.
const [t, v1] = header.split(",").map((p) => p.split("=")[1]);
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)) && Math.abs(Date.now() / 1000 - Number(t)) < 300;

Answering, and retries

  • Answer with any 2xx within 10 seconds (do slow work afterwards). Anything else, a redirect or no answer is tried again: after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and a day (8 tries in all).
  • A message can arrive more than once, and not always in order: use the event id to skip ones you've seen, and created_at to order them.
  • An endpoint that hasn't answered properly for 48 hours is turned off and the business is told. They can turn it back on in Settings, where every message and its answer is listed.

Errors and limits

Errors come with an HTTP status and a body your app can show people:

{ "error": { "type": "invalid_request", "message": "Check the quantity on item 2.", "field": "lines" } }
400invalid_requestSomething sent isn't right. `field` says which (like lines[0].quantity), and `message` says what to fix.
401unauthorizedNo key, or a key that was revoked or whose maker left the team.
403forbiddenA Read only key tried to add something, or the key's maker can't do this in TallyPip.
403read_onlyThe business is locked (its trial or plan ended) or closed: it can be read but not added to.
404not_foundNo such thing in this business (ids from another business are never found).
409conflictAn Idempotency-Key was used again with a different request.
413too_largeThe body is over 256 KB.
429rate_limitedOver 120 requests a minute for a key. Wait for Retry-After seconds.
500server_errorSomething went wrong on TallyPip's side. Nothing was changed; try again.
503pausedThe API is paused for a short while. Keys are kept; try again later.

Every answer has an X-Request-Id; mention it when you write to us. Anything other than a success changes nothing: a request is all or nothing.

Changes and support

  • /api/v1 stays as it is. We may add endpoints and fields (so ignore names you don't know), but we won't remove or rename any without a new version and plenty of notice.
  • Questions: support@tallypip.com. Found a security problem? Please tell us there first (put “Security” in the subject), before anyone else.
  • Using the API means following our Terms and keeping the business's data private, as our Privacy Policy describes.