Tabba.
FunktionerPriserKontakt
Log indKom i gang
Tabba.

Moderne økonomistyring for virksomheder.

Produkt

  • Funktioner
  • Priser
  • Integrationer
  • Ændringslog

Ressourcer

  • Blog
  • Hjælpecenter
  • Udviklere

Virksomhed

  • Karriere
  • Kontakt

Juridisk

  • Privatlivspolitik
  • Servicevilkår
  • Cookie-politik
  • Sikkerhed

Forbind

  • Twitter
  • GitHub
  • LinkedIn
  • Discord

© 2026 Tabba. Alle rettigheder forbeholdes.

PrivatlivspolitikServicevilkårCookie-politik
Back to developer docs

Guides

  • Getting started
  • Authentication
  • external_id and idempotency
  • Work registration
  • Issuance and auto-send
  • Webhooks
  • Pagination
  • Errors

Webhooks

Webhooks are how your integration learns about changes it did not trigger — an auto-issued invoice, a month-end accrual booking, a Dinero reconcile write. Poll-free, signed, at-least-once.

Registering an endpoint

Register a webhook endpoint in Settings → Developers → Webhooks. The signing secret is shown ONCE at creation — copy it into your integration's secret store.

Per endpoint you choose a subset of event types (or "all events").

Event catalog v1

  • invoice.issued — an invoice was booked into Dinero (whether by cron, by an API call, or by the UI).
  • invoice.send_failed — the invoice booked but sending the PDF failed. Retry the send, not the issue.
  • credit_note.issued — a credit note was booked.
  • accrual.booked — a month-end accrual voucher was booked in Dinero.
  • customer.updated — a customer was written (via UI, API, or Dinero reconcile).
  • product.updated — a product was written.

Every payload carries the same wire shape as the corresponding GET response — the same serializer output — so you can act on the event without an extra round-trip.

Signature verification

Every delivery carries a Tabba-Signature header:

Tabba-Signature: t=1717000000,v1=e2b1c3…

The signature is HMAC-SHA256(secret, "{t}.{rawBody}"), hex-encoded. Verify BEFORE trusting the payload:

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

function verify(header, rawBody, secret) {
  const parts = Object.fromEntries(
    header.split(',').map((p) => p.split('=').map((s) => s.trim())),
  )
  const t = parts.t
  const v1 = parts.v1
  if (!t || !v1) return false
  // Reject payloads > 5 minutes old.
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
  const a = Buffer.from(expected, 'hex')
  const b = Buffer.from(v1, 'hex')
  return a.length === b.length && timingSafeEqual(a, b)
}

The t timestamp prevents replay attacks; reject anything more than a few minutes old.

Envelope

{
  "id": "evt_01H…",
  "type": "invoice.issued",
  "created": "2026-07-06T10:00:00Z",
  "data": {
    "object": "invoice",
    "id": "…",
    "external_id": "clickup:proj_ACME_migration",
    "status": "issued",
    "dinero_invoice_guid": "…",
    "lines": [ … ]
  }
}
  • id — the event's stable id. Dedupe on this — deliveries are at-least-once (a retry can arrive after the original was already handled).
  • type — one of the catalog above.
  • data — the fat payload; the full resource, not a stub.

Retries

A failed delivery (non-2xx response or timeout) is retried with exponential backoff for ~24 hours. After that it lands in the delivery log as failed and stops retrying. You can inspect and manually replay a delivery from Settings → Developers → Webhooks.

Best practice: return 200 fast (queue the work internally) and verify the signature in the queue worker if the payload matters — this way a slow database doesn't hold up the receiver.

Rotating the signing secret

Rotate the secret from Settings → Developers → Webhooks. The old secret keeps signing for a short overlap window so your receiver can accept both while you deploy the new value.