Hotel API

Connect your hotel’s systemto Hourly.

For a hotel’s IT team and its PMS or channel-manager supplier. Send rooms and prices, read bookings, record check-ins, and hear about every booking the moment it happens.

Getting a key

Each hotel has its own keys. A hotel’s manager creates them on the hotel’s Connections page in the front desk, or your Hourly account manager can make one for you. The full key is shown once, when it is made: store it in your system’s settings, never in an email or chat.

When you create a key you choose what it may do:

PermissionWhat it allows
availability:writeSend rooms and prices
availability:readRead rooms and prices
bookings:readRead bookings
checkins:writeRecord check-ins

Rotating a key makes a new one with the same permissions; the old key keeps working for 24 hours so you can switch over without a gap. Revoking stops a key at once.

Authentication

Every request goes to https://hourly.ng/api/v1 over HTTPS and carries the key as a bearer token. Live keys start hk_live_, Sandbox keys hk_test_. Requests and responses are JSON. Times are ISO 8601 in UTC; dates and start times called date and start are Lagos time (Africa/Lagos, UTC+1, no daylight saving). Money is always whole kobo (₦1 = 100 kobo).

Keys work while the hotel is onboarding, live or paused on Hourly. Once a hotel has left Hourly its keys answer 401 unauthorized. A 401 carries no Hourly-Mode or rate-limit headers and doesn’t count toward the limit.

curl https://hourly.ng/api/v1/hotel \
  -H "Authorization: Bearer hk_live_3fa9c2d1_…"

Live and Sandbox

A Live key changes the hotel’s real rooms and prices and records real check-ins. A Sandbox key reads the same real data, and checks every write exactly as Live would, answering the same way with "sandbox": true added, but changes nothing. Build and test with Sandbox, then switch to a Live key.

Every response says which kind of key was used in the Hourly-Mode header: live or sandbox.

Rate limits

Each key may make 120 requests a minute. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining. Past the limit you get 429 rate_limited with a Retry-After header in seconds; wait that long before trying again. Send up to 500 times in one PUT /availability rather than one request per time.

Idempotency

Writes (PUT, PATCH, POST) accept an optional Idempotency-Key header, such as a UUID. If a request times out, send it again with the same key: you get the first answer back, byte for byte, with an Idempotent-Replayed: true header, and nothing happens twice. The same key with a different request is refused with 422 idempotency_mismatch. Keys are kept for 24 hours.

Only successful writes are stored. A refused request (any error) releases its key, so you can retry it later with the same key: a check-in sent too early, for example. If the first request with a key is still being worked on, a second one waits up to 5 seconds for its answer, then gets 409 conflict; try again shortly.

Errors

Every error has the same shape. The message is written for a person; act on the code.

{
  "error": {
    "code": "invalid_request",
    "message": "Some items could not be applied. Nothing was changed.",
    "details": [
      {
        "index": 1,
        "code": "rooms_below_booked",
        "message": "2 rooms are already booked for this time, so \"rooms\" can't be less than 2."
      }
    ]
  }
}
CodeStatusWhen
unauthorized401No key, a key we don’t recognise, one that was revoked or has expired, or a hotel that has left Hourly.
forbidden403The key is fine but isn’t allowed to do this. The message names the permission it needs.
not_found404No such booking, room type or path for this hotel.
conflict409The request is valid but the current state refuses it: a room code already in use, a check-in the desk would refuse, or the same Idempotency-Key still being worked on.
payload_too_large413The body is over 1 MB. Send fewer items per request.
invalid_request422Something in the request isn’t right. "details" says what, item by item.
idempotency_mismatch422This Idempotency-Key was already used with a different request.
rate_limited429More than 120 requests in a minute. Wait for Retry-After seconds.
server_error500Something went wrong on our side. Retry with the same Idempotency-Key.

Problems with one item of PUT /availability, or with one query parameter, are listed in details as { index or field, code, message }:

