Tabba.
IntegrationerSådan virker TabbaFeatures

© 2026 Tabba. Alle rettigheder forbeholdes.

Udviklere
Udviklere
Udviklere
Getting startedQuickstartAuthenticationAccounting platformsexternal_id and idempotencyWork registrationIssuance and auto-sendWebhooksPaginationErrors

Andre spor

AI clientsAPI reference

Work registration

Setting a person's hours and completion on a line, as totals.

Registering work tells Tabba how far one person has come on one line of an invoice. Hours and completion feed the earned, billed, deferred and work-in-progress figures the workspace shows and the month-end accrual posts from.

The endpoint

PUT /work-registrations

There is one work-registration endpoint. It sets one person's figures on one line per request, and it is absolute: you state where the person stands, not what changed.

Totals, not increments

Send the total hours the person has on the line, and the completion rate they stand at, as a fraction from 0 to 1. Tabba reads what is registered and registers the signed difference.

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:crm:acme-retainer",
    "occurrence": "2026-07-01",
    "line": "5f8e2c1a-3b4d-4e6f-8a9b-0c1d2e3f4a5b",
    "user": "email:maja@your-company.example",
    "hours": 12,
    "completion_rate": 0.6
  }'

This says that Maja has 12 hours in total on the line, and that her part is 60 % done.

{
  "object": "work_registration",
  "invoice_id": "…",
  "invoice_line_id": "…",
  "user_id": "…",
  "accrual_month": "2026-07-01",
  "hours": 12,
  "hours_delta": 3,
  "completion_rate": 0.6,
  "external_id": null
}

hours is her total on the line after the request, and hours_delta is what the request registered: 3 here, because 9 hours were registered before. Sending the same figures again registers nothing and answers hours_delta: 0, so a retried sync is safe with no idempotency key.

hours and completion_rate are each optional, and a request states at least one. A figure left out, or sent as null, stays as it is.

One request at a time per person and line

Two requests that set one person's hours on one line at the same moment both measure against the same total, and the difference can be registered twice. Send them one after the other.

Naming the line

A line is named on an invoice, or on an occurrence of a recurring series. Send exactly one of the two forms:

  • On an invoice: invoice and line, where line is the id of the invoice line.
  • On an occurrence: series, occurrence and line. occurrence is the first day of the occurrence's billing period, YYYY-MM-DD, as GET /recurring-series/{ref}/occurrences states it. line is the id of the recurring line, the same for every occurrence, or the line_id of a one-time line as its occurrence states it.

invoice and series take a reference: the id, or ext: and your own external_id. line is always an id.

An occurrence that is not an invoice yet is made one by the registration, as when an admin registers work on it in the workspace. The answer's invoice_id is that invoice.

Naming the person

user takes the same references as everywhere else:

  • email: and the person's address.
  • ext: and your own id, after you have tagged the person with PATCH /users/{ref}.
  • The person's id.

People are not created through the API: they are invited in the workspace. GET /users lists the people who can work in the key's company.

The person must hold a place on the line, as one of its assignees. A registration for someone who does not is refused with NOT_ASSIGNED (409). The registration is credited to the person it names.

The entry month

Every registration lands in the month the request arrives in, in the Europe/Copenhagen time zone, never in a month you state. The answer's accrual_month is that month's first day. There is one registration per line, person and month, and it is the one the workspace reads.

A lower total is a reduction. It is drawn down across the person's months, newest first.

Refusals

CodeStatusWhat happened
NOT_ASSIGNED409The person holds no place on the line
HOURS_BILLED_LOCKED409The hours are of an hourly line that its invoice has billed
LINE_NOT_FOUND404The invoice or the occurrence has no such line
INVOICE_NOT_FOUND404invoice names no invoice of the company
SERIES_NOT_FOUND404series names no recurring series of the company
OCCURRENCE_OUT_OF_RANGE404occurrence is not the first day of a billing period of the series
OCCURRENCE_SKIPPED409An admin skipped the occurrence
NOT_FOUND404user names no person who can work in the company
DUPLICATE_EXTERNAL_ID409Another month's registration of the person on the line carries the external_id
VALIDATION_FAILED422See field_errors

One request raises a person's total by at most 1,000 hours, a guard against a slipped decimal. A larger rise is a VALIDATION_FAILED with HOURS_CHANGE_TOO_LARGE under hours.

external_id and idempotency

Naming every record by your own key, and why a retried create makes nothing twice.

Issuance and auto-send

Issuing an invoice yourself or leaving it to the daily run, a booked invoice that was not delivered, and credit notes.

På denne side

The endpointTotals, not incrementsNaming the lineNaming the personThe entry monthRefusals

Denne side

Vis som MarkdownÅbn i Claude