Data connections
Pull your bookings, calendar, reviews and earnings out of Host Port and into Google Sheets, Excel, Power BI or a dashboard of your own. Read-only, and only the listings you choose.
Paste one link into Google Sheets, Excel or Power BI and the data appears. It refreshes on its own.
Set up a spreadsheet →Plain HTTPS, JSON out, four datasets, plus a webhook for new bookings and cancellations. Enough to build an owner dashboard or feed a CRM.
Read the API notes →Data connections are part of the paid plan and are only available to the account that owns the listings. A co-host can't create one. Nothing is shared until you create a connection, and you can turn one off at any moment.
Create a connection
- Open Settings from your account menu and find Data connections, below Direct payments.
- Click New connection. Give it a name you'll recognise later, such as Owner dashboard or Bookkeeper.
- Tick the listings it may read, and the data it may serve: Bookings, Calendar, Reviews, Earnings.
- Decide whether to include guest names and contact details. Leave this off for owner dashboards and anything you'll share; turn it on only for a tool that genuinely needs to reach guests.
- Click Create connection. You'll see a data link and an API key. Copy the one you need now.
Turn off stops the link and the key immediately and permanently. Anything still using them gets an error rather than data. Deleting a listing removes its history from every connection.
Google Sheets
In any cell, enter:
=IMPORTDATA("https://ntnzabbhtivotmfhwdnm.supabase.co/functions/v1/data-api/v1/bookings?token=hpl_…")The sheet fills with one row per booking and a header row. Google refreshes the data roughly once an hour; you don't need to do anything.
Choose which data the link serves
The word after data-api/ picks the data. Change it to any of the four, with the same token:
| Link ends with | You get |
|---|---|
/bookings?token=… | One row per booking, cancelled ones included and marked |
/calendar?token=… | One row per listing per night: booked, blocked or available |
/reviews?token=… | One row per guest review |
/earnings?token=… | The same monthly numbers as your Earnings page |
Choose the dates
Without dates, bookings and calendar cover the last 12 months and the next 6; reviews and earnings cover the last 12 months. To pick your own range, add from and to after the token:
…/bookings?token=hpl_…&from=2026-01-01&to=2026-12-31Dates are YYYY-MM-DD. to is the day after the last one you want, and a range can cover up to 24 months.
Excel and Power BI
Excel: Data → Get Data → From Web, paste the link, then Load. Use Refresh All to pull the latest. The file is UTF-8 with a byte-order mark, so names with accents come through correctly.
Power BI: Get Data → Web, paste the link. Power BI treats it as a CSV source and you can schedule refreshes as usual.
Keep the link private
If a link leaks: open Settings, find the connection and click Rotate. The old link stops working at once and you get a new one.
The API, for developers
- Base URL
https://ntnzabbhtivotmfhwdnm.supabase.co/functions/v1/data-api/v1- Auth
Authorization: Bearer hpk_…(the API key, never in a URL)- Method
GETonly- Formats
- JSON by default; add
?format=csvfor CSV - Schema
- A
GETon the base URL with no credential returns a JSON manifest of every dataset, column and limit
curl -H "Authorization: Bearer hpk_…" \
"https://ntnzabbhtivotmfhwdnm.supabase.co/functions/v1/data-api/v1/bookings?from=2026-01-01&to=2026-12-31"Every dataset answers the same shape:
{
"dataset": "bookings",
"from": "2026-01-01",
"to": "2026-12-31",
"updated_since": null,
"page": null,
"page_size": null,
"has_more": false,
"generated_at": "2026-09-17T05:27:14.008Z",
"rows": [ { "booking_id": "…", "property_name": "Manly Beach House", … } ]
}The /v1 at the end of the address is the API version. Links and keys handed out before the address gained it keep working at the older address, and a future /v2 can change shape without breaking anyone on /v1.
Two credentials, two rules
- The API key (
hpk_) goes in theAuthorizationheader. A key placed in a URL is refused and logged, so you'll be asked to rotate it. - The data link token (
hpl_) goes in the URL and serves CSV only. It's meant for spreadsheets; a program should use the key.
Pulling only what changed
Bookings and reviews accept updated_since: an ISO 8601 instant such as 2026-09-17T04:00:00Z, or a plain YYYY-MM-DD, which means midnight UTC. You get the rows changed at or after that moment, still inside the from and to window. A cancellation counts as a change, and so does a reply to a review or hiding one.
curl -H "Authorization: Bearer hpk_…" \
"https://ntnzabbhtivotmfhwdnm.supabase.co/functions/v1/data-api/v1/bookings?updated_since=2026-09-17T04:00:00Z&to=2028-01-01"Keep the response's generated_at and send it as the next updated_since. Widen to if you want changes to stays far in the future. Calendar and earnings have no "last changed" moment, so updated_since on those is a 400 rather than silently ignored.
Paging
Without page, a call returns everything in the window, up to 20,000 rows; above that it's a 413 and you should narrow the dates. With page (1-based) and an optional page_size (1 to 1,000, default 1,000) you get one page at a time. In JSON, page, page_size and has_more sit beside rows; in CSV, the X-Has-More header is true or false. Rows come in a fixed order per dataset, so pages never overlap or skip. Each page is one call against the daily limit.
Caching
Responses carry Cache-Control: private, max-age=300. Polling more often than every five minutes returns the same data and counts against your limits.
Limits
| Limit | Value | When hit |
|---|---|---|
| Calls per connection per minute | 60 | 429 with Retry-After: 60 |
| Calls per connection per day | 500 | 429, daily_limit. Resets at midnight UTC, which is 10 am Sydney, 11 am in daylight saving |
| Calls per IP address per minute | 120 | 429 |
| Rows per call, without paging | 20,000 | 413, too_many_rows: narrow the dates or use page |
| Rows per page, with paging | 1,000 | 400, bad_page_size |
| Date span per call | 24 months | 400, bad_dates |
Errors
Errors are JSON with an error code and, where useful, a message.
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_dates, bad_format, bad_updated_since, updated_since_not_supported, bad_page_size | Check from, to, format, updated_since, page and page_size. updated_since only works on bookings and reviews |
| 402 | subscription_lapsed | The account's Host Port plan has lapsed. Feeds resume when it's renewed |
| 403 | dataset_not_allowed | This connection wasn't given that data. The response lists what it does allow |
| 404 | not_found | Unknown credential, a credential that's been turned off, a key in the URL, a link in the header, or an unknown dataset. Deliberately the same answer for all of them |
| 413 | too_many_rows | Narrow the date range |
| 429 | rate_limited, daily_limit | Slow down; see Retry-After |
Bookings
One row per booking whose stay overlaps the date range. Cancelled bookings are included and say so in status. Not included: host blocks, blocked nights mirrored from a linked listing, Booking.com's "closed" calendar markers, and unpaid direct checkouts that expire within minutes.
| Column | Type | Meaning |
|---|---|---|
booking_id | id | Host Port's id for the booking. Stable across calls |
property_id, property_name | id, text | The listing |
timezone | text | The listing's time zone, e.g. Australia/Sydney, for reading timestamps locally |
status | text | pending, confirmed, modified, cancelled, no_show |
channel | text | direct, manual, airbnb, booking, vrbo, expedia, hotels_com, agoda, google, other |
channel_name | text | The channel's own display name where it sent one |
source | text | channex (live channel connection), ical_import, manual |
check_in, check_out | date | Local calendar dates, never shifted by time zone |
nights | integer | |
stay_status | text | past once check-in has arrived in the listing's zone, otherwise upcoming |
booked_at | timestamp | When the booking was made, as an ISO 8601 instant |
cancelled_at | timestamp | Empty unless cancelled |
guests, adults, children, infants | integer | |
guest_name, guest_email, guest_phone, guest_country | text | Empty unless the connection includes guest details |
currency | text | Applies to every amount on the row |
total_amount | money | The booking total as the channel reported it |
host_payout_amount | money | Where the channel reported it; often empty |
platform_fee_amount | money | Host Port's fee on a direct booking |
amount_paid_amount, amount_refunded_amount | money | Direct bookings paid through Host Port; empty for channel bookings |
payment_status | text | unpaid, authorized, paid, failed, partially_refunded, refunded, voided |
created_at, updated_at | timestamp | When Host Port first saw and last changed the row. Send updated_since to fetch only rows changed after a moment |
Calendar
One row per listing per night in the date range. The state is the same one the app's calendar shows.
| Column | Type | Meaning |
|---|---|---|
property_id, property_name | id, text | The listing |
night | date | Local calendar date of the night |
state | text | booked (a live booking), blocked (a host block, a block mirrored from a linked listing, a Booking.com closure or a closed date), available |
booking_id | id | On booked nights, when the connection also has Bookings |
Reviews
One row per review received in the date range. Reviews you've hidden, and ones the channel hides, are left out.
| Column | Type | Meaning |
|---|---|---|
review_id | id | |
property_id, property_name | id, text | The listing |
channel | text | As the channel names itself: AirBNB, BookingCom, Expedia |
received_at | timestamp | When the review arrived |
overall_score | number | 0 to 10, the channel manager's common scale. An Airbnb 5-star review is 10 |
content, reply | text | The guest's words and your reply. May contain line breaks; in CSV these are quoted correctly |
is_replied | boolean | |
guest_name | text | Empty unless the connection includes guest details |
updated_at | timestamp | When the review, your reply or its visibility last changed. Pair it with updated_since |
Earnings
The same numbers as your Earnings page, grouped by listing, channel, month, currency and whether the stay is past or upcoming. Bookings are placed by check-in date. Cancelled bookings and blocks are excluded.
| Column | Type | Meaning |
|---|---|---|
property_id, property_name | id, text | The listing |
channel | text | Same values as bookings |
month | date | First day of the month, by check-in |
currency | text | Rows never mix currencies. Don't add rows in different currencies together |
stay_status | text | past or upcoming |
bookings, nights | integer | |
guest_paid_amount | money | What guests paid in total |
channel_fee_amount | money | The channel's commission, plus Host Port's fee on direct bookings |
payout_amount | money | Guest paid minus fees |
fee_known_bookings | integer | How many of the bookings actually reported their fee. Trust payout_amount only when this equals bookings. A channel that reports no commission makes the payout look higher than it is |
no_price_bookings | integer | Bookings that arrived without a price |
lead_days_sum, lead_known_bookings | integer | Divide the first by the second for the average days between booking and check-in. Only bookings that arrived live are counted, so an initial import doesn't drag the average to zero |
Webhooks: get told about new bookings and cancellations
A data connection can also push. Instead of your tool asking for the bookings feed every so often, Host Port sends each new booking and each cancellation to an address you give it, within about a minute of it happening. For a developer only: a spreadsheet can't receive a webhook.
Turn it on
- Create a connection that includes Bookings (webhooks send booking rows, so a connection without Bookings has no Webhook button).
- On the connection, click Webhook. Enter your endpoint's
https://address and tick New booking, Cancelled booking or both. - Click Test and save. Host Port sends a signed
pingto your address and only saves the webhook if it answers with a 2xx status. - Copy the signing secret (
hpw_…). It's shown once. Use it to check every message really came from Host Port.
From that moment, bookings made or cancelled after the webhook was turned on are sent. Bookings from before are not; pull those from the feed. Only the listings the connection covers are sent, and guest names and contact details follow the connection's setting.
What arrives
A POST with a JSON body. data is one row of the Bookings dataset, exactly as the feed would return it at the moment the message was queued.
POST https://your-endpoint.example/hostport
Content-Type: application/json
X-HostPort-Event: booking.created
X-HostPort-Delivery: 8123
X-HostPort-Timestamp: 1758172800
X-HostPort-Signature: sha256=3f0a…
{
"id": "8123",
"event": "booking.created",
"occurred_at": "2026-09-18T04:40:00.512Z",
"data": { "booking_id": "…", "property_name": "Manly Beach House", "status": "confirmed", "check_in": "2026-10-03", … }
}| Event | When |
|---|---|
booking.created | A booking was made on one of the connection's listings. Blocks, mirrored blocks, Booking.com closures and unpaid direct checkouts are never sent |
booking.cancelled | A booking was cancelled. data.status is cancelled |
ping | The test message sent when you save or press Test. data holds only the connection id and name |
Check the signature
Join the X-HostPort-Timestamp header, a full stop and the raw request body, take an HMAC-SHA256 of that with your signing secret, and compare the hex result with the header. Compare with a constant-time function, and treat a timestamp more than five minutes old as stale.
// Node
import { createHmac, timingSafeEqual } from 'node:crypto'
function verify(req, rawBody, secret) {
const ts = req.headers['x-hostport-timestamp']
const sig = req.headers['x-hostport-signature'] // "sha256=…"
const expected = 'sha256=' + createHmac('sha256', secret).update(ts + '.' + rawBody).digest('hex')
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false
return sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
}Use the raw body bytes, not a re-serialised object. Any change to whitespace or key order changes the signature.
Answer quickly, then do the work
Respond with any 2xx status within 10 seconds. Anything else counts as a failure: a 4xx, a 5xx, a redirect (they're not followed), a timeout, or no connection. Do the slow part after you've answered.
Retries, pausing, resuming
- A failed message is tried again with a growing wait: about 2 minutes, then 4, 8, 16, 32, then every hour, up to 10 attempts.
- When a message uses its tenth attempt, the webhook is paused. Nothing more is sent, the connection shows paused in Settings, and the owner gets an email. Bookings that happen while paused are still queued.
- Fix your endpoint, then press Resume. Everything that failed is sent again, in order, and the queue carries on.
- A message can arrive more than once, for example if your endpoint answered slowly and Host Port had already given up. Use
id(also inX-HostPort-Delivery) to ignore a repeat.
Timing and order
Host Port looks for changes every minute, so a message arrives one to two minutes after the booking. Messages for one connection are sent in the order they were queued, but if one fails and is retried it will arrive after later ones, so never assume the order. At most 100 messages a minute are sent to one endpoint; the rest follow in the next minute.
Replace, test, turn off
Edit webhook saves a new address and a new signing secret, and the old secret stops working at once. Test sends a ping with the current secret. Webhook off removes the address, the secret and anything still waiting to be sent. Turning the whole connection off does the same.
https:// address. Private network addresses, localhost, and Host Port's own domains are refused.Dates and money
- Dates (
check_in,night,month) are the listing's local calendar dates, asYYYY-MM-DD. They are never shifted by time zone. - Timestamps (
booked_at,received_at,updated_at) are ISO 8601 instants with an offset. Convert to the listing'stimezoneto read them locally. - Money columns end in
_amount, are in major units (dollars, not cents) with two decimals, and every money row carries acurrency. Never add amounts across currencies. - CSV files are UTF-8 with a byte-order mark and Windows line endings, so Excel opens them cleanly. A cell that starts with
=,+,-or@is prefixed with an apostrophe so a spreadsheet won't run it as a formula.
Questions
A booking is missing from the bookings feed
Three kinds of calendar entry are deliberately not bookings: your own blocks, nights blocked because a linked listing is booked, and Booking.com's "closed" markers that arrive through iCal. All three show as blocked in the calendar feed instead. An unpaid direct checkout that hasn't been completed is also left out until it's paid.
The earnings payout doesn't match my bank
Check fee_known_bookings against bookings on that row. If they differ, some bookings didn't report their commission and the payout is overstated for that row. Guest-paid totals are unaffected.
My Google Sheet isn't updating
Google refreshes an IMPORTDATA link roughly hourly on its own schedule; you can't force it from the sheet. A change made in Host Port a moment ago can take up to an hour to appear.
Everything returns a 404
The connection has been turned off, the credential is in the wrong place (key in the URL, or link in the header), or the dataset name in the path is misspelt. Create a fresh connection if in doubt.
Can a co-host set this up?
No. Only the account that owns the listings can create, rotate or turn off a connection. A co-host sees the Data connections card with a note saying so.