Item codeMeaning
invalid_itemThe item is not a JSON object.
invalid_room_type"roomType" is missing.
unknown_room_typeNo room type at this hotel has that id or code.
room_type_inactiveThe room type is switched off in Hourly.
invalid_date"date" is not a real YYYY-MM-DD date.
invalid_start"start" is not HH:MM, or not on the hour or half hour (:00 or :30).
invalid_length"lengthHours" is not 2, 3 or 6.
ends_after_midnightThe stay would end after midnight. Stays end by midnight.
outside_selling_windowThe time has already started, or is more than 56 days ahead (today counts as day 1).
invalid_rooms"rooms" is not a whole number from 0 to 200.
rooms_below_bookedFewer rooms than are already booked for that time.
invalid_price"priceKobo" is not a whole number from 1 to 500,000,000 kobo (₦5,000,000).
invalid_status"status" is not "open" or "closed".
duplicate_itemThe same room type, date, start and length appears twice.
ambiguous_slotHourly has two times matching this item. Ask the hotel to close one at the desk, then send it again.

Endpoints

GET/hotel

Which hotel the key belongs to, its room types with your codes for them, and what the key may do. A good first call to check a key works.

Permission: any key

Request
GET https://hourly.ng/api/v1/hotel
Authorization: Bearer hk_live_…
Response 200
{
  "hotel": {
    "id": "cm3h0telex4mple01",
    "name": "Example Hotel Ikeja",
    "slug": "example-hotel-ikeja",
    "area": "LAGOS_IKEJA_AIRPORT",
    "timeZone": "Africa/Lagos"
  },
  "roomTypes": [
    {
      "id": "cm3rt0deluxe00001",
      "externalCode": "DLX-K",
      "name": "Deluxe King",
      "maxGuests": 2,
      "active": true
    },
    {
      "id": "cm3rt0standard0002",
      "externalCode": null,
      "name": "Standard Queen",
      "maxGuests": 2,
      "active": true
    }
  ],
  "key": {
    "prefix": "hk_live_3fa9c2d1",
    "mode": "live",
    "scopes": [
      "availability:write",
      "availability:read",
      "bookings:read",
      "checkins:write"
    ]
  }
}

PATCH/room-types/{id}

Sets your system’s own code for a room type, so later calls can name it by your code. Send null to clear it.

Permission: availability:write (Send rooms and prices) · Errors: conflict, not_found, invalid_request

  • Codes are 1 to 64 printable characters (no spaces at either end) and unique within the hotel. A code another room type already uses, or one equal to another room type’s id, answers 409 conflict.
  • The hotel’s managers can also set codes on their Connections page.
Request
PATCH https://hourly.ng/api/v1/room-types/cm3rt0standard0002
Authorization: Bearer hk_live_…
Content-Type: application/json
Idempotency-Key: 3f1c7a2e-1b9d-4c55-9a51-0d6f8e2b7c40

{
  "externalCode": "STD-Q"
}
Response 200
{
  "roomType": {
    "id": "cm3rt0standard0002",
    "externalCode": "STD-Q",
    "name": "Standard Queen",
    "maxGuests": 2,
    "active": true
  }
}

GET/availability

The times on sale (and closed) for a range of Lagos dates, with rooms, bookings and price for each.

Permission: availability:read (Read rooms and prices) · Errors: invalid_request, not_found

Query parameterMeaning
fromFirst Lagos date, YYYY-MM-DD. Required.
toLast Lagos date, YYYY-MM-DD, at most 14 days including "from". Required.
roomTypeOptional. A room type id or your code for it.
  • "available" is rooms minus booked, and 0 when the time is closed.
Request
GET https://hourly.ng/api/v1/availability?from=2026-11-21&to=2026-11-21
Authorization: Bearer hk_live_…
Response 200
{
  "slots": [
    {
      "id": "cm3s1ot1000x01",
      "roomType": {
        "id": "cm3rt0deluxe00001",
        "externalCode": "DLX-K",
        "name": "Deluxe King"
      },
      "date": "2026-11-21",
      "start": "10:00",
      "lengthHours": 3,
      "startsAt": "2026-11-21T09:00:00.000Z",
      "endsAt": "2026-11-21T12:00:00.000Z",
      "rooms": 4,
      "booked": 1,
      "available": 3,
      "priceKobo": 1800000,
      "status": "open"
    },
    {
      "id": "cm3s1ot1400x01",
      "roomType": {
        "id": "cm3rt0deluxe00001",
        "externalCode": "DLX-K",
        "name": "Deluxe King"
      },
      "date": "2026-11-21",
      "start": "14:00",
      "lengthHours": 3,
      "startsAt": "2026-11-21T13:00:00.000Z",
      "endsAt": "2026-11-21T16:00:00.000Z",
      "rooms": 3,
      "booked": 0,
      "available": 3,
      "priceKobo": 1800000,
      "status": "open"
    }
  ]
}

