Skip to content

Guest messaging and CRM

A guest messaging product or a CRM follows a guest through a stay: a booking arrives, a message goes out before arrival, another after departure, and the history stays in one place. This recipe shows what the API gives you for that.

Read the next section first, because it decides what you can build: by default the API tells you when a stay happens and what was booked, and it does not tell you who the guest is or how to reach them.

  • Messages that follow the stay on their own: before arrival, at check-in, after departure, and stopped when a booking is cancelled.
  • A CRM that learns about new, changed and cancelled bookings as they happen.

Grondia sends some guest emails itself, such as the booking confirmation and the cancellation. Agree with the campground what your product sends on top of them, so a guest does not get the same news twice.

Guest personal data is redacted by default on both lists. It is available on request, decided case by case with Felix (Credentials; the Personal data guide lists what each call returns).

You read By default With the personal-data scope
Bookings Dates, pitch or pitch type, number of guests, status, total and references. guest_first_name and guest_last_name come back empty and guest_email as "[redacted]" bookings:pii fills the two names and the email. A booking has no phone number and no postal address either way
Customers id and date_joined. first_name and last_name come back empty, email as "[redacted]" and phone_number empty customers:pii fills the two names, the email and the phone number

Neither personal-data scope can be created in the admin. Without one, your product cannot reach a guest: there is no address to send to. Do not import "[redacted]" as an email address, and do not treat an empty name as a missing guest.

If you need contact data, write to Felix, felix@grondia.com, or to Siro, siro@grondia.com, say which campground and which fields, and why. Webhook bodies never carry personal data, with or without the scopes.

Scope Opens Who can create the key
bookings:read GET bookings/, GET bookings/{id}/ The campground’s administrator
customers:read GET customers/ The campground’s administrator
webhooks:write The webhook endpoints, to receive events instead of polling On request: ask Felix or Siro
bookings:pii, customers:pii Lifts the redaction above On request, case by case

A key without webhooks:write still works: poll GET bookings/?updated_since=<timestamp>, which returns bookings changed since then, oldest change first (Syncing changes).

Anna Müller books pitch A12 at Camping Beispielsee for 14 to 17 July.

  1. Grondia: Anna Müller books pitch A12

  2. Grondia to Your messaging or CRM product: booking.booked. Ids, dates, pitch type

  3. Your messaging or CRM product to Grondia: GET bookings/{id}/

  4. Grondia to Your messaging or CRM product: The booking, guest fields redacted unless bookings:pii

  5. Your messaging or CRM product: Schedule the pre-arrival message for the day before

  6. Grondia to Your messaging or CRM product: booking.changed. Anna moves her arrival

  7. Your messaging or CRM product to Grondia: GET bookings/{id}/

  8. Your messaging or CRM product: Reschedule

  9. Grondia to Your messaging or CRM product: guest.checked_in

  10. Your messaging or CRM product: Send the welcome message

  11. Grondia to Your messaging or CRM product: guest.checked_out

  12. Your messaging or CRM product: Send the thank-you message

The webhook says that something happened and names the booking. The booking itself, and who the guest is, you fetch.

Register one endpoint with POST webhooks/. booking.booked, booking.changed and guest.created are opt-in: an endpoint that leaves event_types empty does not receive them, so name them.

Terminal window
curl https://api.campone.ch/api/v1/public/webhooks/ \
-H "Authorization: Bearer ck_a1b2cXXXXXXXXXXXXXXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-d '{
"url": "https://crm.example.com/hooks/grondia",
"event_types": ["booking.booked", "booking.changed", "booking.cancelled",
"guest.checked_in", "guest.checked_out"]
}'

The answer holds the signing secret once. Verify X-Grondia-Signature on every delivery and answer 2xx within 10 seconds; the webhook reference describes the headers, and a failed delivery is retried up to five more times. The Webhooks guide shows the signature check in code and the delivery log.

Event Opt-in Use it to
booking.booked Yes A new booking, whatever its first status. Create the contact or deal, start the sequence
booking.changed Yes Dates, guests, price or status changed, or the booking was confirmed. Reschedule
booking.cancelled No Stop the sequence
guest.checked_in No The welcome message
guest.checked_out No The thank-you message or a review request
guest.created Yes A guest profile now exists. Carries an id and a reference only; read the section below before you rely on it

