Install & activate
CalNative Booking is a standard WordPress plugin. It needs WordPress 6.0+ and PHP 8.0+, plus a Google account whose calendar you want to take bookings into.
Install the plugin
- After purchase you receive a license and a download link for calnative-booking.zip.
- In WordPress go to Plugins → Add New → Upload Plugin, choose the zip, then Install Now and Activate.
- On activation the plugin creates its bookings table and adds a CalNative Booking menu to the admin sidebar.
- You will be prompted to connect your account to verify the license — click Allow & Continue.
Updating later
With an active license, updates appear under Plugins like any other plugin. One click and you are current.
Never delete the plugin to reinstall it. Deleting removes every setting and your entire booking history. To install a zip over an existing copy, use Plugins → Add New → Upload Plugin and choose Replace current with uploaded when WordPress asks.
License & renewals
CalNative Booking is sold as an annual subscription covering one website. Licensing and billing are handled by Freemius.
Activating
Activation happens when you connect your account after installing. You can check the status any time under CalNative Booking → Account.
What happens if the license lapses
The plugin needs an active license to take bookings. If it expires or is deactivated, the booking widget stops serving available times and stops accepting submissions — this applies to the version already installed, not just to future updates.
Your data is safe. Existing bookings stay in your WordPress database and in Google Calendar, and you can still view, search and cancel them in the admin. Renewing restores the widget immediately, with nothing to reinstall or reconfigure.
Moving to a new site
Deactivate the license on the old site first — from CalNative Booking → Account — so the activation slot is released.
Quick setup checklist
The full setup takes about 20 minutes, most of it inside Google Cloud Console. Work through these in order:
- Create a Google service account and download its JSON key.
- Share your calendar with the service account address.
- Paste the Calendar ID, Host Email and JSON into Settings → General.
- Set your working hours, slot duration and buffer under Settings → Booking.
- Place the shortcode
[calnative_booking] on a page.
- Make a test booking and check it lands in your calendar and inbox.
Everything after step 6 — appearance, video meetings, email wording — is optional polish. The widget works with sensible defaults out of the box.
Create a service account
CalNative connects to Google using a service account — a robot identity with its own email address. Unlike an OAuth login it never expires, never needs reconnecting, and no visitor is ever sent to a Google consent screen.
Steps
- Open Google Cloud Console and create a project, or pick an existing one.
- Go to APIs & Services → Library, search for Google Calendar API, and enable it.
- Go to APIs & Services → Credentials → Create Credentials → Service account.
- Give it a name such as
calnative-booking and click Done.
- Open the service account, go to the Keys tab, then Add Key → Create new key → JSON. The file downloads automatically.
Keep that JSON file somewhere safe — it is the credential. Anyone holding it can access the calendars you share with it.
The service account address looks like name@project-id.iam.gserviceaccount.com. You need it for the next step.
Share your calendar
A service account starts with access to nothing. You have to share your calendar with it explicitly.
- Open Google Calendar.
- Hover the calendar you want to use, click the three-dot menu, then Settings and sharing.
- Under Share with specific people, click Add people and paste the service account address.
- Set the permission to Make changes to events and click Send.
- Scroll down to Integrate calendar and copy the Calendar ID.
The permission level matters. See all event details is not enough — the plugin needs to create events, not just read them.
Using your main Gmail calendar? The Calendar ID is simply your Gmail address.
Connect the plugin
Go to CalNative Booking → Settings → General and fill in three fields.
Calendar ID
From the sharing screen — either something@group.calendar.google.com or your Gmail address.
Host Email
The Google account that owns the calendar. This receives booking notifications and is queried for busy times, so it must be the real account address.
Don't change Host Email to fix an email-sending problem. It is used as a calendar identity, so changing it stops busy times being detected and bookings start landing on times you are already busy. Use the
Sender Address setting instead.
Service Account JSON
Open the downloaded .json file in a text editor, select everything, and paste it into the field. Paste the whole file including the outer braces.
Save, then move on to Booking settings.
How availability is calculated
Every time a visitor opens the widget, the plugin asks Google for your free/busy data in real time. Nothing is duplicated into WordPress, so your calendar is always the single source of truth.
What blocks a slot
- Any busy event on the Calendar ID you configured.
- Any busy event on the Host Email calendar.
- Days outside your working days, or outside working hours.
- Dates you have blocked manually.
- Slots inside your minimum-notice window, or beyond the schedule limit.
- Slots already booked through the widget and held by the database lock.
Because both calendars are checked, an event in your personal calendar can block a slot even if it is not on the booking calendar. That is usually what you want — you cannot be in two places at once.
Caching
Availability responses are cached for 30 seconds to keep the widget quick under traffic. A booking made elsewhere may take up to half a minute to disappear from the widget.
Working hours & days
Under Settings → Booking you decide when you are bookable.
Working days
Tick the days of the week that accept bookings. Unticked days are grayed out in the calendar and cannot be selected.
Start and end time
The daily window slots are generated inside. With a 09:00–17:00 window and 30-minute slots, the last slot starts at 16:30 — a slot is only offered if it finishes within the window.
Week start
Choose whether the calendar begins on Sunday or Monday, to match local convention.
Timezone
Set the timezone your working hours are expressed in. If you leave it empty the plugin falls back to your WordPress site timezone. Visitors always see times converted to their own timezone, with a label under the calendar so there is no ambiguity.
Duration, buffer & notice
Slot duration
How long an appointment lasts. This is also the length of the event created in Google Calendar.
Buffer time
A gap added after each appointment before the next slot can start. With a 30-minute duration and a 10-minute buffer, slots begin at 09:00, 09:40, 10:20 and so on.
Minimum notice
Stops last-minute bookings. Set to 2 hours and nothing inside the next two hours is offered — useful if you need time to prepare or travel.
Schedule limit
How many months ahead visitors can book. The default is 3. Lower it if your availability changes often; raise it for work booked far in advance.
Blocked dates & holidays
Under Settings → Booking, the blocked dates picker lets you close individual days — holidays, leave, or anything else — without touching your working hours.
Blocked dates are absolute. They override working days and cannot be booked even if the calendar is otherwise free.
For a one-off busy afternoon, an all-day or timed event in Google Calendar is quicker — the widget picks it up automatically. Blocked dates are best for planned closures you want recorded in the plugin itself.
How double bookings are prevented
Two things stop the same slot being taken twice.
Slot locking
When a booking is submitted, the plugin takes an atomic database lock on that exact slot before creating the calendar event. If two people submit the same slot within moments of each other, one succeeds and the other is told the slot has just gone, then returned to the times list.
Live free/busy checks
Availability is read from Google at request time rather than from a local copy, so events created in Google Calendar directly — by you, or by another tool — are respected immediately.
Rate limiting
Each IP address is limited to 5 booking attempts per 10 minutes, which blunts automated submissions without affecting real visitors.
Choosing a design
Three layouts are available under Settings → Design.
Classic
Calendar first, then times, then the form — one step at a time. The most familiar pattern and the safest choice for a general audience.
Combined
Calendar and times side by side on one screen. Fewer clicks, and good on wide content areas. Needs more horizontal room.
Picker
Month and year dropdowns instead of arrow navigation. Useful when people book several months out and clicking forward would be tedious.
You can override the design per page with a shortcode attribute — see Shortcode attributes.
Colors, fonts & sizing
Settings → Styling covers the visual controls without any CSS.
- Primary color — selected date, focus underlines, floating labels, success icon.
- Button colors — background, text, and hover states for the submit button.
- Background and text color — the widget surface and its type.
- Font family — set to
inherit to adopt your theme's font instead of loading another.
- Text size — small, medium or large base size.
- Border radius and max width — the widget shell.
- RTL — right-to-left layout for Arabic, Hebrew and similar.
- Locale — the language of month and weekday labels (18 supported).
These settings also style your booking emails — the header bar uses your primary color, buttons use your button colors, and the card follows your border radius.
Custom CSS
For anything the Styling panel does not cover, use the Custom CSS field at the bottom of Settings → Styling. It loads only on pages containing the widget.
The widget is built on CSS variables set on .calnative-booking-widget, so most changes are one line:
.calnative-booking-widget {
--calnative-primary: #2d6a4f;
--calnative-btn-bg: #2d6a4f;
--calnative-radius: 8px;
--calnative-font: inherit;
--calnative-max-width: 520px;
}
Useful class names for targeting specific elements:
.calnative-day--selected — the chosen date
.calnative-day--today — today's date
.calnative-slot-btn — individual time buttons
.calnative-submit — the confirm button
.calnative-field input — form inputs
.calnative-timezone — the timezone row
All widget classes are prefixed calnative- and do not collide with theme or WooCommerce classes. If a rule will not apply, prefixing it with .calnative-booking-widget almost always resolves it.
Form fields
Under Settings → Form you choose which fields appear and which are required.
Name and email are always present — without them a booking cannot be confirmed. Surname, phone, company and message are each optional, and can be shown or hidden and marked required or not.
Keep the form short. Every extra field costs completions, and anything you can ask during the appointment does not need to be on the booking form. The message field is usually the most valuable addition, since it lets people explain what they need in their own words.
The shortcode
Place this on any page or post:
[calnative_booking]
It works anywhere WordPress renders shortcodes — the block editor (Shortcode block), Elementor, Divi, WPBakery and the classic editor. There is no separate block or widget to find.
Where to put it
A dedicated booking page linked from your navigation works best. Adding it to a busy page competes with everything else for attention, and the widget needs a reasonable amount of width to breathe.
You can place it on several pages if you want different presentation for different audiences — see the attributes below.
Shortcode attributes
Attributes override the global settings for that page only. Everything else falls back to your saved settings.
[calnative_booking design="combined" max_width="520"]
| Attribute | Values | Effect |
design | classic, picker, combined | Layout for this instance |
text_size | small, medium, large | Base font size |
max_width | number, e.g. 520 | Maximum width in pixels |
booking_title | text | Heading above the widget |
booking_desc | text | Short description under the heading |
A narrow sidebar placement, for example:
[calnative_booking design="classic" max_width="380" text_size="small"]
Testing before you go live
Always make one real booking through the widget before announcing it. Check all five of these:
- Available times load when you pick a date.
- The booking submits and shows the success step.
- An event appears in Google Calendar at the right time.
- The guest confirmation email arrives, with its calendar attachment.
- Your host notification email arrives.
Test logged out. Use a private/incognito window. Logged-in administrators bypass page caching, so a widget that works for you can still be broken for visitors — see
Times not loading.
Afterwards, cancel the test booking from CalNative Booking → Bookings so it does not block a real slot.
Host & guest emails
Two emails go out on every booking, configured under Settings → Email. Each has its own Send email toggle and can be switched off independently.
Guest confirmation
Sent to the person booking. Includes the date and time in their own timezone, a meeting link if video is enabled, a cancellation link, and an .ics attachment so they can add it to any calendar app.
Host notification
Sent to your Host Email with the booking details and whatever the guest entered in the form.
Templates
Templates are plain text. The branded card — header bar, colors, spacing — is applied automatically when the email is sent, so there is no HTML to write. If you are upgrading from an older version and your templates still contain markup, click Reset to default template on each.
Template tokens
Tokens are replaced with real values at send time.
| Token | Becomes |
{name} | Guest first name |
{full_name} | Guest full name |
{email} | Guest email |
{phone} | Guest phone |
{company} | Guest company |
{message} | Whatever they wrote in the message field |
{date} | Appointment date |
{time} | Appointment time with timezone |
{host_name} | Your name |
{title} | Session title |
{meet_link} | Plain meeting URL |
{meeting_button} | Styled “Join Meeting” button |
{cancel_link} | Plain cancellation URL |
{cancel_button} | Styled cancellation link |
Prefer the _button tokens over the plain link versions — they arrive pre-styled and need no HTML from you. A line containing only a token that turns out empty is removed automatically, so no blank gaps are left behind.
Sender address & SMTP
By default emails are sent from your Host Email. That is fine on many sites, but it causes trouble when you send through an SMTP service.
Why SMTP rejects some emails
SMTP providers only send from an address you have verified with them — normally the account you sign in with. If your Host Email is a Gmail address but your SMTP signs in as you@yourdomain.com, it is being asked to send mail claiming to come from an address it does not own. Many providers reject that, often silently.
The fix
Set Send emails from under Settings → Email to the address your SMTP is authorized to use. Your Host Email stays untouched, so calendar availability keeps working.
Never change Host Email to line these up. It doubles as a calendar identity for free/busy checks — change it and busy times stop being detected.
If emails still do not arrive
- Send a test email from your SMTP plugin. If that fails, the problem is the SMTP setup, not CalNative.
- Using Gmail SMTP with two-factor authentication? It needs an App Password, not your account password.
- Check both Send email toggles are on.
- Check your spam folder before ruling anything out.
Cancellation links
Every guest email can carry a cancellation link, added with the {cancel_button} token. The link is signed, so only the person who received it can use it — the URL cannot be guessed or altered.
When a guest cancels, the calendar event is deleted, the booking is marked canceled in your admin, and you receive a notification.
Cancellation deadline
Set how close to the appointment self-cancellation stays available — 24 hours, for example. Inside that window the link explains that it is too late and asks the guest to contact you directly, which is usually what you want for anything you have already prepared for.
Choosing a provider
Turn on Generate meeting link on each booking under Settings → Video and every booking gets a link automatically — in the calendar event, the confirmation email, and the calendar attachment.
| Provider | Setup | Link per booking | Requires |
| Jitsi Meet | None | Unique | Nothing |
| Static link | Paste one URL | Always the same | A room you already have |
| Zoom | OAuth app | Unique | Zoom account |
| Google Meet | Admin change | Unique | Google Workspace |
On a personal Gmail account, choose Jitsi or a static link. Google Meet link generation is not available on personal accounts at all — see Google Meet for why.
All four are included and switching is a dropdown. Existing bookings keep the links they were created with.
Jitsi Meet
Jitsi is open-source video with a free public instance. No account, no API key, no app — guests click the link and it opens in their browser. For most people it is the right choice.
Select Jitsi Meet and set a Room prefix. The prefix brands the URL; a random suffix is added per booking so no two guests share a room:
Room prefix: consultation
Generated: https://meet.jit.si/consultation-a1b2c3d4e5
https://meet.jit.si/consultation-9fK2mQ7xLp
Rooms on the public instance are unlocked by default — anyone with the link can join. The link is unguessable and only goes to you and your guest, which is fine for ordinary consultations. For confidential sessions, set a password from inside the meeting once it starts, or run your own Jitsi instance and point at it with the static link option.
Static link
If you already run every call from a personal room — a Zoom Personal Meeting Room, a recurring Google Meet, a Teams link — this attaches that URL to every booking. Select Static link and paste it into Meeting URL.
This is the only option where every booking shares one link. If two guests book back to back and the first runs over, the second walks into the first session. Fine for well-spaced appointments; risky on a dense calendar, where a unique-room provider is safer.
It is also the escape hatch for anything not on the list — a self-hosted Jitsi, Whereby, a phone bridge. If it is a URL, this will send it.
Zoom
Zoom creates a real meeting per booking, scheduled at the right time in your account.
- Sign in at marketplace.zoom.us and choose Develop → Build App.
- Select Server-to-Server OAuth — not a general OAuth app, which is for logging other people in.
- Copy the Account ID, Client ID and Client Secret.
- Add the meeting write scope so the app may create meetings, then activate it.
- Paste all three credentials into Settings → Video.
Server-to-Server OAuth is correct here because the plugin acts as you, on your own account, with nobody present to click “allow” — exactly what a booking made at 2am needs.
Google Meet & delegation
This is the option that surprises people, so it is worth understanding rather than just following steps.
CalNative talks to Google through a service account. Google will not attach a Meet conference to an event created by a service account — a Meet room has to belong to a person. The only way around it is domain-wide delegation: authorizing the service account to impersonate a real user, so the event and its Meet room are created as that human.
Impersonation is a serious permission, so only a Google Workspace administrator can grant it. Personal Gmail accounts have no admin console and no domain, so there is nothing to delegate. That is the whole reason the feature is Workspace-only.
If you do have Workspace admin
- In Google Cloud Console → IAM & Admin → Service Accounts, edit your service account, tick Enable Google Workspace Domain-wide Delegation, and copy the numeric Client ID.
- In admin.google.com → Security → Access and data control → API controls → Manage domain-wide delegation, add that Client ID with this scope exactly:
https://www.googleapis.com/auth/calendar
- Set Host Email to the Workspace user who owns the calendar — that is the account being impersonated.
- Wait a few minutes for it to propagate, then make a test booking.
The confirmation email should show a Join Meeting button. If not, check your PHP error log for Google Meet link failed — in practice that nearly always means the Client ID or scope was not saved correctly. The scope must match character for character; a trailing space breaks it.
A failed Meet link never affects anything else. Delegation is used only for link generation, so if it is not configured, availability and bookings carry on and the booking simply arrives without a link.
The bookings dashboard
CalNative Booking → Bookings lists every booking ever made, newest first.
- Search by name, email, phone or company.
- Status tabs — All, Confirmed, Canceled, each with a count.
- Timeframe — All, Future or Past.
- Date range — set From, To, or just one of the two for an open-ended range. A range replaces the timeframe tabs rather than narrowing them, so a range in the past still shows its bookings while the Future tab is selected. Clear range returns you to the tab you were on.
- Sort by appointment date, ascending or descending.
- Load more paging for long histories.
- Export CSV — download the filtered list as a spreadsheet. See Exporting to CSV.
Each row shows the appointment date and time, the guest's details, status, and when the booking was made. If the guest left a message it appears underneath. Where a video link exists, a Meeting button opens it.
All times display in your configured plugin timezone, so the list matches your calendar rather than your server's clock.
Canceling & deleting
Canceling
Cancel on a confirmed booking deletes the Google Calendar event, marks the booking canceled, and frees the slot for someone else. The record stays in your list for reference.
Canceling does not email the guest automatically — if they need to know, contact them directly so you can explain and offer another time.
Deleting
Delete permanently removes the record from your list. It only appears once a booking has been canceled or its time has passed — an upcoming confirmed booking must be canceled first, so the calendar event is removed properly rather than orphaned.
Deleting removes the booking record only. Your Google Calendar is left untouched, so past appointments remain in your calendar history. Deletion cannot be undone.
Exporting to CSV
Export CSV on the Bookings page downloads the bookings you are currently looking at as a spreadsheet — for invoicing, reporting, or importing into a CRM.
The export follows your filters
Whatever is on screen is what lands in the file. Status tab, timeframe or date range, search term and sort order all carry through, so narrowing to a single month before exporting gives you that month and nothing else. Clear the filters first if you want the whole history.
The Load more button only affects what the page displays. The export always contains every matching booking, not just the rows you have scrolled into view.
Columns
| Column | Contains |
ID | Internal booking record number |
Date / Time | The appointment slot, as YYYY-MM-DD and HH:MM:SS |
Name / Surname | Guest name fields |
Email / Phone / Company | Whichever contact fields you collect |
Message | Anything the guest wrote in the message field |
Status | confirmed or cancelled |
Meet link | The video meeting URL, where one exists |
Event ID | The Google Calendar event identifier |
Booked at | When the booking was submitted |
Dates and times are written in plain YYYY-MM-DD form rather than your display format, so they sort correctly in a spreadsheet and reimport cleanly elsewhere.
Opening the file
The file opens directly in Excel, Numbers and Google Sheets. It is UTF-8 with a byte order mark, so accented names and non-Latin scripts arrive intact rather than as mojibake.
You may notice a leading apostrophe on some values — most often phone numbers beginning with +. That is deliberate. Spreadsheets treat a cell starting with =, +, - or @ as a formula, so the plugin marks those cells as text. The apostrophe is not part of the stored value and does not appear when the cell is read.
The export contains your guests' personal data. Treat the downloaded file the way you would treat the bookings table itself — see
Where your data lives.
Times not loading (403)
Could not load times. Please try again. — with a 403 on /wp-json/calnative/v1/slots in the browser console.
A 403 means the request was rejected before it reached Google, so this is never a calendar problem. There are three causes.
1. Page caching (most common)
On version 1.1.5 and earlier the widget sent a WordPress security token that expires after 24 hours. A cached booking page served visitors an expired token and every request was refused. The tell-tale sign is that it works for you as an administrator — logged-in users bypass the cache — but fails for everyone else.
Fix: update to 1.1.6 or later, which removes the token entirely, then clear your cache once. Alternatively, exclude the booking page from caching.
2. License
An expired or deactivated license returns 403 on the same endpoint. Check CalNative Booking → Account.
3. A security plugin or firewall
Some security plugins block /wp-json/ entirely. Whitelist /wp-json/calnative/v1/.
Telling them apart
Open this in a logged-out browser tab, replacing the domain and date:
https://yoursite.com/wp-json/calnative/v1/slots?date=2026-08-20
Times returned as JSON means the plugin and Google are fine and it is caching. license_required points at the license. An HTML error page points at a firewall.
Busy times showing as free
If bookings land on times you are already busy, work through these in order.
- Check for a 403 first. When the availability request is blocked the widget deliberately leaves every day selectable rather than showing an empty calendar — so this is usually a symptom of the caching issue, not a separate fault.
- Check calendar sharing. The service account needs Make changes to events, not just read access.
- Check Host Email. It must be the Google account that owns the calendar — it is queried directly for busy times.
- Check the event blocks time. Events marked Free rather than Busy in Google Calendar do not block slots, which is correct behavior.
- Wait 30 seconds. Availability is cached briefly, so a very recent change may not have surfaced yet.
Block themes
On version 1.1.4 and earlier, block themes such as Twenty Twenty-Five could break the widget, with calNativeBooking is not defined in the console and a request to a URL beginning undefined.
The cause was timing: the plugin passed its configuration to JavaScript from inside the shortcode, and block themes print footer scripts before shortcode content is processed, so the data arrived too late.
Fixed in 1.1.5. If you are seeing this, update.
Wrong times displayed
If booking history or confirmation emails show times offset from the actual booking, check Settings → Booking → Timezone. Left empty, the plugin falls back to your WordPress site timezone; if that is also wrong, everything shifts with it.
Version 1.1.4 fixed several inconsistencies where the server timezone was used instead of the plugin timezone. If you are on an earlier version, update before investigating further.
Slot times in the widget follow the time format from Settings → General in WordPress, so switching between 24-hour and am/pm is done there, not in the plugin.
Emails not arriving
WordPress sends mail through your server by default, which many hosts do unreliably. An SMTP plugin is the usual fix, and most email problems are SMTP problems rather than plugin problems.
- Send a test email from your SMTP plugin. If it does not arrive, stop here and fix SMTP first.
- Check both Send email toggles under Settings → Email.
- Check the Sender Address matches what your SMTP is allowed to send from.
- Using Gmail SMTP with two-factor authentication? Generate an App Password — your normal password will be refused.
- Check spam.
If the calendar event is created but no emails arrive, the booking side is working correctly and the problem is entirely in mail delivery.
Requirements
- WordPress 6.0 or later
- PHP 8.0 or later
- A Google Cloud project with the Google Calendar API enabled
- A Google service account with a JSON key
- An active CalNative Booking license
Google Meet link generation additionally requires Google Workspace with admin access. Every other feature works on a personal Gmail account.
The plugin is translation-ready — all strings use the calnative-booking text domain and a .pot file ships in languages/.
Where your data lives
Booking data is stored in your WordPress database and your Google Calendar. It never passes through our servers — we are a plugin vendor, not a booking service, and we have no access to what your visitors submit.
That has a consequence worth stating plainly: you are the data controller for your visitors' booking data. You are responsible for your own privacy policy, for any consent you need to collect, and for securing the server where the data is stored.
What is stored
- Form fields the guest submitted, in a custom database table
- The Google Calendar event ID, so bookings can be canceled later
- Booking status and creation timestamp
- Short-lived transients for rate limiting and slot locks
Deleting the plugin from
Plugins in WordPress permanently drops the bookings table and every setting.
Export anything you need first — deactivating is safe, deleting is not.
Still stuck?
Every license includes email support, and we aim to reply within one business day. If the answer isn't here, get in touch — include your site URL and what you're seeing, and we'll take a look.