Spring til indhold

Webhooks

En webhook lader Kuvert fortælle dit system, at noget er sket — et kort er udstedt, et beløb er trukket, en ordre er betalt eller refunderet, et bord er booket eller aflyst, et arrangement er udgivet, ændret eller aflyst. Hver leverance er signeret, så du kan bevise, at den kom fra os.

Opret et endepunkt

Under Indstillinger → Webhooks tilføjer du den URL, hændelserne skal sendes til. Du får en hemmelighed tilbage — den bruges til at verificere signaturen og skal opbevares som en adgangskode.

Et system kan også tilmelde sig selv med en API-nøgle, uden at nogen åbner dashboardet. Det er sådan en integration, du ikke selv har skrevet, kobler sig på: den får din nøgle og registrerer sin egen adresse.

List endepunkter

GET/v1/api/webhooks

Forretningens endepunkter — også dem, der er oprettet i dashboardet — hver med sin hemmelighed og udfaldet af den seneste leverance.

curl https://api.kuvert.dk/v1/api/webhooks \
  -H "Authorization: Bearer $KUVERT_API_KEY"
200 OKJSON
{
  "endpoints": [
    {
      "id": "weh_5e8a1c3f7b9d42e6a0c4f8b2d6e1a3c9",
      "url": "https://dit-system.dk/kuvert",
      "events": [
        "giftcard.issued",
        "giftcard.redeemed"
      ],
      "status": "active",
      "secret": "whsec_docs-example-not-a-real-secret",
      "lastDeliveryAt": "2026-09-29T09:12:44.000Z",
      "lastDeliveryOk": true,
      "lastDeliveryDetail": null,
      "consecutiveFailures": 0,
      "createdAt": "2026-09-01T07:30:00.000Z"
    }
  ]
}

Svarfelter

Endepunkterne står under endpoints. Hvert har disse felter:

idstring
Endepunktets id (weh_…).
urlstring
Adressen, hændelserne sendes til.
eventsstring[]
Hændelserne, endepunktet abonnerer på. En tom liste betyder alle.
status"active" | "disabled"
Kun active får leverancer.
secretstring
Hemmeligheden (whsec_…), signaturen beregnes med.
lastDeliveryAtstring | null
Hvornår der sidst blev forsøgt en leverance. null, hvis der aldrig er sendt noget.
lastDeliveryOkboolean | null
Om den seneste leverance fik 2xx. null betyder aldrig forsøgt — ikke fejlet.
lastDeliveryDetailstring | null
Hvorfor den seneste fejlede: en HTTP-status eller en transportfejl. null, når den lykkedes.
consecutiveFailuresinteger
Fejlede leverancer i træk. Den første, der lykkes, nulstiller tallet.
createdAtstring
Hvornår endepunktet blev oprettet.

Statuskoder

  • 200

    Listen, eventuelt tom.

En fejl har altid formen { "error": "…" }. Ethvert kald kan desuden svare 401, 403, 429 og 500 — se Fejl.

Tilføj et endepunkt

POST/v1/api/webhooks

Registrerer en adresse og svarer med endepunktet og dets hemmelighed. Det får leverancer med det samme.

curl -X POST https://api.kuvert.dk/v1/api/webhooks \
  -H "Authorization: Bearer $KUVERT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://dit-system.dk/kuvert",
    "events": [
      "giftcard.issued",
      "giftcard.redeemed"
    ]
  }'
201 CreatedJSON
{
  "endpoint": {
    "id": "weh_5e8a1c3f7b9d42e6a0c4f8b2d6e1a3c9",
    "url": "https://dit-system.dk/kuvert",
    "events": [
      "giftcard.issued",
      "giftcard.redeemed"
    ],
    "status": "active",
    "secret": "whsec_docs-example-not-a-real-secret",
    "lastDeliveryAt": null,
    "lastDeliveryOk": null,
    "lastDeliveryDetail": null,
    "consecutiveFailures": 0,
    "createdAt": "2026-09-29T09:00:00.000Z"
  }
}

