Skip to content
Back to help

Build an integration

From discovery to slot in 20 lines

Four sample programs that pull a tenant’s services + look up the earliest available slot. JavaScript / Python / Bash / Go. Working code, not pseudocode: paste + replace the tenant slug + run.

The pattern

Every integration follows the same three-step pattern:

  1. 01Pull /api/mcp/<tenant> to discover services + the canonical booking URL.
  2. 02Pick a service by id (or just the first public one).
  3. 03Pull /api/mcp/<tenant>/availability/next?service=<id> to get the earliest slot + the deep-link booking URL.

Direct the user to the booking URL with the slot preselected: they confirm + complete the booking through the standard web flow.

Samples

Four languages

JavaScript (Node 18+, Bun, Deno)

goldenhour-availability.js
// Look up the earliest available slot for a service on goldenhour.
// Works with Node 18+, Bun, or Deno — native fetch + JSON.

const BASE = "https://goldenhourhq.com";
const TENANT = process.env.GOLDENHOUR_TENANT || "sobetan";
const SERVICE_ID = process.env.GOLDENHOUR_SERVICE_ID;

async function findNextSlot() {
  // Discovery: pull the per-tenant manifest first.
  const tenantUrl = `${BASE}/api/mcp/${encodeURIComponent(TENANT)}`;
  const tenant = await fetch(tenantUrl).then((r) => {
    if (!r.ok) throw new Error(`tenant fetch ${r.status}`);
    return r.json();
  });
  console.log(`Booking with ${tenant.business.name} (${tenant.business.mode})`);
  console.log(`Booking URL: ${tenant.business.booking_url}`);

  // Pick a service (first public one if no env override).
  const service = SERVICE_ID
    ? tenant.services.find((s) => s.id === SERVICE_ID)
    : tenant.services[0];
  if (!service) throw new Error("no public services found");
  console.log(`Service: ${service.name} (${service.duration_minutes}min)`);

  // Compact next-slot lookup.
  const nextUrl = `${BASE}/api/mcp/${encodeURIComponent(TENANT)}` +
    `/availability/next?service=${encodeURIComponent(service.id)}`;
  const next = await fetch(nextUrl).then((r) => r.json());

  if (!next.next_slot) {
    console.log("No slots available in the next 30 days.");
    return;
  }
  console.log(`Next slot: ${next.next_slot} (${next.timezone})`);
  console.log(`Booking deep link: ${next.booking_url}`);
}

findNextSlot().catch((err) => {
  console.error(err);
  process.exit(1);
});

Python (3.10+)

goldenhour_availability.py
# Look up the earliest available slot. Python 3.10+, stdlib only.
import os
import sys
import urllib.parse
import urllib.request
import json

BASE = "https://goldenhourhq.com"
TENANT = os.environ.get("GOLDENHOUR_TENANT", "sobetan")
SERVICE_ID = os.environ.get("GOLDENHOUR_SERVICE_ID")


def fetch_json(url: str) -> dict:
    with urllib.request.urlopen(url, timeout=10) as resp:
        return json.loads(resp.read())


def main() -> None:
    tenant_url = f"{BASE}/api/mcp/{urllib.parse.quote(TENANT)}"
    tenant = fetch_json(tenant_url)
    print(f"Booking with {tenant['business']['name']} "
          f"({tenant['business']['mode']})")
    print(f"Booking URL: {tenant['business']['booking_url']}")

    services = tenant["services"]
    if SERVICE_ID:
        service = next((s for s in services if s["id"] == SERVICE_ID), None)
    else:
        service = services[0] if services else None
    if not service:
        sys.exit("no public services found")
    print(f"Service: {service['name']} ({service['duration_minutes']}min)")

    next_url = (
        f"{BASE}/api/mcp/{urllib.parse.quote(TENANT)}/availability/next"
        f"?service={urllib.parse.quote(service['id'])}"
    )
    nxt = fetch_json(next_url)
    if not nxt.get("next_slot"):
        print("No slots available in the next 30 days.")
        return
    print(f"Next slot: {nxt['next_slot']} ({nxt['timezone']})")
    print(f"Booking deep link: {nxt['booking_url']}")


