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
Authorizationheader asBearer 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": 12550is 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
statusisdraft(not in the books yet),sent(in the books and owing),paidorvoid. A bill's isopen,paidorvoid. - 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.
| Request | Key | What it does |
|---|---|---|
| GET /me | Any | Which business and person the key acts for. Use it to check a key works. |
| GET /customers | Any | Customers, newest first, with what each owes on sent invoices.Filters: email, name (exact, ignoring capitals: to find one before adding it) |
| POST /customers | Read and change | Add a customer. |
| GET /customers/{id} | Any | One customer. |
| GET /projects | Any | Projects (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 /projects | Read and change | Start a project for a customer. |
| GET /projects/{id} | Any | One project with its numbers. |
| PATCH /projects/{id} | Read and change | Change a project: send only what changes (its status, to complete or cancel it; its dates; its name). |
| GET /classes | Any | Classes (Settings › Advanced), newest first, each with its full name. |
| POST /classes | Read and change | Add a class (or a sub-class, with parent_id). |
| GET /classes/{id} | Any | One class. |
| PATCH /classes/{id} | Read and change | Change a class: its name, what it sits under, or make it inactive. |
| GET /locations | Any | Locations (or what the business calls them: stores, properties…), newest first. |
| POST /locations | Read and change | Add a location (or a sub-location, with parent_id). |
| GET /locations/{id} | Any | One location. |
| PATCH /locations/{id} | Read and change | Change a location: its name, what it sits under, or make it inactive. |
| GET /vendors | Any | Vendors, newest first.Filters: email, name (exact, ignoring capitals) |
| POST /vendors | Read and change | Add a vendor. |
| GET /vendors/{id} | Any | One vendor. |
| GET /products | Any | Products and services, newest first. |
| POST /products | Read and change | Add a service or non-inventory item. |
| GET /products/{id} | Any | One product or service. |
| GET /invoices | Any | Invoices, newest first (without their lines).Filters: customer_id, status (draft, sent, paid, void), number |
| POST /invoices | Read and change | Make an invoice: a draft, or sent (in the books and owing). It isn't emailed. |
| GET /invoices/{id} | Any | One invoice with its lines. |
| GET /payments | Any | Payments received on invoices, newest first.Filters: invoice_id, customer_id |
| POST /payments | Read and change | Record a payment on a sent invoice. |
| GET /payments/{id} | Any | One payment. |
| GET /bills | Any | Bills, newest first (without their lines).Filters: vendor_id, status (open, paid, void) |
| POST /bills | Read and change | Enter a bill to pay later. |
| GET /bills/{id} | Any | One bill with its lines. |
| GET /expenses | Any | Expenses (paid straight away), newest first.Filters: vendor_id |
| POST /expenses | Read and change | Record an expense paid from a bank, card or cash account. |
| GET /expenses/{id} | Any | One expense with its lines. |
| GET /accounts | Any | The chart of accounts, each saying what it can be chosen for (pay_from, deposit_to, expense_category, income). |
| GET /tax-rates | Any | Saved sales tax rates, to put on invoice lines. |
| GET /webhooks | Any | The webhook endpoints this key added. |
| POST /webhooks | Any | Subscribe: send these events to this https address. The answer has its signing secret, once. |
| DELETE /webhooks/{id} | Any | Unsubscribe (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"] }'| url | string | Required. An https address on the public internet (not a private or internal one). |
| events | list or "all" | Which events, from the list below. Default: all of them. |
| description | string | Optional. 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.created | Customer added |
| vendor.created | Vendor added |
| product.created | Product or service added |
| invoice.created | Invoice made (draft or sent) |
| invoice.sent | Invoice sent (in the books and owing) |
| invoice.paid | Invoice paid in full |
| invoice.voided | Invoice voided |
| payment.received | Payment received on an invoice |
| bill.created | Bill entered |
| expense.created | Expense 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
- From
TallyPip-Signature, taket(the time it was signed) andv1. - Work out HMAC-SHA256 of
t, a dot and the raw body (exactly as received), with yourwhsec_…secret as the key, in hex. - It must equal
v1(compare in constant time), andtmust 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
2xxwithin 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
idto skip ones you've seen, andcreated_atto 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" } }| 400 | invalid_request | Something sent isn't right. `field` says which (like lines[0].quantity), and `message` says what to fix. |
| 401 | unauthorized | No key, or a key that was revoked or whose maker left the team. |
| 403 | forbidden | A Read only key tried to add something, or the key's maker can't do this in TallyPip. |
| 403 | read_only | The business is locked (its trial or plan ended) or closed: it can be read but not added to. |
| 404 | not_found | No such thing in this business (ids from another business are never found). |
| 409 | conflict | An Idempotency-Key was used again with a different request. |
| 413 | too_large | The body is over 256 KB. |
| 429 | rate_limited | Over 120 requests a minute for a key. Wait for Retry-After seconds. |
| 500 | server_error | Something went wrong on TallyPip's side. Nothing was changed; try again. |
| 503 | paused | The 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/v1stays 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.