Kom igång
- Gå till Inställningar › Webhooks och klicka Lägg till mottagare. Du behöver vara admin.
- Klistra in adressen. Den ska börja med https://.
- Välj händelserna mottagaren ska få och klicka Lägg till.
- Klicka Skicka test och välj en händelse. Mottagaren får ett exempel med påhittade uppgifter, och under Leveranser ser du vad som skickades och vad mottagaren svarade.
En anläggning kan ha upp till tio mottagare. Du kan pausa en mottagare, ändra den eller ta bort den. Platschefer ser mottagarna och leveranserna men kan inte ändra dem.
Händelser
| Händelse | Typ | När |
|---|---|---|
| Ny bokning | booking.created | En kund bokar online eller ni bokar åt en kund. |
| Ändrad bokning | booking.changed | Tid, längd, deltagare, produkter, tillval, skydd, rabatt eller presentkort, kontaktuppgifter, personal eller status ändras. |
| Avbokning | booking.cancelled | Kunden eller ni avbokar. |
| Betalning mottagen | payment.received | En betalning registreras: kort, Swish och Klarna via Stripe, betallänk, faktura eller manuellt. |
| Återbetalning | payment.refunded | Pengar betalas tillbaka från Denira eller från Stripe, eller efter en förlorad tvist. |
| Ny på väntelistan | waitlist.joined | En kund ställer sig i kö till en fullbokad tid. |
booking.changed har fältet changes som säger vad som ändrades: time, duration, participants, products, addons, protections, voucher, customer, staff eller status. Status ändras när bokningen startas, avslutas eller när starten ångras. Bokningar som läses in från Bookspot skickar ingenting.
Så ser ett anrop ut
Varje händelse är ett POST-anrop med JSON. data innehåller bokningen, betalningen eller väntelisteplatsen som den var direkt efter ändringen. Vid en betalning läses bokningen en kort stund efter betalningen, så att dess betalstatus är uppdaterad. Belopp är heltal i öre: 90000 är 900 kr.
POST /din-adress HTTP/1.1
content-type: application/json
webhook-id: msg_k57d…
webhook-timestamp: 1780302612
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{
"id": "msg_k57d…",
"type": "booking.created",
"createdAt": "2026-06-01T08:30:12.000Z",
"test": false,
"facility": {
"id": "mh7c…",
"name": "Kajakcentret"
},
"data": {
"booking": {
"id": "sample_booking",
"bookingNumber": "2026060112345",
"status": "confirmed",
"paymentStatus": "paid",
"testBooking": false,
"createdAt": "2026-06-01T08:30:00.000Z",
"cancelledAt": null,
"createdBy": "customer",
"experience": {
"id": "sample_experience",
"name": "Kajaktur",
"nameEn": "Kayak tour"
},
"duration": {
"id": "sample_duration",
"name": "2 timmar",
"nameEn": "2 hours"
},
"startDate": "2026-06-14",
"startTime": "10:00",
"endDate": "2026-06-14",
"endTime": "12:00",
"timeZone": "Europe/Stockholm",
"participantCount": 2,
"participants": [
{
"category": {
"id": "sample_adult",
"name": "Vuxen",
"nameEn": "Adult"
},
"quantity": 2,
"unitPrice": 40000
}
],
"products": [],
"addons": [
{
"id": "sample_addon",
"name": "Fika",
"nameEn": "Coffee and bun",
"quantity": 2,
"unitPrice": 5000
}
],
"staff": [],
"customer": {
"firstName": "Anna",
"lastName": "Andersson",
"email": "anna@example.com",
"phone": "+46701234567",
"comment": "Vi kommer med cykel."
},
"locale": "sv",
"totalAmount": 90000,
"vatAmount": 18000,
"discountAmount": 0,
"discountCodes": [],
"giftCardAmount": 0,
"cancellationProtection": 5000,
"rebookingProtection": null,
"currency": "SEK"
}
}
}Fält i varje händelse
| Fält | Innehåll |
|---|---|
| id | Händelsens id, samma som huvudet webhook-id. Samma vid varje nytt försök. |
| type | Händelsen, till exempel booking.created. |
| createdAt | När det hände, ISO 8601 i UTC. |
| test | true för det Skicka test skickar. |
| facility | Anläggningens id och namn. |
| data | booking, payment och booking, eller waitlistEntry, beroende på händelsen. booking.changed har också changes. |
Bokningen: booking
| Fält | Innehåll |
|---|---|
| id, bookingNumber | Bokningens id och bokningsnummer. |
| status | pending, confirmed, started, completed, cancelled eller no_show. |
| paymentStatus | unpaid, partially_paid, paid, refunded eller partially_refunded. |
| testBooking | true om bokningen är markerad som testbokning. |
| createdAt, cancelledAt | När den bokades och avbokades (null om den inte är avbokad). |
| createdBy | customer om kunden bokade, staff om ni bokade. |
| experience, duration | Upplevelsen och den valda längden, med id, name och nameEn. |
| startDate, startTime, endDate, endTime | Lokal tid i timeZone (Europe/Stockholm). |
| participantCount, participants | Antal deltagare, och per priskategori: category, quantity och unitPrice. |
| products, addons | Produkter och tillval med id, name, nameEn, quantity och unitPrice. |
| staff | Personal på bokningen, med id och name. |
| customer | firstName, lastName, email, phone och comment (högst 2 000 tecken). |
| locale | sv eller en, språket kunden bokade på. |
| totalAmount, vatAmount | Totalpris och moms. |
| discountAmount, discountCodes | Rabatt och rabattkoderna. |
| giftCardAmount | Belopp betalt med presentkort. Presentkortets kod skickas aldrig. |
| cancellationProtection, rebookingProtection | Priset för avbokningsskydd och ombokningsskydd, null om det inte valdes. |
| currency | SEK. |
Betalningen: payment
| Fält | Innehåll |
|---|---|
| id, amount, currency | Betalningens id och belopp i öre. En återbetalning är en egen rad med positivt belopp. |
| method | stripe, payment_link, invoice eller manual. |
| paidAt | När betalningen kom in (payment.received). |
| refundedAt, reason, source | När pengarna betalades tillbaka, varför, och var: denira, stripe_dashboard eller dispute (payment.refunded). |
Väntelisteplatsen: waitlistEntry
| Fält | Innehåll |
|---|---|
| id, joinedAt | Platsens id och när kunden ställde sig i kö. |
| experience, duration | Upplevelsen och längden kunden vill ha. |
| date, startTime, timeZone | Den fullbokade tiden. |
| groupSize | Hur många kunden vill boka för. |
| staff | Personen kunden valde, eller null. |
| customer | name, email och phone. Kunden lämnar e-post, mobilnummer eller båda. |
| locale | sv eller en. |
Kontrollera signaturen
Anropen signeras enligt Standard Webhooks (standardwebhooks.com). Hemligheten (whsec_…) visas och kopieras under mottagaren i Inställningar › Webhooks. Signaturen är HMAC-SHA256 av webhook-id, webhook-timestamp och den oförändrade kroppen, ihopsatta med punkter.
// Installera: npm install standardwebhooks express
import express from "express";
import { Webhook } from "standardwebhooks";
const webhook = new Webhook(process.env.DENIRA_WEBHOOK_SECRET); // whsec_… från Denira
const app = express();
app.post("/denira", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = webhook.verify(req.body.toString(), req.headers);
} catch {
return res.sendStatus(400);
}
// event är kontrollerad. Svara snabbt och gör jobbet efteråt.
res.sendStatus(200);
});import crypto from "node:crypto";
function isFromDenira(secret, headers, rawBody) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signatures = headers["webhook-signature"];
if (!id || !timestamp || !signatures || !/^\d+$/.test(timestamp)) return false;
// Avvisa anrop som är äldre än fem minuter
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = Buffer.from(
`v1,${crypto.createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest("base64")}`,
);
// Jämför i konstant tid, så att svarstiden inte avslöjar något
return signatures.split(" ").some((signature) => {
const given = Buffer.from(signature);
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
}Byter du hemlighet i Denira signeras nästa anrop med den nya direkt, så byt den hos mottagaren samtidigt.
Leverans och nya försök
- Svara med en 2xx-status inom 15 sekunder. Allt annat räknas som misslyckat, och omdirigeringar följs inte.
- Ett misslyckat anrop görs om efter 5 sekunder, 5 minuter, 30 minuter, 2 timmar, 5 timmar, 10 timmar och 10 timmar. Efter åtta försök markeras det som misslyckat, och du kan skicka det igen från loggen.
- Samma händelse kan komma mer än en gång. Använd webhook-id för att hoppa över dubbletter.
- Händelser kan komma i en annan ordning än de hände. createdAt och bokningens fält visar läget.
- Ett test eller ett Skicka igen görs en gång. En anläggning kan göra 20 i rad och sedan ett nytt var tredje minut.
- En anläggning kan skicka 600 händelser av varje slag i rad och sedan tio i minuten, och en händelse större än 16 kB skickas inte. Det som inte skickas syns som en varning under mottagaren.
- Händelser och leveranser sparas i 30 dagar.
Recept: Zapier
- Skapa en Zap i Zapier. Välj Webhooks by Zapier som trigger och händelsen Catch Hook.
- Kopiera adressen Zapier visar (https://hooks.zapier.com/hooks/catch/…).
- Lägg till den som mottagare i Denira och välj händelser, till exempel Ny bokning.
- Klicka Skicka test i Denira, och sedan Test trigger i Zapier. Exemplet visas med alla fält.
- Lägg till nästa steg, till exempel en rad i Google Sheets eller en kontakt i Mailchimp, och välj fälten, till exempel Data Booking Customer Email.
- Lägg gärna ett filter före steget som bara släpper igenom Test är false, så att testerna inte hamnar i kalkylarket. Publicera sedan Zappen.
Webhooks by Zapier ingår bara i Zapiers betalda abonnemang. Zapier kontrollerar inte signaturen.
Recept: Make
- Skapa ett scenario i Make och lägg till modulen Webhooks › Custom webhook. Klicka Add, ge den ett namn och spara.
- Kopiera adressen Make visar (till exempel https://hook.eu1.make.com/…).
- Lägg till den som mottagare i Denira och välj händelser.
- Make väntar nu på ett exempel. Klicka Skicka test i Denira så lär sig Make fälten.
- Lägg till nästa modul och välj fälten under data, till exempel data.booking.customer.email.
- Lägg ett filter efter webhooken som kräver att test är false, och slå på scenariot.
Webhooks ingår i Makes gratisplan.