Krop

urlstringPåkrævet
En offentlig https-adresse, højst 2048 tegn. Interne og lokale adresser afvises.
eventsstring[]Påkrævet
Hændelserne, der skal sendes. En tom liste betyder alle — også dem, der kommer til senere.

Svarfelter

Endepunktet står under endpoint, med de samme felter som ved List endepunkter.

Statuskoder

  • 201

    Endepunktet er oprettet.

  • 400invalid_webhook_endpoint

    Kroppen matcher ikke: adressen er ikke en offentlig https-adresse, eller en hændelse findes ikke.

En fejl har altid formen { "error": "…" }. Ethvert kald kan desuden svare 401, 403, 429 og 500 — se Fejl.

Ret et endepunkt

PATCH/v1/api/webhooks

Retter adresse, hændelser eller status. Endepunktets id står i kroppen, ikke i stien — send kun de felter, du vil ændre.

curl -X PATCH https://api.kuvert.dk/v1/api/webhooks \
  -H "Authorization: Bearer $KUVERT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "weh_5e8a1c3f7b9d42e6a0c4f8b2d6e1a3c9",
    "status": "disabled"
  }'
200 OKJSON
{
  "endpoint": {
    "id": "weh_5e8a1c3f7b9d42e6a0c4f8b2d6e1a3c9",
    "url": "https://dit-system.dk/kuvert",
    "events": [
      "giftcard.issued",
      "giftcard.redeemed"
    ],
    "status": "disabled",
    "secret": "whsec_docs-example-not-a-real-secret",
    "lastDeliveryAt": "2026-09-29T09:12:44.000Z",
    "lastDeliveryOk": true,
    "lastDeliveryDetail": null,
    "consecutiveFailures": 0,
    "createdAt": "2026-09-01T07:30:00.000Z"
  }
}

Krop

idstringPåkrævet
Endepunktets id (weh_…).
urlstringValgfri
Ny adresse, med samme krav som ved oprettelse.
eventsstring[]Valgfri
En ny liste af hændelser. Den erstatter den gamle helt.
status"active" | "disabled"Valgfri
disabled standser leverancerne uden at slette endepunktet.

Svarfelter

Det rettede endepunkt står under endpoint.

Statuskoder

  • 200

    Endepunktet er rettet.

  • 400invalid_request

    Kroppen matcher ikke — typisk et manglende id.

  • 404webhook_endpoint_not_found

    Endepunktet findes ikke på din forretning.

En fejl har altid formen { "error": "…" }. Ethvert kald kan desuden svare 401, 403, 429 og 500 — se Fejl.

Bruges ofte sammen med

Slet et endepunkt

DELETE/v1/api/webhooks/{id}

Fjerner endepunktet for altid. Vil du bare holde pause, så sæt status til disabled i stedet.

curl -X DELETE https://api.kuvert.dk/v1/api/webhooks/weh_5e8a1c3f7b9d42e6a0c4f8b2d6e1a3c9 \
  -H "Authorization: Bearer $KUVERT_API_KEY"
200 OKJSON
{
  "deleted": true
}

Stiparametre

idstringPåkrævet
Endepunktets id (weh_…).

Svarfelter

deletedboolean
Altid true.

Statuskoder

  • 200

    Endepunktet er slettet.

  • 404webhook_endpoint_not_found

    Endepunktet findes ikke på din forretning.

En fejl har altid formen { "error": "…" }. Ethvert kald kan desuden svare 401, 403, 429 og 500 — se Fejl.

Bruges ofte sammen med

Leverancen

Hver hændelse sendes som en POST med en JSON-krop i fire felter: id er leverancens eget id (whevt_…), type er hændelsen, createdAt er tidspunktet, og data er det, hændelsen handler om. Tre headers følger med: content-type, kuvert-event-type og kuvert-signature.

