Guidly API
Version 1 · Last updated: August 13, 2026
The Guidly API lets you read your bookings, clients and payments, and receive real-time webhooks when things happen. It is what the Guidly integration for Zapier is built on, and it is equally usable from Make, n8n, or your own code.
API access requires a Guidly Pro plan. Every endpoint is scoped to the provider who owns the API key — there is no way to reach another provider's data.
Authentication
All requests are authenticated with an API key. Create one in the Guidly dashboard under Integrations → API & Zapier.
Guidly stores only a SHA-256 hash of your key, so it is displayed once, at creation, and cannot be retrieved afterwards. If you lose it, revoke it and create another. Revocation takes effect immediately — every request checks it.
Send the key on every request, in either header:
Authorization: Bearer gdly_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
X-API-Key: gdly_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
The base URL for all endpoints is:
https://api.guidly.ca/api/v1
To check a key works, call GET /me:
curl https://api.guidly.ca/api/v1/me \
-H "Authorization: Bearer $GUIDLY_API_KEY"
{
"id": "d957fd57-8574-40a5-a513-795ec33456e5",
"email": "you@example.com",
"first_name": "Wayne",
"last_name": "Shimoon",
"full_name": "Wayne Shimoon",
"plan": "pro",
"profile_url": "https://guidly.ca/wayne-shimoon",
"connection_label": "Wayne Shimoon"
}
Errors
Errors return a JSON body with an error field describing what went wrong.
| Status | Meaning |
|---|---|
400 | The request was malformed — a missing required field, an unknown event name, or a non-HTTPS webhook URL. |
401 | No API key was sent, or the key is invalid or revoked. |
403 | The key is valid but the account is not on a Pro plan. |
404 | The resource does not exist, or belongs to another provider. |
429 | Rate limited. See Rate limits. |
{ "error": "API access requires a Guidly Pro plan.", "upgrade_url": "https://guidly.ca" }
Endpoints
Account
Returns the provider the API key belongs to. Useful as a connection test.
Returns the list of event names you can subscribe to.
{ "events": ["booking.created", "booking.confirmed", "booking.cancelled",
"booking.rescheduled", "payment.received", "client.created"] }
Bookings
Your bookings, most recent session date first.
| Parameter | Description |
|---|---|
limit | 1–100. Defaults to 25. |
status | Filter by booking status, e.g. pending, confirmed, cancelled. |
payment_status | Filter by payment status: none, unpaid, paid, failed. |
curl "https://api.guidly.ca/api/v1/bookings?status=confirmed&limit=5" \
-H "Authorization: Bearer $GUIDLY_API_KEY"
{ "bookings": [ { /* booking object */ } ] }
A single booking. Returns 404 if it belongs to another provider.
Payments
Bookings that have been paid for. Same object shape as /bookings, filtered to payment_status: "paid". Accepts limit.
{ "payments": [ { /* booking object */ } ] }
Clients
Your clients — both people you added directly and anyone who has booked you. Accepts limit.
{ "clients": [ { /* client object */ } ] }
Add a client. Either email or phone is required, along with first_name.
This endpoint is match-or-create: if someone already exists with the same email (or, when no email is given, the same phone), that client is returned instead of a duplicate being made. The created field tells you which happened, so a job that re-runs over the same source data will not produce duplicates.
curl -X POST https://api.guidly.ca/api/v1/clients \
-H "Authorization: Bearer $GUIDLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"first_name":"Dana","last_name":"Whitfield","email":"dana@example.com"}'
{ "created": true, "client": { /* client object */ } }
Clients created through this endpoint deliberately do not fire the client.created webhook. That prevents an automation which creates clients and an automation which listens for them from triggering each other in a loop.
Webhooks
Rather than polling, subscribe a URL and Guidly will POST to it when something happens. Delivery is typically within a second.
Subscribe. Requires event and target_url. The URL must be absolute and https://.
curl -X POST https://api.guidly.ca/api/v1/webhooks \
-H "Authorization: Bearer $GUIDLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"event":"booking.confirmed","target_url":"https://example.com/hooks/guidly"}'
{
"id": "7b88b22c-035b-41e9-93f8-ae5ae65ce097",
"event": "booking.confirmed",
"target_url": "https://example.com/hooks/guidly",
"secret": "0c35f88254c4b9b9a47e352f263573ae..."
}
Keep the secret — it is what you verify signatures with. Subscribing the same event and URL twice is idempotent: you get the original subscription back rather than a duplicate.
Unsubscribe. Returns 204. Deleting a subscription that no longer exists is also a success.
List your subscriptions, including their health.
{
"webhooks": [
{
"id": "7b88b22c-035b-41e9-93f8-ae5ae65ce097",
"event": "booking.confirmed",
"target_url": "https://example.com/hooks/guidly",
"source": "api",
"created_at": "2026-08-13T14:52:10.442Z",
"last_delivery_at": "2026-08-13T14:55:26.751Z",
"failure_count": 0,
"disabled_at": null
}
]
}
Delivery, retries and failures
- Each delivery is attempted up to three times, with roughly 1s and 4s backoff between attempts.
- A
4xxresponse other than408or429stops retries immediately — your endpoint rejected the payload, so resending an identical body would fail identically. - Each attempt times out after 5 seconds. Acknowledge quickly with a
2xxand do your processing afterwards. failure_countcounts consecutive failures and resets to zero on any success.- After 20 consecutive failures a subscription is disabled (
disabled_atis set) and stops receiving events. Re-subscribing the same event and URL revives it.
Deliveries are not strictly ordered, and a retry can arrive after a later event. Use the X-Guidly-Delivery header to make your handler idempotent if a duplicate would cause a problem.
Events and payloads
| Event | Fires when |
|---|---|
booking.created | A client books a session. Fires when the booking is made — for paid or approval-required sessions, this is before it is confirmed. |
booking.confirmed | A session is confirmed, either approved by the provider or paid for by the client. The meeting link is populated by this point. |
booking.cancelled | A session is cancelled by either party. |
booking.rescheduled | A session moves to a new time. starts_at carries the new time. |
payment.received | A session is paid for, by card or recorded as paid by the provider. Delivered alongside booking.confirmed when payment is what confirmed it. |
client.created | A provider adds a client in Guidly. Not fired for clients created via POST /clients. |
Every webhook body has the same envelope. data holds a booking object for the booking and payment events, and a client object for client.created.
{
"event": "booking.confirmed",
"created_at": "2026-08-13T14:55:26.751Z",
"data": { /* booking or client object */ }
}
Headers
| Header | Value |
|---|---|
X-Guidly-Event | The event name, e.g. booking.confirmed. |
X-Guidly-Delivery | A unique id for this delivery attempt. Use it to deduplicate. |
X-Guidly-Signature | sha256=<hex digest> — see below. |
User-Agent | Guidly-Webhooks/1.0 |
Verifying signatures
Every delivery is signed with the subscription's secret using HMAC-SHA256 over the raw request body. Verify it before trusting a payload — your webhook URL is reachable by anyone who learns it.
Compute the digest over the raw bytes you received, before any JSON parsing. Re-serialising the parsed object produces different bytes and the signature will not match.
// Node.js / Express
const crypto = require('crypto');
app.post('/hooks/guidly',
express.raw({ type: 'application/json' }), // raw body, not express.json()
(req, res) => {
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.GUIDLY_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const received = req.get('x-guidly-signature') || '';
// Constant-time compare, so a mismatch cannot be found by timing.
const ok = expected.length === received.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
if (!ok) return res.status(401).end();
const { event, data } = JSON.parse(req.body);
res.status(200).end(); // acknowledge first
handle(event, data); // then do the work
});
# Python / Flask
import hmac, hashlib
from flask import request
@app.post("/hooks/guidly")
def guidly_hook():
expected = "sha256=" + hmac.new(
SECRET.encode(), request.get_data(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Guidly-Signature", "")):
return "", 401
payload = request.get_json()
return "", 200
Object reference
Booking
| Field | Type | Notes |
|---|---|---|
id | string | UUID. Stable primary key — use it to deduplicate. |
status | string | pending, confirmed, or cancelled. |
payment_status | string | none (free session), unpaid, paid, or failed. |
starts_at | string | ISO 8601, UTC. |
timezone | string | IANA zone the client booked in, e.g. America/Toronto. Use it to render a local time. |
duration_hours | number | May be fractional, e.g. 0.5. |
duration_minutes | integer | The same value in minutes, for convenience. |
topic | string | Session title. |
meeting_mode | string | zoom, teams, or in_person. |
meeting_link | string | Join URL. Empty until confirmed, and always empty for in-person sessions. |
price | number | Total for the session, not an hourly rate. 0 for a free session. |
currency | string | ISO code, e.g. CAD. Do not assume — providers can bill in more than one. |
paid_via | string | stripe, offline, or empty. |
client | object | Nested client object. |
provider | object | The provider: id, name, email, specialty, timezone, profile URL. |
client_name | string | Flat duplicate of client.full_name. |
client_email | string | Flat duplicate of client.email. |
provider_name | string | Flat duplicate of provider.full_name. |
dashboard_url | string | Deep link to the booking in the Guidly dashboard. |
{
"id": "9f1c2b7e-4a3d-4e51-8b6f-2c9d7a1e5b30",
"status": "confirmed",
"payment_status": "paid",
"starts_at": "2026-09-04T18:00:00.000Z",
"timezone": "America/Toronto",
"duration_hours": 1,
"duration_minutes": 60,
"topic": "Session with Wayne Shimoon",
"meeting_mode": "zoom",
"meeting_link": "https://us02web.zoom.us/j/85512349876",
"price": 120,
"currency": "CAD",
"paid_via": "stripe",
"client": {
"id": "3b7d1f92-6c48-4a20-9e15-7d4c8a2f6b11",
"first_name": "Dana",
"last_name": "Whitfield",
"full_name": "Dana Whitfield",
"email": "dana.whitfield@example.com",
"phone": "+16135550142",
"timezone": "America/Toronto",
"created_at": "2026-06-12T14:22:05.000Z"
},
"provider": {
"id": "7b47321f-f007-4506-8ffe-62bad8eb264e",
"first_name": "Wayne",
"last_name": "Shimoon",
"full_name": "Wayne Shimoon",
"email": "wayne@example.com",
"specialty": "Business coaching",
"timezone": "America/Toronto",
"profile_url": "https://guidly.ca/wayne-shimoon"
},
"client_name": "Dana Whitfield",
"client_email": "dana.whitfield@example.com",
"provider_name": "Wayne Shimoon",
"dashboard_url": "https://guidly.ca/?booking=9f1c2b7e-4a3d-4e51-8b6f-2c9d7a1e5b30"
}
Client
| Field | Type | Notes |
|---|---|---|
id | string | UUID. May be null on a booking whose client record has since been removed. |
first_name | string | |
last_name | string | May be empty. |
full_name | string | |
email | string | May be empty — a client can be created with a phone number only. |
phone | string | May be empty. |
timezone | string | IANA zone, may be empty. |
created_at | string | ISO 8601. |
Rate limits and versioning
Requests are limited to 200 per 15 minutes per IP address across the Guidly API. Exceeding it returns 429. Webhooks are the intended way to react to events — polling /bookings on a short interval will hit the limit and lag behind real-time delivery anyway.
The version is in the path. Fields will be added to responses over time, so parse defensively and ignore what you do not recognise. Existing fields will not be renamed or removed within v1; a breaking change would ship as /api/v2 with v1 continuing to work.
Using Zapier instead
If you would rather not write code, the Guidly integration for Zapier exposes all six events as triggers and POST /clients as an action, letting you connect Guidly to QuickBooks, Google Sheets, Mailchimp and thousands of other tools without a server of your own. Create an API key under Integrations → API & Zapier and paste it when Zapier asks you to connect your account.