Docs/Channels/Airbnb

Airbnb Reservations

List, get, accept, decline, and cancel Airbnb reservations. Includes price breakdown and full guest details. Cursor-paginated like every other list endpoint — walk pages with ?cursor=<pagination.nextCursor> until pagination.hasMore is false.

GET/v1/channels/airbnb/reservations

Parameters

account_idstring

Scope the response to ONE connected Airbnb account. The value is the Airbnb host id — the same accounts[].externalAccountId that GET /v1/connect/airbnb returns and DELETE /v1/connect/airbnb?accountId= accepts. Omit it to read every connected account (the default). An id that is not connected to this workspace returns 404 not_found with field: "account_id" and your own ids in valid_values. This is not the X-Account-Id header, which carries a connection id and cannot tell two Airbnb hosts apart.

cursorstring

Opaque cursor from the previous response's pagination.nextCursor. Omit for the first page.

limitinteger

Max items per page. Default 50, max 100.

listing_idstring

Filter to one Airbnb listing id.

statusstring

Filter by status (pending, accepted, denied, cancelled, completed, failed_verification, request_voided). Omit to receive all statuses.

start_datestring

ISO 8601 (YYYY-MM-DD) lower bound on the date range filter.

end_datestring

ISO 8601 (YYYY-MM-DD) upper bound on the date range filter.

Accept, decline or cancel

