# Tripletex Webhooks

Tripletex (Norwegian accounting) sends a JSON webhook when an object you subscribed to changes: an
invoice is charged, a customer is created, an order is updated, a product is deleted. Subscriptions
are created through the API (`POST /v2/event/subscription`); there is no dashboard UI for it.
Webhooks are **not signed**. The only built-in authentication is a custom header you choose when
creating the subscription (`authHeaderName` / `authHeaderValue`), which Tripletex sends as-is on
every callback.

## When to Use This Skill

* How do I receive Tripletex webhooks?
* How do I authenticate Tripletex event subscription callbacks?
* Does Tripletex sign webhook payloads? (No: use `authHeaderName` / `authHeaderValue`)
* How do I handle `invoice.charged`, `customer.create`, `order.update` or `product.delete`?
* Why does my Tripletex webhook endpoint return 401?

## Verification (core)

```javascript
const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyTripletexRequest(
  headers,
  secret = process.env.TRIPLETEX_WEBHOOK_SECRET,
  headerName = process.env.TRIPLETEX_WEBHOOK_HEADER || 'Authorization'
) {
  if (!secret) return false;                          // never accept when unconfigured
  const received = headers[headerName.toLowerCase()]; // Node lowercases header names
  return Boolean(received) && safeEqual(received, secret); // whole value, no Bearer parsing
}

```

* The header name is whatever you set as `authHeaderName` (the examples read it from
  `TRIPLETEX_WEBHOOK_HEADER`, default `Authorization`). Look it up case-insensitively.
* Compare the **whole** value with `TRIPLETEX_WEBHOOK_SECRET`, exactly as you typed it as
  `authHeaderValue`. If that was `Bearer abc123`, the secret is `Bearer abc123`. Do not strip or
  parse the `Bearer ` prefix.
* Missing header or mismatch: `401`. Malformed body (after authenticating): `400`. Handled: `200`.
* Read the body raw (`express.raw({ type: '*/*' })`) and authenticate before parsing it.
* **A static shared credential authenticates the sender, not the body, and gives no replay
  protection** (no signature, no timestamp). Use HTTPS and rotate the credential if it leaks.
* Python: `hmac.compare_digest` on bytes (encode both sides, or compare SHA-256 digests); passing
  `str` raises `TypeError` on non-ASCII input.
* Basic credentials in the `targetUrl` and `?token=` query parameters are also supported by
  Tripletex, but they aren't covered by this skill; prefer the custom header.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/tripletex-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/tripletex-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/tripletex-webhooks/examples/fastapi/).

## Event Types

Events are named `object.verb`. The body is `{ subscriptionId, event, id, value }`; `value` is the
object DTO (shaped by the subscription's optional `fields`), or `null` for deletes.

| Event | Sent When |
| --- | --- |
| `invoice.charged` | A new invoice has been created and charged |
| `customer.create` / `.update` / `.delete` | A customer is created / updated / deleted |
| `order.create` / `.update` / `.delete` | An order is created / updated / deleted |
| `product.create` / `.update` / `.delete` | A product is created / updated / deleted |
| `supplier.*`, `employee.*`, `account.*`, `project.*`, `contact.*`, `archiverelation.*`, `voucher.*` | Same create / update / delete verbs |
| `notification.sent` | A notification was sent to the employee (token owner) |
| `voucherstatus.ready` | A new voucher is ready to be processed from the voucher reception |
| `closegroup.create` | A close group is created (postings matched/closed), incl. automatic closes |
| `closegroup.delete` | A close group is deleted (postings reopened); `value` is `null` |

You must subscribe to each verb separately. `GET https://tripletex.no/v2/event` lists the events
currently available.

## Environment Variables

```bash
TRIPLETEX_WEBHOOK_HEADER=Authorization                    # your authHeaderName
TRIPLETEX_WEBHOOK_SECRET=Bearer your-long-random-secret   # your authHeaderValue, exactly

```

## Local Development

Tripletex can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 tripletex --path /webhooks/tripletex

```

Use the printed URL as the `targetUrl` of the event subscription.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/tripletex-webhooks/references/overview.md) - What Tripletex webhooks are, events and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/tripletex-webhooks/references/setup.md) - Create an event subscription with an auth header
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/tripletex-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/tripletex-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests