Dates, times and money
Three kinds of value come back from the API. Each has one rule that prevents most bugs.
| Kind | Looks like | What it means | The rule |
|---|---|---|---|
| Stay date | 2026-07-14 |
A day on the campground’s own calendar. No time, no zone. | Never convert it. Compare it with the campground’s today, not yours. |
| Timestamp | 2026-07-14T22:30:00Z |
A moment in time, in UTC. | Convert it to the campground’s time zone before you take a day from it. |
| Amount | "105.00" |
A decimal string with two places, in the campground’s currency. | Parse it with a decimal type, never a float. |
The rest of this page takes them in turn, starting with the time zone that gives stay dates their meaning. The Core concepts page has the short version.
The campground’s time zone
Section titled “The campground’s time zone”Every campground has a time zone, and it belongs to the campground, not to you
and not to Grondia’s servers. For a Swiss campground it is Europe/Zurich. A
campground in another country has the zone of that country, for example
Europe/Berlin for a German one: Grondia derives it from the campground’s
country unless the zone was set by hand. Where nothing is known, it is
Europe/Zurich.
The API does not return the zone, and neither do webhooks. Ask the campground that gave you the key, and store the zone next to the key.
Grondia’s servers run in UTC, but these calls work out “today” in the campground’s zone:
GET ari/starts the feed at the campground’s today when you send nostart, and moves an earlierstartup to it.- Creating a booking
is refused when the check-in is more than 30 days before the campground’s
today. The online booking rules run on the campground’s clock too, for
example a cutoff hour after which bookings for the next day close
(
booking_cutoff_hourin the ARI feed).
Use the same today when you compare or filter. If your servers run in UTC, their date is still yesterday’s for the first hour after midnight in a Swiss winter and for the first two hours in a Swiss summer. Work out the campground’s today from its zone:
from datetime import datetimefrom zoneinfo import ZoneInfo
today = datetime.now(ZoneInfo("Europe/Zurich")).date()// "en-CA" prints a date as YYYY-MM-DDconst today = new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Zurich",}).format(new Date());Clocks change twice a year in Switzerland. That needs no care for stay dates, which are calendar days, or for timestamps, which are UTC. Only converting between the two does: let a time zone library do it from the zone name, and never add a fixed offset.
Stay dates
Section titled “Stay dates”A stay date is YYYY-MM-DD, with no time and no zone. It means a day on the
campground’s own calendar, in requests and in responses alike. Do not convert
it: the check-in date 2026-07-14 is the 14th at the campground, wherever you
are.
| Where | Fields |
|---|---|
| A booking | check_in_date and check_out_date, in requests and responses |
| Booking list filters | check_in_from and check_in_to, applied to check_in_date |
| Site availability | check_in and check_out on GET sites/, which then lists only the sites free for the whole stay |
| ARI feed | start, end, days[].date and restrictions.max_booking_date |
| ARI restrictions | start and end, and date in each row you read or write |
| Booking events | check_in and check_out in the booking object |
| Desk payments | payment_date in payment.recorded, the date the campground recorded for the payment. It can be null. |
A few rules follow from that:
- A stay is counted in nights, and the departure day is not one. Anna
Müller’s stay on pitch A12 from
2026-07-14to2026-07-17is three nights.check_out_datemust be aftercheck_in_date, and a stay may not be longer than 365 nights. - The API has no times of day. A stay has a check-in date and a check-out date, and nothing for the hour of arrival or departure.
- Creating a booking refuses dates that are far from today. A check-in more than 30 days in the past is refused, and so is one further ahead than the campground’s booking horizon, which is three years unless the campground has set another.
Date windows in the ARI feed
Section titled “Date windows in the ARI feed”The two ARI calls read a window of dates, and their end parameters do not
mean the same thing.
| Call | start |
end |
|---|---|---|
GET ari/ |
The first night, inclusive. Default: the campground’s today. An earlier date is moved up to today. | Exclusive: the day after the last night. Default: start plus 90 days. |
GET ari/restrictions/ |
The first date, inclusive. Default: the campground’s today. | Inclusive: the last date. Default: start plus 90 days. |
Both calls cut a longer window down to 370 days after start without an error,
so read start and end in the answer to see the window you got. In the feed,
each entry of days is one night, and its date is the evening the night
begins.
Timestamps
Section titled “Timestamps”A timestamp is a moment, not a day. The API writes every timestamp in ISO 8601 and in UTC.
| Where | Fields |
|---|---|
| A booking | created_at, updated_at |
| A customer | date_joined |
| An invoice | issued_at, the time the invoice was created |
| A till transaction | created_at |
booking.* events, guest.checked_in and guest.checked_out |
booking.updated_at |
pitch.status_changed |
pitch.updated_at |
guest.created |
guest.created_at |
invoice.issued |
invoice.issued_at, the time the invoice was created |
payment.settled |
payment.paid_at, when Grondia recorded the payment as paid (null when nothing recorded a time), and payment.updated_at, the latest of when the payment was created, paid and refunded |
payment.recorded |
No timestamp: it carries the date payment_date (see Stay dates) |
The X-Grondia-Timestamp header of a webhook |
Unix time in seconds, such as 1784068200. It is the time of that delivery attempt, and it changes on every retry. |
payment.paid_at is the only place the API gives the time of an online
payment: no call lists payments. It is when Grondia recorded the payment as
paid, not when the money arrived. Grondia stamps it as it processes the payment
provider’s confirmation, so a payment it learns of later, for example in a
reconcile run after a missed confirmation, carries the later time. null means
nothing recorded a time. X-Grondia-Timestamp is the time of the delivery
attempt, not of the payment.
Responses and webhook bodies write the same moment slightly differently:
| Where | Written as |
|---|---|
| Responses to your calls | 2026-07-14T22:30:00Z, or 2026-07-14T22:30:00.104837Z when the moment has fractions of a second |
| Webhook bodies | 2026-07-14T22:30:00+00:00, or 2026-07-14T22:30:00.104837+00:00 |
Real values nearly always carry fractions of a second, so expect the longer
form. Both forms mean UTC. Parse them with an ISO 8601 parser rather than
comparing them as strings, because the same moment is two different strings. Python before 3.11
cannot read the trailing Z: replace it with +00:00 first.
Take the day in the campground’s zone
Section titled “Take the day in the campground’s zone”Anna Müller books pitch A12 at 00:30 on 1 July, Swiss summer time. The
booking’s created_at reads 2026-06-30T22:30:12.481903Z. Cutting the first ten
characters gives 30 June, a day early. Convert to the campground’s zone first,
and only then take the date:
from datetime import datetimefrom zoneinfo import ZoneInfo
created_at = "2026-06-30T22:30:12.481903Z"moment = datetime.fromisoformat(created_at.replace("Z", "+00:00"))print(moment.astimezone(ZoneInfo("Europe/Zurich")).date()) # 2026-07-01const createdAt = "2026-06-30T22:30:12.481903Z";const day = new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Zurich",}).format(new Date(createdAt));console.log(day); // 2026-07-01Asking for changes with updated_since
Section titled “Asking for changes with updated_since”GET bookings/
takes an updated_since parameter that lists the bookings whose updated_at is
at or after a moment, oldest first. It is the only place where Grondia acts on a timestamp
you send, and it reads the value like this:
| You send | Grondia reads it as |
|---|---|
2026-07-14T22:30:00Z |
That moment. |
2026-07-15T00:30:00%2B02:00 |
That moment, with the plus sign written as %2B. |
2026-07-15T00:30:00 |
00:30 on the campground’s clock. A value with no offset is read in the campground’s zone, not in UTC. |
2026-07-15 |
The start of that day in the campground’s zone. |
2026-07-14T22:30:00+00:00, with the plus sign as it is |
Refused with 400. A URL turns a plus sign into a space. |
Let your HTTP client encode the parameter, or send Z. With curl,
--data-urlencode does the encoding:
curl -G https://api.campone.ch/api/v1/public/bookings/ \ -H "Authorization: Bearer ck_a1b2cXXXXXXXXXXXXXXXXXXXXXXXX" \ --data-urlencode "updated_since=2026-07-14T22:30:00+00:00"To follow changes, remember the newest updated_at you have seen and send it
unchanged next time: it ends in Z, so it needs no encoding. The filter means
“at or after”, so the booking at the boundary comes back once more. Recognise it
by its id and updated_at and skip it.
Till transactions have no time of their own
Section titled “Till transactions have no time of their own”A till transaction accepts an occurred_at timestamp, but Grondia does not
store it. The transaction is dated by the moment Grondia receives it, which is
its created_at. A sale that your till posts late is therefore dated late.
Every amount is a decimal string with two decimal places, such as "105.00".
It is a string on purpose: a JSON number becomes a binary float in most
languages, and a float cannot hold 0.10 exactly.
| Where | Fields |
|---|---|
| Bookings | total_price, such as "139.50" |
| Sites | base_price_per_night, such as "42.00". This is the pitch’s base price, not what a stay costs: use a booking’s total_price or the ARI feed for that. |
| Invoices | total_chf, such as "139.50" |
| ARI feed | days[].price, such as "42.00". This is the nightly price of the pitch alone. Person fees, Kurtaxe and extras are not in it. |
| Till products | unit_price, such as "4.50", and tax_rate, a percentage such as "8.10" |
| Till transactions | total_amount, items[].unit_price and items[].line_total. items[].quantity has three places, such as "1.500". |
| Cancel answer | The amounts in refund when money is refunded, such as cancellation_fee and refund_amount |
| Events | booking.total in booking events, payment.amount in payment.settled and payment.recorded, and invoice.total in invoice.issued, each with a currency beside it. |
A booking’s total_price includes the Kurtaxe where the campground charges one
(see the glossary). You cannot set a price: total_price
is read-only, and Grondia prices the booking you create from the campground’s
own prices.
Read an amount with a decimal type. Python has one built in. JavaScript has
none, so keep the string, or count in whole cents with BigInt:
from decimal import Decimal
total = Decimal(booking["total_price"]) # Decimal("139.50")print(Decimal("0.10") + Decimal("0.20")) # 0.30print(0.10 + 0.20) # 0.30000000000000004const toCents = (amount) => { if (!/^-?\d+(\.\d{1,2})?$/.test(amount)) throw new Error(`Not an amount: ${amount}`); const [whole, fraction = ""] = amount.split("."); return BigInt(whole + fraction.padEnd(2, "0"));};
const fromCents = (cents) => { const sign = cents < 0n ? "-" : ""; const digits = (cents < 0n ? -cents : cents).toString().padStart(3, "0"); return `${sign}${digits.slice(0, -2)}.${digits.slice(-2)}`;};
console.log(fromCents(toCents("139.50") + toCents("0.05"))); // 139.55console.log(0.1 + 0.2); // 0.30000000000000004Which currency
Section titled “Which currency”A campground has one currency for its prices: CHF unless it is set up with another, written as an ISO 4217 code. Only two places name it:
- the
currencyfield of the ARI feed, and - the
currencyfield of a webhook payload, which names the currency of that booking, payment or invoice.
The responses of the other calls carry amounts with no currency beside them.
Read the currency once with GET ari/
(the sites:read scope is enough) and store it with the key, with the time
zone.
An invoice’s total_chf is named for the Swiss franc, but it holds the
invoice’s total as Grondia stores it, in the invoice’s own currency, with no
conversion.
Rounding
Section titled “Rounding”Grondia rounds amounts to two decimal places, and invoice totals half up. A
campground can also set a rate to round its unit prices in steps of 0.05, which
in francs is 5 Rappen, the smallest Swiss coin, so that prices from that rate
are multiples of 0.05, such as 38.05 and never 38.03. The rate decides it;
the API only shows the result.
Take the amounts as you receive them:
- Do not round them again, and do not recompute a total from its parts.
- Do not assume that every amount is a multiple of 0.05. Only prices from a rate with that setting are.
Amounts you send
Section titled “Amounts you send”You never send a price with a booking. The one call that takes amounts is
POST pos/transactions/,
and it checks them:
- Amounts cannot be negative.
- Each
line_totalmust be within 0.01 ofquantitytimesunit_price, or the call answers400. Grondia stores the product of the two, to two decimals. total_amountmust be within 0.01 of the sum of the line totals, or the call answers400. Grondia stores the sum.
Send line totals that already add up, and you will not see either error.
Related
Section titled “Related”- Core concepts: the short version of this page, and the life of a booking.
- Glossary: Kurtaxe, Saison and the other German words you will meet in amounts and dates.
- Personal data: what the API redacts in bookings and customers.
- Webhooks: the headers and the body of a delivery, which write their timestamps differently.
- Syncing changes:
updated_sincein a sweep. - Errors: what a date in a query that is not a real date answers.
- Channel managers, Shop and restaurant tills and Accounting and ERP: the recipes that work with these dates and amounts.