Hoppa till innehållet

Webhooks

En webhook låter Kuvert tala om för ditt system att något har hänt, t.ex. att ett kort har utfärdats, ett belopp har dragits, en order har betalats eller återbetalats, ett bord har bokats eller avbokats, eller ett arrangemang har publicerats, ändrats eller ställts in. Varje leverans är signerad, så att du kan bevisa att den kom från oss.

Skapa en slutpunkt

Under Inställningar → Webhooks lägger du till den URL som händelserna ska skickas till. Du får en hemlighet tillbaka. Den används för att verifiera signaturen och ska förvaras som ett lösenord.

Ett system kan också anmäla sig själv med en API-nyckel, utan att någon öppnar kontrollpanelen. Det är en sådan integration som du inte själv skrev som ansluter sig: den får din nyckel och registrerar sin egen adress.

Lista slutpunkter

GET/v1/api/webhooks

Företagets slutpunkter, även de som har skapats i kontrollpanelen, var och en med sin hemlighet och utfallet av den senaste leveransen.

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

Svarsfält

Slutpunkterna ligger under endpoints. Var och en har de här fälten:

idstring
Slutpunktens id (weh_…).
urlstring
Adressen som händelserna skickas till.
eventsstring[]
Händelserna som slutpunkten prenumererar på. En tom lista betyder alla.
status"active" | "disabled"
Bara active får leveranser.
secretstring
Hemligheten (whsec_…) som signaturen beräknas med.
lastDeliveryAtstring | null
När en leverans senast försöktes. null om inget någonsin skickats.
lastDeliveryOkboolean | null
Om den senaste leveransen fick 2xx. null betyder aldrig försökt och inte misslyckad.
lastDeliveryDetailstring | null
Varför den senaste misslyckades: en HTTP-status eller ett transportfel. null när den lyckades.
consecutiveFailuresinteger
Misslyckade leveranser i rad. Den första som lyckas nollställer talet.
createdAtstring
När slutpunkten skapades.

Statuskoder

  • 200

    Listan, eventuellt tom.

Ett fel har alltid formen { "error": "…" }. Varje anrop kan dessutom svara 401, 403, 429 och 500. Se Fel.

Lägg till en slutpunkt

POST/v1/api/webhooks

Registrerar en adress och svarar med slutpunkten och dess hemlighet. Den får leveranser direkt.

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

Body

urlstringObligatorisk
En publik https-adress, högst 2048 tecken. Interna och lokala adresser avvisas.
eventsstring[]Obligatorisk
Händelserna som ska skickas. En tom lista betyder alla, även de som tillkommer senare.

Svarsfält

Slutpunkten ligger under endpoint, med samma fält som vid Lista slutpunkter.

Statuskoder

  • 201

    Slutpunkten är skapad.

  • 400invalid_webhook_endpoint

    Bodyn stämmer inte: adressen är inte en publik https-adress, eller en händelse finns inte.

Ett fel har alltid formen { "error": "…" }. Varje anrop kan dessutom svara 401, 403, 429 och 500. Se Fel.

Ändra en slutpunkt

PATCH/v1/api/webhooks

Ändrar adress, händelser eller status. Slutpunktens id står i bodyn, inte i sökvägen. Skicka bara de fält du vill ändra.

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

Body

idstringObligatorisk
Slutpunktens id (weh_…).
urlstringValfri
En ny adress, med samma krav som när den skapas.
eventsstring[]Valfri
En ny lista med händelser. Den ersätter den gamla helt.
status"active" | "disabled"Valfri
disabled stoppar leveranserna utan att radera slutpunkten.

Svarsfält

Den ändrade slutpunkten ligger under endpoint.

Statuskoder

  • 200

    Slutpunkten är ändrad.

  • 400invalid_request

    Bodyn stämmer inte, oftast för att id saknas.

  • 404webhook_endpoint_not_found

    Slutpunkten finns inte hos ditt företag.

Ett fel har alltid formen { "error": "…" }. Varje anrop kan dessutom svara 401, 403, 429 och 500. Se Fel.

Används ofta tillsammans med

Radera en slutpunkt

DELETE/v1/api/webhooks/{id}

Tar bort slutpunkten för gott. Vill du bara pausa, sätt status till disabled i stället.

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

Sökvägsparametrar

idstringObligatorisk
Slutpunktens id (weh_…).

Svarsfält

deletedboolean
Alltid true.

Statuskoder

  • 200

    Slutpunkten är raderad.

  • 404webhook_endpoint_not_found

    Slutpunkten finns inte hos ditt företag.

