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

# Create Hotel Stays (v2)

> Create up to 5,000 hotel stays in one request; their emissions are calculated asynchronously

[← Hotel Stays API](/api-reference/hotel-stays/overview)

Create one or many hotel stays (Scope 3 category 6). Say where each stay was, when, for how many nights and how many
rooms; Dcycle applies the hotel emission factor of its country and calculates the emissions in the background. The
stays are the ones [`/v1/hotel-stays`](/api-reference/hotel-stays/list) reads and edits.

The request checks every record and writes them all at once, then answers `202 Accepted` with an
[ingest job](/api-reference/ingest-jobs/get) that calculates them (see
[how bulk ingestion works](/api-reference/ingest-jobs/overview)). Follow it with
[`GET /v2/ingest-jobs/{id}`](/api-reference/ingest-jobs/get) or, for a job submitted with an API key, the
[`ingest_job.finished`](/api-reference/webhooks/overview) webhook.

> [!WARNING]
>
> **Beta.** Part of the bulk ingest API, in beta. The contract may still change before general availability.

```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "https://api.dcycle.io/v2/hotel-stays" \
  -H "x-api-key: ${DCYCLE_API_KEY}" \
  -H "x-organization-id: ${DCYCLE_ORG_ID}" \
  -H "Idempotency-Key: 6f5e4d3c-2b1a-4098-8f7e-6d5c4b3a2918" \
  -H "Content-Type: application/json" \
  -d '{
    "records": [
      {
        "check_in_date": "2026-03-04",
        "check_out_date": "2026-03-07",
        "country": "ES",
        "rooms": 2,
        "hotel_name": "Hotel Gran Vía",
        "address": "Gran Vía 25, Madrid",
        "name": "Sales kick-off",
        "client_row_id": "STAY-2026-0311"
      },
      {
        "check_in_date": "2026-03-10",
        "nights": 2,
        "country": "gb"
      }
    ]
  }'
```

```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os
import uuid

import requests

response = requests.post(
    "https://api.dcycle.io/v2/hotel-stays",
    headers={
        "x-api-key": os.environ["DCYCLE_API_KEY"],
        "x-organization-id": os.environ["DCYCLE_ORG_ID"],
        "Idempotency-Key": str(uuid.uuid4()),  # keep it to retry this same batch
    },
    json={
        "records": [
            {
                "check_in_date": "2026-03-04",
                "nights": 3,
                "country": "FR",
                "address": "Paris",
                "client_row_id": "STAY-2026-0312",
            }
        ]
    },
    timeout=60,
)
response.raise_for_status()
job = response.json()
print(job["id"], job["status"])
```

```json 202 theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "id": "2a19f8e7-d6c5-4b4a-9382-71605f4e3d2c",
  "status": "processing",
  "entity_type": "hotel_stays",
  "operation": "create",
  "source": "api_bulk",
  "counts": { "submitted": 2, "succeeded": 0, "failed": 0 },
  "chunks_total": 1,
  "chunks_done": 0,
  "created_at": "2026-10-21T09:12:44.031Z",
  "finished_at": null,
  "links": {
    "self": "/v2/ingest-jobs/2a19f8e7-d6c5-4b4a-9382-71605f4e3d2c",
    "items": "/v2/ingest-jobs/2a19f8e7-d6c5-4b4a-9382-71605f4e3d2c/items"
  }
}
```

```json 422 theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "https://code.dcycle.io/api-reference/errors#BULK_RECORDS_REJECTED",
  "title": "Unprocessable Entity",
  "status": 422,
  "code": "BULK_RECORDS_REJECTED",
  "detail": "1 of the submitted records are invalid; nothing was written.",
  "errors": [
    {
      "source_row_index": 0,
      "client_row_id": "STAY-2026-0311",
      "error_code": "COUNTRY_NOT_SUPPORTED",
      "error_params": { "field": "country", "value": "AQ" }
    }
  ],
  "rejected": 1,
  "request_id": "b2248a31-0f1b-4a7d-83ce-b29bd5977b4c"
}
```

```json 200 (dry_run=true) theme={"theme":{"light":"github-light","dark":"github-dark"}}
{ "dry_run": true, "submitted": 2, "valid": 2 }
```

## How it works

1. **Every record is checked**, its country included: one without a hotel emission factor (not in
   [List Available Countries](/api-reference/hotel-stays/available-countries)) is rejected here. If any record is
   invalid the **whole** request is rejected with `422` and nothing is written; `rejected` is the number of invalid
   records and `errors` lists them (up to 100).
2. **The stays are written at once**, with status `pending`, in the `x-organization-id` organization. The request does
   not look up any address.
3. **The job calculates them** in the background, in chunks. A stay sent with an `address` is first placed on the map
   (best effort: an address that cannot be found does not fail the stay). Then its emissions are calculated: `rooms` ×
   nights × the hotel room-night emission factor of its country (DEFRA, or Greenview where DEFRA has none). Each stay ends `active` or `error`; its job
   item is `failed` with `CALCULATION_FAILED` when no emissions came out of it.
4. **When every chunk is done** the job is `completed` (or `completed_with_errors`). For a job submitted with an API
   key, `ingest_job.finished` is sent to the webhook endpoints of the `x-organization-id` organization.

### Coming from `POST /v1/hotel-stays`

The record is the stay v1 takes, with these differences:

