> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gridbeds.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Listing reports

> Tell Gridbeds what became of each listing on your channel and how it performs, so the host sees it beside the bookings you send.

<Warning>
  **Not served yet.** This page describes the final contract, version `1.0.0`. Until the sandbox opens with the first test key, try it against a mock of the contract ([Testing](/en/distribution-api/testing)).
</Warning>

Listing reports go the other way from the rest of the API: you tell Gridbeds what became of each listing on your channel and how it performs. Gridbeds shows it to the host in its dashboard (views, clicks to booking, whether a listing is published and promoted) and joins it with the bookings that arrive from you. Every operation here, reading a report back included, needs the scope `write:listings`, and Gridbeds sends no webhook for them: it is the receiver.

## The state of a listing

`PUT /properties/{property_id}/listing` replaces the state of your listing of a property. Send it whenever the state changes on your side:

```json theme={null}
{
  "status": "published",
  "rejection": null,
  "public_url": "https://channel.example/listings/12345",
  "promoted": true,
  "promoted_until": "2026-11-30T22:00:00Z",
  "updated_at": "2026-10-18T09:30:00Z"
}
```

| Field | |
| - | - |
| `status` | `published`: live on your channel. `unpublished`: not live, by your or the host's choice. `pending_review`: in your moderation. `rejected`: refused by your moderation. Open enum. |
| `rejection` | Required with `rejected`, refused with any other status. A `reason` (`photos`, `description`, `legal`, `price`, `duplicate`, `policy` or `other`; open enum) and a `text` the host reads: plain text of at most 500 characters, in the host's language when you can, with no HTML or links. |
| `public_url` | The public HTTPS page of the listing on your channel. Required with `published`, `null` otherwise. |
| `promoted` | Whether the listing is promoted (featured or boosted) on your channel now. |
| `promoted_until` | When the promotion ends; `null` when it has no end or the listing is not promoted. |
| `updated_at` | When the state changed on your side. |

* **A full replacement, idempotent:** sending the same body again changes nothing.
* **The latest `updated_at` wins.** A report older than the one held changes nothing and answers the one held, so retries and reordering are harmless. An `updated_at` more than 300 seconds ahead of Gridbeds's clock is refused (`invalid_parameters`, `errors[].code` `updated_at_in_future`), so one wrong clock cannot freeze the report for good.
* A property you cannot read as active or inactive answers the same `404` as `GET /properties/{property_id}` for an unknown id. A report already held is kept, and shown to the host after a withdrawal.
* The answer is the report Gridbeds holds, with `property_id` and `received_at`. A report costs 1 unit of your rate limit.

`GET /properties/{property_id}/listing` reads back the report Gridbeds holds, and answers `404` until you send one.

## Daily statistics

`POST /listing-stats` takes daily aggregates, one item per property and property-local date, at most 1,000 in a request:

```json theme={null}
{
  "data": [
    { "property_id": "prp_1qnykkykp69yv8d59hxk5410m9", "date": "2026-10-17", "views": 214, "clicks_to_booking": 9, "impressions": 3870, "saves": 4 }
  ]
}
```

| Field | |
| - | - |
| `date` | The property's local date: not after its today, and not more than 90 days before it. |
| `views` | Views of the listing's own page. |
| `clicks_to_booking` | Clicks that left for the booking (`booking.url` or `url_template`) or sent a booking request. |
| `impressions` | Appearances in search results or lists. Optional. |
| `saves` | Times the listing was saved to a favourites list. Optional. |

* **Idempotent per property and date.** A resend overwrites the day, so send today's partial figures as often as you like and correct a past day by sending it again.
* **All or nothing.** A request with any invalid item stores nothing and answers `400` with an `errors[]` entry per invalid item (`pointer` `/data/<index>/<member>`). Fix it and resend the whole batch, which the idempotency makes safe.
* **An item is invalid** when its property was never distributed to you (`unknown_property`), when its date is after the property's today or more than 90 days before it (`date_out_of_range`), or when the batch names the same property and date twice (`duplicate_day`). A property withdrawn or deleted since still takes the days on which it was distributed.
* The answer says how many property-days were stored (`accepted`) and when (`received_at`).
* Days are kept 25 months, so a host can compare a month with the same month a year before, and are then deleted.
* A request costs `max(1, ceil(items / 100))` units of your rate limit.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.