En hel leveranceHTTP
POST /kuvert HTTP/1.1
Host: dit-system.dk
content-type: application/json
kuvert-event-type: giftcard.redeemed
kuvert-signature: t=1790673164,v1=067aae7b381a2e42b518ef5781e9747638aa4c987e4f68396bef9a06a78d71a8

{"id":"whevt_c81f5a2d9e3b47c6a0d4e8f1b5c9a2d7","type":"giftcard.redeemed","createdAt":"2026-09-29T09:12:44.118Z","data":{"code":"KUV-7K9F-2XQD","amountOre":6400,"remainingBalanceOre":43600}}

Signaturen i eksemplet er ægte. Med hemmeligheden whsec_docs-example-not-a-real-secret giver opskriften under Signaturen præcis den v1 — kør din egen verificering på den, med tidstolerancen slået fra, da tidspunktet ligger i fortiden.

Hændelser

giftcard.issued

Et gavekort er udstedt.

Eksempel på leveranceJSON
{
  "id": "whevt_c81f5a2d9e3b47c6a0d4e8f1b5c9a2d7",
  "type": "giftcard.issued",
  "createdAt": "2026-09-29T09:12:44.118Z",
  "data": {
    "code": "KUV-M4TQ-8WZR",
    "initialOre": 50000,
    "orderId": "ord_7d3f0b9c1e2a45f68b4c9d0e1f2a3b5c"
  }
}

giftcard.redeemed

Et beløb er trukket fra et kort.

Eksempel på leveranceJSON
{
  "id": "whevt_c81f5a2d9e3b47c6a0d4e8f1b5c9a2d7",
  "type": "giftcard.redeemed",
  "createdAt": "2026-09-29T09:12:44.118Z",
  "data": {
    "code": "KUV-7K9F-2XQD",
    "amountOre": 6400,
    "remainingBalanceOre": 43600
  }
}

order.paid

En ordre er betalt.

Eksempel på leveranceJSON
{
  "id": "whevt_c81f5a2d9e3b47c6a0d4e8f1b5c9a2d7",
  "type": "order.paid",
  "createdAt": "2026-09-29T09:12:44.118Z",
  "data": {
    "orderId": "ord_7d3f0b9c1e2a45f68b4c9d0e1f2a3b5c",
    "totalOre": 50000,
    "cardCodes": [
      "KUV-M4TQ-8WZR"
    ]
  }
}

order.refunded

En ordre er refunderet.

Eksempel på leveranceJSON
{
  "id": "whevt_c81f5a2d9e3b47c6a0d4e8f1b5c9a2d7",
  "type": "order.refunded",
  "createdAt": "2026-09-29T09:12:44.118Z",
  "data": {
    "orderId": "ord_7d3f0b9c1e2a45f68b4c9d0e1f2a3b5c",
    "refundedOre": 50500
  }
}

ticket.issued

En billet er udstedt af en betalt ordre.

Eksempel på leveranceJSON
{
  "id": "whevt_c81f5a2d9e3b47c6a0d4e8f1b5c9a2d7",
  "type": "ticket.issued",
  "createdAt": "2026-09-29T09:12:44.118Z",
  "data": {
    "code": "TKT-H3NW-7PKC",
    "eventId": "evt_1a9c4e7b2d5f48a3b6c9e0d2f4a7b1c8",
    "orderId": "ord_7d3f0b9c1e2a45f68b4c9d0e1f2a3b5c"
  }
}

order.disputed

Køberens kortudsteder har bestridt betalingen. Ordrens gavekort og billetter er sat på pause og kan ikke bruges.

Eksempel på leveranceJSON
{
  "id": "whevt_c81f5a2d9e3b47c6a0d4e8f1b5c9a2d7",
  "type": "order.disputed",
  "createdAt": "2026-09-29T09:12:44.118Z",
  "data": {
    "orderId": "ord_7d3f0b9c1e2a45f68b4c9d0e1f2a3b5c",
    "frozenCards": 1,
    "frozenTickets": 0
  }
}

order.dispute_closed

Sagen er afgjort. won: true betyder, at pausen er hævet og kortene virker igen; false at de er annulleret.