* A stay has at least one night: `check_out_date` is after `check_in_date` (v1 accepts the same day).
* A stay cannot be in the future, neither its check-in nor its check-out.
* A country without a hotel emission factor is rejected in the request (`COUNTRY_NOT_SUPPORTED`); v1 stores the stay
  and it ends in `error` when it is calculated.
* There is no `organization_id`, nor any other field not listed below: the stays belong to the `x-organization-id`
  organization.
* There is no custom emission factor, as in v1.

## Request

### Headers

**x-api-key** `string` *required*

Your API key (see [Authentication](/api-reference/introduction#authentication)).

**x-organization-id** `string` *required*

The organization the stays belong to. A record cannot name another organization: send each organization's stays
with its own `x-organization-id`. The job and its `ingest_job.finished` webhook belong to that organization too, so
register a [webhook endpoint](/api-reference/webhooks/overview#groups-of-organizations) in each organization you send
as.

**Idempotency-Key** `string` *required*

1 to 255 characters, one per batch, looked up per organization across every v2 bulk route. Within 24 hours, the
same key with the same route and body returns the original job (with `Idempotent-Replayed: true`) instead of
creating the stays again; the same key with another body or on another route is `422 IDEMPOTENCY_KEY_REUSED`, and
while the first request is still being processed `409 IDEMPOTENCY_KEY_IN_PROGRESS`. After 24 hours the key is free,
and a request with it is a new one. See [The Idempotency-Key](/api-reference/ingest-jobs/overview#the-idempotency-key).

### Query Parameters

**dry\_run** `boolean` (default: false)

Check only: the same `422` for invalid records, nothing written, no job; `200` with
`{"dry_run": true, "submitted": n, "valid": n}` when every record is valid. It does not check or store the
`Idempotency-Key`.

### Body Parameters

**records** `array[object]` *required*

The hotel stays to create, 1 to 5,000. A field not listed here is rejected (`SCHEMA_INVALID`).

**record**

**check\_in\_date** `date` *required*

Day of the check-in, `YYYY-MM-DD`, from 1970, not in the future.

**check\_out\_date** `date`

Day of the check-out, after `check_in_date` (at least one night) and not in the future. Send it or `nights`; if
you send both, `nights` must be the days between the two dates.

**nights** `integer`

Nights stayed, 1 to 366. The check-out is `check_in_date` plus `nights` days, and cannot be in the future.

**country** `string` *required*

The hotel's country, ISO 3166-1 alpha-2 (e.g. `ES`), in any case; it is stored in upper case. It must have a
hotel emission factor: one of [List Available Countries](/api-reference/hotel-stays/available-countries)
(`COUNTRY_NOT_SUPPORTED` otherwise).

**rooms** `integer` (default: 1)

Rooms booked, 1 to 10,000.

**name** `string`

A label for the stay, e.g. the trip or the traveller, up to 255 characters.

**hotel\_name** `string`

The hotel's name, up to 255 characters.

**address** `string`

The hotel's address or city, 1 to 500 characters, used to place the hotel on a map. It does not change the
emissions, which depend on `country`.

**client\_row\_id** `string`

Your own reference for the record, up to 255 characters, echoed in errors and in the job's items.

## Response

`202 Accepted` with the job (`entity_type: hotel_stays`, `operation: create`) and a `Location` header to it. To read
the result:

* [`GET /v2/ingest-jobs/{id}/items`](/api-reference/ingest-jobs/list-items) has one item per record, in the order
  sent. Its `entity_id` is the stay's id. A `failed` item carries its `error_code`.
* [List Hotel Stays](/api-reference/hotel-stays/list) returns the stays with their status and emissions (`co2e`).

> [!WARNING]
>
> If `chunks_done` stops moving, see [A job that does not finish](/api-reference/ingest-jobs/get#a-job-that-does-not-finish):
> never resend the batch with a new `Idempotency-Key`, because its hotel stays are already stored.

## Errors

[Problem details](/api-reference/errors). Per-record codes in `errors[].error_code` of a `422 BULK_RECORDS_REJECTED`:

| `error_code` | Cause |
| - | - |
| `SCHEMA_INVALID` | The record does not match the schema: a missing or unknown field (e.g. `organization_id`), no `check_out_date` and no `nights`, a check-out on or before the check-in, `nights` that does not match the dates, a stay in the future, a country that is not two letters, more than 10,000 rooms... `error_params.errors` says where. |
| `COUNTRY_NOT_SUPPORTED` | `country` has no hotel emission factor: it is not in [List Available Countries](/api-reference/hotel-stays/available-countries). |

Request-level errors: `401` for a missing or invalid API key, `403 LOGGED_USER_NOT_MEMBER` when the key's user is not a
member of the organization, `422 REQUEST_VALIDATION_FAILED` for a malformed body (no `records`, more than 5,000, no
`Idempotency-Key`), `422 IDEMPOTENCY_KEY_REUSED`, `409 IDEMPOTENCY_KEY_IN_PROGRESS`, and `429` beyond the
[rate limits](/api-reference/rate-limits).

## Limits

| Limit | Value |
| - | - |
| Records per request | 1 to 5,000 |
| Stays per calculation chunk | 200 |

## Related Endpoints

> **[Delete Hotel Stays (v2)](/api-reference/hotel-stays/delete-v2)**
>
> Delete stays by id

> **[List Available Countries](/api-reference/hotel-stays/available-countries)**
>
> The countries a stay can be in

> **[Get Ingest Job](/api-reference/ingest-jobs/get)**
>
> Follow the calculation

> **[List Hotel Stays](/api-reference/hotel-stays/list)**
>
> The stays and their emissions


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