Public API: receive real-time booking webhooks

All help ressources
>
Public API: receive real-time booking webhooks

Webhooks are the fastest way to stay in sync with Nowistay. Instead of polling the API every few minutes to detect new or changed reservations, you tell Nowistay where to send the events, and your service receives them as soon as they happen. This article shows you how to create a webhook, verify the signature, and handle retries.

Quick reminder: what the public API is

The Nowistay public REST API lets your tools and partners read and write Nowistay data over HTTPS. You authenticate with OAuth2 (authorization code + PKCE), receive an access token, and call endpoints under /public/v1/*. Webhooks are the push counterpart: same data, same shape as GET /public/v1/bookings/{id}, but Nowistay pushes them to you when something changes.

If you have not set up OAuth yet, start with the main guide: Using the Nowistay public REST API. It covers registration, the consent flow, scopes and how to test calls from the docs playground.

What you get

  • Real-time delivery of booking.created and booking.updated events.
  • The same JSON body as GET /public/v1/bookings/{id}, including dates, channel, status, amounts, mission-relevant fields and your custom booking parameters.
  • HMAC-SHA256 signature so you can verify each request really comes from Nowistay.
  • Automatic retries with exponential backoff when your endpoint fails or times out.
  • Auto-disable after 72 hours of continuous failures (you can reactivate from the API).

Before you start

  • You need the AI Channel Manager subscription on the property. Without it, deliveries are skipped.
  • The OAuth token used to create the webhook must carry the webhooks.write scope. POST /public/v1/webhooks also requires bookings.read, because the payload is a full booking detail.
  • Your receiving URL must be reachable on the public internet, served over HTTPS in production, and respond within 10 seconds with a 2xx status.
  • One webhook is tied to one property. You can register up to 3 webhooks per property.

Step 1. Create a webhook

curl -X POST https://api.nowistay.com/public/v1/webhooks \
  -H "Authorization: Bearer nis_access_token_example" \
  -H "Content-Type: application/json" \
  -d '{
    "propertyId": 101,
    "url": "https://partner.example.com/nowistay/webhooks",
    "events": ["booking.created", "booking.updated"],
    "description": "PMS sync"
  }'

Response:

{
  "id": 801,
  "propertyId": 101,
  "url": "https://partner.example.com/nowistay/webhooks",
  "events": ["booking.created", "booking.updated"],
  "description": "PMS sync",
  "status": "active",
  "createdAt": "2026-05-27T16:40:00Z",
  "updatedAt": "2026-05-27T16:40:00Z",
  "signingSecret": "whsec_9npwH6J8QyZp1L8U3xqG7sN2"
}

Save the signingSecret now. It is returned only once, at creation. There is no way to read it again from the API. If you lose it, delete the webhook and create a new one.

Step 2. Receive your first event

Each delivery is an HTTP POST to your URL, with these headers:

  • Content-Type: application/json
  • User-Agent: Nowistay-Webhooks/1.0
  • Nowistay-Webhook-Id: 801
  • Nowistay-Webhook-Event-Id: evt_LkP2sxR9zV8QyW... (unique per event)
  • Nowistay-Webhook-Event-Type: booking.updated
  • Nowistay-Webhook-Timestamp: 1779292800 (Unix seconds)
  • Nowistay-Webhook-Signature: v1=3a7b...e1c (HMAC-SHA256 hex)

And a JSON body shaped like this:

{
  "id": "evt_LkP2sxR9zV8QyW...",
  "type": "booking.updated",
  "createdAt": "2026-05-27T16:40:00Z",
  "apiVersion": "2026-05-27",
  "propertyId": 101,
  "bookingId": 9001,
  "data": {
    "booking": {
      "id": 9001,
      "propertyId": 101,
      "arrival": "2026-07-14",
      "departure": "2026-07-18",
      "status": "confirmed",
      "channel": "airbnb",
      "reservationNumber": "HMABCDE",
      "guest": { "firstName": "Maya", "lastName": "M." },
      "amounts": { "amount": "620.00", "currency": "EUR" },
      "identity": { "status": "verified" }
    }
  }
}

Your endpoint should answer with any 2xx status (200 or 204 are typical). Anything else, or a response that takes more than 10 seconds, counts as a failure and triggers a retry.

Step 3. Verify the signature

Always verify the signature before trusting the body. Without verification, anyone who guesses your URL could send fake events to your service.

Nowistay computes the signature as:

signature = "v1=" + HMAC_SHA256(
  signing_secret,
  timestamp + "." + event_id + "." + raw_request_body
).hex()

Recompute it on your side using the Nowistay-Webhook-Timestamp, Nowistay-Webhook-Event-Id headers and the exact raw request body (do not parse and re-serialize the JSON before verifying, or you risk byte differences).

Node.js example

import crypto from "node:crypto";

function verify(req, signingSecret) {
  const timestamp = req.headers["nowistay-webhook-timestamp"];
  const eventId = req.headers["nowistay-webhook-event-id"];
  const signature = req.headers["nowistay-webhook-signature"];
  const rawBody = req.rawBody; // Buffer of the raw HTTP body

  const signed = Buffer.concat([
    Buffer.from(`${timestamp}.${eventId}.`, "utf8"),
    rawBody,
  ]);
  const expected = "v1=" + crypto
    .createHmac("sha256", signingSecret)
    .update(signed)
    .digest("hex");

  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Python example

import hmac, hashlib

def verify(request, signing_secret: str) -> bool:
    timestamp = request.headers["Nowistay-Webhook-Timestamp"]
    event_id = request.headers["Nowistay-Webhook-Event-Id"]
    signature = request.headers["Nowistay-Webhook-Signature"]
    raw_body = request.body  # bytes

    signed = f"{timestamp}.{event_id}.".encode("utf-8") + raw_body
    expected = "v1=" + hmac.new(
        signing_secret.encode("utf-8"),
        signed,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(signature, expected)

Also check that the timestamp is recent (for example, within the last 5 minutes) to protect against replay attacks.

What is (and is not) in the payload

The data.booking object mirrors what GET /public/v1/bookings/{id} returns when called without the bookings.sensitive.read scope. That means:

  • Included: dates, channel, status, amounts, guest first name and last name, adults/children, custom booking parameters (if enabled on the webhook).
  • Not included: guest email, phone, address, city, postal code, country, language and the smart-lock code. These are considered sensitive and Nowistay does not push them to webhook subscribers.

When your tool needs one of those sensitive fields, fetch the booking detail from the API with a token that carries the bookings.sensitive.read scope. The bookingId in the webhook envelope is the right input for that call.

curl https://api.nowistay.com/public/v1/bookings/9001 \
  -H "Authorization: Bearer nis_access_token_with_sensitive_scope"

Retries and the 72-hour auto-disable

If your endpoint returns a non-2xx status, times out, or fails for any other reason, Nowistay does not drop the event. It schedules a retry, with exponential backoff and a bit of jitter:

  • 1st retry: about 1 minute after the failure.
  • 2nd retry: about 2 minutes later.
  • 3rd retry: about 4 minutes later, then 8, 16, 32 and so on.
  • Cap: retries are spaced by at most 6 hours.

This continues until your endpoint accepts the event, or until the webhook has been failing for more than 72 hours straight. At that point Nowistay sets the webhook status to disabled and stops sending new events to it. Failure counters reset every time a delivery succeeds, so an isolated incident never trips the auto-disable.

Delivery records are kept for 30 days, then cleaned up.

Reactivate a disabled webhook

A disabled webhook can be turned back on from the API once your endpoint is healthy again. Send a PATCH with status: "active":

curl -X PATCH https://api.nowistay.com/public/v1/webhooks/801 \
  -H "Authorization: Bearer nis_access_token_example" \
  -H "Content-Type: application/json" \
  -d '{"status": "active"}'

Nowistay does not replay the events that fired while the webhook was disabled. If you missed some, do a one-time backfill with GET /public/v1/bookings using a date filter, then resume your webhook-driven flow.

Pause, resume, delete

You can temporarily pause deliveries (during a maintenance window, for example) without losing the webhook config:

# Pause
curl -X PATCH https://api.nowistay.com/public/v1/webhooks/801 \
  -H "Authorization: Bearer nis_access_token_example" \
  -H "Content-Type: application/json" \
  -d '{"status": "paused"}'

# Resume
curl -X PATCH https://api.nowistay.com/public/v1/webhooks/801 \
  -H "Authorization: Bearer nis_access_token_example" \
  -H "Content-Type: application/json" \
  -d '{"status": "active"}'

# Delete permanently
curl -X DELETE https://api.nowistay.com/public/v1/webhooks/801 \
  -H "Authorization: Bearer nis_access_token_example"

You can also patch the url, the events list or the description in the same call. Changing the URL does not rotate the signing secret.

Inspect your webhooks

# List all webhooks (optionally filtered by property)
curl "https://api.nowistay.com/public/v1/webhooks?propertyId=101" \
  -H "Authorization: Bearer nis_access_token_example"

# Read one webhook
curl https://api.nowistay.com/public/v1/webhooks/801 \
  -H "Authorization: Bearer nis_access_token_example"

The list response shows the same fields as the create response, minus the signing secret (it is only returned at creation).

URL rules and limits

  • Absolute HTTP or HTTPS URL, at most 2048 characters. HTTPS required in production.
  • No basic-auth credentials in the URL (https://user:pass@... is rejected).
  • No localhost, no private or reserved IP ranges. Nowistay also resolves the hostname at delivery time and blocks calls that resolve to private addresses.
  • No HTTP redirects. Nowistay does not follow them, the delivery is marked failed if your server redirects.
  • Max 3 webhooks per property, across all your apps.
  • Description: optional, max 120 characters.

Good practices

  • Verify the signature every time. It is the only thing that proves the call really comes from Nowistay.
  • Be idempotent. The same event id can be redelivered after a failure. Store id from the body and skip if you have already processed it.
  • Answer fast. Acknowledge the call with a 2xx right away and process the event in a background job if it is heavy.
  • Log the headers. Keep Nowistay-Webhook-Event-Id and the request id in your logs. It speeds up support.
  • Treat the webhook as a hint, not the source of truth. If your business logic needs sensitive fields or the very latest data, call GET /public/v1/bookings/{id} after receiving the event.

Where to go next

Ready to Put Your Rental on Autopilot?

Join 300+ property managers who save hours every week with AI-powered guest communication.