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

Webhooks

Signed, at-least-once events for changes your integration did not make.

Webhooks are how your integration learns of changes it did not make: an invoice the daily run issued, a month-end accrual entry the close booked, a customer changed in the accounting system. Tabba posts a signed event to an HTTPS address of yours, and tries again when your server does not take it.

Registering an endpoint

An admin or a bookkeeper of the company registers a webhook endpoint in the company's settings, under Developers (/workspace/settings/company/developers). An endpoint is bound to that company, and subscribes to one or more event types. It can be disabled and enabled again.

The endpoint's signing secret, whsec_ and 32 characters, is shown once, when the endpoint is registered. Copy it into your integration's secret store.

The address must be a public HTTPS host name. An address that is not https, that carries a user name or a password, that is an IP address, or whose host name resolves only inside a network (localhost, .local, .internal and the like) is refused. A host name that resolves to an address that is not public is never connected to.

Events

TypeSent when
invoice.issuedThe accounting system booked an invoice: by the daily run, by an admin in the workspace, or by a call to the issue endpoint. Sent once per invoice
invoice.send_failedAn invoice was booked, and its delivery to the customer is not known to have succeeded. Sent beside invoice.issued, never instead of it
credit_note.issuedThe accounting system booked a credit note: one an admin or a key asked for, or one the end of a recurring series queued. Sent once per credit note
accrual.bookedThe month-end close posted a customer's accrual entry. Not sent for a month the accounting system had closed, which a later month carries
customer.updatedA customer was created, changed, switched off or deleted, in Tabba or in the accounting system
product.updatedA product was created, changed, switched off or deleted, in Tabba or in the accounting system

A change made in the accounting system is seen at the next import, and the event is sent then. A change your own key made is announced like any other.

The event

{
  "object": "event",
  "id": "evt_3f9c1e0a7b2d4c5e8f6a1b0c9d8e7f6a",
  "type": "invoice.issued",
  "created_at": "2026-07-01T08:00:00.000Z",
  "data": {
    "object": "invoice",
    "id": "…",
    "external_id": "crm:acme-migration",
    "status": "issued",
    "issued_at": "2026-07-01T08:00:00.000Z",
    "platform_ref": "…",
    "platform_number": 1042,
    "send_error": null,
    "lines": [ … ]
  }
}
  • id: evt_ and 32 hex digits. It is the same for every endpoint the event went to, and for every retry and replay of it. Tell a repeat by this id.
  • type: one of the six above.
  • created_at: when the event was announced.
  • data: the whole record, as its GET endpoint answers it, so you can act on the event without a second request. An invoice is as GET /invoices/{ref} answers it, a customer as GET /customers/{ref}, a product as GET /products/{ref}, and an accrual entry as GET /accruals.

What a customer or a product event adds

The API lists and reads only records the accounting system still offers, so the event is how you learn that one was switched off or deleted. In customer.updated and product.updated, data carries two more fields:

  • archived_at: when the accounting system stopped offering the record, switched off or deleted. null while it offers it.
  • deleted_at: when the accounting system stopped holding the record. null while it holds it.

A deleted record has external_id: null: the id is free for another record. Name it by its id. Where Tabba holds nothing of a deleted record but its id, data is only object, id, external_id, archived_at and deleted_at.

Two changes to one customer in quick succession may both carry the later state.

Verifying the signature

Every delivery carries two headers:

Tabba-Signature: t=1782892800,v1=e2b1c3…
Tabba-Event-Id: evt_3f9c1e0a7b2d4c5e8f6a1b0c9d8e7f6a

t is when the attempt was signed, in unix seconds. v1 is the HMAC-SHA256, in hex, of the timestamp, a period and the request body, under the endpoint's signing secret. Compute it over the body byte for byte as it arrived, not over a body you parsed and serialised again. Verify before you trust the payload:

import { createHmac, timingSafeEqual } from 'node:crypto'

function verify(header, rawBody, secret) {
  const parts = Object.fromEntries(
    header.split(',').map((part) => part.trim().split('=', 2)),
  )
  const t = parts.t
  const v1 = parts.v1
  if (!t || !v1 || !/^\d+$/.test(t)) return false

  // Refuse a timestamp more than 300 seconds from your own clock.
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false

  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest()
  const presented = Buffer.from(v1, 'hex')
  return expected.length === presented.length && timingSafeEqual(expected, presented)
}

The timestamp is what stops an old request from being replayed against you: refuse one more than 300 seconds from your own clock. The event type is in the body only. There is no header for it.

Delivery and retries

Answer with a 2xx within ten seconds. What your server answers decides what happens next:

Your answerWhat Tabba does
2xxThe delivery is delivered
5xx, 408, 429, or no answer in timeTries again later
Any other status, a redirect among themFails the delivery at once

A delivery is tried again eight times, after waits of 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour, 2 hours, 4 hours and 8 hours: about sixteen hours in all. After the last one the delivery is failed. A 4xx such as 400, 401 or 404 is read as the endpoint's own configuration, so another post would change nothing and none is made. A redirect is not followed.

Delivery is at least once. A server that took the post and did not answer in time is posted to again, under the same event id. A retry carries the same body, byte for byte, under a new signature.

An admin reads an endpoint's deliveries, with the status each was answered with, on the Developers page, and replays one from there. A replay posts the same body under the same event id, with a new signature.

Take the event, answer 200, and do the work afterwards: queue it inside your own system so that a slow database cannot run past the ten seconds. Verify the signature before you queue.

Rotating the signing secret

An admin rotates the secret on the Developers page. The new secret is shown once. There is no overlap: the old secret signs nothing from the next delivery on.

A delivery signed with the new secret before your receiver holds it fails your verification. If the receiver answers such a delivery with a 4xx, the delivery is failed at once, and an admin replays it once the new secret is deployed.

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.

Pagination

Cursors, the order of a list, and following a list with updated_since.

På denne side

Registering an endpointEventsThe eventWhat a customer or a product event addsVerifying the signatureDelivery and retriesRotating the signing secret

Denne side

Vis som MarkdownÅbn i Claude