Work registration
Registering work is how you tell Tabba's recognition engine "here is how far along a given line of a given invoice is." Hours + completion feed the same earned / billed / deferred / WIP figures the UI shows.
The endpoint
PUT /api/v1/work-registrations
There is exactly one work-registration endpoint. It is a single-item PUT
(no batch), and it is absolute — you send the totals for a
(line, user) pair, not deltas.
Absolute-set semantics
Send the total hours the user has logged against the line, and their completion rate (0–1), and Tabba computes the internal delta for you.
curl -X PUT https://www.tabba.io/api/v1/work-registrations \
-H "Authorization: Bearer $TABBA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"series": "ext:clickup:acme-retainer",
"occurrence": "2026-07-01",
"line": "5f8e…",
"user": "email:jane@acme.example",
"hours": 12,
"completion_rate": 0.6
}'
Meaning: "Jane has 12 hours total on this line; her part is 60% done."
Repeat calls are idempotent by construction — you never compute deltas,
which means a retried sync is safe with no separate idempotency key.
Both hours and completion_rate are optional; you can send one, the
other, or both.
Naming the occurrence (Option B)
You always name the exact occurrence:
- Series + period —
series: <ref>, occurrence: <YYYY-MM-DD>. TheYYYY-MM-DDis the first day of the billing period (e.g.2026-07-01for the July invoice). Virtual and materialized states are hidden from you — Tabba materializes on demand. - One-time invoice —
invoice: <ref>. No occurrence needed.
Exactly one of {series, occurrence} or {invoice} must be sent.
Entry-month bucketing
Every registration lands in the month it was made (the "entry month"), not the month you sent as the occurrence. This mirrors the UI's behaviour and prevents accidental back-dating of ledger figures.
A negative delta — say Jane's total drops from 12 hours to 10 — draws down newest-first across earlier month buckets, so you never break a closed accounting period from a nightly catch-up sync.
Naming the user
The user field accepts the same three refs as everywhere else:
ext:your-id— after you tag the user withPATCH /users/{ref}.email:someone@example.com— the user's login email.- Raw UUID.
Users cannot be created via the API — invitations stay in the UI.
Read the roster with GET /users to see what's available.
Typed 409 when no occurrence exists
If you register work against a series that's paused, cancelled, or past
its horizon, the API returns 409 with a typed code:
{
"error": {
"type": "conflict",
"code": "OCCURRENCE_NOT_FOUND",
"message": "This series has no current occurrence — extend or resume.",
"request_id": "req_…"
}
}
Codes you can branch on:
SERIES_CANCELLED— the series is cancelled; register against a new series.OCCURRENCE_NOT_FOUND— the period is past the current horizon; extend the series or wait for the cadence to reach it.OCCURRENCE_SKIPPED— the occurrence was explicitly skipped by an admin.