PUT/availability

Creates or updates times: how many rooms are on sale and at what price. A time is one room type, Lagos date, start and length.

Permission: availability:write (Send rooms and prices) · Errors: invalid_request, idempotency_mismatch

  • Send 1 to 500 items. All or nothing: if any item is wrong, nothing changes and the 422 lists every problem by its index.
  • "roomType" is the Hourly id or your code. "start" is Lagos time, on the hour or half hour. "lengthHours" is 2, 3 or 6, and the stay must end by midnight.
  • "rooms" may not go below the rooms already booked (rooms_below_booked). Send "rooms": 0 or "status": "closed" to stop selling a time; leave "status" out to keep an existing time’s status (a new time opens). Times up to 56 days ahead.
  • Prices are whole kobo: ₦18,000 is 1800000.
Request
PUT https://hourly.ng/api/v1/availability
Authorization: Bearer hk_live_…
Content-Type: application/json
Idempotency-Key: 3f1c7a2e-1b9d-4c55-9a51-0d6f8e2b7c40

{
  "items": [
    {
      "roomType": "DLX-K",
      "date": "2026-11-21",
      "start": "10:00",
      "lengthHours": 3,
      "rooms": 4,
      "priceKobo": 1800000
    },
    {
      "roomType": "DLX-K",
      "date": "2026-11-21",
      "start": "14:00",
      "lengthHours": 3,
      "rooms": 3,
      "priceKobo": 1800000,
      "status": "open"
    }
  ]
}
Response 200
{
  "created": 1,
  "updated": 1,
  "slots": [
    {
      "id": "cm3s1ot1000x01",
      "roomType": {
        "id": "cm3rt0deluxe00001",
        "externalCode": "DLX-K",
        "name": "Deluxe King"
      },
      "date": "2026-11-21",
      "start": "10:00",
      "lengthHours": 3,
      "startsAt": "2026-11-21T09:00:00.000Z",
      "endsAt": "2026-11-21T12:00:00.000Z",
      "rooms": 4,
      "booked": 1,
      "available": 3,
      "priceKobo": 1800000,
      "status": "open"
    },
    {
      "id": "cm3s1ot1400x01",
      "roomType": {
        "id": "cm3rt0deluxe00001",
        "externalCode": "DLX-K",
        "name": "Deluxe King"
      },
      "date": "2026-11-21",
      "start": "14:00",
      "lengthHours": 3,
      "startsAt": "2026-11-21T13:00:00.000Z",
      "endsAt": "2026-11-21T16:00:00.000Z",
      "rooms": 3,
      "booked": 0,
      "available": 3,
      "priceKobo": 1800000,
      "status": "open"
    }
  ]
}

GET/bookings

Bookings at the hotel, oldest change first. Poll with updatedSince to catch up on anything a webhook may have missed.

Permission: bookings:read (Read bookings) · Errors: invalid_request

Query parameterMeaning
updatedSinceOptional. An ISO 8601 time with a zone, such as 2026-11-20T08:00:00Z (a time without Z or an offset is refused). Bookings changed at or after it.
statusOptional. pending_review, confirmed, checked_in, completed, cancelled or no_show.
limitOptional. 1 to 100; 50 if left out.
cursorOptional. "nextCursor" from the previous page, as it is.
  • "nextCursor" is null on the last page.
