{"openapi":"3.1.0","info":{"title":"Simple Scheduler Open API","version":"v1","description":"Public REST API for Simple Scheduler tenants. Authentication is via\nscoped `sk_live_*` API keys issued from\n`/app/settings/integrations/api-keys`. Pass the key as a Bearer\ntoken in the `Authorization` header. Every operation declares the\ncapability it requires via the `x-required-capability` extension.\n\n## Capabilities\n\n`customers:read` — Read customer records and recent appointment summaries.\n\n`customers:write` — Create or update customer records (alias of `leads:write` with converted status).\n\n`leads:write` — Push new leads/contacts via POST /api/v1/leads.\n\n`appointments:read` — Read appointments. (v1: 501 Not Implemented; spec only.)\n\n`appointments:write` — Create or update appointments. (v1: 501 Not Implemented; spec only.)\n\n`webhooks:manage` — Manage outbound webhook subscriptions for this tenant."},"servers":[{"url":"https://app.simplescheduler.com","description":"Production"},{"url":"https://staging.simplescheduler.com","description":"Staging (per tenant)"}],"components":{"securitySchemes":{"BearerApiKey":{"type":"http","scheme":"bearer","bearerFormat":"sk_live_*","description":"Pass your live key as `Authorization: Bearer sk_live_...`. NEVER place the key in the URL — query parameters land in CDN logs and proxy logs."}},"schemas":{"ErrorResponse":{"type":"object","required":["code"],"properties":{"code":{"type":"string","description":"Machine-readable error code; stable across versions.","example":"unauthenticated"},"message":{"type":"string","description":"Human-readable detail (may be absent)."},"required":{"type":"string","description":"Capability the request was missing (403 only)."},"retry_after_seconds":{"type":"integer","description":"Seconds until the rate-limit window resets (429 only)."},"issues":{"type":"array","description":"Field-level validation issues (400 only).","items":{"type":"object","properties":{"path":{"type":"array","items":{"type":["string","integer"]}},"message":{"type":"string"}}}}}},"LeadCreate":{"type":"object","required":["email"],"description":"A third-party lead/contact record. At least an email is required so the dedupe-by-email policy can match against existing customers. Either `first_name` + `last_name` or `company_name` should be present for a useful record.","properties":{"email":{"type":"string","format":"email","maxLength":320},"phone":{"type":"string","description":"E.164 preferred; +1 prefix added automatically for 10-digit US numbers.","maxLength":32},"first_name":{"type":"string","maxLength":80},"last_name":{"type":"string","maxLength":80},"company_name":{"type":"string","maxLength":120},"address_line1":{"type":"string","maxLength":200},"address_line2":{"type":"string","maxLength":200},"city":{"type":"string","maxLength":100},"state":{"type":"string","maxLength":50},"postal_code":{"type":"string","maxLength":20},"country":{"type":"string","maxLength":2,"description":"ISO-3166-1 alpha-2; default 'US'."},"notes":{"type":"string","maxLength":2000,"description":"Free-form notes; rendered verbatim in the customer profile."},"metadata":{"type":"object","description":"Optional opaque JSON pass-through; stored on the customer for downstream use.","additionalProperties":true}}},"LeadResponse":{"type":"object","required":["id","lead_status","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"lead_status":{"type":"string","enum":["new","contacted","qualified","converted","unqualified"]},"deduplicated":{"type":"boolean","description":"True if this email matched an existing customer (idempotent replay)."},"created_at":{"type":"string","format":"date-time"}}},"Customer":{"type":"object","required":["id","tenant_id","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string","format":"uuid"},"is_company":{"type":"boolean"},"first_name":{"type":["string","null"]},"last_name":{"type":["string","null"]},"company_name":{"type":["string","null"]},"email":{"type":["string","null"],"format":"email"},"phone":{"type":["string","null"]},"address_line1":{"type":["string","null"]},"city":{"type":["string","null"]},"state":{"type":["string","null"]},"postal_code":{"type":["string","null"]},"country":{"type":["string","null"]},"lead_status":{"type":["string","null"],"enum":[null,"new","contacted","qualified","converted","unqualified"]},"lead_source":{"type":["string","null"]},"is_active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"CustomerListResponse":{"type":"object","required":["customers","next_cursor"],"properties":{"customers":{"type":"array","items":{"$ref":"#/components/schemas/Customer"}},"next_cursor":{"type":["string","null"],"description":"Opaque cursor for the next page; null when the result set is exhausted."}}}}},"security":[{"BearerApiKey":[]}],"tags":[{"name":"Leads","description":"Ingest third-party leads / contacts."},{"name":"Customers","description":"Read customers + recent activity."},{"name":"Appointments","description":"Read appointments. Write surface stubbed in v1."}],"paths":{"/api/v1/leads":{"post":{"operationId":"createLead","summary":"Create a new lead","tags":["Leads"],"description":"Insert (or dedupe-against-existing) a customer record with `lead_status='new'`. The issuing key's `label` is recorded as `lead_source` so internal users can trace the origin.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Caller-supplied UUID. Re-sending the same key within 24h returns the original 201 payload (no duplicate row). Scoped per tenant.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeadCreate"},"example":{"email":"jane@example.com","first_name":"Jane","last_name":"Doe","phone":"+15555550100","notes":"Webflow contact form, requested cleaning quote"}}}},"responses":{"201":{"description":"Created (or returned existing on idempotent replay).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeadResponse"}}}},"400":{"description":"Invalid input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthenticated. Authorization header missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Missing capability.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Idempotency key reused with a conflicting payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limited. Inspect the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-required-capability":"leads:write"}},"/api/v1/customers":{"get":{"operationId":"listCustomers","summary":"List customers (keyset pagination)","tags":["Customers"],"description":"Returns up to 25 customers per page, ordered by `created_at` desc. Pass `cursor` to fetch subsequent pages. The cursor is opaque and stable across requests.","parameters":[{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque cursor returned by the previous response (`next_cursor`)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":25}}],"responses":{"200":{"description":"Page of customers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerListResponse"}}}},"401":{"description":"Unauthenticated. Authorization header missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Missing capability.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limited. Inspect the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-required-capability":"customers:read"},"post":{"operationId":"createCustomer","summary":"Create a customer (alias of /leads with status='converted')","tags":["Customers"],"description":"v1 stub: behaves identically to `POST /api/v1/leads` except `lead_status` is set to `converted`. Reserved as a separate route for forward-compatibility.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Caller-supplied UUID. Re-sending the same key within 24h returns the original 201 payload (no duplicate row). Scoped per tenant.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeadCreate"}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeadResponse"}}}},"401":{"description":"Unauthenticated. Authorization header missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Missing capability.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limited. Inspect the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-required-capability":"customers:write"}},"/api/v1/customers/{id}":{"get":{"operationId":"getCustomer","summary":"Get a single customer","tags":["Customers"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Customer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Customer"}}}},"401":{"description":"Unauthenticated. Authorization header missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Missing capability.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limited. Inspect the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-required-capability":"customers:read"}},"/api/v1/appointments":{"get":{"operationId":"listAppointments","summary":"List appointments (v1 stub — 501)","tags":["Appointments"],"description":"Spec exists for forward-compatibility; the handler returns `501 Not Implemented` in v1. Will be filled in once tenant demand surfaces.","responses":{"401":{"description":"Unauthenticated. Authorization header missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Missing capability.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limited. Inspect the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"501":{"description":"Not implemented in v1.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-required-capability":"appointments:read"}}}}