Ett fel har alltid formen { "error": "…" }. Varje anrop kan dessutom svara 401, 403, 429 och 500. Se Fel.

Används ofta tillsammans med

Leveransen

Varje händelse skickas som en POST med en JSON-body i fyra fält: id är leveransens eget id (whevt_…), type är händelsen, createdAt är tidpunkten, och data är det händelsen handlar om. Tre headers följer med: content-type, kuvert-event-type och kuvert-signature.

En hel leveransHTTP
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 exemplet är äkta. Med hemligheten whsec_docs-example-not-a-real-secret ger receptet under Signaturen exakt den v1. Kör din egen verifiering på den, med tidstoleransen avstängd, eftersom tidpunkten ligger bakåt i tiden.

Händelser

giftcard.issued

Ett presentkort är utgivet.

Exempel på leveransJSON
{
  "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

Ett belopp dras från ett kort.

Exempel på leveransJSON
{
  "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 order är betald. På en biljettorder följer köparens svar på kvällens frågor med i answers, t.ex. ett lagnamn, vart och ett med frågan så som den löd när den besvarades.

Exempel på leveransJSON
{
  "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 order är återbetald.

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

ticket.issued

En biljett är utgiven från en betald order. Orderns svar på kvällens frågor följer med i answers på varje biljett.

Exempel på leveransJSON
{
  "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

Köparens kortutgivare har bestridit betalningen. Orderns presentkort och biljetter är pausade och kan inte användas.

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

order.dispute_closed

Ärendet är löst. won: true betyder att pausen är hävd och korten fungerar igen; false att de är annullerade.

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

booking.created

Ett bord har bokats av gästen på bokningssidan, per telefon eller vid dörren.

Exempel på leveransJSON
{
  "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 bordsbokning har fått ny tid, nytt datum, nytt antal, nya bord eller en ny status. Hela bokningen följer med, och status säger var den står.

Exempel på leveransJSON
{
  "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 bordsbokning är avbokad, av gästen eller av restaurangen. Bokningar som flyttas in från ett annat system skickar inga händelser.

Exempel på leveransJSON
{
  "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

Ett arrangemang har lagts ut till försäljning. Hela kvällen följer med (namn, tider, plats, serie, line-up och genrer) i samma form som listan på GET /v1/merchants/{slug}, med eventId i stället för id.

Exempel på leveransJSON
{
  "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

Något offentligt på ett publicerat arrangemang har ändrats. changedFields anger vad, t.ex. lineup, eller tiers när ett pris har ändrats.

Exempel på leveransJSON
{
  "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

Ett publicerat evenemang har ställts in. cancelNote är meddelandet ni skrev till gästerna.

Exempel på leveransJSON
{
  "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

Ett publicerat evenemang har satts tillbaka till utkast. Ett utkast skickar aldrig händelser.

Exempel på leveransJSON
{
  "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

Varje leverans bär en kuvert-signature-header med en tidsstämpel och en HMAC:

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

Signaturen är HMAC-SHA256 i hex över strängen tidsstämpel, punkt, den råa bodyn, beräknad med din slutpunkts hemlighet:

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

Verifiera 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

Svara snabbt

  • Bekräfta med 2xx så snart du har mottagit. Lägg det tunga arbetet i en kö.
  • Leverans sker efter bästa förmåga. En slutpunkt som ligger nere håller aldrig tillbaka ett köp hos företaget.
  • Vi väntar högst 5 sekunder på svaret. Allt annat än 2xx inom den tiden räknas som en misslyckad leverans, även en omdirigering, som vi inte följer.

Nya försök

Misslyckas en leverans skickar vi den igen med allt längre pauser (1 minut, 5 minuter, 30 minuter, 2 timmar, 6 timmar, 12 timmar och 24 timmar), totalt 8 försök under knappt två dygn. Det är samma body med samma id varje gång, men med en ny signatur, så att tidsstämpeln är färsk. Använd id:t för att känna igen en händelse som ni redan har tagit emot: når ert svar inte fram skickar vi den igen.

Hur den senaste leveransen gick står på slutpunkten: Lista slutpunkter svarar med lastDeliveryAt, lastDeliveryOk, lastDeliveryDetail och consecutiveFailures, och detsamma står under Inställningar → Webhooks. Vi stänger aldrig av en slutpunkt på eget initiativ. Bara du kan sätta den till disabled, och en avstängd slutpunkt får inte heller några nya försök.

Krav på URL

Slutpunkten måste vara en offentligt tillgänglig HTTPS-adress. Interna adresser avvisas med avsikt, så att en slutpunkt inte kan användas för att nå in i vårt eget nätverk.

Var sidan till hjälp?