Guide to Tripletex Webhooks: Features and Best Practices
Tripletex webhooks tell your integration when accounting data changes: an invoice is charged, a customer or product is created or updated, a voucher is ready to process, or postings are matched. If you build on Tripletex, the Norwegian accounting and ERP platform, webhooks are how those changes reach your own systems without polling the API.
This guide covers how Tripletex event subscriptions work, the events and payload format, how to authenticate callbacks with a custom header, how delivery and auto-disable work, and the practices that keep a Tripletex receiver in sync.
What are Tripletex webhooks?
Tripletex calls its webhooks event subscriptions. You create them through API2, one per event, and Tripletex sends a POST to the subscription's targetUrl each time the event happens. Events are named object.verb, such as product.create, and where an object has several verbs, each needs its own subscription.
Tripletex doesn't sign callbacks. It recommends "some form of authentication against the targetUrl callback, to prevent misuse" and supports three: a custom header, Basic credentials in the URL, or a token in the query string. The custom header is the recommended one: you set authHeaderName and authHeaderValue on the subscription, Tripletex sends that header "as-is for every webhook POST callback", and your receiver compares it.
Tripletex webhook features
| Feature | Details |
|---|---|
| Registration | POST https://tripletex.no/v2/event/subscription, one per event |
| Event names | object.verb. GET /v2/event lists the events available now |
| Method and format | POST with a JSON body |
| Payload | subscriptionId, event, id and value |
| Field filtering | Optional fields on the subscription, in API2 format |
| Authentication | Custom header (recommended), Basic credentials in the URL, or a token in the query string. No signature |
| Retries | At increasing intervals over 30 hours |
| Auto-disable | After a week of constant failures, DISABLED_TOO_MANY_ERRORS |
| Source IPs | Not published |
Common events
Tripletex documents these events and says the list will grow:
| Events | Fires when |
|---|---|
invoice.charged | A new invoice has been created and charged |
customer.*, supplier.*, contact.*, employee.* | The record is created, updated or deleted |
order.*, product.*, project.* | The record is created, updated or deleted |
account.*, voucher.*, archiverelation.* | A booking account, voucher or document archive relation is created, updated or deleted |
voucherstatus.ready | A new voucher is ready to be processed from the voucher reception |
closegroup.create, closegroup.delete | Postings are matched and closed, including automatic matches such as OCR matching of incoming payments, or a close group is deleted and its postings reopened |
notification.sent | A notification has been sent to the employee who owns the token |
Here .* stands for create, update and delete, each a separate subscription.
Every callback has the same four fields:
{
"subscriptionId": 1042,
"event": "product.create",
"id": 58213,
"value": { "id": 58213, "version": 0, "name": "Consulting hour", "number": "100" }
}
Tripletex's own examples use placeholders, so the values above are illustrative. id is the triggering object's ID, and value is the object as API2 would return it, filtered by the subscription's fields. For delete events, value is null and only id identifies what was removed. closegroup.delete works the same way, so Tripletex suggests keeping the postings list from the matching closegroup.create and reversing it on delete.
Setting up Tripletex webhooks
Generate a long random secret, for example with
openssl rand -hex 32.Create a session token. API2 uses Basic authentication with username
0for your own company and the session token as the password.Create one subscription per event:
curl -X POST "https://tripletex.no/v2/event/subscription" \ -u "0:$TRIPLETEX_SESSION_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "event": "product.create", "targetUrl": "https://example.com/webhooks/tripletex", "fields": "*", "authHeaderName": "Authorization", "authHeaderValue": "Bearer YOUR_LONG_RANDOM_SECRET" }'Wait a few seconds for the subscription to reach all of Tripletex's servers, then run a full sync of the objects you subscribed to, as Tripletex recommends.
Check status any time with
GET /v2/event/subscription.
In Hookdeck. Create a source with the Tripletex type, enter the authHeaderName value in API Key Header and the exact authHeaderValue in API Key, including any Bearer prefix, and use the source URL as the targetUrl. Hookdeck matches the name case-insensitively and the value exactly. The source accepts POST. It checks a header, so use the custom header option rather than a token in the query string.
For local development, hookdeck listen 3000 tripletex --path /webhooks/tripletex with the Hookdeck CLI gives you a public HTTPS URL, with no account required.
Securing Tripletex webhooks
Look up the header named in authHeaderName and compare its whole value with authHeaderValue in constant time.
const crypto = require('crypto');
const express = require('express');
const app = express();
function safeEqual(received, expected) {
const a = Buffer.from(received, 'utf8');
const b = Buffer.from(expected, 'utf8');
// timingSafeEqual throws on buffers of different lengths
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post('/webhooks/tripletex', express.raw({ type: '*/*' }), (req, res) => {
const secret = process.env.TRIPLETEX_WEBHOOK_SECRET; // your authHeaderValue
if (!secret) return res.sendStatus(500); // fail closed when unconfigured
const name = process.env.TRIPLETEX_WEBHOOK_HEADER || 'Authorization'; // your authHeaderName
const received = req.get(name); // case-insensitive lookup
if (typeof received !== 'string' || !safeEqual(received, secret)) {
return res.sendStatus(401);
}
let callback;
try {
callback = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
// Queue { event, id, value } for a worker. value is null for deletes.
res.sendStatus(200);
});
The same check in Python:
import hmac
import os
def verify_tripletex_request(headers) -> bool:
secret = os.environ.get("TRIPLETEX_WEBHOOK_SECRET")
if not secret:
return False # fail closed
name = os.environ.get("TRIPLETEX_WEBHOOK_HEADER", "Authorization")
received = headers.get(name) # case-insensitive in Starlette and Flask
if not received:
return False
return hmac.compare_digest(received.encode("utf-8"), secret.encode("utf-8"))
A matching header proves the sender knows the secret. It doesn't prove the body is unchanged, and a captured callback can be replayed later, because there's no signature or timestamp. Use HTTPS, a long random value, and update the subscription and your receiver together if it may have leaked. Tripletex's other options are weaker: Basic credentials in the URL are also a fixed header, and a token in the query string tends to end up in logs.
Make Tripletex webhooks production-ready. Hookdeck Event Gateway checks your custom header on every Tripletex callback, accepts each one straight away so subscriptions don't drift towards being disabled, and keeps every event for replay.
Tripletex webhook limitations and pain points
A week of failures disables the subscription
The Problem: After a long outage or a broken deployment, events stop arriving and nothing tells your service why.
Why It Happens: Tripletex retries at increasing intervals over 30 hours. "If the delivery fails constantly over a week the event subscription will be disabled with status DISABLED_TOO_MANY_ERRORS." A successful delivery resets the counter.
Workarounds:
- Poll
GET /v2/event/subscriptionand alert on any status other thanACTIVE. - Re-enable a disabled subscription with
PUT /event/subscription/{id}, then sync to catch up.
How Hookdeck Can Help: Hookdeck accepts each callback as it arrives, so Tripletex sees successful deliveries even while your service is down. Hookdeck then retries delivery to your service on your own schedule and lets you replay any event.
One subscription per verb
The Problem: You subscribe to order.create and miss every update and deletion.
Why It Happens: "For the endpoints with several verbs you will need to subscribe to each individual event."
Workarounds:
- Script subscription creation for every verb you need.
- Compare the subscriptions you expect with
GET /v2/event/subscriptionat startup.
How Hookdeck Can Help: All subscriptions can point at one Hookdeck source, and Hookdeck filters can route each event value to the right destination.
Rapid changes can skip versions
The Problem: Your copy of an object shows a version it never had in between, or misses an intermediate state.
Why It Happens: "If an object is modified several times in rapid succession, the webhook system might skip over a version number or two, but sends over the latest version."
Workarounds:
- Apply an update only if its
versionis higher than the one you have. - Run the nightly sync Tripletex recommends.
How Hookdeck Can Help: Every callback is stored in Hookdeck with its body, so you can see which versions arrived and when, and replay them after fixing ordering logic.
Deletes carry no data
The Problem: A delete event arrives and you no longer know what the object was.
Why It Happens: "Events of type delete only include the object ID, and not the object itself."
Workarounds:
- Keep enough local state, keyed on the Tripletex ID, to act on a delete.
- For
closegroup.delete, keep the postings from the matchingclosegroup.create.
How Hookdeck Can Help: Because Hookdeck keeps the earlier create and update events, you can look up what an object looked like before it was deleted.
Best practices
Store the whole header value
Compare against the exact authHeaderValue, prefix included.
Deduplicate on subscriptionId, event, id and version
Retries over 30 hours can deliver a callback more than once. A key built from those fields, using value.version where it's present, lets you skip repeats. See our guide to webhook idempotency.
Use fields to keep payloads small
Request only the fields you use, in the same format as an API2 fields parameter.
Keep the nightly sync
Tripletex recommends one for now. Treat webhooks as the fast path and the sync as the safety net.
Acknowledge fast and process asynchronously
Verify, store, respond with 200, then do the work. See why to process webhooks asynchronously.
Conclusion
Tripletex sends a JSON POST per subscribed object.verb event, with subscriptionId, event, id and value, and recommends a custom header for authentication. Compare that header in constant time over HTTPS, subscribe to every verb you need, honour object versions, keep enough state for deletes, and watch for disabled subscriptions.
Hookdeck Event Gateway sits in front of your receiver, checks the header, accepts each callback straight away, and keeps it until your service has processed it.
Get started with Hookdeck for free and handle Tripletex webhooks reliably in minutes.