Skip to content

Accounting and ERP

This recipe is for accounting and ERP software that wants a campground’s invoices and payments without retyping them. Know the size of it before you start: the API gives you the money facts (which invoices exist, their number, total and status, and which payments arrived for which booking). It does not give you the documents (lines, VAT by rate, due date, PDF) or a debtor’s address. What you can and cannot get has the full list.

Grondia already ships connectors that push invoices into the campground’s own accounting product, for example bexio, lexoffice and sevDesk. They are in the admin under App Store, section “Accounting & ERP”, and Grondia maintains them. A campground that uses one of them may not need your integration, so ask which it has. If your product is not among them, this recipe is the way in. To talk about where your product fits next to them, write to Felix, felix@grondia.com, or to Siro, siro@grondia.com.

  • Its invoices in your ledger as they are created, and the online payments and the payments recorded at the desk, without a manual export. Some desk payments raise no event: see Which payments raise an event.
  • One key per campground, with only the scopes you use, which the administrator can revoke at any time.
Scope Opens Who can create the key
invoices:read GET invoices/ The campground’s administrator
bookings:read GET bookings/{id}/, to look up the booking behind an invoice or a payment The campground’s administrator
webhooks:write The webhook endpoints, to be told about invoices and payments instead of polling On request: ask Felix or Siro

A key without webhooks:write still works: you poll GET invoices/. Guest names and email on a booking come back redacted unless the key also carries bookings:pii, which is decided case by case (Credentials).

Anna Müller books pitch A12 at Camping Beispielsee for 14 to 17 July: three nights at 35.00, an invoice of 105.00. She pays 65.00 online when she books and the remaining 40.00 in cash at reception, where the desk enters it with the admin’s Record payment action.

  1. Grondia: Anna pays 65.00 online

  2. Grondia to Your accounting product: payment.settled. Booking, amount, provider, paid_at

  3. Grondia: The payment confirms the booking, and an invoice record is created

  4. Grondia to Your accounting product: invoice.issued. Number, total, status OFFEN

  5. Grondia: Reception records Anna's 40.00 in cash (Record payment)

  6. Grondia to Your accounting product: payment.recorded. Booking, amount, method, payment_date

  7. Grondia: Anna checks out, the invoice is settled

  8. Your accounting product to Grondia: GET invoices/. On a schedule

  9. Grondia to Your accounting product: Status is now ERLEDIGT

Had reception used the Collect payment action instead, nothing would have been sent for the 40.00 (below).

The last step is the part people miss. Grondia sends no event when an invoice changes, including when it is settled or cancelled. A ledger that only listens to events drifts. Plan a scheduled re-read of recent invoices from the start.

Register one endpoint with POST webhooks/, naming the events you want. payment.recorded is opt-in: an endpoint that leaves event_types empty (“every event”) does not receive it.

Terminal window
curl https://api.campone.ch/api/v1/public/webhooks/ \
-H "Authorization: Bearer ck_a1b2cXXXXXXXXXXXXXXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-d '{
"url": "https://ledger.example.com/hooks/grondia",
"event_types": ["invoice.issued", "payment.settled", "payment.recorded"]
}'

The answer holds the signing secret once. Every delivery carries X-Grondia-Event, X-Grondia-Delivery, X-Grondia-Timestamp and X-Grondia-Signature (HMAC-SHA256 over the timestamp, a dot and the raw body, written sha256=<hex>); the webhook reference describes them, and the Webhooks guide shows the signature check in code. Answer any 2xx within 10 seconds. Anything else is retried up to five more times, 30, 60, 120, 240 and 480 seconds apart (Retries). A retry keeps its X-Grondia-Delivery.

Event Sent when Reference
invoice.issued An invoice record is created, which for a booking happens when the booking is confirmed (a booking that is still pending has no invoice). It is not sent again when the invoice is paid, changed, settled or cancelled invoice.issued
payment.settled An online payment succeeds payment.settled
payment.recorded (opt-in) A payment record is created on a booking: a new, positive receipt that does not mirror an online refund and was not carried over from the campground’s previous system. The admin’s Record payment action writes one, and so do several other paths (the list below). A payment taken with Collect payment is not one of these payment.recorded

A body is built when the delivery is made, so it shows the record as it is then, not as it was when the event happened. It holds no guest data. These are the bodies for Anna’s stay:

