{"openapi":"3.1.0","info":{"title":"Just a Good Guy API","version":"1.0.0","description":"Bookings, availability, customers and requests for your business. Secret-key endpoints are for your server; public endpoints are what the website widget uses."},"servers":[{"url":"https://justagoodguy.us/api"}],"components":{"securitySchemes":{"SecretKey":{"type":"http","scheme":"bearer","description":"Secret key from Dashboard → Settings → Developers (starts with sk_). Keep it on your server."},"PublishableKey":{"type":"apiKey","in":"header","name":"X-GG-Key","description":"Publishable key (pk_...). Safe to use in the browser."}},"schemas":{"Service":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"kind":{"type":"string","enum":["appointment","visit","class","quote"]},"durationMin":{"type":"number"},"priceCents":{"type":["number","null"]},"description":{"type":["string","null"]}},"required":["id","name","kind","durationMin","priceCents","description"]},"Error":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]},"Staff":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"color":{"type":"string"}},"required":["id","name","color"]},"SlotDay":{"type":"object","properties":{"date":{"type":"string"},"label":{"type":"string"},"slots":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string"},"label":{"type":"string"},"staffIds":{"type":"array","items":{"type":"string"}}},"required":["start","label","staffIds"]}}},"required":["date","label","slots"]},"Booking":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","example":"confirmed"},"source":{"type":"string"},"start":{"type":"string","example":"2026-11-03T19:00:00.000Z"},"end":{"type":"string"},"timezone":{"type":"string"},"service":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"]},"staff":{"type":["object","null"],"properties":{"id":{"type":"string"},"name":{"type":["string","null"]}},"required":["id","name"]},"customer":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"phone":{"type":["string","null"]},"email":{"type":["string","null"]}},"required":["id","name","phone","email"]},"priceCents":{"type":["number","null"]},"notes":{"type":["string","null"]}},"required":["id","status","source","start","end","timezone","service","staff","customer","priceCents","notes"]},"CreateBooking":{"type":"object","properties":{"service_id":{"type":"string"},"start":{"type":"string","description":"Exact start from /v1/availability (ISO 8601)"},"staff_id":{"type":"string"},"customer":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"}},"required":["name"]},"notes":{"type":"string","maxLength":1000}},"required":["service_id","start","customer"]},"Customer":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"phone":{"type":["string","null"]},"email":{"type":["string","null"]},"visitCount":{"type":"number"},"tags":{"type":"array","items":{"type":"string"}}},"required":["id","name","phone","email","visitCount","tags"]}},"parameters":{}},"paths":{"/v1/services":{"get":{"tags":["Services"],"summary":"List services","security":[{"SecretKey":[]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Service"}}},"required":["data"]}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Bad or missing key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Time no longer available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/staff":{"get":{"tags":["Services"],"summary":"List team members","security":[{"SecretKey":[]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Staff"}}},"required":["data"]}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Bad or missing key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Time no longer available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/availability":{"get":{"tags":["Availability"],"summary":"Open times for a service","security":[{"SecretKey":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"service_id","in":"query"},{"schema":{"type":"string"},"required":false,"name":"staff_id","in":"query"},{"schema":{"type":"string","description":"ISO date-time to start from (default now)"},"required":false,"description":"ISO date-time to start from (default now)","name":"from","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":31},"required":false,"name":"days","in":"query"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"timezone":{"type":"string"},"data":{"type":"array","items":{"$ref":"#/components/schemas/SlotDay"}}},"required":["timezone","data"]}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Bad or missing key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Time no longer available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/bookings":{"get":{"tags":["Bookings"],"summary":"List bookings","security":[{"SecretKey":[]}],"parameters":[{"schema":{"type":"string"},"required":false,"name":"from","in":"query"},{"schema":{"type":"string"},"required":false,"name":"to","in":"query"},{"schema":{"type":"string","description":"Comma-separated statuses"},"required":false,"description":"Comma-separated statuses","name":"status","in":"query"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Booking"}}},"required":["data"]}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Bad or missing key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Time no longer available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Bookings"],"summary":"Create a booking","security":[{"SecretKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBooking"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Bad or missing key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Time no longer available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/bookings/{id}":{"get":{"tags":["Bookings"],"summary":"Get a booking","security":[{"SecretKey":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Bad or missing key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Time no longer available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/bookings/{id}/reschedule":{"post":{"tags":["Bookings"],"summary":"Move a booking","security":[{"SecretKey":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"start":{"type":"string"},"staff_id":{"type":"string"},"notify":{"type":"boolean"}},"required":["start"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Bad or missing key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Time no longer available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/bookings/{id}/cancel":{"post":{"tags":["Bookings"],"summary":"Cancel a booking","security":[{"SecretKey":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string"},"notify":{"type":"boolean"}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Bad or missing key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Time no longer available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/customers":{"get":{"tags":["Customers"],"summary":"List or search customers","security":[{"SecretKey":[]}],"parameters":[{"schema":{"type":"string"},"required":false,"name":"q","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":500},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Customer"}}},"required":["data"]}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Bad or missing key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Time no longer available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Customers"],"summary":"Create or update a customer (matched by phone or email)","security":[{"SecretKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"}},"required":["name"]}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Customer"}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Bad or missing key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Time no longer available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/customers/{id}":{"get":{"tags":["Customers"],"summary":"Get a customer","security":[{"SecretKey":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Customer"}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Bad or missing key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Time no longer available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/callbacks":{"get":{"tags":["Requests"],"summary":"Open call-back requests","security":[{"SecretKey":[]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"customerId":{"type":"string"},"windowStart":{"type":"string"},"windowEnd":{"type":"string"},"reason":{"type":["string","null"]},"status":{"type":"string"}},"required":["id","customerId","windowStart","windowEnd","reason","status"]}}},"required":["data"]}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Bad or missing key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Time no longer available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/leads":{"get":{"tags":["Requests"],"summary":"Quote requests and messages","security":[{"SecretKey":[]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"customerId":{"type":"string"},"kind":{"type":"string"},"details":{"type":"string"},"status":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","customerId","kind","details","status","createdAt"]}}},"required":["data"]}}}},"400":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Bad or missing key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Time no longer available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/public/config":{"get":{"tags":["Public (widget)"],"summary":"Widget configuration","description":"Business name, colors, greeting, whether calls can be taken right now.","security":[{"PublishableKey":[]}],"responses":{"200":{"description":"OK"}}}},"/public/services":{"get":{"tags":["Public (widget)"],"summary":"Services for display","description":"Bookable services with price labels.","security":[{"PublishableKey":[]}],"responses":{"200":{"description":"OK"}}}},"/public/slots":{"get":{"tags":["Public (widget)"],"summary":"Open times","description":"Query: serviceId, staffId?, from (ms), days (max 21).","security":[{"PublishableKey":[]}],"responses":{"200":{"description":"OK"}}}},"/public/bookings":{"post":{"tags":["Public (widget)"],"summary":"Book without AI","description":"Body: { serviceId, start (ms), staffId?, customer: { name, phone?, email? }, notes?, address? }.","security":[{"PublishableKey":[]}],"responses":{"200":{"description":"OK"}}}},"/public/callbacks":{"post":{"tags":["Public (widget)"],"summary":"Request a call-back","description":"Body: { name, phone, reason?, windowStart? }. Scheduled inside the business’s call hours.","security":[{"PublishableKey":[]}],"responses":{"200":{"description":"OK"}}}},"/public/callback-windows":{"get":{"tags":["Public (widget)"],"summary":"Call-back times","description":"Whether someone can take a call right now (callNow), and up to 10 call-back windows in the next 7 days: { start, end, label }.","security":[{"PublishableKey":[]}],"responses":{"200":{"description":"OK"}}}},"/public/leads":{"post":{"tags":["Public (widget)"],"summary":"Send a message or quote request","description":"Body: { name, phone?, email?, details, serviceId?, subject?, pageUrl?, fields?, source?: \"widget\" | \"site_form\" }. Needs a phone or an email. Returns { lead: { id }, emailed }: emailed is true only when a copy really went to their email.","security":[{"PublishableKey":[]}],"responses":{"200":{"description":"OK"}}}},"/public/events":{"post":{"tags":["Public (widget)"],"summary":"Record a page view (sendBeacon)","description":"text/plain body with JSON { key, type: \"page_view\" | \"widget_open\" | \"widget_ping\", url?, referrer? }. Returns 204. Counts visitors without cookies.","security":[{"PublishableKey":[]}],"responses":{"200":{"description":"OK"}}}},"/public/conversation":{"get":{"tags":["Public (widget)"],"summary":"A chat so far","description":"Query: visitorToken, conversationId?. The last 50 messages ({ id, role: user | assistant | owner, content, cards, createdAt }) and whether a person has taken over.","security":[{"PublishableKey":[]}],"responses":{"200":{"description":"OK"}}}},"/public/conversation/stream":{"get":{"tags":["Public (widget)"],"summary":"Live replies (Server-Sent Events)","description":"Query: visitorToken, conversationId?. Streams messages the owner sends after taking over the chat.","security":[{"PublishableKey":[]}],"responses":{"200":{"description":"OK"}}}},"/public/chat":{"post":{"tags":["Public (widget)"],"summary":"Talk to the AI (Server-Sent Events)","description":"Body: { message, visitorToken, conversationId? }. Streams events: start, delta, status, card, replace, done, error.","security":[{"PublishableKey":[]}],"responses":{"200":{"description":"OK"}}}}},"webhooks":{}}