Gå til innhold

Webhooks

En webhook lar Kuvert fortelle systemet ditt at noe har skjedd, for eksempel at et kort er utstedt, et beløp er trukket, en ordre er betalt eller refundert, et bord er booket eller avbestilt, eller at et arrangement er publisert, endret eller avlyst. Hver levering er signert, så du kan bevise at den kom fra oss.

Opprett et endepunkt

Under Innstillinger → Webhooks legger du til URL-en hendelsene skal sendes til. Du får en hemmelighet tilbake. Den brukes til å verifisere signaturen og skal oppbevares som et passord.

Et system kan også abonnere på seg selv med en API-nøkkel, uten at noen åpner dashbordet. Det er slik en integrasjon du ikke selv har skrevet kobler seg på: den får din nøkkel og registrerer sin egen adresse.

List endepunkter

GET/v1/api/webhooks

Bedriftens endepunkter, også de som er opprettet i dashbordet, hvert med sin hemmelighet og utfallet av den siste leveringen.

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

Endepunktene ligger under endpoints. Hvert har disse feltene:

idstring
Endepunktets id (weh_…).
urlstring
Adressen hendelsene sendes til.
eventsstring[]
Hendelsene endepunktet abonnerer på. En tom liste betyr alle.
status"active" | "disabled"
Bare active får leveranser.
secretstring
Hemmeligheten (whsec_…) signaturen regnes ut med.
lastDeliveryAtstring | null
Når det sist ble forsøkt en leveranse. null hvis ingenting er sendt.
lastDeliveryOkboolean | null
Om den siste leveringen fikk 2xx. null betyr aldri forsøkt og ikke feilet.
lastDeliveryDetailstring | null
Hvorfor den siste feilet: en HTTP-status eller en transportfeil. null når den lyktes.
consecutiveFailuresinteger
Feilede leveranser på rad. Den første som lykkes, nullstiller tallet.
createdAtstring
Når endepunktet ble opprettet.

Statuskoder

  • 200

    Listen, eventuelt tom.

En feil har alltid formen { "error": "…" }. Alle kall kan dessuten svare 401, 403, 429 og 500. Se Feil.

Legg til et endepunkt

POST/v1/api/webhooks

Registrerer en adresse og svarer med endepunktet og hemmeligheten. Det får leveranser med en gang.

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"
  }
}

Kropp

urlstringPåkrevd
En offentlig https-adresse, høyst 2048 tegn. Interne og lokale adresser avvises.
eventsstring[]Påkrevd
Hendelsene som skal sendes. En tom liste betyr alle, også de som kommer til senere.

Svarfelter

Endepunktet ligger under endpoint, med de samme feltene som ved List endepunkter.

Statuskoder

  • 201

    Endepunktet er opprettet.

  • 400invalid_webhook_endpoint

    Kroppen stemmer ikke: adressen er ikke en offentlig https-adresse, eller en hendelse finnes ikke.

En feil har alltid formen { "error": "…" }. Alle kall kan dessuten svare 401, 403, 429 og 500. Se Feil.

Endre et endepunkt

PATCH/v1/api/webhooks

Endrer adresse, hendelser eller status. Endepunktets id står i kroppen, ikke i stien. Send bare feltene du vil endre.

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"
  }
}

Kropp

idstringPåkrevd
Endepunktets id (weh_…).
urlstringValgfri
Ny adresse, med de samme kravene som ved opprettelse.
eventsstring[]Valgfri
En ny liste over hendelser. Den erstatter den gamle helt.
status"active" | "disabled"Valgfri
disabled stanser leveransene uten å slette endepunktet.

Svarfelter

Det endrede endepunktet ligger under endpoint.

Statuskoder

  • 200

    Endepunktet er endret.

  • 400invalid_request

    Kroppen stemmer ikke, som regel fordi id mangler.

  • 404webhook_endpoint_not_found

    Endepunktet finnes ikke hos bedriften din.

En feil har alltid formen { "error": "…" }. Alle kall kan dessuten svare 401, 403, 429 og 500. Se Feil.

Brukes ofte sammen med

Slett et endepunkt

DELETE/v1/api/webhooks/{id}

Fjerner endepunktet for godt. Vil du bare ta en pause, setter du 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
}

Stiparametere

idstringPåkrevd
Endepunktets id (weh_…).

Svarfelter

deletedboolean
Alltid true.

Statuskoder

  • 200

    Endepunktet er slettet.

  • 404webhook_endpoint_not_found

    Endepunktet finnes ikke hos bedriften din.

En feil har alltid formen { "error": "…" }. Alle kall kan dessuten svare 401, 403, 429 og 500. Se Feil.

Brukes ofte sammen med

Leveransen

Hver hendelse sendes som en POST med en JSON-kropp i fire felter: id er leveransens egen id (whevt_…), type er hendelsen, createdAt er tidspunktet, og data er det hendelsen handler om. Tre headere følger med: content-type, kuvert-event-type og kuvert-signature.

En hel leveranseHTTP
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 eksempelet er ekte. Med hemmeligheten whsec_docs-example-not-a-real-secret gir oppskriften under Signaturen nøyaktig den v1. Kjør din egen verifisering på den, med tidstoleransen slått av, siden tidspunktet ligger i fortiden.