Eksempel på leveranceJSON
{
  "id": "whevt_c81f5a2d9e3b47c6a0d4e8f1b5c9a2d7",
  "type": "order.dispute_closed",
  "createdAt": "2026-09-29T09:12:44.118Z",
  "data": {
    "orderId": "ord_7d3f0b9c1e2a45f68b4c9d0e1f2a3b5c",
    "won": true,
    "disputedOre": 50500
  }
}

booking.created

Et bord er booket — af gæsten på bookingsiden, i telefonen eller ved døren.

Eksempel på leveranceJSON
{
  "id": "whevt_c81f5a2d9e3b47c6a0d4e8f1b5c9a2d7",
  "type": "booking.created",
  "createdAt": "2026-09-29T09:12:44.118Z",
  "data": {
    "bookingId": "bkg_6c0e3a9f5b1d47e2a8c4f0b6d2e9a5c1",
    "date": "2026-10-02",
    "startsAt": "2026-10-02T17:30:00.000Z",
    "endsAt": "2026-10-02T19:30:00.000Z",
    "guests": 4,
    "status": "confirmed",
    "source": "storefront",
    "guestName": "Mette Hansen",
    "guestEmail": "mette@dit-domaene.dk",
    "guestPhone": "+45 12 34 56 78",
    "tableIds": [
      "btb_0d4a8e2c6f1b43a9b7e5c3d1f9a2e6b8"
    ]
  }
}

booking.changed

En bordbooking har fået ny tid, dato, antal eller borde, eller en ny status. Hele bookingen følger med, og status siger, hvor den står.

Eksempel på leveranceJSON
{
  "id": "whevt_c81f5a2d9e3b47c6a0d4e8f1b5c9a2d7",
  "type": "booking.changed",
  "createdAt": "2026-09-29T09:12:44.118Z",
  "data": {
    "bookingId": "bkg_6c0e3a9f5b1d47e2a8c4f0b6d2e9a5c1",
    "date": "2026-10-02",
    "startsAt": "2026-10-02T17:30:00.000Z",
    "endsAt": "2026-10-02T19:30:00.000Z",
    "guests": 6,
    "status": "confirmed",
    "source": "storefront",
    "guestName": "Mette Hansen",
    "guestEmail": "mette@dit-domaene.dk",
    "guestPhone": "+45 12 34 56 78",
    "tableIds": [
      "btb_0d4a8e2c6f1b43a9b7e5c3d1f9a2e6b8"
    ]
  }
}

booking.cancelled

En bordbooking er aflyst, af gæsten eller af restauranten. Bookinger, der flyttes ind fra et andet system, sender ingen hændelser.

Eksempel på leveranceJSON
{
  "id": "whevt_c81f5a2d9e3b47c6a0d4e8f1b5c9a2d7",
  "type": "booking.cancelled",
  "createdAt": "2026-09-29T09:12:44.118Z",
  "data": {
    "bookingId": "bkg_6c0e3a9f5b1d47e2a8c4f0b6d2e9a5c1",
    "date": "2026-10-02",
    "startsAt": "2026-10-02T17:30:00.000Z",
    "endsAt": "2026-10-02T19:30:00.000Z",
    "guests": 4,
    "status": "cancelled",
    "source": "storefront",
    "guestName": "Mette Hansen",
    "guestEmail": "mette@dit-domaene.dk",
    "guestPhone": "+45 12 34 56 78",
    "tableIds": [
      "btb_0d4a8e2c6f1b43a9b7e5c3d1f9a2e6b8"
    ]
  }
}

event.published

Et arrangement er sat til salg. Hele aftenen følger med — navn, tider, sted, serie, line-up og genrer — i samme form som listen på GET /v1/merchants/{slug}, med eventId i stedet for id.

