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.
Check the connectors first
Section titled “Check the connectors first”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.
What the campground gets
Section titled “What the campground gets”- 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.
Scopes
Section titled “Scopes”| 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).
How the pieces fit
Section titled “How the pieces fit”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.
Grondia: Anna pays 65.00 online
Grondia to Your accounting product:
payment.settled. Booking, amount, provider,paid_atGrondia: The payment confirms the booking, and an invoice record is created
Grondia to Your accounting product:
invoice.issued. Number, total, statusOFFENGrondia: Reception records Anna's 40.00 in cash (Record payment)
Grondia to Your accounting product:
payment.recorded. Booking, amount, method,payment_dateGrondia: Anna checks out, the invoice is settled
Your accounting product to Grondia:
GET invoices/. On a scheduleGrondia 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.
The events
Section titled “The events”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.
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_atis 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 isnullwhen nothing recorded a time. Store it when the event comes: no API call returns it, because the API has no payment list.updated_atis the latest of when the payment was created, paid and refunded, here the same moment aspaid_at.X-Grondia-Timestampis the time of the delivery attempt, not of the payment. A payment recorded at the desk carries its day instead, inpayment_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. methodonpayment.recordedsays how the guest paid. It is one ofcash,coin,bank_transfer,card,twint,twint_qr,postcard,voucher,stripe_online,wallee_online,stripe_link,grondia_pay_linkorota_collected. Avoucheris a redeemed gift voucher andota_collectedis 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_onlinemay repeat an online payment you already received aspayment.settled; several of Grondia’s own reports drop such a record as a duplicate. Comparebooking_referenceandamountbefore booking it. The methodswallee_online,stripe_linkandgrondia_pay_linkare 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.
Which payments raise an event
Section titled “Which payments raise an event”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.
Read the invoices
Section titled “Read the invoices”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" } ]}curl "https://api.campone.ch/api/v1/public/invoices/?page_size=200" \ -H "Authorization: Bearer ck_a1b2cXXXXXXXXXXXXXXXXXXXXXXXX"# pip install requestsimport osfrom datetime import datetime, timezone
import requests
key = os.environ["GRONDIA_API_KEY"]headers = {"Authorization": f"Bearer {key}"}
def invoices_back_to(cutoff): """Every invoice issued at or after `cutoff`, newest first.""" url = "https://api.campone.ch/api/v1/public/invoices/?page_size=200" while url: response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() page = response.json() for invoice in page["results"]: # Python 3.11 or later reads the trailing Z if datetime.fromisoformat(invoice["issued_at"]) < cutoff: return yield invoice url = page["next"]
# What you stored: when your last run started, and the `issued_at` of every# invoice you still hold as OFFEN or PICKUP, or settled within the last few weeks.last_run = datetime(2026, 7, 14, tzinfo=timezone.utc)watched_issued_at = [datetime(2026, 1, 12, 9, 30, tzinfo=timezone.utc)]
# Read back to the older of the two, not to a fixed number of days.cutoff = min([last_run, *watched_issued_at])for invoice in invoices_back_to(cutoff): print(invoice["invoice_number"], invoice["status"], invoice["total_chf"])// invoices.mjs, run with: node invoices.mjsconst key = process.env.GRONDIA_API_KEY;
async function* invoicesBackTo(cutoff) { let url = "https://api.campone.ch/api/v1/public/invoices/?page_size=200"; while (url) { const response = await fetch(url, { headers: { Authorization: `Bearer ${key}` }, }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const page = await response.json(); for (const invoice of page.results) { if (new Date(invoice.issued_at) < cutoff) return; yield invoice; } url = page.next; }}
// What you stored: when your last run started, and the `issued_at` of every// invoice you still hold as OFFEN or PICKUP, or settled within the last few weeks.const lastRun = new Date("2026-07-14T00:00:00Z");const watchedIssuedAt = [new Date("2026-01-12T09:30:00Z")];
// Read back to the older of the two, not to a fixed number of days.const cutoff = new Date(Math.min(lastRun, ...watchedIssuedAt));for await (const invoice of invoicesBackTo(cutoff)) { console.log(invoice.invoice_number, invoice.status, invoice.total_chf);}How to read an invoice:
statusis one ofOFFEN,ERLEDIGT,STORNIERTorPICKUP: open, settled, cancelled, and printed and waiting to be collected at reception (it becomesERLEDIGTonce 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
OFFENinvoice until departure. An operator can re-open a settled invoice; a cancelled invoice stays cancelled. Do not readstatusas “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_idisnullfor an invoice that belongs to a till sale or to a seasonal-pitch contract, not to a booking.issued_atis when the invoice record was created, in UTC.invoice_numberis unique within a campground, not across campgrounds (INV-2026-0042can 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 onid, and do not parse it.total_chfis 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).
Keep the ledger in step
Section titled “Keep the ledger in step”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:
- Keep on a watch list, with its
issued_at, every invoice you hold asOFFENorPICKUP, and every invoice that settled (ERLEDIGT) within the last few weeks. - On each run, read back from the newest invoice to the older of your last run
and the oldest
issued_aton the watch list. - Update what you find. Drop a
STORNIERTinvoice 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 anERLEDIGTinvoice 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.
What you can and cannot get
Section titled “What you can and cannot get”| 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.
Go-live checklist
Section titled “Go-live checklist”- You asked whether the campground already uses one of Grondia’s accounting connectors.
- The administrator created a key with
invoices:read(andbookings:readif you look up bookings);webhooks:writeis agreed if you want events. - Your endpoint is HTTPS, verifies
X-Grondia-Signatureon the raw body, and answers2xxwithin 10 seconds before doing slow work. - You name
payment.recordedinevent_types, because it is opt-in. - Your handlers are idempotent: key an invoice on its
id, a payment on the event name andpayment.id. - You store the
paid_atof eachpayment.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.
In the sandbox
Section titled “In the sandbox”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.
Related
Section titled “Related”- Webhooks, Syncing changes, Rate limits and Dates, times and money: the guides this recipe leans on.
- Shop and restaurant tills: the sales a till posts appear nowhere in the invoice list.
- Guest messaging and CRM: the same events for a product that talks to guests.
- When your integration works, ask for the review: Partner program.