Skip to content

Errors

Branch on the HTTP status first; it tells you what kind of problem you have and whether a retry can help. The body then says why, in one of four shapes. The shapes differ because different layers of the API produce them, and making them one would change the API, so this page documents them as they are.

Status Meaning Retry?
200 A read, an update, or the repeat of an earlier write: an Idempotency-Key replay, a booking whose external_reference already exists, a till sale whose external_ref is known
201 A booking, a webhook endpoint or a till sale was created
202 A webhook replay was queued
204 A webhook endpoint was removed
400 The request is wrong: a missing or invalid field, an unknown site, a booking that cannot be cancelled, a date in the query that is not a real date (which), a body that is not JSON No. Fix the request.
401 The key is missing, unknown, revoked or expired (Credentials) No
403 The key lacks the scope this endpoint needs No
404 No such record at this campground, or a page past the end of a list No
409 The request clashes with the campground’s data: the pitch is taken, the external_reference belongs to another key, the Idempotency-Key was used for another request, the webhook endpoint is paused Not unchanged
422 The campground’s date rules refuse the stay: a minimum stay, no arrivals or departures on that day, or a night not on sale No. Offer other dates.
429 Your key’s rate limit is used up After Retry-After seconds
503 A webhook replay could not be queued Yes, later
Other 5xx A fault on our side Yes, with a growing pause and, for a write, the same Idempotency-Key
{ "detail": "not_found" }

Most errors look like this. detail holds either a code in snake_case or an English sentence. The codes:

detail Status When
invalid_key 401 The key is unknown or mistyped
key_inactive 401 The key was revoked or has expired
not_found 404 No such booking, webhook endpoint, delivery or till sale at this campground
site_not_found 400 POST bookings/: site_id is not one of the campground’s public sites
site_or_site_type_required 400 POST bookings/: neither a site_id nor a site type the campground knows
create_failed 400 POST bookings/: the booking could not be made for a reason not listed here
no_pitch_available 409 POST bookings/ by site type: no pitch of that type is free
external_reference_conflict 409 POST bookings/: a booking with this external_reference exists, and your key did not make it
webhook_inactive 409 Webhook replay: the endpoint was removed or paused
enqueue_failed 503 Webhook replay: it could not be queued

The sentences come from the framework the API is built on, and from the checks on date parameters:

  • 401 "Authentication credentials were not provided.": no Authorization header, or one that is not Bearer ck_....
  • 403 "You do not have permission to perform this action.": the scope is missing.
  • 404 "Invalid page.": a page past the end.
  • 429 "Request was throttled. Expected available in 37 seconds."
  • 400 for a date in the query that is not a real date (see Bad dates in a query), or a body that is not valid JSON.

Match on the status and on the codes, never on the wording of a sentence.

{ "detail": "site_unavailable", "reason": "overlap_or_block" }

A few errors add a key that says more:

Body Status Sent by
{"detail": "site_unavailable", "reason": "overlap_or_block"} 409 POST bookings/: the pitch is booked, blocked or held for those dates
{"detail": "not_cancellable", "status": "completed"} 400 POST bookings/{id}/cancel/: only reservation_request, pending and confirmed bookings can be cancelled; status is the booking’s
{"detail": "Idempotency-Key was reused with a different request.", "code": "idempotency_key_reuse"} 409 Any POST or PUT that carries an Idempotency-Key (Retrying safely)
{"detail": "Departures are not permitted on 2026-07-17 for this pitch category.", "code": "closed_to_departure", "date": "2026-07-17"} 422 POST bookings/: a date rule of the pitch type refuses the stay, and nothing is created. code is the first rule the stay breaks, checked in this order: stop_sell, closed_to_arrival, min_stay, closed_to_departure. date is the closed night for stop_sell, the departure date for closed_to_departure and the arrival date otherwise. detail is an English sentence for a person
{
"check_out_date": [
{ "code": "invalid", "detail": "must be after check_in_date", "params": {} }
]
}

When the body you sent does not pass the checks, each key is the name of a field (non_field_errors when the problem is not one field), and each item carries a code, a sentence in detail and params. Codes in lower case come from the field checks (required for a field you left out, invalid), codes in capitals from the campground’s rules, such as MAX_OCCUPANCY for more guests than the pitch takes. Sent with 400 by POST bookings/, POST webhooks/ and POST pos/transactions/.

POST bookings/ also answers 409 in this shape when the booking is refused at the moment it is saved, for example with SITE_UNAVAILABLE when the pitch is taken, or ADMIN_BLOCK when the campground has blocked it. So a taken pitch can come back in shape 2 or in shape 3; handle both.

{ "channel": ["This channel label is reserved."] }

The same idea without codes: each item is a sentence. Sent with 400 by POST bookings/ when the channel (or source) label is one Grondia reserves for itself, and by PUT ari/restrictions/ when a row does not validate. That one answers with a list, one object per row you sent, empty for the rows that were fine:

[{}, { "date": ["Date has wrong format. Use one of these formats instead: YYYY-MM-DD."] }]

The API reference describes every error with one model, PublicError (detail, error and reason, all optional). Read it as a rough summary: no endpoint sends error today, and shapes 3 and 4 are not in it.

Every date in a query is checked before the API uses it. A value that is not a real date answers 400, with a sentence in detail that names the parameter:

Parameter Value Answer
check_in, check_out on sites/; start, end on ari/ and ari/restrictions/ Not a valid YYYY-MM-DD date 400, a sentence in detail
check_in_from, check_in_to on bookings/ Not a real date, such as tomorrow, 14.07.2026 or 2026-02-30 400, for example {"detail": "check_in_from must be an ISO date (YYYY-MM-DD)."}
updated_since on bookings/ Not a real date or date and time, such as yesterday, 2026-13-45 or 2026-07-14T25:00:00 400, {"detail": "updated_since must be an ISO date or datetime."}

The same value is refused every time, so do not retry it: build dates with a date library, send them as YYYY-MM-DD (for updated_since, send the updated_at values the API gave you), and fix the value.

The rules a stay’s dates have to meet, such as the longest stay and how far from today a booking may start, are in Dates, times and money.

For your logs, a line from any error body:

def describe_error(status: int, body) -> str:
"""One log line from an error body of any of the four shapes."""
if not isinstance(body, (dict, list)): # a 5xx body is HTML, not JSON
return f"{status} {body!r}"[:300]
if isinstance(body, dict) and isinstance(body.get("detail"), str): # shapes 1 and 2
more = ", ".join(f"{key}={value}" for key, value in body.items() if key != "detail")
return f"{status} {body['detail']}" + (f" ({more})" if more else "")
rows = body if isinstance(body, list) else [body] # shapes 3 and 4
problems = []
for number, row in enumerate(rows):
for field, items in (row or {}).items():
for item in items if isinstance(items, list) else [items]:
coded = isinstance(item, dict) and "code" in item
text = f"{item['code']}: {item.get('detail')}" if coded else str(item)
where = f"row {number}, {field}" if isinstance(body, list) else field
problems.append(f"{where}: {text}")
return f"{status} " + "; ".join(problems)

The sandbox key is real and read-only, so you can see shape 1 for yourself: a wrong key answers 401 with invalid_key, POST bookings/ answers 403 (the key has no bookings:write), GET bookings/999999999/ answers 404 with not_found, and ?page=99 answers 404 with "Invalid page.". The errors of writes (400, 409, 422) need a key that may write; ask Felix, felix@grondia.com, or Siro, siro@grondia.com.