For integrations

Webhooks

Denira sends a request to an address you choose when someone books, changes or cancels a booking, pays, is refunded or joins the waitlist. The address can be a webhook in Zapier or Make, or your own system. It is included in every plan.

Getting started

  1. Go to Inställningar › Webhooks (Settings › Webhooks) and click Lägg till mottagare (Add receiver). You need to be an admin.
  2. Paste the address. It must start with https://.
  3. Choose the events the receiver should get and click Lägg till (Add).
  4. Click Skicka test (Send test) and pick an event. The receiver gets an example with made-up details, and under Leveranser (Deliveries) you see what was sent and what the receiver answered.

A facility can have up to ten receivers. You can pause, change or delete a receiver. Managers see receivers and deliveries but can't change them.

Events

Events
EventTypeWhen
New bookingbooking.createdA customer books online or you book for a customer.
Changed bookingbooking.changedTime, duration, participants, products, add-ons, protections, discount or gift card, contact details, staff or status changes.
Cancellationbooking.cancelledThe customer or you cancel.
Payment receivedpayment.receivedA payment is recorded: card, Swish and Klarna through Stripe, payment link, invoice or manual.
Refundpayment.refundedMoney is paid back from Denira or from Stripe, or after a lost dispute.
Joined the waitlistwaitlist.joinedA customer queues for a fully booked time.

booking.changed has a changes field that says what changed: time, duration, participants, products, addons, protections, voucher, customer, staff or status. The status changes when the booking is started, completed or its start is undone. Bookings imported from Bookspot send nothing.

What a request looks like

Each event is a POST request with JSON. data holds the booking, payment or waitlist entry as it was right after the change. For a payment, the booking is read a moment after the payment, so its payment status is up to date. Amounts are integers in öre (1/100 SEK): 90000 is 900 SEK.

A request for a new booking
POST /your-endpoint 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"
    }
  }
}

Fields in every event

Every event
FieldContents
idThe event's id, the same as the webhook-id header. The same on every retry.
typeThe event, for example booking.created.
createdAtWhen it happened, ISO 8601 in UTC.
testtrue for what Skicka test (Send test) sends.
facilityThe facility's id and name.
databooking, payment and booking, or waitlistEntry, depending on the event. booking.changed also has changes.

The booking: booking

booking
FieldContents
id, bookingNumberThe booking's id and booking number.
statuspending, confirmed, started, completed, cancelled or no_show.
paymentStatusunpaid, partially_paid, paid, refunded or partially_refunded.
testBookingtrue if the booking is marked as a test booking.
createdAt, cancelledAtWhen it was booked and cancelled (null if not cancelled).
createdBycustomer if the customer booked, staff if you did.
experience, durationThe experience and the chosen duration, with id, name and nameEn.
startDate, startTime, endDate, endTimeLocal time in timeZone (Europe/Stockholm).
participantCount, participantsNumber of participants, and per price category: category, quantity and unitPrice.
products, addonsProducts and add-ons with id, name, nameEn, quantity and unitPrice.
staffStaff on the booking, with id and name.
customerfirstName, lastName, email, phone and comment (at most 2,000 characters).
localesv or en, the language the customer booked in.
totalAmount, vatAmountTotal price and VAT.
discountAmount, discountCodesDiscount and the discount codes.
giftCardAmountAmount paid with a gift card. The gift card's code is never sent.
cancellationProtection, rebookingProtectionThe price of cancellation and rebooking protection, null if not chosen.
currencySEK.

The payment: payment

payment
FieldContents
id, amount, currencyThe payment's id and amount in öre. A refund is its own row with a positive amount.
methodstripe, payment_link, invoice or manual.
paidAtWhen the payment came in (payment.received).
refundedAt, reason, sourceWhen the money was paid back, why, and where: denira, stripe_dashboard or dispute (payment.refunded).

The waitlist entry: waitlistEntry

waitlistEntry
FieldContents
id, joinedAtThe entry's id and when the customer joined.
experience, durationThe experience and the duration the customer wants.
date, startTime, timeZoneThe fully booked time.
groupSizeHow many the customer wants to book for.
staffThe person the customer chose, or null.
customername, email and phone. The customer leaves an email address, a mobile number or both.
localesv or en.

Checking the signature

Requests are signed the Standard Webhooks way (standardwebhooks.com). The secret (whsec_…) is shown and copied under the receiver in Inställningar › Webhooks (Settings › Webhooks). The signature is HMAC-SHA256 of webhook-id, webhook-timestamp and the unchanged body, joined with dots.

Node.js with the Standard Webhooks library
// Install: npm install standardwebhooks express
import express from "express";
import { Webhook } from "standardwebhooks";

const webhook = new Webhook(process.env.DENIRA_WEBHOOK_SECRET); // whsec_… from 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 is verified. Answer quickly and do the work afterwards.
  res.sendStatus(200);
});
The same check without a library
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;
  // Reject requests older than five minutes
  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")}`,
  );
  // Compare in constant time, so the response time gives nothing away
  return signatures.split(" ").some((signature) => {
    const given = Buffer.from(signature);
    return given.length === expected.length && crypto.timingSafeEqual(given, expected);
  });
}

If you change the secret in Denira, the next request is signed with the new one straight away, so change it at the receiver at the same time.

Delivery and retries

  • Answer with a 2xx status within 15 seconds. Anything else counts as failed, and redirects are not followed.
  • A failed request is retried after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours. After eight attempts it is marked failed, and you can send it again from the log.
  • The same event can arrive more than once. Use webhook-id to skip duplicates.
  • Events can arrive in a different order than they happened. createdAt and the booking's fields show the state.
  • A test or a resend is tried once. A facility can make 20 in a row and then one more every three minutes.
  • A facility can send 600 events of each type in a row and then ten a minute, and an event larger than 16 kB is not sent. What isn't sent shows as a warning under the receiver.
  • Events and deliveries are kept for 30 days.

Recipe: Zapier

  1. Create a Zap in Zapier. Choose Webhooks by Zapier as the trigger and the event Catch Hook.
  2. Copy the address Zapier shows (https://hooks.zapier.com/hooks/catch/…).
  3. Add it as a receiver in Denira and choose events, for example New booking.
  4. Click Skicka test (Send test) in Denira, then Test trigger in Zapier. The example shows with every field.
  5. Add the next step, for example a row in Google Sheets or a contact in Mailchimp, and pick the fields, for example Data Booking Customer Email.
  6. Consider a filter before that step that only lets Test is false through, so tests stay out of the spreadsheet. Then publish the Zap.

Webhooks by Zapier is only in Zapier's paid plans. Zapier doesn't check the signature.

Recipe: Make

  1. Create a scenario in Make and add the module Webhooks › Custom webhook. Click Add, give it a name and save.
  2. Copy the address Make shows (for example https://hook.eu1.make.com/…).
  3. Add it as a receiver in Denira and choose events.
  4. Make now waits for an example. Click Skicka test (Send test) in Denira and Make learns the fields.
  5. Add the next module and pick the fields under data, for example data.booking.customer.email.
  6. Add a filter after the webhook that requires test to be false, and turn the scenario on.

Webhooks are in Make's free plan.

Get started today

Create the account yourself, or let us set everything up with setup help. Start costs SEK 0 a month.

Create account

No card needed for Start. Setup help: free for the first 20 businesses.

Ask a question

We reply within 24 hours.

Moving from Bookspot?

The import brings over experiences, prices and bookings, and setup help is free for you.

Questions?

Write to us and we will reply within 24 hours.