Eksempel på leveranceJSON
{
  "id": "whevt_c81f5a2d9e3b47c6a0d4e8f1b5c9a2d7",
  "type": "event.published",
  "createdAt": "2026-09-29T09:12:44.118Z",
  "data": {
    "eventId": "evt_1a9c4e7b2d5f48a3b6c9e0d2f4a7b1c8",
    "slug": "vortex-dimension-2026-10-03",
    "name": "Vortex Dimension",
    "startsAt": "2026-10-03T21:00:00.000Z",
    "endsAt": "2026-10-04T03:00:00.000Z",
    "venue": "Club Vortex",
    "bannerUrl": "https://api.kuvert.dk/v1/media/med_2b7e4c9a1f3d48e6b0a5c8d2f1e7a4b9",
    "bannerWidth": 1080,
    "bannerHeight": 1350,
    "salesCloseAt": "2026-10-03T21:00:00.000Z",
    "status": "published",
    "cancelNote": null,
    "series": null,
    "seriesDate": null,
    "subtitle": "No Boyz On Deck",
    "lineup": [
      {
        "name": "Faustix",
        "role": "DJ",
        "time": "2026-10-03T23:00:00.000Z"
      }
    ],
    "genres": [
      "House",
      "Techno"
    ],
    "doorsAt": "2026-10-03T21:00:00.000Z",
    "lastEntryAt": "2026-10-04T01:30:00.000Z",
    "dressCode": null,
    "ticketsUrl": null
  }
}

event.updated

Noget offentligt på et udgivet arrangement er ændret. changedFields siger hvad — fx lineup, eller tiers, når en pris er ændret.

Eksempel på leveranceJSON
{
  "id": "whevt_c81f5a2d9e3b47c6a0d4e8f1b5c9a2d7",
  "type": "event.updated",
  "createdAt": "2026-09-29T09:12:44.118Z",
  "data": {
    "eventId": "evt_1a9c4e7b2d5f48a3b6c9e0d2f4a7b1c8",
    "slug": "vortex-dimension-2026-10-03",
    "name": "Vortex Dimension",
    "startsAt": "2026-10-03T21:00:00.000Z",
    "endsAt": "2026-10-04T03:00:00.000Z",
    "venue": "Club Vortex",
    "bannerUrl": "https://api.kuvert.dk/v1/media/med_2b7e4c9a1f3d48e6b0a5c8d2f1e7a4b9",
    "bannerWidth": 1080,
    "bannerHeight": 1350,
    "salesCloseAt": "2026-10-03T21:00:00.000Z",
    "status": "published",
    "cancelNote": null,
    "series": null,
    "seriesDate": null,
    "subtitle": "No Boyz On Deck",
    "lineup": [
      {
        "name": "Faustix",
        "role": "DJ",
        "time": "2026-10-03T23:00:00.000Z"
      }
    ],
    "genres": [
      "House",
      "Techno"
    ],
    "doorsAt": "2026-10-03T21:00:00.000Z",
    "lastEntryAt": "2026-10-04T01:30:00.000Z",
    "dressCode": null,
    "ticketsUrl": null,
    "changedFields": [
      "lineup"
    ]
  }
}

event.canceled

Et udgivet arrangement er aflyst. cancelNote er den besked, I skrev til gæsterne.

Eksempel på leveranceJSON
{
  "id": "whevt_c81f5a2d9e3b47c6a0d4e8f1b5c9a2d7",
  "type": "event.canceled",
  "createdAt": "2026-09-29T09:12:44.118Z",
  "data": {
    "eventId": "evt_1a9c4e7b2d5f48a3b6c9e0d2f4a7b1c8",
    "slug": "vortex-dimension-2026-10-03",
    "name": "Vortex Dimension",
    "startsAt": "2026-10-03T21:00:00.000Z",
    "endsAt": "2026-10-04T03:00:00.000Z",
    "venue": "Club Vortex",
    "bannerUrl": "https://api.kuvert.dk/v1/media/med_2b7e4c9a1f3d48e6b0a5c8d2f1e7a4b9",
    "bannerWidth": 1080,
    "bannerHeight": 1350,
    "salesCloseAt": "2026-10-03T21:00:00.000Z",
    "status": "canceled",
    "cancelNote": "Aftenen er aflyst. Alle billetter refunderes automatisk.",
    "series": null,
    "seriesDate": null,
    "subtitle": "No Boyz On Deck",
    "lineup": [
      {
        "name": "Faustix",
        "role": "DJ",
        "time": "2026-10-03T23:00:00.000Z"
      }
    ],
    "genres": [
      "House",
      "Techno"
    ],
    "doorsAt": "2026-10-03T21:00:00.000Z",
    "lastEntryAt": "2026-10-04T01:30:00.000Z",
    "dressCode": null,
    "ticketsUrl": null
  }
}

