How to call the GoHighLevel API from a Supabase Edge Function
A Deno Edge Function that authenticates to GHL, checks calendar availability, and books — with secrets, signature verification, CORS, and the deploy pattern this site's own booking runs on.
Skip the scaffold. Download the starter Edge Function — a Deno handler with GHL auth from secrets, the free-slots and book-appointment calls, CORS + OPTIONS handling, and a signature-verification stub. Drop it into supabase/functions and deploy.
When to use an Edge Function instead of n8n
n8n is the right tool when a non-developer will maintain the automation, or when you want a visual audit trail of every step. A Supabase Edge Function — Deno running at the edge — is the right tool when the glue lives inside a Supabase app you already own: you want it version-controlled with the rest of your code, deployed with supabase functions deploy, and running with sub-second cold starts next to your database. GHL webhooks in, GHL API calls out, no separate automation platform to keep alive.
This site's own booking runs on exactly this pattern: get-availability and book-appointment Edge Functions call the GoHighLevel calendar API and are what the booking widget (and the Vapi voice agent) hit. What follows is that pattern, generalized.
Prerequisites
- A Supabase project and the Supabase CLI installed (
supabase logindone) - A GHL Private Integration token scoped to whatever the function does
- Deno basics — the function is a single
Deno.servehandler
Step 1: Scaffold the function
supabase functions new ghl-availability
That creates supabase/functions/ghl-availability/index.ts with a Deno.serve handler. Everything below goes in that file.
Step 2: Authenticate to GHL from Deno — secrets, not code
Never paste the token into the source. Set it as a Supabase secret and read it from the environment at runtime:
supabase secrets set GHL_TOKEN=pit-xxxx... GHL_LOCATION_ID=abc123
// index.ts
const token = Deno.env.get("GHL_TOKEN")!;
const headers = {
"Authorization": `Bearer ${token}`,
"Version": "2021-07-28",
"Content-Type": "application/json",
};The Version header is mandatory on the GHL v2 API — a valid token without it still returns 401. If you hit that, the 401 fix applies here too; the auth rules are identical whether the caller is n8n or Deno.
Step 3: The availability / booking handler
Parse the request, call the GHL calendar API, return JSON. Keep it to one GHL call per invocation — an Edge Function that fans out to five endpoints is slow and hard to debug.
Deno.serve(async (req) => {
const { startDate, endDate, timezone } = await req.json();
const cal = Deno.env.get("GHL_CALENDAR_ID")!;
const url = new URL(
`https://services.leadconnectorhq.com/calendars/${cal}/free-slots`);
url.searchParams.set("startDate", startDate);
url.searchParams.set("endDate", endDate);
url.searchParams.set("timezone", timezone);
const res = await fetch(url, { headers });
const data = await res.json();
return new Response(JSON.stringify(data), {
headers: { "Content-Type": "application/json", ...cors },
});
});Step 4: Verify inbound webhook signatures
If the function receives a GHL webhook (rather than being called by your own app), verify it's really from GHL before acting — otherwise anyone with the URL can POST fake events. Check the shared secret or signature header GHL sends, and reject on mismatch. Same principle as any webhook receiver: trust nothing until it's verified.
Step 5: Deploy and (optionally) schedule
supabase functions deploy ghl-availability # public URL: # https://<project-ref>.supabase.co/functions/v1/ghl-availability
For recurring jobs (nightly sync, pipeline hygiene), trigger the function on a schedule with pg_cron + pg_net from inside Postgres, so the whole thing stays within Supabase.
Common gotchas
1. CORS. If a browser calls the function (a booking widget), you must return CORS headers and handle the OPTIONS preflight explicitly — Deno doesn't add them for you. Missing CORS is the #1 "works in curl, fails in the browser" cause.
2. Cold starts. Edge Functions cold-start fast but not instantly. On a latency-sensitive path (a live voice call), keep the function warm or accept the first-call delay — see the latency notes in the Vapi voice agent guide.
3. Secret rotation. When you rotate the GHL token, supabase secrets set the new value and redeploy — running functions read secrets at boot, so a rotation without redeploy leaves the old token in memory until the next cold start.
4. The service-role key. If the function writes to your database, it uses the service-role key, which bypasses RLS. Scope what the function does carefully — see Supabase RLS patterns for multi-tenant agencies.
Need this workflow customized or debugged? Book a 30-min scope call →
Stuck configuring this workflow?
I build and fix n8n workflows for agencies and SaaS teams. 30-minute scope call — bring the problem, leave with an estimate.