{"openapi":"3.0.3","info":{"title":"PraxBook Salon API","version":"1.0.0","description":"PraxBook Pro API — read your services and availability, list and manage bookings, browse your CRM, and react to events. A small, predictable REST API over HTTPS that returns JSON. Authenticate with a Bearer API key minted in the console under Settings -> Embed & API."},"servers":[{"url":"https://booking.thenextbeacon.com"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}}},"security":[{"bearerAuth":[]}],"paths":{"/api/ext/v1/bookings":{"get":{"summary":"List your bookings, scoped to your team, ordered by id. Cursor-paginated: pass starting_after with the previous page's nextCursor to fetch the next page; stop when hasMore is false.","tags":["Bookings"],"parameters":[{"name":"limit","in":"query","required":false,"description":"Max rows to return (default 50, max 200).","schema":{"type":"string"}},{"name":"starting_after","in":"query","required":false,"description":"Booking id cursor — return rows after this id. Use the nextCursor from the previous page.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"uid":{"type":"string"},"id":{"type":"integer"},"title":{"type":"string"},"start":{"type":"string"},"end":{"type":"string"},"status":{"type":"string"},"serviceSlug":{"type":"string"},"amountChf":{"type":"number"},"clientName":{"type":"string"},"clientEmail":{"type":"string"},"clientPhone":{"type":"string"}}}},"hasMore":{"type":"boolean"},"nextCursor":{"type":"integer"}}},"example":{"data":[{"uid":"abc123def456ghi789jkl0","id":4821,"title":"Épilation Sourcils","start":"2026-06-01T08:00:00Z","end":"2026-06-01T08:30:00Z","status":"accepted","serviceSlug":"epilation-sourcils","amountChf":30.0,"clientName":"Marie Dupont","clientEmail":"marie@example.com","clientPhone":"+41 79 123 45 67"}],"hasMore":true,"nextCursor":4821}}}}}},"post":{"summary":"Create a booking. Identify the service by serviceSlug OR eventTypeId. The end time is computed from durationMin, or the service's default length when omitted. staffId and note are optional.","tags":["Bookings"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"serviceSlug":{"type":"string"},"eventTypeId":{"type":"integer"},"start":{"type":"string"},"durationMin":{"type":"integer"},"attendee":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"}}},"staffId":{"type":"integer"},"note":{"type":"string"}}},"example":{"serviceSlug":"epilation-sourcils","eventTypeId":102,"start":"2026-06-01T10:00:00","durationMin":30,"attendee":{"name":"Marie Dupont","email":"marie@example.com","phone":"+41 79 123 45 67"},"staffId":7,"note":"First visit — slight latex allergy."}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"uid":{"type":"string"},"id":{"type":"integer"},"status":{"type":"string"}}},"example":{"uid":"abc123def456ghi789jkl0","id":4821,"status":"accepted"}}}}}}},"/api/ext/v1/bookings/{uid}":{"get":{"summary":"Fetch a single booking by its uid.","tags":["Bookings"],"parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"uid":{"type":"string"},"id":{"type":"integer"},"title":{"type":"string"},"start":{"type":"string"},"end":{"type":"string"},"status":{"type":"string"},"serviceSlug":{"type":"string"},"amountChf":{"type":"number"},"clientName":{"type":"string"},"clientEmail":{"type":"string"},"clientPhone":{"type":"string"}}},"example":{"uid":"abc123def456ghi789jkl0","id":4821,"title":"Épilation Sourcils","start":"2026-06-01T08:00:00Z","end":"2026-06-01T08:30:00Z","status":"accepted","serviceSlug":"epilation-sourcils","amountChf":30.0,"clientName":"Marie Dupont","clientEmail":"marie@example.com","clientPhone":"+41 79 123 45 67"}}}}}}},"/api/ext/v1/bookings/{uid}/cancel":{"post":{"summary":"Cancel a booking by uid (sets its status to 'cancelled').","tags":["Bookings"],"parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"status":{"type":"string"}}},"example":{"ok":true,"status":"cancelled"}}}}}}},"/api/ext/v1/bookings/{uid}/reschedule":{"post":{"summary":"Move a booking to a new time. The end is recomputed from durationMin (or the service length); a staff clash returns 409.","tags":["Bookings"],"parameters":[{"name":"uid","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"start":{"type":"string"},"durationMin":{"type":"integer"}}},"example":{"start":"2026-06-02T11:45:00","durationMin":30}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"uid":{"type":"string"},"start":{"type":"string"},"end":{"type":"string"}}},"example":{"ok":true,"uid":"abc123def456ghi789jkl0","start":"2026-06-02T09:45:00Z","end":"2026-06-02T10:15:00Z"}}}}}}},"/api/ext/v1/services":{"get":{"summary":"List your bookable services.","tags":["Services"],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"services":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"title":{"type":"string"},"durationMinutes":{"type":"integer"},"price":{"type":"number"},"currency":{"type":"string"}}}}}},"example":{"services":[{"slug":"epilation-sourcils","title":"Épilation Sourcils","durationMinutes":30,"price":30.0,"currency":"CHF"}]}}}}}}},"/api/ext/v1/services/{slug}":{"get":{"summary":"Fetch a single service's detail by slug, including its specific bookingFields.","tags":["Services"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string"},"title":{"type":"string"},"durationMinutes":{"type":"integer"},"price":{"type":"number"},"currency":{"type":"string"},"description":{"type":"string"},"bookingFields":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"required":{"type":"boolean"},"label":{"type":"object","properties":{"en":{"type":"string"},"fr":{"type":"string"}}}}}}}},"example":{"slug":"epilation-sourcils","title":"Épilation Sourcils","durationMinutes":30,"price":30.0,"currency":"CHF","description":"Mise en forme des sourcils à la cire.","bookingFields":[{"name":"notes","type":"textarea","required":false,"label":{"en":"Notes","fr":"Notes / Précisions"}}]}}}}}},"patch":{"summary":"Update a service's price, duration and/or title. Send only the fields you want to change (at least one is required). price is in your salon's currency; duration is in minutes.","tags":["Services"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"price":{"type":"number"},"duration":{"type":"integer"},"title":{"type":"string"}}},"example":{"price":35.0,"duration":45,"title":"Épilation Sourcils"}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"slug":{"type":"string"},"price":{"type":"number"},"duration":{"type":"integer"},"title":{"type":"string"}}},"example":{"ok":true,"slug":"epilation-sourcils","price":35.0,"duration":45,"title":"Épilation Sourcils"}}}}}}},"/api/ext/v1/availability":{"get":{"summary":"Available start times for a service on a given day (ISO 8601 datetimes).","tags":["Services"],"parameters":[{"name":"serviceSlug","in":"query","required":false,"description":"Slug of the service to check.","schema":{"type":"string"}},{"name":"date","in":"query","required":false,"description":"Day to check, formatted YYYY-MM-DD.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"serviceSlug":{"type":"string"},"date":{"type":"string"},"slots":{"type":"array","items":{"type":"string"}}}},"example":{"serviceSlug":"epilation-sourcils","date":"2026-06-01","slots":["2026-06-01T10:00:00.000+02:00","2026-06-01T10:30:00.000+02:00"]}}}}}}},"/api/ext/v1/forms/global":{"get":{"summary":"Fetch the global booking questions (standard fields) with localization.","tags":["Forms"],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"fields":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"required":{"type":"boolean"},"label":{"type":"object","properties":{"en":{"type":"string"},"fr":{"type":"string"}}}}}}}},"example":{"fields":[{"name":"name","type":"name","required":true,"label":{"en":"Full name","fr":"Nom complet"}},{"name":"referralSource","type":"radio","required":false,"label":{"en":"How did you hear about us?","fr":"Comment avez-vous entendu parler de nous?"},"options":[{"label":{"en":"Instagram","fr":"Instagram"},"value":"instagram"}]}]}}}}}}},"/api/ext/v1/forms":{"get":{"summary":"List all active service-specific (intake) forms and their attachments.","tags":["Forms"],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"forms":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"description":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"required":{"type":"boolean"},"label":{"type":"object","properties":{"en":{"type":"string"},"fr":{"type":"string"}}}}}},"links":{"type":"array","items":{"type":"object","properties":{"scope":{"type":"string"},"ref":{"type":"string"}}}}}}}}},"example":{"forms":[{"id":1,"name":"Consent Form","description":"Lash lift consent","fields":[{"name":"allergy","type":"checkbox","required":true,"label":{"en":"I have no allergies","fr":"Je n'ai pas d'allergies"}}],"links":[{"scope":"service","ref":"rehaussement-cils"}]}]}}}}}}},"/api/ext/v1/clients":{"get":{"summary":"List your clients with visit counts and last-visit dates, ordered by id. Cursor-paginated: pass starting_after with the previous page's nextCursor to fetch the next page; stop when hasMore is false.","tags":["Clients"],"parameters":[{"name":"limit","in":"query","required":false,"description":"Max rows to return (default 50, max 200).","schema":{"type":"string"}},{"name":"starting_after","in":"query","required":false,"description":"Client id cursor — return rows after this id. Use the nextCursor from the previous page.","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Filter by name or email substring.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"email":{"type":"string"},"name":{"type":"string"},"phone":{"type":"string"},"totalVisits":{"type":"integer"},"lastVisit":{"type":"string"}}}},"hasMore":{"type":"boolean"},"nextCursor":{"type":"integer"}}},"example":{"data":[{"email":"marie@example.com","name":"Marie Dupont","phone":"+41 79 123 45 67","totalVisits":4,"lastVisit":"2026-05-12T14:30:00"}],"hasMore":true,"nextCursor":318}}}}}}},"/api/ext/v1/clients/{email}":{"get":{"summary":"A client's profile plus their recent bookings (team-scoped).","tags":["Clients"],"parameters":[{"name":"email","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"},"name":{"type":"string"},"phone":{"type":"string"},"notes":{"type":"string"},"birthDate":{"type":"string"},"totalVisits":{"type":"integer"},"bookings":{"type":"array","items":{"type":"object","properties":{"uid":{"type":"string"},"id":{"type":"integer"},"title":{"type":"string"},"start":{"type":"string"},"end":{"type":"string"},"status":{"type":"string"},"serviceSlug":{"type":"string"},"amountChf":{"type":"number"},"clientName":{"type":"string"},"clientEmail":{"type":"string"},"clientPhone":{"type":"string"}}}}}},"example":{"email":"marie@example.com","name":"Marie Dupont","phone":"+41 79 123 45 67","notes":"Prefers afternoon appointments.","birthDate":"1990-03-21","totalVisits":4,"bookings":[{"uid":"abc123def456ghi789jkl0","id":4821,"title":"Épilation Sourcils","start":"2026-05-12T12:30:00Z","end":"2026-05-12T13:00:00Z","status":"accepted","serviceSlug":"epilation-sourcils","amountChf":30.0,"clientName":"Marie Dupont","clientEmail":"marie@example.com","clientPhone":"+41 79 123 45 67"}]}}}}}}},"/api/ext/v1/business":{"get":{"summary":"Your salon's business profile (address, contact, hours, socials).","tags":["Catalog"],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"address":{"type":"string"},"street":{"type":"string"},"city":{"type":"string"},"postalCode":{"type":"string"},"country":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"website":{"type":"string"},"hours":{"type":"array","items":{"type":"object","properties":{"days":{"type":"array","items":{"type":"integer"}},"opens":{"type":"string"},"closes":{"type":"string"}}}},"currency":{"type":"string"},"latitude":{"type":"number"},"longitude":{"type":"number"},"priceRange":{"type":"string"},"paymentAccepted":{"type":"string"},"socials":{"type":"object","properties":{"instagram":{"type":"string"},"tiktok":{"type":"string"},"googleMaps":{"type":"string"},"directions":{"type":"string"}}}}},"example":{"name":"My Salon","address":"Rue du Commerce 12, 1204 Genève","street":"Rue du Commerce 12","city":"Genève","postalCode":"1204","country":"CH","phone":"+41 22 123 45 67","email":"hello@example.com","website":"https://example.com","hours":[{"days":[1,2,3,4,5],"opens":"09:00","closes":"18:00"}],"currency":"CHF","latitude":46.2044,"longitude":6.1432,"priceRange":"$$","paymentAccepted":"Cash, Visa, Mastercard, TWINT","socials":{"instagram":"https://instagram.com/mysalon","tiktok":"","googleMaps":"https://maps.google.com/?cid=123","directions":"https://maps.google.com/dir/?api=1&destination=..."}}}}}}}},"/api/ext/v1/reviews":{"get":{"summary":"Your approved customer reviews plus the aggregate rating.","tags":["Catalog"],"parameters":[{"name":"limit","in":"query","required":false,"description":"Max reviews to return (default 50, max 200).","schema":{"type":"string"}},{"name":"offset","in":"query","required":false,"description":"Reviews to skip for pagination (default 0).","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"ratingValue":{"type":"number"},"ratingCount":{"type":"integer"},"reviews":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"authorName":{"type":"string"},"rating":{"type":"integer"},"body":{"type":"string"},"verified":{"type":"boolean"},"reply":{"type":"string"},"createdAt":{"type":"string"}}}}}},"example":{"ratingValue":4.8,"ratingCount":126,"reviews":[{"id":91,"authorName":"Marie D.","rating":5,"body":"Service impeccable, je reviendrai !","verified":true,"reply":"Merci Marie, à bientôt !","createdAt":"2026-05-10T16:22:00"}]}}}}}}},"/api/ext/v1/deals":{"get":{"summary":"Your currently-live last-minute deals.","tags":["Catalog"],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"deals":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"discountPct":{"type":"integer"},"scope":{"type":"string"},"serviceSlug":{},"couponCode":{"type":"string"},"endsAt":{"type":"string"}}}}}},"example":{"deals":[{"title":"Mardi tranquille -20%","discountPct":20,"scope":"all","serviceSlug":null,"couponCode":"DEAL-K7M2PQ","endsAt":"2026-06-03T18:00:00+02:00"}]}}}}}}},"/api/ext/v1/memberships":{"get":{"summary":"Your active membership plans.","tags":["Catalog"],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"memberships":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"priceChf":{"type":"number"},"interval":{"type":"string"},"includedCredits":{"type":"integer"},"discountPct":{"type":"number"},"perks":{"type":"string"}}}}}},"example":{"memberships":[{"id":3,"name":"VIP mensuel","priceChf":49.0,"interval":"monthly","includedCredits":1,"discountPct":10.0,"perks":"1 soin offert + 10% sur tout le reste."}]}}}}}}},"/api/ext/v1/waitlist":{"get":{"summary":"Your active waitlist entries (clients waiting for a spot).","tags":["Catalog"],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"entries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"service":{"type":"string"},"notes":{"type":"string"},"language":{"type":"string"},"createdAt":{"type":"string"}}}}}},"example":{"entries":[{"id":12,"name":"Marie D.","email":"marie@example.com","phone":"","service":"pose-classique","notes":"","language":"fr","createdAt":"2026-05-20T10:00:00"}]}}}}}},"post":{"summary":"Add a client to the waitlist; they're alerted automatically in their language when a matching slot frees up.","tags":["Catalog"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string"},"service":{"type":"string"},"locale":{"type":"string"}}},"example":{"name":"Marie Dupont","email":"marie@example.com","service":"pose-classique","locale":"fr"}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"id":{"type":"integer"}}},"example":{"ok":true,"id":13}}}}}}},"/api/ext/v1/templates":{"get":{"summary":"List your message templates (event, channel, locales).","tags":["Catalog"],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"templates":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"event":{"type":"string"},"channel":{"type":"string"},"active":{"type":"boolean"},"isDefault":{"type":"boolean"},"locales":{"type":"array","items":{"type":"string"}}}}}}},"example":{"templates":[{"id":4,"name":"Booking cancelled","event":"booking_cancellation","channel":"email","active":true,"isDefault":true,"locales":["fr","en"]}]}}}}}}},"/api/ext/v1/emails/send":{"post":{"summary":"Send a styled (branded HTML) email to a customer — via one of your templates ('event') or a custom subject/body wrapped in your salon's branded shell. Sent on demand (transactional).","tags":["Catalog"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"to":{"type":"string"},"subject":{"type":"string"},"body":{"type":"array","items":{"type":"string"}},"ctaLabel":{"type":"string"},"ctaUrl":{"type":"string"},"locale":{"type":"string"}}},"example":{"to":"marie@example.com","subject":"A little reminder","body":["Your next visit is coming up.","See you soon!"],"ctaLabel":"Book again","ctaUrl":"https://booking.thenextbeacon.com/book/your-salon","locale":"fr"}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"delivered":{"type":"boolean"}}},"example":{"ok":true,"delivered":true}}}}}}},"/api/ext/v1/reviews/request":{"post":{"summary":"Send a styled review-request email (uses your configured review URL + copy). Trigger it from your POS/CRM after a visit. delivered=false if no review URL is configured.","tags":["Catalog"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"to":{"type":"string"},"name":{"type":"string"},"locale":{"type":"string"}}},"example":{"to":"marie@example.com","name":"Marie","locale":"fr"}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"delivered":{"type":"boolean"}}},"example":{"ok":true,"delivered":true}}}}}}},"/api/ext/v1/coupons/validate":{"post":{"summary":"Check whether a coupon code is valid for a service and base price, and what it would discount. This is a read — it does NOT redeem the code.","tags":["Catalog"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string"},"serviceSlug":{"type":"string"},"baseChf":{"type":"number"}}},"example":{"code":"DEAL-K7M2PQ","serviceSlug":"epilation-sourcils","baseChf":30.0}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean"},"discountChf":{"type":"number"},"reason":{}}},"example":{"valid":true,"discountChf":6.0,"reason":null}}}}}}},"/api/ext/v1/loyalty/{email}":{"get":{"summary":"Fetch a client's current loyalty point balance and their last 20 ledger entries.","tags":["Loyalty"],"parameters":[{"name":"email","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"},"balance":{"type":"integer"},"history":{"type":"array","items":{"type":"object","properties":{"points":{"type":"integer"},"reason":{"type":"string"},"createdAt":{"type":"string"}}}}}},"example":{"email":"marie@example.com","balance":340,"history":[{"points":50,"reason":"booking #4821","createdAt":"2026-05-12T14:30:00"},{"points":-100,"reason":"redeemed","createdAt":"2026-04-20T11:00:00"}]}}}}}}},"/api/ext/v1/loyalty/{email}/adjust":{"post":{"summary":"Manually add or remove points for a client. Pass a negative points value to deduct. Returns the new balance after the adjustment.","tags":["Loyalty"],"parameters":[{"name":"email","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"points":{"type":"integer"},"reason":{"type":"string"}}},"example":{"points":50,"reason":"In-store gift"}}}},"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"newBalance":{"type":"integer"}}},"example":{"ok":true,"newBalance":390}}}}}}},"/api/ext/v1/packages/purchases":{"get":{"summary":"List paid package purchases, newest first (max 100). Filter by buyer email with the optional email query param.","tags":["Packages"],"parameters":[{"name":"email","in":"query","required":false,"description":"Filter to purchases by this buyer email address.","schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"purchases":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"email":{"type":"string"},"name":{"type":"string"},"package":{"type":"string"},"kind":{"type":"string"},"code":{"type":"string"},"status":{"type":"string"},"createdAt":{"type":"string"}}}}}},"example":{"purchases":[{"id":17,"email":"marie@example.com","name":"Marie Dupont","package":"10-Visit Pack","kind":"credits","code":"PKG-ABC123","status":"paid","createdAt":"2026-05-28T09:15:00"}]}}}}}}}}}