{
"event": "invoice.issued",
"tenant": "beispielsee",
"invoice": {
"id": 90417,
"number": "INV-2026-0042",
"booking_id": 51277,
"booking_reference": "7F3A9C21DE",
"total": "105.00",
"currency": "CHF",
"status": "OFFEN",
"issued_at": "2026-07-01T08:30:00.123456+00:00"
}
}
{
"event": "payment.settled",
"tenant": "beispielsee",
"payment": {
"id": 3302,
"booking_id": 51277,
"booking_reference": "7F3A9C21DE",
"external_reference": "",
"amount": "65.00",
"currency": "CHF",
"status": "succeeded",
"provider": "stripe",
"paid_at": "2026-07-01T08:29:58.402117+00:00",
"updated_at": "2026-07-01T08:29:58.402117+00:00"
}
}
{
"event": "payment.recorded",
"tenant": "beispielsee",
"payment": {
"id": 8120,
"kind": "desk",
"booking_id": 51277,
"booking_reference": "7F3A9C21DE",
"amount": "40.00",
"currency": "CHF",
"method": "cash",
"payment_date": "2026-07-14"
}
}

How to read the payment events:

  • paid_at is when Grondia recorded the payment as paid, in UTC, not when the money arrived. A payment Grondia learns of later, for example when a reconcile run finds one whose confirmation from the payment provider was missed, carries the later time. It is null when nothing recorded a time. Store it when the event comes: no API call returns it, because the API has no payment list. updated_at is the latest of when the payment was created, paid and refunded, here the same moment as paid_at. X-Grondia-Timestamp is the time of the delivery attempt, not of the payment. A payment recorded at the desk carries its day instead, in payment_date.
  • Match payments to invoices by booking. A payment names the booking (booking_id, booking_reference), not an invoice, and a booking can have more than one invoice.
  • The two payment events have separate id spaces. An online payment 3302 and a desk payment 3302 can both exist. Key a payment on the event name and payment.id.
  • method on payment.recorded says how the guest paid. It is one of cash, coin, bank_transfer, card, twint, twint_qr, postcard, voucher, stripe_online, wallee_online, stripe_link, grondia_pay_link or ota_collected. A voucher is a redeemed gift voucher and ota_collected is money the guest paid to an online travel agency, which pays the campground later; neither is cash in the till.
  • Online money can look like two payments. A desk record with method stripe_online may repeat an online payment you already received as payment.settled; several of Grondia’s own reports drop such a record as a duplicate. Compare booking_reference and amount before booking it. The methods wallee_online, stripe_link and grondia_pay_link are money on top of the booking’s online payment, not repeats.
  • There is no event for a refund, or for a desk payment that is cancelled afterwards, and the API has no payment list to read them from.

The admin has two desk actions for taking money on a booking, and only one of them, Record payment, raises payment.recorded. Other actions of the desk raise it too, and so do a few records that Grondia writes without anyone at the desk:

How the money reached the campground Event
The guest pays online payment.settled
The desk uses Record payment on a booking payment.recorded
The guest pays the open remainder online through a pay link, on top of the first payment payment.recorded, with method wallee_online, stripe_link or grondia_pay_link
Other actions of the desk that record a payment: the guest-level collective payment (Sammelzahlung) that settles several open invoices of one guest, and a deposit the operator books as received by bank transfer (the admin’s deposit-received action) payment.recorded, with the method of that record
Records Grondia writes without anyone at the desk: a stored-card charge on arrival or for a no-show or late-cancellation fee, a payment an online travel agency collected, or the automatic balance charge of a split payment payment.recorded, with the method of that record
The desk uses Collect payment on a booking (card terminal, cash, Postcard, TWINT QR or a bank transfer; a single booking also takes a voucher), or on several bookings at once None. The payment is stored as a till transaction on the booking
A till sale charged to the guest’s pitch None. It becomes lines on the invoice
A sale posted by a till through the till recipe None
A refund, or a desk payment cancelled afterwards None

Grondia counts a payment taken with Collect payment as paid, exactly like the others, but the API does not announce it and has no list to read it from. So the payment events do not add up to what a booking has paid, and there is no complete way through the API to know that. Reconcile payments against what the campground itself reports (its bank statements and its own payment reports and daily close). An invoice that has reached ERLEDIGT is a hint that Grondia found the booking covered when it settled the invoice, not proof: an operator can also set that status by hand. If you need a payment list, write to Felix, felix@grondia.com, or to Siro, siro@grondia.com.

GET invoices/ lists the campground’s invoices, newest first, in pages of 50 (up to 200 with ?page_size=200). It takes no filter. Each invoice is six fields (PublicInvoice):

{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 90417,
"invoice_number": "INV-2026-0042",
"booking_id": 51277,
"total_chf": "105.00",
"status": "OFFEN",
"issued_at": "2026-07-01T08:30:00.123456Z"
}
]
}
Terminal window
curl "https://api.campone.ch/api/v1/public/invoices/?page_size=200" \
-H "Authorization: Bearer ck_a1b2cXXXXXXXXXXXXXXXXXXXXXXXX"

