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"
}
]
}'
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"])
{
"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"
}
}
{
"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"
}
{ "dry_run": true, "submitted": 2, "valid": 2 }
Create Hotel Stays (v2)
Create up to 5,000 hotel stays in one request; their emissions are calculated asynchronously
POST
/
v2
/
hotel-stays
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"
}
]
}'
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"])
{
"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"
}
}
{
"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"
}
{ "dry_run": true, "submitted": 2, "valid": 2 }
← Hotel Stays API
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
Coming from
The record is the stay v1 takes, with these differences:
Request-level errors:
/v1/hotel-stays reads and edits.
The request checks every record and writes them all at once, then answers 202 Accepted with an
ingest job that calculates them (see
how bulk ingestion works). Follow it with
GET /v2/ingest-jobs/{id} or, for a job submitted with an API key, the
ingest_job.finished webhook.
Beta. Part of the bulk ingest API, in beta. The contract may still change before general availability.
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"
}
]
}'
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"])
{
"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"
}
}
{
"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"
}
{ "dry_run": true, "submitted": 2, "valid": 2 }
How it works
- Every record is checked, its country included: one without a hotel emission factor (not in
List Available Countries) is rejected here. If any record is
invalid the whole request is rejected with
422and nothing is written;rejectedis the number of invalid records anderrorslists them (up to 100). - The stays are written at once, with status
pending, in thex-organization-idorganization. The request does not look up any address. - The job calculates them in the background, in chunks. A stay sent with an
addressis 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 endsactiveorerror; its job item isfailedwithCALCULATION_FAILEDwhen no emissions came out of it. - When every chunk is done the job is
completed(orcompleted_with_errors). For a job submitted with an API key,ingest_job.finishedis sent to the webhook endpoints of thex-organization-idorganization.
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_dateis aftercheck_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 inerrorwhen it is calculated. - There is no
organization_id, nor any other field not listed below: the stays belong to thex-organization-idorganization. - There is no custom emission factor, as in v1.
Request
Headers
string
required
Your API key (see Authentication).
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 in each organization you send
as.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.Query Parameters
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
array[object]
required
The hotel stays to create, 1 to 5,000. A field not listed here is rejected (
SCHEMA_INVALID).Show record
Show record
date
required
Day of the check-in,
YYYY-MM-DD, from 1970, not in the future.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.integer
Nights stayed, 1 to 366. The check-out is
check_in_date plus nights days, and cannot be in the future.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
(COUNTRY_NOT_SUPPORTED otherwise).integer
default:"1"
Rooms booked, 1 to 10,000.
string
A label for the stay, e.g. the trip or the traveller, up to 255 characters.
string
The hotel’s name, up to 255 characters.
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.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}/itemshas one item per record, in the order sent. Itsentity_idis the stay’s id. Afaileditem carries itserror_code.- List Hotel Stays returns the stays with their status and emissions (
co2e).
If
chunks_done stops moving, see 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. Per-record codes inerrors[].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. |
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.
Limits
| Limit | Value |
|---|---|
| Records per request | 1 to 5,000 |
| Stays per calculation chunk | 200 |
Related Endpoints
Delete Hotel Stays (v2)
Delete stays by id
List Available Countries
The countries a stay can be in
Get Ingest Job
Follow the calculation
List Hotel Stays
The stays and their emissions
Was this page helpful?