Request
GET https://hourly.ng/api/v1/bookings
Authorization: Bearer hk_live_…
Response 200
{
  "bookings": [
    {
      "reference": "HR-7K2Q9",
      "status": "confirmed",
      "roomType": {
        "id": "cm3rt0deluxe00001",
        "externalCode": "DLX-K",
        "name": "Deluxe King"
      },
      "slotId": "cm3s1otex4mple001",
      "date": "2026-11-20",
      "start": "13:00",
      "lengthHours": 3,
      "startsAt": "2026-11-20T12:00:00.000Z",
      "endsAt": "2026-11-20T15:00:00.000Z",
      "guest": {
        "name": "Adaeze Okafor"
      },
      "company": null,
      "extras": [],
      "extensions": [],
      "payment": {
        "timing": "upfront",
        "totalKobo": 1800000,
        "roomKobo": 1800000,
        "extrasKobo": 0,
        "discountKobo": 0,
        "commissionKobo": 270000,
        "dueAtDeskKobo": 0,
        "paidAtHotelAt": null
      },
      "checkedInAt": null,
      "checkedOutAt": null,
      "cancelledAt": null,
      "createdAt": "2026-11-20T09:12:04.000Z",
      "updatedAt": "2026-11-20T09:12:04.000Z"
    }
  ],
  "nextCursor": "eyJ1IjoiMjAyNi0xMS0yMFQwOToxMjowNC4wMDBaIiwiaSI6ImNtM2IwMDEifQ"
}

GET/bookings/{reference}

One booking by its reference, such as HR-7K2Q9.

Permission: bookings:read (Read bookings) · Errors: not_found

Request
GET https://hourly.ng/api/v1/bookings/HR-7K2Q9
Authorization: Bearer hk_live_…
Response 200
{
  "booking": {
    "reference": "HR-7K2Q9",
    "status": "confirmed",
    "roomType": {
      "id": "cm3rt0deluxe00001",
      "externalCode": "DLX-K",
      "name": "Deluxe King"
    },
    "slotId": "cm3s1otex4mple001",
    "date": "2026-11-20",
    "start": "13:00",
    "lengthHours": 3,
    "startsAt": "2026-11-20T12:00:00.000Z",
    "endsAt": "2026-11-20T15:00:00.000Z",
    "guest": {
      "name": "Adaeze Okafor"
    },
    "company": null,
    "extras": [],
    "extensions": [],
    "payment": {
      "timing": "upfront",
      "totalKobo": 1800000,
      "roomKobo": 1800000,
      "extrasKobo": 0,
      "discountKobo": 0,
      "commissionKobo": 270000,
      "dueAtDeskKobo": 0,
      "paidAtHotelAt": null
    },
    "checkedInAt": null,
    "checkedOutAt": null,
    "cancelledAt": null,
    "createdAt": "2026-11-20T09:12:04.000Z",
    "updatedAt": "2026-11-20T09:12:04.000Z"
  }
}

POST/bookings/{reference}/check-in

Records that the guest has arrived, exactly as the front desk would. No body.

Permission: checkins:write (Record check-ins) · Errors: conflict, not_found

  • Check-in opens 15 minutes before the start time. When the desk would refuse, the answer is 409 conflict with the desk’s reason, such as “Check-in opens 15 minutes before the start time.”
Request
POST https://hourly.ng/api/v1/bookings/HR-7K2Q9/check-in
Authorization: Bearer hk_live_…
Response 200
{
  "booking": {
    "reference": "HR-7K2Q9",
    "status": "checked_in",
    "roomType": {
      "id": "cm3rt0deluxe00001",
      "externalCode": "DLX-K",
      "name": "Deluxe King"
    },
    "slotId": "cm3s1otex4mple001",
    "date": "2026-11-20",
    "start": "13:00",
    "lengthHours": 3,
    "startsAt": "2026-11-20T12:00:00.000Z",
    "endsAt": "2026-11-20T15:00:00.000Z",
    "guest": {
      "name": "Adaeze Okafor"
    },
    "company": null,
    "extras": [],
    "extensions": [],
    "payment": {
      "timing": "upfront",
      "totalKobo": 1800000,
      "roomKobo": 1800000,
      "extrasKobo": 0,
      "discountKobo": 0,
      "commissionKobo": 270000,
      "dueAtDeskKobo": 0,
      "paidAtHotelAt": null
    },
    "checkedInAt": "2026-11-21T08:50:31.000Z",
    "checkedOutAt": null,
    "cancelledAt": null,
    "createdAt": "2026-11-20T09:12:04.000Z",
    "updatedAt": "2026-11-20T09:12:04.000Z"
  }
}

POST/bookings/{reference}/check-out

Records that the guest has left and the room is free for cleaning. No body.

Permission: checkins:write (Record check-ins) · Errors: conflict, not_found

  • Only a checked-in guest can be checked out; anything else is 409 conflict.