How to read an invoice:

  • status is one of OFFEN, ERLEDIGT, STORNIERT or PICKUP: open, settled, cancelled, and printed and waiting to be collected at reception (it becomes ERLEDIGT once collected; it says nothing about payment). A cancelled invoice stays in the list.
  • Settled means checked out, not paid. Grondia settles an invoice when the stay is completed and payments cover the booking. A guest who paid in full in advance still has an OFFEN invoice until departure. An operator can re-open a settled invoice; a cancelled invoice stays cancelled. Do not read status as “paid”, and do not add up payment events to find out: some payments raise no event (see Which payments raise an event).
  • The total can still change. Extras, Kurtaxe and till sales added during the stay change the amount, and adding a charge re-opens a settled invoice. Settlement uses the final amount. Book your entry when the status is ERLEDIGT, or re-read the total on every run.
  • booking_id is null for an invoice that belongs to a till sale or to a seasonal-pitch contract, not to a booking.
  • issued_at is when the invoice record was created, in UTC.
  • invoice_number is unique within a campground, not across campgrounds (INV-2026-0042 can exist at two of them), and its format is not fixed: invoices imported from a previous system keep their old numbers. Key your ledger on the campground and the number, or on id, and do not parse it.
  • total_chf is the total in the campground’s currency, whatever that is. The list has no currency field; the event does.
  • Money is a decimal string. Parse it with a decimal type (Money, and which currency).

Page from the newest invoice back and compare what you find with what you stored. Do not use a fixed window such as 90 days. An invoice is created when the booking is confirmed and settles at departure, so a stay confirmed in January for August is still OFFEN seven months after its issued_at, and a seasonal-pitch contract invoice can be written seasons ahead. A short window never sees those settle. Instead:

  1. Keep on a watch list, with its issued_at, every invoice you hold as OFFEN or PICKUP, and every invoice that settled (ERLEDIGT) within the last few weeks.
  2. On each run, read back from the newest invoice to the older of your last run and the oldest issued_at on the watch list.
  3. Update what you find. Drop a STORNIERT invoice at once: a cancelled invoice is final, the admin writes a new invoice instead, and your read-back since the last run finds that one. Drop an ERLEDIGT invoice after a few weeks, because a late charge re-opens a settled invoice.

The list has no filter and no lookup by id, so an old open invoice costs the pages between it and today: 200 invoices a page, counted against the key’s hourly request limit (1000 by default), which every other call of the key shares. A nightly run is typical. The first import pages through everything once. Syncing changes covers paging and the updated_since sweep, which exists for bookings but not for invoices, and Rate limits says how the hourly budget counts.

You can You cannot, today
List invoices: id, number, booking, total, status, creation time Invoice lines, VAT by rate, due date, a PDF
Be told when an invoice record is created Be told when it is paid, changed, settled or cancelled
Be told of online payments, with the time Grondia recorded them as paid, and of payments recorded at the desk, with their day, by booking Be told of payments taken with Collect payment, of refunds, or of a desk payment cancelled afterwards. There is no payment list either
Read the booking behind an invoice A debtor’s postal address. Name and email only with bookings:pii, and a booking carries no address either way

Tell Felix or Siro which of these you need; they decide what to build, and you should not work around a gap that may be closed.

A sale posted by a till through the till recipe is stored as a till transaction. That endpoint creates no invoice and raises no payment event, so it appears nowhere in GET invoices/ or in the events above.

  • You asked whether the campground already uses one of Grondia’s accounting connectors.
  • The administrator created a key with invoices:read (and bookings:read if you look up bookings); webhooks:write is agreed if you want events.
  • Your endpoint is HTTPS, verifies X-Grondia-Signature on the raw body, and answers 2xx within 10 seconds before doing slow work.
  • You name payment.recorded in event_types, because it is opt-in.
  • Your handlers are idempotent: key an invoice on its id, a payment on the event name and payment.id.
  • You store the paid_at of each payment.settled, because no API call returns it.
  • A scheduled re-read catches settled and cancelled invoices, and reaches back to your oldest open invoice, not a fixed number of days.
  • You do not treat the payment events as the complete list of payments, and you reconcile against the campground’s own reports.
  • You parse money as decimals and know the campground’s currency.
  • You agreed with the campground, and with Felix or Siro, how to get the data the API does not carry.

The sandbox key carries invoices:read, so it can call GET invoices/. It has no webhooks:write, so you cannot register an endpoint or receive the three events there. For a test campground with webhooks enabled, write to Felix, felix@grondia.com, or to Siro, siro@grondia.com.