{"openapi":"3.1.0","info":{"title":"Nolmy (Appointment Management System) API","version":"1.0.0","description":"Machine-readable REST API for discovering services, checking real-time appointment availability, and automating bookings with verified local businesses and independent professionals.","contact":{"name":"Nolmy API Support","url":"https://nolmy.com/developers","email":"support@nolmy.com"},"license":{"name":"MIT"},"x-versioning-policy":"There is one API surface today, reported on every marketplace discovery response via the API-Version response header (currently \"1\") — there is no /v1 URL prefix because there is only one version to distinguish it from. A breaking change (removed field, changed type, changed auth requirement) will ship at a new URL prefix (/v2/...) rather than silently altering this one; additive, non-breaking changes (a new optional field or endpoint) do not bump the version. A deprecated endpoint or field will carry a Deprecation response header and a Sunset header naming the removal date before it disappears — nothing in this API is removed without both."},"servers":[{"url":"https://nolmy.com","description":"Primary Production / Staging Server"}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Self-serve, read-only API key (POST /api/keys). Grants exactly the scopes listed in x-scopes below — discovery data that is already public with no key at all. Cannot be used to create, reschedule, or cancel appointments.","x-scopes":{"businesses:read":"Read business profiles, locations, and working hours","professionals:read":"Read professional profiles, services, and ratings","services:read":"Browse services catalog, categories, and pricing","availability:read":"Check available real-time time slots for businesses and professionals","reviews:read":"Read verified client reviews and ratings"}},"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"User session token (the same token issued at login), passed as a Bearer token. Required for booking, rescheduling, cancelling, and any other write or user-scoped action."}},"headers":{"API-Version":{"description":"The API surface version serving this response. See info[\"x-versioning-policy\"] below.","schema":{"type":"string","example":"1"}},"RateLimit-Limit":{"description":"Total requests allowed per day for the presented API key. Only present when a valid X-API-Key was sent — an anonymous caller has no per-caller quota to report.","schema":{"type":"integer","example":1000}},"RateLimit-Remaining":{"description":"Requests left in the current daily window for the presented key.","schema":{"type":"integer","example":997}},"RateLimit-Reset":{"description":"Seconds until the window resets and the count returns to zero.","schema":{"type":"integer","example":86000}},"Retry-After":{"description":"Seconds to wait before retrying. Present on 429 (API key over quota) and on the transient 503 a full database connection pool returns.","schema":{"type":"integer","example":120}}},"schemas":{"Error":{"type":"object","description":"Every error response in this API — auth, validation, business rules, and unexpected failures alike — has exactly this shape. `error` is always machine-readable; there is no separate human-readable message field because the code is the message (see lib/server/errorCodes.ts). Parameterised codes append their argument after a colon, e.g. `CANCEL_WINDOW:24` or `RATE_LIMITED:120`.","properties":{"error":{"type":"string","description":"A known code from this list, optionally followed by `:<arg>` for a parameterised one (e.g. `RATE_LIMITED:120`). An unrecognized error is never leaked to the client — anything unexpected is masked as SERVER_ERROR.","examples":["PAYMENT_PENDING","ACCOUNT_HAS_BOOKINGS","AUTH_REQUIRED","FORBIDDEN","INVALID_API_KEY","INVALID_CREDENTIALS","EMAIL_TAKEN","RESET_LINK_INVALID","ACCOUNT_DELETE_UNSUPPORTED","NOT_FOUND","APPOINTMENT_NOT_FOUND","BUSINESS_NOT_FOUND","PROFESSIONAL_NOT_FOUND","SERVICE_NOT_FOUND","CLIENT_NOT_FOUND","USER_NOT_FOUND","MISSING_FIELDS","INVALID_INPUT","INVALID_DATE_FORMAT","INVALID_TIME_FORMAT","INVALID_STATUS","PASSWORD_TOO_SHORT","CRM_CLIENT_STAFF_ONLY","PREFERRED_REMINDER_CHANNEL_UNAVAILABLE","BUSINESS_UNDER_REVIEW","PROFILE_INCOMPLETE","BUSINESS_NOT_SUBMITTABLE","BUSINESS_SUSPENDED","BUSINESS_INVALID_TRANSITION","CANNOT_REPORT_OWN_CONTENT","REPORT_ALREADY_RESOLVED","MODERATION_INVALID_STATE","SUPPORT_LINK_INVALID","SUPPORT_INVALID_STATE","SUPPORT_ASSIGNEE_INVALID","SLOT_TAKEN","SESSION_FULL","APPOINTMENT_IN_PAST","RESCHEDULE_IN_PAST","PROFESSIONAL_TIME_OFF","CLIENT_DOUBLE_BOOKED","NOT_SCHEDULED","OUTSIDE_AVAILABILITY","PROFESSIONAL_NOT_AT_BUSINESS","SERVICE_NOT_OFFERED","SERVICE_NOT_OWNED","SERVICE_IN_USE","CANCEL_WINDOW","RESCHEDULE_WINDOW","NOSHOW_NOT_CONFIRMED","NOSHOW_TOO_EARLY","COMPLETE_TOO_EARLY","NOSHOW_LATE_PENDING","NOSHOW_LATE_GRACE","REVIEW_EXISTS","REVIEW_NOT_COMPLETED","REVIEW_NOT_AUTHORIZED","AVAILABILITY_OVERLAP","TIMEOFF_OVERLAP","SCHEDULE_TIME_TAKEN","REQUEST_PENDING","ALERT_ALREADY_RESPONDED","ALERT_INVALID_RESPONSE","ALERT_NOT_RESPONDABLE","AMOUNT_REQUIRED","PAYMENT_NOT_MOCK","PAYMENT_ALREADY_SETTLED","PAYMENT_AMOUNT_MISMATCH","RATE_LIMITED","DB_UNAVAILABLE","SERIALIZATION_CONFLICT","SERVER_ERROR","COUPON_NOT_FOUND","COUPON_CODE_EXISTS","COUPON_INACTIVE","COUPON_NOT_YET_VALID","COUPON_EXPIRED","USAGE_LIMIT_REACHED","CUSTOMER_LIMIT_REACHED","FIRST_TIME_ONLY","BUSINESS_MISMATCH","SERVICE_NOT_ELIGIBLE","CURRENCY_MISMATCH","MIN_SPEND_NOT_MET","INVALID_COUPON_VALUE","INVALID_CODE","INVALID_TYPE","INVALID_VALUE","INVALID_CURRENCY","INVALID_SERVICES","INVALID_BUSINESS_ID"]}},"required":["error"]},"Business":{"type":"object","description":"app/api/businesses/route.ts — serializeBusiness()","properties":{"id":{"type":"string"},"name":{"type":"string"},"workingHours":{"type":"object","properties":{"start":{"type":"string","example":"09:00"},"end":{"type":"string","example":"17:00"}}},"breaks":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string"},"end":{"type":"string"}}}},"userId":{"type":["string","null"]},"cancellationWindowHours":{"type":"integer"},"description":{"type":["string","null"]},"addressLine1":{"type":["string","null"]},"city":{"type":["string","null"]},"state":{"type":["string","null"]},"country":{"type":["string","null"]},"postalCode":{"type":["string","null"]},"timezone":{"type":"string","example":"America/New_York"},"currency":{"type":"string","example":"USD"},"cityKey":{"type":["string","null"],"description":"Normalized match key — internal, but present in every response."},"countryKey":{"type":["string","null"]},"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"image":{"type":["string","null"]}},"required":["id","name","timezone","currency"]},"Professional":{"type":"object","description":"app/api/professionals/route.ts GET","properties":{"id":{"type":"string"},"name":{"type":"string"},"role":{"type":"string"},"bio":{"type":"string"},"image":{"type":["string","null"]},"services":{"type":"array","items":{"type":"string"},"description":"Service ids offered by this professional."},"workingHours":{"type":"object","properties":{"start":{"type":"string"},"end":{"type":"string"}}},"breaks":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string"},"end":{"type":"string"}}}},"businessIds":{"type":"array","items":{"type":"string"}},"rating":{"type":["number","null"],"description":"Denormalized average, null with no reviews yet."},"reviewCount":{"type":"integer"}},"required":["id","name"]},"Service":{"type":"object","description":"app/api/services/route.ts — serialize()","properties":{"id":{"type":"string"},"name":{"type":"string"},"duration":{"type":"integer","description":"Minutes."},"description":{"type":"string"},"price":{"type":"number","description":"Absent (not null) when unset."},"active":{"type":"boolean"},"parent":{"type":["string","null"],"description":"Category name, when this is a sub-service."},"custom":{"type":"boolean"},"capacity":{"type":"integer","description":"Seats per session — 1 for an ordinary 1:1 service, more for a class."},"businessId":{"type":"string"},"ownerProfessionalId":{"type":"string","description":"Set instead of businessId for a professional's own service."},"professionalIds":{"type":"array","items":{"type":"string"}},"canManage":{"type":"boolean","description":"True when the caller's own session may edit this row. Always false for an anonymous or API-key caller."}},"required":["id","name","duration","active","capacity"]},"AvailabilitySlot":{"type":"object","description":"app/api/availability/route.ts, via lib/engines/slotGrid.ts","properties":{"time":{"type":"string","example":"09:00"},"available":{"type":"boolean"},"reason":{"type":"string","description":"Present only when available is false.","enum":["BOOKED","PROFESSIONAL_BREAK","BUSINESS_CLOSED","OUTSIDE_HOURS","TIME_OFF"]}},"required":["time","available"]},"Appointment":{"type":"object","description":"app/api/appointments/route.ts, via lib/server/appointmentView.ts — the owner view (a caller who owns or manages this booking). A caller who can only see that a slot is taken gets a masked subset instead (no client, notes, or payment fields).","properties":{"id":{"type":"string"},"client":{"type":"string"},"clientId":{"type":"string"},"crmClientId":{"type":"string"},"professional":{"type":"string","description":"Professional id."},"business":{"type":"string","description":"Business id."},"date":{"type":"string","format":"date"},"start":{"type":"string"},"end":{"type":"string"},"time":{"type":"string","description":"Same value as start, kept for backward compatibility."},"services":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["Pending","Confirmed","Cancelled","Completed","NoShow"]},"notes":{"type":"string","description":"Customer-visible booking note."},"staffNotes":{"type":"string","description":"Internal staff/business note. Returned only to business/professional callers who own the booking; never returned to customer callers or masked availability views."},"paymentStatus":{"type":"string","enum":["unpaid","paid","refunded"],"description":"The stored payment status. It is NOT proof that money moved online: when paymentIntentId is absent or starts with pi_mock_, nothing was charged and the customer pays the business directly at the appointment. Only a provider reference (pi_… or cs_…) means an online payment was received. A cancelled booking that is still 'paid' with a provider reference has not been refunded yet."},"paymentIntentId":{"type":"string","description":"The payment provider's reference. Absent, 'pending' or starting with pi_mock_ means no online payment was taken. Read-only: no request can set it."},"recurringGroupId":{"type":"string"},"timezone":{"type":"string"},"startUtc":{"type":"string","format":"date-time"},"bookedServices":{"type":"array","items":{"type":"object"},"description":"Priced service snapshot at booking time — present only when one was captured."},"currency":{"type":"string"},"discount":{"type":"object","description":"Present only when a coupon was applied at booking time.","properties":{"couponCode":{"type":"string"},"subtotalMajor":{"type":"number"},"discountMajor":{"type":"number"},"finalMajor":{"type":"number"}}},"packageRedemption":{"type":"object","description":"Present only when a service package session was redeemed at booking time.","properties":{"packagePurchaseId":{"type":"string"},"subtotalMajor":{"type":"number"},"finalMajor":{"type":"number","example":0},"sessionsTotal":{"type":"integer"}}}},"required":["id","professional","business","date","start","end","status"]},"ServicePackageDefinition":{"type":"object","description":"app/api/business/packages/route.ts GET/POST/PATCH — business-authored service-session package definition. This is an entitlement package, not a stored cash wallet.","properties":{"id":{"type":"string"},"businessId":{"type":"string"},"serviceId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"sessionCount":{"type":"integer","minimum":1},"packagePriceMajor":{"type":"number"},"singleSessionPriceMajor":{"type":"number"},"currency":{"type":"string","example":"USD"},"validityDays":{"type":"integer","minimum":1},"isActive":{"type":"boolean"},"purchaseCount":{"type":"integer","description":"Present on GET list responses."},"savings":{"type":"object","properties":{"totalSavingsMajor":{"type":"number"},"savingsPercent":{"type":"number"}}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","businessId","serviceId","title","sessionCount","packagePriceMajor","singleSessionPriceMajor","currency","validityDays","isActive"]},"ServicePackagePurchase":{"type":"object","description":"app/api/business/packages/route.ts PUT — a purchased package entitlement assigned to a customer account.","properties":{"id":{"type":"string"},"packageId":{"type":"string"},"businessId":{"type":"string"},"userId":{"type":"string"},"serviceId":{"type":"string"},"sessionsTotal":{"type":"integer"},"sessionsRedeemed":{"type":"integer"},"packagePriceMajor":{"type":"number"},"singleSessionPriceMajor":{"type":"number"},"currency":{"type":"string"},"purchasedAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["active","exhausted","expired","cancelled"]},"createdById":{"type":["string","null"]}},"required":["id","packageId","businessId","userId","serviceId","sessionsTotal","sessionsRedeemed","currency","expiresAt","status"]},"CustomerPackageBalance":{"type":"object","description":"app/api/packages/route.ts GET — active package balance available to the authenticated customer for booking selection.","properties":{"id":{"type":"string","description":"ServicePackagePurchase id; pass as packagePurchaseId when creating an appointment."},"packageId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"businessId":{"type":"string"},"businessName":{"type":"string"},"serviceId":{"type":"string"},"serviceName":{"type":"string"},"sessionsTotal":{"type":"integer"},"sessionsRedeemed":{"type":"integer"},"sessionsRemaining":{"type":"integer"},"packagePriceMajor":{"type":"number"},"singleSessionPriceMajor":{"type":"number"},"currency":{"type":"string"},"purchasedAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"redemptionCount":{"type":"integer"}},"required":["id","packageId","title","businessId","serviceId","sessionsTotal","sessionsRedeemed","sessionsRemaining","currency","expiresAt"]},"Review":{"type":"object","description":"app/api/reviews/route.ts","properties":{"id":{"type":"string"},"rating":{"type":"integer","minimum":1,"maximum":5},"comment":{"type":["string","null"]},"clientId":{"type":"string"},"clientName":{"type":"string"},"appointmentId":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}},"required":["id","rating","clientId","appointmentId","createdAt"]},"ApiKeyCreated":{"type":"object","description":"app/api/keys/route.ts POST — GeneratedKeyResponse","properties":{"apiKey":{"type":"string","description":"Shown once, never recoverable again — only its hash is stored server-side.","example":"ams_test_..."},"name":{"type":"string"},"tier":{"type":"string","example":"free"},"environment":{"type":"string","enum":["sandbox","live"]},"rateLimit":{"type":"string","example":"1,000 requests per day"},"status":{"type":"string","example":"active"},"scopes":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"instructions":{"type":"string"},"docsUrl":{"type":"string","format":"uri"}},"required":["apiKey","name","environment","scopes","createdAt"]},"ApiKeyOnboarding":{"type":"object","description":"app/api/keys/route.ts GET","properties":{"service":{"type":"string"},"onboarding":{"type":"string"},"freeTier":{"type":"object","properties":{"rateLimit":{"type":"string"},"price":{"type":"string"},"creditCardRequired":{"type":"boolean"},"sandboxEnvironment":{"type":"boolean"}}},"scopes":{"type":"array","items":{"type":"string"}},"scopesNote":{"type":"string"},"generateKeyEndpoint":{"type":"object","properties":{"method":{"type":"string"},"path":{"type":"string"},"samplePayload":{"type":"object"}}},"documentation":{"type":"string","format":"uri"},"openApi":{"type":"string","format":"uri"},"llmsTxt":{"type":"string","format":"uri"}}}}},"paths":{"/api/businesses":{"get":{"operationId":"listBusinesses","summary":"List or search businesses","description":"Query public business profiles, filtered by city, category, or search query. Public — an API key is optional and only adds attribution/quota tracking.","security":[{"ApiKeyAuth":[]},{}],"parameters":[{"name":"city","in":"query","schema":{"type":"string"},"description":"Filter by city"},{"name":"query","in":"query","schema":{"type":"string"},"description":"Text search"}],"responses":{"200":{"description":"An array of businesses, or a single business object when filtered by userId to exactly one match.","headers":{"API-Version":{"description":"The API surface version serving this response. See info[\"x-versioning-policy\"] below.","schema":{"type":"string","example":"1"}},"RateLimit-Limit":{"description":"Total requests allowed per day for the presented API key. Only present when a valid X-API-Key was sent — an anonymous caller has no per-caller quota to report.","schema":{"type":"integer","example":1000}},"RateLimit-Remaining":{"description":"Requests left in the current daily window for the presented key.","schema":{"type":"integer","example":997}},"RateLimit-Reset":{"description":"Seconds until the window resets and the count returns to zero.","schema":{"type":"integer","example":86000}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/Business"}},{"$ref":"#/components/schemas/Business"}]}}}},"401":{"description":"An X-API-Key header was sent but is invalid or revoked","headers":{"API-Version":{"description":"The API surface version serving this response. See info[\"x-versioning-policy\"] below.","schema":{"type":"string","example":"1"}},"RateLimit-Limit":{"description":"Total requests allowed per day for the presented API key. Only present when a valid X-API-Key was sent — an anonymous caller has no per-caller quota to report.","schema":{"type":"integer","example":1000}},"RateLimit-Remaining":{"description":"Requests left in the current daily window for the presented key.","schema":{"type":"integer","example":997}},"RateLimit-Reset":{"description":"Seconds until the window resets and the count returns to zero.","schema":{"type":"integer","example":86000}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The presented API key is over its daily quota","headers":{"API-Version":{"description":"The API surface version serving this response. See info[\"x-versioning-policy\"] below.","schema":{"type":"string","example":"1"}},"RateLimit-Limit":{"description":"Total requests allowed per day for the presented API key. Only present when a valid X-API-Key was sent — an anonymous caller has no per-caller quota to report.","schema":{"type":"integer","example":1000}},"RateLimit-Remaining":{"description":"Requests left in the current daily window for the presented key.","schema":{"type":"integer","example":997}},"RateLimit-Reset":{"description":"Seconds until the window resets and the count returns to zero.","schema":{"type":"integer","example":86000}},"Retry-After":{"description":"Seconds to wait before retrying. Present on 429 (API key over quota) and on the transient 503 a full database connection pool returns.","schema":{"type":"integer","example":120}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any other error not enumerated above. Always the same typed shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/professionals":{"get":{"operationId":"listProfessionals","summary":"List professionals","description":"Query verified professionals, roles, business associations, and ratings. Public — an API key is optional.","security":[{"ApiKeyAuth":[]},{}],"parameters":[{"name":"businessId","in":"query","schema":{"type":"string"},"description":"Filter by business"},{"name":"serviceId","in":"query","schema":{"type":"string"},"description":"Filter by offered service"}],"responses":{"200":{"description":"An array of professionals, or a single professional object when filtered by userId to exactly one match.","headers":{"API-Version":{"description":"The API surface version serving this response. See info[\"x-versioning-policy\"] below.","schema":{"type":"string","example":"1"}},"RateLimit-Limit":{"description":"Total requests allowed per day for the presented API key. Only present when a valid X-API-Key was sent — an anonymous caller has no per-caller quota to report.","schema":{"type":"integer","example":1000}},"RateLimit-Remaining":{"description":"Requests left in the current daily window for the presented key.","schema":{"type":"integer","example":997}},"RateLimit-Reset":{"description":"Seconds until the window resets and the count returns to zero.","schema":{"type":"integer","example":86000}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/Professional"}},{"$ref":"#/components/schemas/Professional"}]}}}},"401":{"description":"An X-API-Key header was sent but is invalid or revoked","headers":{"API-Version":{"description":"The API surface version serving this response. See info[\"x-versioning-policy\"] below.","schema":{"type":"string","example":"1"}},"RateLimit-Limit":{"description":"Total requests allowed per day for the presented API key. Only present when a valid X-API-Key was sent — an anonymous caller has no per-caller quota to report.","schema":{"type":"integer","example":1000}},"RateLimit-Remaining":{"description":"Requests left in the current daily window for the presented key.","schema":{"type":"integer","example":997}},"RateLimit-Reset":{"description":"Seconds until the window resets and the count returns to zero.","schema":{"type":"integer","example":86000}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The presented API key is over its daily quota","headers":{"API-Version":{"description":"The API surface version serving this response. See info[\"x-versioning-policy\"] below.","schema":{"type":"string","example":"1"}},"RateLimit-Limit":{"description":"Total requests allowed per day for the presented API key. Only present when a valid X-API-Key was sent — an anonymous caller has no per-caller quota to report.","schema":{"type":"integer","example":1000}},"RateLimit-Remaining":{"description":"Requests left in the current daily window for the presented key.","schema":{"type":"integer","example":997}},"RateLimit-Reset":{"description":"Seconds until the window resets and the count returns to zero.","schema":{"type":"integer","example":86000}},"Retry-After":{"description":"Seconds to wait before retrying. Present on 429 (API key over quota) and on the transient 503 a full database connection pool returns.","schema":{"type":"integer","example":120}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any other error not enumerated above. Always the same typed shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/services":{"get":{"operationId":"listServices","summary":"List services catalog","description":"Query active services with duration, price, category, and description. Public — an API key is optional.","security":[{"ApiKeyAuth":[]},{}],"parameters":[{"name":"category","in":"query","schema":{"type":"string"},"description":"Filter by category"},{"name":"businessId","in":"query","schema":{"type":"string"},"description":"Filter by business"}],"responses":{"200":{"description":"Array of services","headers":{"API-Version":{"description":"The API surface version serving this response. See info[\"x-versioning-policy\"] below.","schema":{"type":"string","example":"1"}},"RateLimit-Limit":{"description":"Total requests allowed per day for the presented API key. Only present when a valid X-API-Key was sent — an anonymous caller has no per-caller quota to report.","schema":{"type":"integer","example":1000}},"RateLimit-Remaining":{"description":"Requests left in the current daily window for the presented key.","schema":{"type":"integer","example":997}},"RateLimit-Reset":{"description":"Seconds until the window resets and the count returns to zero.","schema":{"type":"integer","example":86000}}},"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Service"}}}}},"401":{"description":"An X-API-Key header was sent but is invalid or revoked","headers":{"API-Version":{"description":"The API surface version serving this response. See info[\"x-versioning-policy\"] below.","schema":{"type":"string","example":"1"}},"RateLimit-Limit":{"description":"Total requests allowed per day for the presented API key. Only present when a valid X-API-Key was sent — an anonymous caller has no per-caller quota to report.","schema":{"type":"integer","example":1000}},"RateLimit-Remaining":{"description":"Requests left in the current daily window for the presented key.","schema":{"type":"integer","example":997}},"RateLimit-Reset":{"description":"Seconds until the window resets and the count returns to zero.","schema":{"type":"integer","example":86000}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The presented API key is over its daily quota","headers":{"API-Version":{"description":"The API surface version serving this response. See info[\"x-versioning-policy\"] below.","schema":{"type":"string","example":"1"}},"RateLimit-Limit":{"description":"Total requests allowed per day for the presented API key. Only present when a valid X-API-Key was sent — an anonymous caller has no per-caller quota to report.","schema":{"type":"integer","example":1000}},"RateLimit-Remaining":{"description":"Requests left in the current daily window for the presented key.","schema":{"type":"integer","example":997}},"RateLimit-Reset":{"description":"Seconds until the window resets and the count returns to zero.","schema":{"type":"integer","example":86000}},"Retry-After":{"description":"Seconds to wait before retrying. Present on 429 (API key over quota) and on the transient 503 a full database connection pool returns.","schema":{"type":"integer","example":120}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any other error not enumerated above. Always the same typed shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/availability":{"get":{"operationId":"checkAvailability","summary":"Check appointment slot availability","description":"Calculate conflict-free booking slots for a given professional/business on a specific date. Public — an API key is optional.","security":[{"ApiKeyAuth":[]},{}],"parameters":[{"name":"professionalId","in":"query","required":true,"schema":{"type":"string"}},{"name":"businessId","in":"query","required":true,"schema":{"type":"string"}},{"name":"date","in":"query","required":true,"schema":{"type":"string","format":"date"}},{"name":"serviceId","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"List of time slots with availability flags","headers":{"API-Version":{"description":"The API surface version serving this response. See info[\"x-versioning-policy\"] below.","schema":{"type":"string","example":"1"}},"RateLimit-Limit":{"description":"Total requests allowed per day for the presented API key. Only present when a valid X-API-Key was sent — an anonymous caller has no per-caller quota to report.","schema":{"type":"integer","example":1000}},"RateLimit-Remaining":{"description":"Requests left in the current daily window for the presented key.","schema":{"type":"integer","example":997}},"RateLimit-Reset":{"description":"Seconds until the window resets and the count returns to zero.","schema":{"type":"integer","example":86000}}},"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AvailabilitySlot"}}}}},"400":{"description":"Missing required query parameters, or the service/professional/business combination is invalid","headers":{"API-Version":{"description":"The API surface version serving this response. See info[\"x-versioning-policy\"] below.","schema":{"type":"string","example":"1"}},"RateLimit-Limit":{"description":"Total requests allowed per day for the presented API key. Only present when a valid X-API-Key was sent — an anonymous caller has no per-caller quota to report.","schema":{"type":"integer","example":1000}},"RateLimit-Remaining":{"description":"Requests left in the current daily window for the presented key.","schema":{"type":"integer","example":997}},"RateLimit-Reset":{"description":"Seconds until the window resets and the count returns to zero.","schema":{"type":"integer","example":86000}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"An X-API-Key header was sent but is invalid or revoked","headers":{"API-Version":{"description":"The API surface version serving this response. See info[\"x-versioning-policy\"] below.","schema":{"type":"string","example":"1"}},"RateLimit-Limit":{"description":"Total requests allowed per day for the presented API key. Only present when a valid X-API-Key was sent — an anonymous caller has no per-caller quota to report.","schema":{"type":"integer","example":1000}},"RateLimit-Remaining":{"description":"Requests left in the current daily window for the presented key.","schema":{"type":"integer","example":997}},"RateLimit-Reset":{"description":"Seconds until the window resets and the count returns to zero.","schema":{"type":"integer","example":86000}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No professional or business matches the given ids","headers":{"API-Version":{"description":"The API surface version serving this response. See info[\"x-versioning-policy\"] below.","schema":{"type":"string","example":"1"}},"RateLimit-Limit":{"description":"Total requests allowed per day for the presented API key. Only present when a valid X-API-Key was sent — an anonymous caller has no per-caller quota to report.","schema":{"type":"integer","example":1000}},"RateLimit-Remaining":{"description":"Requests left in the current daily window for the presented key.","schema":{"type":"integer","example":997}},"RateLimit-Reset":{"description":"Seconds until the window resets and the count returns to zero.","schema":{"type":"integer","example":86000}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The presented API key is over its daily quota","headers":{"API-Version":{"description":"The API surface version serving this response. See info[\"x-versioning-policy\"] below.","schema":{"type":"string","example":"1"}},"RateLimit-Limit":{"description":"Total requests allowed per day for the presented API key. Only present when a valid X-API-Key was sent — an anonymous caller has no per-caller quota to report.","schema":{"type":"integer","example":1000}},"RateLimit-Remaining":{"description":"Requests left in the current daily window for the presented key.","schema":{"type":"integer","example":997}},"RateLimit-Reset":{"description":"Seconds until the window resets and the count returns to zero.","schema":{"type":"integer","example":86000}},"Retry-After":{"description":"Seconds to wait before retrying. Present on 429 (API key over quota) and on the transient 503 a full database connection pool returns.","schema":{"type":"integer","example":120}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any other error not enumerated above. Always the same typed shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/reviews":{"get":{"operationId":"listReviews","summary":"List verified reviews for a professional","description":"Read verified client reviews and ratings for a professional. Public — an API key is optional.","security":[{"ApiKeyAuth":[]},{}],"parameters":[{"name":"professionalId","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Array of reviews, most recent first","headers":{"API-Version":{"description":"The API surface version serving this response. See info[\"x-versioning-policy\"] below.","schema":{"type":"string","example":"1"}},"RateLimit-Limit":{"description":"Total requests allowed per day for the presented API key. Only present when a valid X-API-Key was sent — an anonymous caller has no per-caller quota to report.","schema":{"type":"integer","example":1000}},"RateLimit-Remaining":{"description":"Requests left in the current daily window for the presented key.","schema":{"type":"integer","example":997}},"RateLimit-Reset":{"description":"Seconds until the window resets and the count returns to zero.","schema":{"type":"integer","example":86000}}},"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Review"}}}}},"400":{"description":"Missing professionalId","headers":{"API-Version":{"description":"The API surface version serving this response. See info[\"x-versioning-policy\"] below.","schema":{"type":"string","example":"1"}},"RateLimit-Limit":{"description":"Total requests allowed per day for the presented API key. Only present when a valid X-API-Key was sent — an anonymous caller has no per-caller quota to report.","schema":{"type":"integer","example":1000}},"RateLimit-Remaining":{"description":"Requests left in the current daily window for the presented key.","schema":{"type":"integer","example":997}},"RateLimit-Reset":{"description":"Seconds until the window resets and the count returns to zero.","schema":{"type":"integer","example":86000}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"An X-API-Key header was sent but is invalid or revoked","headers":{"API-Version":{"description":"The API surface version serving this response. See info[\"x-versioning-policy\"] below.","schema":{"type":"string","example":"1"}},"RateLimit-Limit":{"description":"Total requests allowed per day for the presented API key. Only present when a valid X-API-Key was sent — an anonymous caller has no per-caller quota to report.","schema":{"type":"integer","example":1000}},"RateLimit-Remaining":{"description":"Requests left in the current daily window for the presented key.","schema":{"type":"integer","example":997}},"RateLimit-Reset":{"description":"Seconds until the window resets and the count returns to zero.","schema":{"type":"integer","example":86000}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"The presented API key is over its daily quota","headers":{"API-Version":{"description":"The API surface version serving this response. See info[\"x-versioning-policy\"] below.","schema":{"type":"string","example":"1"}},"RateLimit-Limit":{"description":"Total requests allowed per day for the presented API key. Only present when a valid X-API-Key was sent — an anonymous caller has no per-caller quota to report.","schema":{"type":"integer","example":1000}},"RateLimit-Remaining":{"description":"Requests left in the current daily window for the presented key.","schema":{"type":"integer","example":997}},"RateLimit-Reset":{"description":"Seconds until the window resets and the count returns to zero.","schema":{"type":"integer","example":86000}},"Retry-After":{"description":"Seconds to wait before retrying. Present on 429 (API key over quota) and on the transient 503 a full database connection pool returns.","schema":{"type":"integer","example":120}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any other error not enumerated above. Always the same typed shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/appointments":{"get":{"operationId":"listAppointments","summary":"List appointments","description":"List the authenticated user's own bookings, or a business's appointments. Requires a real user session — not available to a bare API key.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Array of appointments","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Appointment"}}}}},"401":{"description":"Missing or invalid session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Database connection pool temporarily exhausted — retryable","headers":{"Retry-After":{"description":"Seconds to wait before retrying. Present on 429 (API key over quota) and on the transient 503 a full database connection pool returns.","schema":{"type":"integer","example":120}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any other error not enumerated above. Always the same typed shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createAppointment","summary":"Create an appointment","description":"Schedule a new appointment for a client with a professional. Requires a real user session — not available to a bare API key. An agent booking on behalf of a user must present that user's own session Bearer token.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["professional","business","date","start","services"],"properties":{"professional":{"type":"string","description":"Professional id."},"business":{"type":"string","description":"Business id."},"date":{"type":"string","format":"date"},"start":{"type":"string","example":"14:00"},"services":{"type":"array","items":{"type":"string"},"description":"One or more service ids."},"notes":{"type":"string"},"familyMemberId":{"type":"string","description":"Customer booking on behalf of a saved family member."},"recurringFrequency":{"type":"string","enum":["none","weekly","biweekly"]},"recurringCount":{"type":"integer","minimum":1,"maximum":12},"couponCode":{"type":"string","description":"Optional coupon code. Mutually exclusive with packagePurchaseId."},"packagePurchaseId":{"type":"string","description":"Optional customer package purchase id from GET /api/packages. Redeems one matching service-session entitlement on the first occurrence only. Mutually exclusive with couponCode."}}}}}},"responses":{"201":{"description":"Created appointment details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Appointment"}}}},"401":{"description":"Missing or invalid session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The requested slot was just taken by another booking (SLOT_TAKEN) or the session is full","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any other error not enumerated above. Always the same typed shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/business/packages":{"get":{"operationId":"listBusinessPackages","summary":"List service packages for the authenticated business","description":"Returns package definitions owned by the signed-in business, including purchase counts and savings calculations. Requires a business user session.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Package definitions for the business","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ServicePackageDefinition"}}}}},"401":{"description":"Missing or invalid session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Signed-in user is not a business","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any other error not enumerated above. Always the same typed shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createBusinessPackage","summary":"Create a service package definition","description":"Creates a non-wallet service-session package definition for one service owned by the signed-in business.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["serviceId","title","sessionCount","packagePriceMajor","singleSessionPriceMajor","currency","validityDays"],"properties":{"serviceId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"sessionCount":{"type":"integer","minimum":1,"maximum":100},"packagePriceMajor":{"type":"number","minimum":0},"singleSessionPriceMajor":{"type":"number","minimum":0},"currency":{"type":"string","example":"USD"},"validityDays":{"type":"integer","minimum":1,"maximum":3650}}}}}},"responses":{"201":{"description":"Created package definition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServicePackageDefinition"}}}},"400":{"description":"Invalid package fields or service ownership mismatch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Signed-in user is not a business","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any other error not enumerated above. Always the same typed shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updateBusinessPackage","summary":"Update a service package definition","description":"Updates a package definition owned by the signed-in business, including activation/deactivation.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["id"],"properties":{"id":{"type":"string"},"serviceId":{"type":"string"},"title":{"type":"string"},"description":{"type":["string","null"]},"sessionCount":{"type":"integer","minimum":1,"maximum":100},"packagePriceMajor":{"type":"number","minimum":0},"singleSessionPriceMajor":{"type":"number","minimum":0},"currency":{"type":"string"},"validityDays":{"type":"integer","minimum":1,"maximum":3650},"isActive":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated package definition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServicePackageDefinition"}}}},"400":{"description":"Invalid package fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Package not found for this business","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any other error not enumerated above. Always the same typed shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"operationId":"assignBusinessPackagePurchase","summary":"Assign a purchased package to a customer account","description":"Creates a ServicePackagePurchase entitlement for a customer account. In the pilot collect-at-location model this records a package the business has sold/assigned outside online checkout.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["packageId","userId"],"properties":{"packageId":{"type":"string"},"userId":{"type":"string","description":"Customer user id receiving the entitlement."}}}}}},"responses":{"201":{"description":"Created package purchase entitlement","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServicePackagePurchase"}}}},"400":{"description":"Missing fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Package or customer not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any other error not enumerated above. Always the same typed shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/packages":{"get":{"operationId":"listCustomerPackageBalances","summary":"List package balances for the authenticated customer","description":"Returns active, unexpired package purchase balances for the signed-in customer. Optional businessId/serviceId filters match the booking page selector. The returned id is the packagePurchaseId accepted by POST /api/appointments.","security":[{"BearerAuth":[]}],"parameters":[{"name":"businessId","in":"query","schema":{"type":"string"},"description":"Filter balances to one business."},{"name":"serviceId","in":"query","schema":{"type":"string"},"description":"Filter balances to one service."}],"responses":{"200":{"description":"Active package balances","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CustomerPackageBalance"}}}}},"401":{"description":"Missing or invalid session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Signed-in user is not a customer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"description":"Any other error not enumerated above. Always the same typed shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/keys":{"post":{"operationId":"createApiKey","summary":"Self-serve read-only API key generation for AI agents","description":"Generate an instant sandbox / free-tier API key without sales intervention. The key grants only the read-only ApiKeyAuth scopes above.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","default":"Agent Key"},"environment":{"type":"string","enum":["sandbox","live"],"default":"sandbox"}}}}}},"responses":{"201":{"description":"New API key with rate limit and granted scopes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreated"}}}},"default":{"description":"Any other error not enumerated above. Always the same typed shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"getApiKeyOnboarding","summary":"API key onboarding information","description":"Instructions on self-serve key generation, rate limits, and scopes.","responses":{"200":{"description":"Onboarding metadata","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyOnboarding"}}}},"default":{"description":"Any other error not enumerated above. Always the same typed shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}