Quickstart
From a key to recognised revenue, in curl, Node and Python.
This page takes a key from its first call to recognised revenue: prove the key works, create a customer and a product, draft an invoice, register work on it and read back what it earned. Everything runs against a real company, since there is no sandbox. Use a throwaway customer and product, and delete them when you are done.
1. Mint a key
An admin or a bookkeeper of the company mints a key in the company's settings,
under Developers (/workspace/settings/company/developers). Select the company
you will test against in the top bar first, and choose the scope read_write. The raw tba_… key is shown once: put it in an
environment variable before you close the dialog.
export TABBA_API_KEY="tba_…"2. Read the configuration
GET /config confirms that the key is live and which company it is bound to.
It also answers the company's base currency, which the invoice in step 7 is
written in.
curl https://www.tabba.io/api/v1/config \
-H "Authorization: Bearer $TABBA_API_KEY"A 401 means the key is missing (UNAUTHORIZED), or unknown or revoked
(INVALID_API_KEY). A 403 with SCOPE_INSUFFICIENT means a read key on a
write endpoint. See Errors.
3. Write the client once
The rest of this page is one call per step, so wrap the boilerplate now. On a
failure, read the code and the request_id: code is what you branch on,
and request_id is what you quote when you ask about a call.
# A shell function is enough: base URL, key, JSON.
tabba() {
local method="$1" path="$2"
shift 2
curl -sS -X "$method" "https://www.tabba.io/api/v1$path" \
-H "Authorization: Bearer $TABBA_API_KEY" \
-H "Content-Type: application/json" "$@"
}4. Create a customer
Stamp your own key on it. external_id is how you name the customer from here
on, and repeating the call with the same external_id updates that customer
instead of making a second one.
name, country_key, is_person and send_method are required, and
send_method: "email" requires an email to send to. The customer is written
to the accounting system first, so it is a real customer there.
tabba POST /customers -d '{
"external_id": "quickstart:test-corp",
"name": "Test Corp",
"country_key": "DK",
"is_person": false,
"send_method": "email",
"email": "billing@test.example"
}'The answer is 201 for a new customer and 200 when the external_id was
taken and the customer was updated.
If the create is refused with 422
One of the two accounting systems files every customer in a VAT zone and a
customer group. There the create is refused with VALIDATION_FAILED, and
field_errors names vat_zone_ref and customer_group_ref. Read a customer
the company already has with GET /customers, and send the same two values.
See Accounting platforms.
Read the customer back by your own key at any time. ext: in the path names
it:
tabba GET /customers/ext:quickstart:test-corp5. Create a product
A product states the revenue account a line that bills it posts to.
account_number is an account of the company's chart, and unit is one the
accounting system knows, so take all three of account_number, account_ref
and unit from a product the company already has:
tabba GET /productsThen create yours with those values:
tabba POST /products -d '{
"external_id": "quickstart:consulting",
"product_number": "QS-001",
"name": "Consulting",
"account_number": 1000,
"account_ref": "1000",
"unit": "hours",
"default_price": 1200
}'6. Find who does the work
Every invoice line names at least one person, because revenue is recognised per person, per line. People are invited in the workspace and never created through the API. Read the people who can work in the key's company and find yourself:
tabba GET /users
# { "object": "list", "data": [ { "object": "user", "id": "…", "email": "…", … } ], … }You do not need the person's id for what follows: a person is named by
email:<address> wherever a request names one.
7. Draft an invoice
One hourly line, with you on it at 1,200 per hour. The invoice is a draft: nothing is asked of the accounting system and no customer hears from you until it is issued.
A request names a record by reference
A field named for a record (customer, product, user, invoice,
series) takes a reference: the record's id, or ext: and your own
external_id. A person is also named by email: and their address. A field
that ends in _id states an id, and is what a response answers.
Write the invoice in the base_currency step 2 answered, and send
auto_send: false. A new invoice has auto_send on unless the request says
otherwise, and the daily run issues such an invoice by itself.
tabba POST /invoices -d '{
"external_id": "quickstart:inv-001",
"customer": "ext:quickstart:test-corp",
"currency": "DKK",
"title": "Quickstart",
"auto_send": false,
"lines": [{
"product": "ext:quickstart:consulting",
"recognition_method": "hourly",
"description_override": "Consulting, July",
"assignees": [{
"user": "email:you@your-company.example",
"hourly_rate_override": 1200
}]
}]
}'
# Keep the id of the line from the answer: .lines[0].id
export LINE_ID="…"Two kinds of line
An hourly line bills registered hours at each person's rate: every person
on it states hourly_rate_override, and the line states no agreed_value.
A value line bills a fixed agreed_value and is recognised by completion:
its people share it by distribution_percentage, a fraction from 0 to 1
that sums to 1 over the line, or evenly where nobody states one.
8. Register the work
This is the call that moves revenue. hours is the person's total on the
line, not an increment: Tabba registers the difference against what is
registered already, so sending the same total again registers nothing.
tabba PUT /work-registrations -d '{
"invoice": "ext:quickstart:inv-001",
"line": "'"$LINE_ID"'",
"user": "email:you@your-company.example",
"hours": 12
}'The answer states the person's total after the request (hours) and what the
request registered (hours_delta).
9. Read the revenue back
Twelve hours at 1,200 is 14,400 earned. The invoice has not been issued, so nothing is billed, and all of it is work in progress.
tabba GET /invoices/ext:quickstart:inv-001/recognitionearned is what the registered work has earned, and billed is what issued
invoices have billed, net of booked credit notes. The difference is deferred
(billed ahead of the work) or wip (earned ahead of the invoice). The figures
are in the company's base currency and are the ones the month-end accrual
posts from. Issuing the invoice moves the amount from wip to billed: see
Issuance and auto-send.
Clean up after yourself
The customer and the product you made are real records in the accounting
system. Delete the invoice first, then the customer and the product: a
customer or a product that an invoice still names is refused with
DELETE_BLOCKED_BY_REFERENCES. A draft invoice is deleted with its lines and
the work registered on them. An issued invoice is never deleted.
tabba DELETE /invoices/ext:quickstart:inv-001
tabba DELETE /customers/ext:quickstart:test-corp
tabba DELETE /products/ext:quickstart:consultingWhere to go next
- Authentication: scope, the company a key is bound to, and rotation.
- external_id and idempotency: the upsert contract you just relied on, in full.
- Work registration: totals, the entry month, and registering on an occurrence of a recurring series.
- Webhooks: how you hear of changes your integration did not make.
- Connect an AI client instead: the same module over MCP, where every write is a plan first and a separate commit applies it.