Hendelser

giftcard.issued

Et gavekort er utstedt.

Eksempel på leveranseJSON
{
  "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øp er trukket fra et kort.

Eksempel på leveranseJSON
{
  "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. På en billettordre følger kjøperens svar på kveldens spørsmål med i answers, for eksempel et lagnavn, hvert med spørsmålet slik det lød da det ble besvart.

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

order.refunded

En ordre er refundert.

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

ticket.issued

En billett er utstedt av en betalt ordre. Ordrens svar på kveldens spørsmål følger med i answers på hver billett.

Eksempel på leveranseJSON
{
  "id": "whevt_c81f5a2d9e3b47c6a0d4e8f1b5c9a2d7",
  "type": "ticket.issued",
  "createdAt": "2026-09-29T09:12:44.118Z",
  "data": {
    "code": "TKT-H3NW-7PKC",
    "eventId": "evt_1a9c4e7b2d5f48a3b6c9e0d2f4a7b1c8",
    "orderId": "ord_7d3f0b9c1e2a45f68b4c9d0e1f2a3b5c",
    "answers": [
      {
        "questionId": "eqs_5c1e9a7d3b2f48a6",
        "label": "Holdnavn",
        "answer": "Ølnørderne"
      }
    ],
    "seller": {
      "slug": "club-vortex",
      "name": "Club Vortex"
    },
    "host": {
      "slug": "club-vortex",
      "name": "Club Vortex"
    },
    "cohosts": []
  }
}

order.disputed

Kjøperens kortutssteder har bestridt betalingen. Ordren sine gavekort og billetter er satt på pause og kan ikke brukes.

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

order.dispute_closed

Saken er avgjort. won: true betyr at pauseringen er hevet og kortene virker igjen; false at de er annullert.

Eksempel på leveranseJSON
{
  "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 av gjesten på bookingsiden, på telefon eller i døra.

Eksempel på leveranseJSON
{
  "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 bordreservasjon har fått nytt tidspunkt, ny dato, nytt antall, nye bord eller en ny status. Hele bookingen følger med, og status sier hvor den står.

Eksempel på leveranseJSON
{
  "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 bordreservasjon er avbestilt, av gjesten eller av restauranten. Bookinger som flyttes inn fra et annet system, sender ingen hendelser.

Eksempel på leveranseJSON
{
  "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 satt i salg. Hele kvelden følger med (navn, tider, sted, serie, line-up og sjangre) i samme form som listen på GET /v1/merchants/{slug}, med eventId i stedet for id.

Eksempel på leveranseJSON
{
  "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,
    "host": {
      "slug": "club-vortex",
      "name": "Club Vortex"
    },
    "cohosts": []
  }
}

event.updated

Noe offentlig på et publisert arrangement er endret. changedFields sier hva, for eksempel lineup, eller tiers når en pris er endret.

Eksempel på leveranseJSON
{
  "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,
    "host": {
      "slug": "club-vortex",
      "name": "Club Vortex"
    },
    "cohosts": [],
    "changedFields": [
      "lineup"
    ]
  }
}

event.canceled

Et publisert arrangement er avlyst. cancelNote er beskjeden dere skrev til gjestene.

Eksempel på leveranseJSON
{
  "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,
    "host": {
      "slug": "club-vortex",
      "name": "Club Vortex"
    },
    "cohosts": []
  }
}

event.unpublished

Et publisert arrangement er satt tilbake til kladd. En kladd sender aldri hendelser.

Eksempel på leveranseJSON
{
  "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,
    "host": {
      "slug": "club-vortex",
      "name": "Club Vortex"
    },
    "cohosts": []
  }
}

Signaturen

Hver levering 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å kroppen, beregnet med endepunktets hemmelighet:

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

Verifiser 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 raskt

  • Kvitter med 2xx så snart du har mottatt. Legg det tunge arbeidet i en kø.
  • Levering skjer etter beste evne. Et endepunkt som er nede, holder aldri tilbake et kjøp hos bedriften.
  • Vi venter høyst 5 sekunder på svaret. Alt annet enn 2xx innen da teller som en mislykket levering, også en omdirigering, som vi ikke følger.

Nye forsøk

Feiler en levering, sender vi den på nytt med økende pauser (1 minutt, 5 minutter, 30 minutter, 2 timer, 6 timer, 12 timer og 24 timer), totalt 8 forsøk over knapt to døgn. Det er den samme kroppen med den samme id-en hver gang, men med en ny signatur, så tidsstempelet er ferskt. Bruk id-en til å kjenne igjen en hendelse dere allerede har tatt imot: kommer ikke svaret fram, sender vi den på nytt.

Hvordan den siste leveringen gikk, står på endepunktet: List endepunkter svarer med lastDeliveryAt, lastDeliveryOk, lastDeliveryDetail og consecutiveFailures, og det samme står under Innstillinger → Webhooks. Vi slår aldri av et endepunkt på egen hånd. Bare du kan sette det til disabled, og et endepunkt som er slått av, får heller ingen nye forsøk.

Krav til URL

Endepunktet må være en offentlig tilgjengelig HTTPS-adresse. Interne adresser avvises med vilje, så et endepunkt ikke kan brukes til å nå inn i vårt eget nettverk.

Var siden nyttig?