if __name__ == "__main__":
    main()

Bash (curl + jq)

goldenhour-availability.sh
#!/usr/bin/env bash
# Earliest-slot lookup. Needs curl + jq.
set -euo pipefail

BASE="${BASE:-https://goldenhourhq.com}"
TENANT="${GOLDENHOUR_TENANT:-sobetan}"

# Pull the tenant summary.
tenant_json=$(curl -fsSL "${BASE}/api/mcp/${TENANT}")
echo "Booking with $(echo "$tenant_json" | jq -r '.business.name')"
echo "Booking URL: $(echo "$tenant_json" | jq -r '.business.booking_url')"

# Pick the first public service (or override with GOLDENHOUR_SERVICE_ID).
service_id="${GOLDENHOUR_SERVICE_ID:-$(echo "$tenant_json" | jq -r '.services[0].id')}"
[ -z "${service_id}" ] && { echo "no service id" >&2; exit 1; }

# Compact next-slot lookup.
next_json=$(curl -fsSL \
  "${BASE}/api/mcp/${TENANT}/availability/next?service=${service_id}")

next_slot=$(echo "$next_json" | jq -r '.next_slot // empty')
if [ -z "${next_slot}" ]; then
  echo "No slots available in the next 30 days."
  exit 0
fi
echo "Next slot: ${next_slot} ($(echo "$next_json" | jq -r '.timezone'))"
echo "Booking deep link: $(echo "$next_json" | jq -r '.booking_url')"

Go (1.21+)

main.go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"time"
)

const baseURL = "https://goldenhourhq.com"

type tenantResp struct {
	Business struct {
		Name       string `json:"name"`
		Mode       string `json:"mode"`
		BookingURL string `json:"booking_url"`
	} `json:"business"`
	Services []struct {
		ID              string `json:"id"`
		Name            string `json:"name"`
		DurationMinutes int    `json:"duration_minutes"`
	} `json:"services"`
}

type nextResp struct {
	NextSlot   *string `json:"next_slot"`
	Timezone   string  `json:"timezone"`
	BookingURL string  `json:"booking_url"`
}

func fetchJSON(target string, out interface{}) error {
	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Get(target)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 400 {
		return fmt.Errorf("status %d", resp.StatusCode)
	}
	return json.NewDecoder(resp.Body).Decode(out)
}

func main() {
	tenant := os.Getenv("GOLDENHOUR_TENANT")
	if tenant == "" {
		tenant = "sobetan"
	}

	var t tenantResp
	if err := fetchJSON(baseURL+"/api/mcp/"+url.PathEscape(tenant), &t); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	fmt.Printf("Booking with %s (%s)\n", t.Business.Name, t.Business.Mode)
	fmt.Printf("Booking URL: %s\n", t.Business.BookingURL)

	if len(t.Services) == 0 {
		fmt.Fprintln(os.Stderr, "no public services")
		os.Exit(1)
	}
	svc := t.Services[0]

	var n nextResp
	if err := fetchJSON(
		baseURL+"/api/mcp/"+url.PathEscape(tenant)+
			"/availability/next?service="+url.QueryEscape(svc.ID), &n,
	); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	if n.NextSlot == nil {
		fmt.Println("No slots available.")
		return
	}
	fmt.Printf("Next slot: %s (%s)\n", *n.NextSlot, n.Timezone)
	fmt.Printf("Booking deep link: %s\n", n.BookingURL)
}

Going deeper

JSON Schema

Strict response validation: see /api/mcp/spec.json.

OpenAPI 3.1

Full request shape: /api/openapi.json.

Curl quick-start

/help/api-quick-start

Related: AI agents + MCP overview