Request
POST https://hourly.ng/api/v1/bookings/HR-7K2Q9/check-out
Authorization: Bearer hk_live_…
Response 200
{
  "booking": {
    "reference": "HR-7K2Q9",
    "status": "completed",
    "roomType": {
      "id": "cm3rt0deluxe00001",
      "externalCode": "DLX-K",
      "name": "Deluxe King"
    },
    "slotId": "cm3s1otex4mple001",
    "date": "2026-11-20",
    "start": "13:00",
    "lengthHours": 3,
    "startsAt": "2026-11-20T12:00:00.000Z",
    "endsAt": "2026-11-20T15:00:00.000Z",
    "guest": {
      "name": "Adaeze Okafor"
    },
    "company": null,
    "extras": [],
    "extensions": [],
    "payment": {
      "timing": "upfront",
      "totalKobo": 1800000,
      "roomKobo": 1800000,
      "extrasKobo": 0,
      "discountKobo": 0,
      "commissionKobo": 270000,
      "dueAtDeskKobo": 0,
      "paidAtHotelAt": null
    },
    "checkedInAt": "2026-11-21T08:50:31.000Z",
    "checkedOutAt": "2026-11-21T11:47:09.000Z",
    "cancelledAt": null,
    "createdAt": "2026-11-20T09:12:04.000Z",
    "updatedAt": "2026-11-20T09:12:04.000Z"
  }
}

The booking object

The same shape in every booking response and every webhook. It holds what the front desk sees: the guest’s name, never their phone, email or ID documents.

FieldMeaning
referenceThe booking’s reference, such as HR-7K2Q9. The guest shows it at the desk.
statuspending_review (being checked by Hourly, hold the room), confirmed, checked_in, completed, cancelled or no_show.
roomType{ id, externalCode, name }: the room type booked, with your code for it.
slotIdThe time that was booked, as in GET /availability.
date, startThe Lagos date (YYYY-MM-DD) and start time (HH:MM).
lengthHoursHours booked, including any extra hours the guest added.
startsAt, endsAtThe same stay in UTC, ISO 8601. endsAt includes extra hours.
guest{ name }: the guest’s name, as the desk sees it. Never their phone, email or ID.
company{ name } when a company booked for its staff, else null.
extrasExtras chosen with the room: [{ name, priceKobo }].
extensionsExtra hours agreed at the desk: [{ hours, priceKobo, paid }].
payment{ timing: upfront | at_hotel, totalKobo, roomKobo, extrasKobo, discountKobo, commissionKobo, dueAtDeskKobo, paidAtHotelAt }. All money in kobo.
checkedInAt, checkedOutAt, cancelledAtWhen each happened (UTC), or null.
createdAt, updatedAtWhen the booking was made and last changed (UTC).
Example
{
  "reference": "HR-7K2Q9",
  "status": "confirmed",
  "roomType": {
    "id": "cm3rt0deluxe00001",
    "externalCode": "DLX-K",
    "name": "Deluxe King"
  },
  "slotId": "cm3s1otex4mple001",
  "date": "2026-11-20",
  "start": "13:00",
  "lengthHours": 3,
  "startsAt": "2026-11-20T12:00:00.000Z",
  "endsAt": "2026-11-20T15:00:00.000Z",
  "guest": {
    "name": "Adaeze Okafor"
  },
  "company": null,
  "extras": [],
  "extensions": [],
  "payment": {
    "timing": "upfront",
    "totalKobo": 1800000,
    "roomKobo": 1800000,
    "extrasKobo": 0,
    "discountKobo": 0,
    "commissionKobo": 270000,
    "dueAtDeskKobo": 0,
    "paidAtHotelAt": null
  },
  "checkedInAt": null,
  "checkedOutAt": null,
  "cancelledAt": null,
  "createdAt": "2026-11-20T09:12:04.000Z",
  "updatedAt": "2026-11-20T09:12:04.000Z"
}

Webhooks

Add your system’s HTTPS address on the hotel’s Connections page and Hourly sends it a signed POST whenever a booking changes. Choose the events you want, or all of them. Sandbox endpoints receive test events only.

