Availability

The thing providers get wrong most often is a home that is no longer free. This is how you keep that honest.

See what is blocked

GET /properties/{id|external_id}/availability
{
  "data": [
    {
      "id": "001d833f-…",
      "start_date": "2026-12-20",
      "end_date": "2027-01-05",
      "reason": "Owner using the home",
      "source": "provider"
    },
    {
      "id": "77c0a1e2-…",
      "start_date": "2026-11-01",
      "end_date": "2027-01-31",
      "reason": "Reserved — lease pending",
      "lease_agreement_id": "5ae35393-…",
      "source": "havnly"
    }
  ]
}

source tells you who put it there:

  • provider — yours, and yours to remove
  • havnly — a home held for a lease or a booking. Ours, and not removable through the API; cancel the placement instead

Block dates

POST /properties/{id|external_id}/availability
{ "start_date": "2026-12-20", "end_date": "2027-01-05", "reason": "Owner using the home" }

Release a block

POST /properties/{id}/availability/{block_id}/release

Only your own. Releasing one of ours answers:

{
  "error": {
    "code": "not_yours_to_release",
    "message": "This home is held for a lease or a booking. Cancel the placement instead."
  }
}

Keeping a calendar in sync

If your own system is the source of truth, the simplest reliable pattern is: read our blocks, ignore anything with source: "havnly", and reconcile the rest against yours. Blocks are cheap — one per stretch of unavailability is fine.

A whole portfolio at once

The per-home calls above are right for a change somebody just made. They are the wrong shape for the job a thousand-home provider actually does every night, which is “here is my current availability, make it so” — a thousand calls, against a rate limit, mostly to say nothing changed.

POST /properties/availability
{
  "replace": true,
  "blocks": [
    { "external_id": "unit-4021", "start_date": "2026-12-20", "end_date": "2026-12-27", "reason": "Owner use" },
    { "external_id": "unit-4022", "start_date": "2027-01-02", "end_date": "2027-01-09" }
  ]
}
{
  "blocked": 2,
  "failed": 0,
  "replaced_existing": 2,
  "homes_touched": 2,
  "results": [
    { "index": 0, "ref": "unit-4021", "ok": true, "property_id": "bef180d5-…" }
  ]
}

Up to 500 blocks per call. Homes are named by your own external_id or by our property_id. Answered per item, so one bad date does not lose the other 499 — 207 when some were refused, with the reason against each.

replace: true is the sync primitive

It clears the blocks you previously set on the homes named in this call, then applies what you sent. That makes the call declarative: send your current calendar and stop tracking what you told us last night.

It never touches a hold Havnly placed. A block created by a lease or a confirmed booking is a household’s home for those dates, and a nightly sync is not entitled to free it. Those survive every replace, and they are the ones GET /properties/{id}/availability marks as source: "havnly".

Homes you do not name are untouched. replace is scoped to the homes in the payload, not to your whole portfolio — so a partial sync cannot wipe the rest.

Retrying

Send an Idempotency-Key. Without one, a retried sync re-applies every block, and with replace: true that is harmless but wasteful; without replace it duplicates them.