The first five share one body, which holds dates and a pitch type but no guest and no pitch number:

{
"event": "booking.booked",
"tenant": "beispielsee",
"booking": {
"id": 51277,
"external_reference": "",
"booking_reference": "7F3A9C21DE",
"status": "confirmed",
"check_in": "2026-07-14",
"check_out": "2026-07-17",
"site_type_code": "motorhome",
"total": "105.00",
"currency": "CHF",
"updated_at": "2026-07-01T08:30:01.204117+00:00"
}
}

The body is built when the delivery is made, so it shows the booking as it is then. check_in and check_out are the campground’s local calendar dates: schedule your messages in the campground’s time zone (Europe/Zurich for a Swiss campground), not in yours.

GET bookings/{id}/ answers the event. With the default scopes, Anna’s booking looks like this:

{
"id": 51277,
"booking_reference": "7F3A9C21DE",
"guest_first_name": "",
"guest_last_name": "",
"guest_email": "[redacted]",
"site_id": 9012,
"site_number": "A12",
"site_category": "Motorhome pitch",
"check_in_date": "2026-07-14",
"check_out_date": "2026-07-17",
"num_adults": 2,
"num_youth": 0,
"num_children": 0,
"status": "confirmed",
"total_price": "105.00",
"created_at": "2026-07-01T08:30:00.918342Z",
"updated_at": "2026-07-01T08:30:01.204117Z",
"external_reference": "",
"channel": ""
}

The customer list holds guests who have an account at the campground, newest first. With the default scopes Anna appears without her name and her contact details:

{
"id": 1042,
"email": "[redacted]",
"first_name": "",
"last_name": "",
"phone_number": "",
"date_joined": "2026-06-20T14:03:22.417051Z"
}

Both lists come in pages of 50 (up to 200 with ?page_size=200).

Situation What happens What to do
You want to message a guest The booking has no usable email by default Agree bookings:pii with Felix, or get the contact data from the campground
A booking is pending, abandoned or cancelled Only confirmed is firm. A pending booking may still be released, and an abandoned one never was firm (life of a booking) Read status before you send anything
The campground does not record check-in or check-out No guest.checked_in or guest.checked_out Fall back to check_in and check_out
The same guest books twice Two bookings, and a booking carries no guest id With bookings:pii, match on email. Without it you cannot tell
A guest booked without an account They are in bookings, not in the customer list Treat bookings as your source for stays
guest.created arrives See below Do not rely on it to build a contact
You need a consent flag, or the guest’s language Neither can be read through the API. When you create a booking you can send language (Create a booking) Ask the campground what it may send, and in which language
Your endpoint is down The delivery is retried five more times, then given up Catch up with GET bookings/?updated_since= (Syncing changes)

The event carries a guest id (a UUID), a guest_number such as GST-000123 and created_at, and nothing else. You cannot do much with it, for three reasons:

  • No endpoint of the API returns a guest by id or guest_number, and no booking or customer response carries either. You cannot join the event to a booking or a customer.
  • The customer list is a different thing: guests with an account, with integer ids. A guest profile also exists for guests without one, such as walk-ins.
  • A replay of a guest.created delivery is accepted (202) but sends nothing, because the delivery log cannot store a guest id. Do not plan on replay for it.

Use it as a signal that a new guest exists at the campground, or leave it out and derive your contacts from bookings. If the campground wants every new guest in a spreadsheet or another tool, the Zapier app fetches the full guest for its “New guest” trigger.

The API reads guests and bookings. There is no endpoint to create or change a guest on its own, to add a note to a booking, or to send a message on Grondia’s behalf. Writing a guest or a note back to Grondia is possible through the Zapier app only.

  • You agreed with Felix or Siro which personal data you get, and from whom, before you built the sending.
  • Nothing in your product treats "[redacted]" as an email address or an empty name as a missing guest.
  • booking.booked, booking.changed and guest.created (if you use it) are in event_types, because they are opt-in.
  • Your handler fetches the booking, and sends only when its status is confirmed.
  • A change or cancellation moves or stops a scheduled message.
  • Messages go out in the campground’s time zone, and you agreed with the campground what Grondia sends itself.
  • Your handlers are idempotent: key a booking on its id.
  • A sweep on updated_since catches what a downtime missed.

When the product works, ask for the review: Partner program.