event.unpublished

Et udgivet arrangement er sat tilbage til kladde. En kladde sender aldrig hændelser.

Eksempel på leveranceJSON
{
  "id": "whevt_c81f5a2d9e3b47c6a0d4e8f1b5c9a2d7",
  "type": "event.unpublished",
  "createdAt": "2026-09-29T09:12:44.118Z",
  "data": {
    "eventId": "evt_1a9c4e7b2d5f48a3b6c9e0d2f4a7b1c8",
    "slug": "vortex-dimension-2026-10-03",
    "name": "Vortex Dimension",
    "startsAt": "2026-10-03T21:00:00.000Z",
    "endsAt": "2026-10-04T03:00:00.000Z",
    "venue": "Club Vortex",
    "bannerUrl": "https://api.kuvert.dk/v1/media/med_2b7e4c9a1f3d48e6b0a5c8d2f1e7a4b9",
    "bannerWidth": 1080,
    "bannerHeight": 1350,
    "salesCloseAt": "2026-10-03T21:00:00.000Z",
    "status": "draft",
    "cancelNote": null,
    "series": null,
    "seriesDate": null,
    "subtitle": "No Boyz On Deck",
    "lineup": [
      {
        "name": "Faustix",
        "role": "DJ",
        "time": "2026-10-03T23:00:00.000Z"
      }
    ],
    "genres": [
      "House",
      "Techno"
    ],
    "doorsAt": "2026-10-03T21:00:00.000Z",
    "lastEntryAt": "2026-10-04T01:30:00.000Z",
    "dressCode": null,
    "ticketsUrl": null
  }
}

Signaturen

Hver leverance bærer en kuvert-signature-header med et tidsstempel og en HMAC:

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

Signaturen er HMAC-SHA256 i hex over strengen tidsstempel, punktum, den rå krop — beregnet med din endepunkts-hemmelighed:

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

Verificér signaturen

# t og v1 fra kuvert-signature, body.json er den rå krop, præcis som den kom ind
printf '%s.%s' "$t" "$(cat body.json)" \
  | openssl dgst -sha256 -hmac "$KUVERT_WEBHOOK_SECRET" -hex
# Sammenlign resultatet med v1

Svar hurtigt

  • Kvittér med 2xx, så snart du har taget imod. Læg det tunge arbejde i en kø.
  • Levering er bedste forsøg — et endepunkt, der er nede, holder aldrig et køb tilbage hos forretningen.
  • Vi venter højst 5 sekunder på svaret. Alt andet end 2xx inden da tæller som en fejlet leverance — også en omdirigering, som vi ikke følger.

Genforsøg

Der er ingen genforsøg. Hver hændelse sendes én gang til hvert endepunkt, og en leverance, der fejler, sendes ikke igen.

Hvordan den seneste leverance gik, står på endepunktet: List endepunkter svarer med lastDeliveryAt, lastDeliveryOk, lastDeliveryDetail og consecutiveFailures, og det samme står under Indstillinger → Webhooks. Vi slår aldrig et endepunkt fra af os selv — kun du kan sætte det til disabled.

Krav til URL

Endepunktet skal være en offentligt tilgængelig HTTPS-adresse. Interne adresser afvises — det er en bevidst spærre mod, at et endepunkt bruges til at nå ind i vores eget netværk.

Var siden nyttig?