POST /v1/channels/airbnb/reservations/{code} acts on a reservation by its Airbnb confirmation code, as the Airbnb account that owns it. If you have the Repull reservation id, POST /v1/reservations/{id}/accept and /decline do the same for booking requests — see Inquiries & Booking Requests (/docs/channels/airbnb/inquiries-and-requests).

  • {"action": "accept"} — accept a pending booking request. Takes no other field.
  • {"action": "decline", "reason": "…", "message": "…"} — decline a pending booking request. reason is one of dates_not_available, not_comfortable, listing_not_ready, different_dates_needed, spam, other; message (1–500 characters) is sent to the guest.
  • {"action": "cancel", "reason": "…"} — cancel a confirmed booking as the host. reason is one of calendar_conflict, maintenance_issue, unable_to_host, other. Host cancellations carry Airbnb penalties.
  • There is no pre-approve action: a pre-approval answers an inquiry, which has no confirmation code yet. Sending it returns 422 invalid_params pointing at POST /v1/conversations/{id}/pre-approval.
  • Errors: 422 invalid_params (names the field), 409 request_no_longer_pending or 409 request_expired (do not retry), 422 airbnb_rejected (Airbnb's reason in message), 403 connection_reauth_required, 429 airbnb_rate_limited, 502 airbnb_error. Send Idempotency-Key to make a retry safe.

Several Airbnb accounts

A workspace can connect more than one Airbnb account. By default this route returns every connected account's rows; pass ?account_id=<airbnb host id> to scope to one. Every row carries accountId + accountName either way (the older hostId / hostName remain as aliases), so you can group without a second call. Airbnb host ids exceed 2^53 — keep them as strings and never parse them as numbers: Number("1000000000000000002") is 1772489413932732200, a different account, and returns 404. A two-account walkthrough lives on the Account scope page (/docs/scoping).

# Every connected account — the default, unchanged
curl 'https://api.repull.dev/v1/channels/airbnb/reservations' \
  -H "Authorization: Bearer sk_live_YOUR_KEY"

# Just one account
curl 'https://api.repull.dev/v1/channels/airbnb/reservations?account_id=1000000000000000002' \
  -H "Authorization: Bearer sk_live_YOUR_KEY"

# An id this workspace has not connected — 404, with your own ids to copy
# {
#   "error": {
#     "code": "not_found",
#     "message": "No connected Airbnb account `1772489413932732200` in this workspace.",
#     "field": "account_id",
#     "value_received": "1772489413932732200",
#     "valid_values": ["1000000000000000002", "80000001"]
#   }
# }

Data freshness

Airbnb reads are served from Repull's local mirror and never call Airbnb upstream, so every response carries a dataFreshness envelope telling you whether to trust it. lastSyncedAt is the last import that actually landed data — a run that failed or was rate-limited never moves it. accounts[] gives the verdict per connected account; it is omitted when the workspace has no Airbnb account to attribute.

  • Top-level stale: true means every connected account is stale — nothing in the response is current. For the single-account workspace this is exactly the old behaviour.
  • Top-level stale: false with reason: "partial_account_staleness" means some accounts are fine and some are not. The response is usable; read accounts[] to see which rows to distrust. The reason is emitted deliberately even though stale is false, so a consumer reading only the aggregate is never told everything is fine while one account is down.
  • Top-level stale: false with no reason means every account is current.
  • With ?account_id=, accounts[] holds exactly that account and the top-level fields mirror it.
  • reason is one of host_disconnected_since_<iso>, host_disconnected, host_not_activated, sync_lag_>_24h, never_synced, or partial_account_staleness. fixUrl is the dashboard screen that resolves it.
{
  "dataFreshness": {
    "lastSyncedAt": "2026-09-18T04:12:09.000Z",
    "stale": false,
    "reason": "partial_account_staleness",
    "fixUrl": "https://repull.dev/dashboard/connections",
    "accounts": [
      {
        "accountId": "1000000000000000002",
        "accountName": "Seaside Stays",
        "lastSyncedAt": "2026-09-18T04:12:09.000Z",
        "stale": false
      },
      {
        "accountId": "80000001",
        "accountName": "Casey",
        "lastSyncedAt": null,
        "stale": true,
        "reason": "host_disconnected",
        "fixUrl": "https://repull.dev/dashboard/connections"
      }
    ]
  }
}

Example

# Page 1 (default 50)
curl 'https://api.repull.dev/v1/channels/airbnb/reservations?limit=50' \
  -H "Authorization: Bearer sk_live_YOUR_KEY"

# Walk to page 2 with the cursor returned by page 1
curl 'https://api.repull.dev/v1/channels/airbnb/reservations?limit=50&cursor=eyJ1IjoiWDE5...' \
  -H "Authorization: Bearer sk_live_YOUR_KEY"

# Filter to one listing's reservations within a date window
curl 'https://api.repull.dev/v1/channels/airbnb/reservations?listing_id=18871326&start_date=2026-06-01&end_date=2026-08-31' \
  -H "Authorization: Bearer sk_live_YOUR_KEY"

# Accept a pending booking request
curl -X POST https://api.repull.dev/v1/channels/airbnb/reservations/HMA1234567 \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Idempotency-Key: accept-HMA1234567" \
  -H "Content-Type: application/json" \
  -d '{"action": "accept"}'

# Decline one
curl -X POST https://api.repull.dev/v1/channels/airbnb/reservations/HMA1234567 \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"action": "decline", "reason": "dates_not_available", "message": "Sorry, those dates are no longer available."}'

Response

{
  "data": [
    {
      "confirmationCode": "HMABC12345",
      "statusType": "accept",
      "accountId": "1000000000000000002",
      "accountName": "Seaside Stays",
      "listingId": "18871326",
      "startDate": "2026-07-06",
      "endDate": "2026-07-08",
      "nights": 2,
      "guestFirstName": "Nowf",
      "guestLastName": "Abugauch",
      "hostCurrency": "CAD",
      "payoutAmount": "750.74"
    }
  ],
  "pagination": {
    "nextCursor": "eyJ1IjoiWDE5MmFXRmtkV04wT21sa2VEb3oifQ",
    "hasMore": true
  },
  "dataFreshness": {
    "lastSyncedAt": "2026-09-18T04:12:09.000Z",
    "stale": false,
    "accounts": [
      {
        "accountId": "1000000000000000002",
        "accountName": "Seaside Stays",
        "lastSyncedAt": "2026-09-18T04:12:09.000Z",
        "stale": false
      }
    ]
  }
}
AI