Skip to content
Documentation

API

Webhooks

A webhook lets Kuvert tell your system that something has happened — a card has been issued, an amount has been debited, an order has been paid or refunded, a table has been booked or cancelled. Each delivery is signed, so you can prove it came from us.

Create an endpoint

Under Settings → Webhooks you add the URL events should be sent to. You get a secret back — it is used to verify the signature and should be stored like a password.

A system can also subscribe to itself with an API key, without anyone opening the dashboard. That is how an integration you have not written yourself hooks in: it gets your key and registers its own address.

MethodPathDoes
GET/v1/api/webhooksLists your endpoints
POST/v1/api/webhooksCreates one and returns the secret
PATCH/v1/api/webhooksEdits address, events or status
DELETE/v1/api/webhooks/{id}Removes it again
Terminal
curl -X POST https://api.kuvert.dk/v1/api/webhooks \
  -H "Authorization: Bearer pk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"url":"https://dit-system.dk/kuvert","events":["giftcard.redeemed"]}'

Events

EventSent when
giftcard.issuedA gift card has been issued.
giftcard.redeemedAn amount has been withdrawn from a card.
order.paidAn order has been paid.
order.refundedAn order has been refunded.
ticket.issuedA ticket has been issued from a paid order.
order.disputedThe buyer's card issuer has disputed the payment. The order's gift cards and tickets have been paused and cannot be used.
order.dispute_closedThe case has been decided. `won: true` means the pause has been lifted and the cards work again; `false` means they have been cancelled.
booking.createdA table has been booked — by the guest on the booking page, on the phone or at the door.
booking.changedA table booking has a new time, date, party size or tables, or a new status. The whole booking comes with it, and `status` says where it stands.
booking.cancelledA table booking has been cancelled, by the guest or by the restaurant. Bookings moved in from another system send no events.

The signature

Each delivery carries a kuvert-signature header with a timestamp and an HMAC:

kuvert-signature: t=1753440000,v1=6f3a…

The signature is HMAC-SHA256 in hex over the string timestamp, period, the raw body — calculated with your endpoint secret:

HMAC_SHA256(secret, "{t}.{raw body}")

Verify in Node

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

function verify(header, rawBody, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(
    header.split(',').map((kv) => kv.split('=')),
  )
  const t = Number(parts.t)
  if (!t || !parts.v1) return false

  // Afvis for gamle leverancer — det stopper genafspilning.
  if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false

  const expected = createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex')

  const a = Buffer.from(expected, 'hex')
  const b = Buffer.from(parts.v1, 'hex')
  return a.length === b.length && timingSafeEqual(a, b)
}

Reply quickly

  • Acknowledge with 2xx as soon as you have received it. Put the heavy work in a queue.
  • Delivery is best effort — an endpoint that is down never holds back a purchase from the business.
  • Expect to receive the same event twice, and make your handling idempotent.

URL requirements

The endpoint must be a publicly accessible HTTPS address. Internal addresses are rejected — it is a deliberate barrier to prevent an endpoint from being used to reach into our own network.