EventWhen
booking.confirmedBooking confirmed
booking.cancelledBooking cancelled
booking.changedBooking moved to another time
booking.extendedStay extended: sent when the guest has paid for the extra hours (an approval alone sends nothing).
booking.checked_inGuest checked in
booking.checked_outGuest checked out
booking.no_showGuest did not arrive
pingTest event, sent only when someone presses “Send a test event”. Carries a sample booking and "test": true, is tried once, and never counts toward switching an endpoint off.

Each request

POST https://your-system.example.com/hourly/webhooks
Content-Type: application/json
Hourly-Event: booking.confirmed
Hourly-Event-Id: evt_5b1f0c9e2a7d4b3c8e6f1a20
Hourly-Signature: t=1763629924,v1=5d41402abc4b2a76b9719d911017c592…

{
  "id": "evt_5b1f0c9e2a7d4b3c8e6f1a20",
  "event": "booking.confirmed",
  "createdAt": "2026-11-20T09:12:04.000Z",
  "mode": "live",
  "data": {
    "booking": {
      "reference": "HR-7K2Q9",
      "status": "confirmed",
      "roomType": {
        "id": "cm3rt0deluxe00001",
        "externalCode": "DLX-K",
        "name": "Deluxe King"
      },
      "slotId": "cm3s1otex4mple001",
      "date": "2026-11-20",
      "start": "13:00",
      "lengthHours": 3,
      "startsAt": "2026-11-20T12:00:00.000Z",
      "endsAt": "2026-11-20T15:00:00.000Z",
      "guest": {
        "name": "Adaeze Okafor"
      },
      "company": null,
      "extras": [],
      "extensions": [],
      "payment": {
        "timing": "upfront",
        "totalKobo": 1800000,
        "roomKobo": 1800000,
        "extrasKobo": 0,
        "discountKobo": 0,
        "commissionKobo": 270000,
        "dueAtDeskKobo": 0,
        "paidAtHotelAt": null
      },
      "checkedInAt": null,
      "checkedOutAt": null,
      "cancelledAt": null,
      "createdAt": "2026-11-20T09:12:04.000Z",
      "updatedAt": "2026-11-20T09:12:04.000Z"
    }
  }
}

Check the signature

Each endpoint has its own signing secret (whsec_…), shown behind Reveal on the Connections page. Hourly-Signature is t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of `${t}.${rawBody}` with that secret. Compute it over the raw bytes before parsing, compare in constant time, and refuse a t more than 5 minutes from your clock.

Node.js
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = process.env.HOURLY_WEBHOOK_SECRET; // whsec_...
const seen = new Set(); // use your database in production

// Read the raw body: the signature is over the exact bytes we sent.
app.post('/hourly/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8');
  const header = req.get('Hourly-Signature') ?? '';
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const t = Number(parts.t);

  const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${raw}`).digest('hex');
  const given = Buffer.from(parts.v1 ?? '', 'hex');
  const fresh = Math.abs(Date.now() / 1000 - t) <= 300;
  if (!fresh || given.length !== 32 || !crypto.timingSafeEqual(given, Buffer.from(expected, 'hex'))) {
    return res.sendStatus(400);
  }

  const eventId = req.get('Hourly-Event-Id');
  res.sendStatus(200); // answer first, then do the work
  if (seen.has(eventId)) return; // a retry of something you already have
  seen.add(eventId);

  const { event, data } = JSON.parse(raw);
  queueBookingUpdate(event, data.booking); // your code
});

Answer fast, expect repeats

  • Answer with any 2xx within 10 seconds, then do the work. Anything else, or no answer, counts as a failure. Redirects are not followed.
  • A failed delivery is tried again after at least 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h: about 45 hours in all. After that it is marked Failed and can be resent from the Connections page. A resend carries the same event id and the original payload.
  • Delivery is at least once, so the same event can arrive twice. Hourly-Event-Id (also id in the body) is the same on every try: store it and ignore one you already have.
  • Events can arrive out of order. Use the booking’s updatedAt, or fetch it with GET /bookings/{reference}, for its latest state.
  • After 5 deliveries in a row end Failed, Hourly switches the endpoint off (test events don’t count). Fix your receiver, send a test event, then switch it back on.

Changelog

  • v1, November 2026: first release. Hotel, room codes, availability, bookings, check-in and check-out, and booking webhooks.

Questions or a problem with a connection? Ask your Hourly account manager.