Owner API
The owner API is everything the dashboard does: read and mutate the tenant, services, staff, customers, bookings, and payments. All routes require an aud: owner JWT from POST /api/auth/login. The token is tenant-scoped: every call must match the tenantId encoded in the token.
Business
/api/businesses/meRead the current tenant.
/api/businesses/meUpdate business fields. Slug is NOT patchable here (set at registration).
businessNamestringBusiness name (max 200 chars).
businessTypestringBusiness type (max 80 chars).
addressLine1stringStreet address (max 200 chars).
addressLine2stringAddress line 2 (max 200 chars).
citystringCity (max 120 chars).
provinceStatestringProvince/state (max 120 chars).
postalCodestringPostal code (max 40 chars).
countrystringCountry (max 120 chars).
phonestringDisplay phone on the public page (max 40 chars).
emailstringContact email (max 200 chars).
websitestringWebsite URL (max 300 chars).
descriptionstringPublic description (max 2000 chars).
logoUrlstringLogo URL (max 500 chars).
coverImageUrlstringCover image URL (max 500 chars).
timezonestringIANA timezone (max 80 chars).
currencystringCurrency code (max 8 chars, e.g. CAD).
countryCodestringISO country code (max 8 chars).
bookingAdvanceDaysintegerHow far ahead customers can book (1-365).
minNoticeHoursintegerMinimum notice in hours (0-168).
bufferMinutesintegerBuffer between bookings (0-120).
maxBookingsPerSlotintegerConcurrent bookings per slot (1-50).
allowGuestBooking0 | 10 = account required, 1 = guest checkout allowed.
requirePayment0 | 10 = no deposit, 1 = deposit required at booking.
depositPercentinteger0-100 percent of price charged as deposit.
cancellationHoursintegerFree-cancellation window in hours (0-720).
cancellationPolicystringFree-text policy (max 4000 chars).
customDomainstringCustom domain CNAME target (max 200 chars).
templatestringTemplate slug. The Zod schema accepts any 1-64 char string; the AI site-content mock currently only knows atelier / salon / spa / clinic. Other slugs are stored as-is and rendered by the dashboard.
templateAccent#rrggbb | nullHex color for template accent. Use null to clear.
Services
/api/servicesList services for the current tenant (includes inactive).
/api/servicesCreate a service.
namestringrequiredService name (1-200 chars).
descriptionstringOne or two sentences (max 2000 chars).
durationMinutesintegerrequired5-480.
priceintegerPrice in minor units (cents). 0-100000.
depositPercentintegerOptional 0-100 percent.
maxAdvanceDaysinteger1-365, optional.
minNoticeHoursinteger0-168, optional.
bufferAfterMinutesinteger0-120. Default 0.
maxBookingsPerSlotinteger1-50. Default 1.
imageUrlstringOptional image URL.
isActive0 | 1Default 1. 0 hides from booking page.
isOnlineBookable0 | 1Default 1. 0 means in-person only.
orderIndexintegerDisplay order. Lower = earlier.
customFormFieldsstringJSON-encoded custom form schema (max 8000 chars).
/api/services/:idUpdate a service. Any subset of the create fields.
/api/services/:idSoft-delete. Flips isActive to 0; historical bookings are preserved.
/api/services/reorderBulk-reorder. Body: { order: ["sv_a", "sv_b", ...] }. Sets order_index to position in array.
Staff
/api/staffList all staff profiles for the current tenant.
/api/staffCreate a staff profile. Optionally links to an existing users row by email.
displayNamestringrequiredDisplay name (1-120 chars).
userEmailstringIf present and the email exists, the staff profile is linked to that user. Otherwise a new users row is created.
titlestringJob title (max 120 chars).
biostring1-2 sentences. Shown on the public page (max 2000 chars).
avatarUrlstringOptional photo URL (max 500 chars).
color#rrggbbHex color used on the calendar (default #6366F1).
isBookable0 | 1Default 1. 0 hides from public booking.
/api/staff/:idRead a staff profile, including their workingHours array.
/api/staff/:idUpdate a staff profile (no userEmail field). Any subset of displayName, title, bio, avatarUrl, color, isBookable.
/api/staff/:idSoft-delete. Flips isBookable to 0; the staff member disappears from public booking.
/api/staff/:id/hoursReplace the staff member's weekly schedule.
hoursarrayrequiredArray of { dayOfWeek: 0-6, isOpen: 0|1, openTime: HH:MM, closeTime: HH:MM }.
Bookings
/api/bookingsList bookings. Supports filters.
statusenumOne of: pending, confirmed, completed, cancelled, no_show.
startDateISO-8601Filter start.
endDateISO-8601Filter end.
staffIdstringcustomerIdstringserviceIdstringlimitintegerDefault 50, max 200.
offsetintegerDefault 0.
curl "https://booking-api.netwit.ca/api/bookings?status=confirmed&startDate=2026-08-01&limit=50" \
-H "authorization: Bearer $OWNER_TOKEN"/api/bookings/statsAggregate stats for the current tenant: today's bookings, upcoming (7d), no-show count, week revenue (all-time cents).
{
"data": {
"todayCount": 8,
"upcomingCount": 47,
"noShowCount": 6,
"weekRevenue": 124000
}
}/api/bookingsManual-create a booking from the dashboard. Either customerId or newCustomer is required.
serviceIdstringrequiredstaffProfileIdstringrequiredstartDatetimeISO-8601requiredcustomerIdstringExisting customer id (mutually exclusive with newCustomer).
newCustomerobjectInline new customer: { firstName, lastName?, email, phone? }.
notesstringBooking-specific notes (max 2000 chars).
statusenumpending | confirmed | completed | cancelled | no_show. Defaults to confirmed.
/api/bookings/:idRead one booking, with joined service, staff, and customer objects.
/api/bookings/:idUpdate a booking. Any subset of notes, internalNotes, status.
/api/bookings/:id/confirmTransition pending → confirmed.
/api/bookings/:id/cancelTransition to cancelled (sets cancelled_at = now). Use /api/payments/:id/refund separately to refund the deposit.
/api/bookings/:id/completeTransition confirmed → completed.
/api/bookings/:id/no-showTransition confirmed → no_show (sets no_show_at = now).
/api/bookings/:id/notesAppend a timestamped internal note. Body: { note: string }.
/api/bookings/:id/rescheduleMove a booking to a new start time. Body: { newStartDatetime, staffProfileId? }.
Customers
/api/customersList customers. Supports q (LIKE on first_name/last_name/email), tag, blacklisted=1, limit, offset.
/api/customers/exportReturns a CSV download. Headers: first_name, last_name, email, phone, total_bookings, total_spent, no_show_count, is_blacklisted.
/api/customersCreate a customer.
firstNamestringrequiredFirst name (1-100 chars).
lastNamestringLast name (max 100 chars).
emailstringEmail (max 200 chars).
phonestringPhone (max 40 chars).
notesstringInternal notes (max 4000 chars).
tagsstring[]Up to 50 tags (each max 50 chars).
/api/customers/:idRead a customer (includes the 25 most recent bookings).
/api/customers/:idUpdate a customer. Fields: firstName, lastName, email, phone, notes, tags, isBlacklisted, blacklistReason, birthday, referralSource.
/api/customers/:idGDPR anonymize: blanks PII fields but keeps the row for audit.
/api/customers/:id/bookingsAll bookings for this customer.
/api/customers/:id/notesAppend a timestamped note. Body: { note: string }.
Payments
/api/payments/businesses/me/stripe/connectStart Stripe Connect onboarding. Returns a redirect URL (onboardingUrl) and accountId.
/api/payments/businesses/me/stripeRead the current Stripe connection: connected, mode (test|live), chargesEnabled, payoutsEnabled, detailsSubmitted.
/api/payments/businesses/me/stripeDisconnect Stripe. Clears stripe_account_id and stripe_charges_enabled.
/api/payments/intentCreate a Stripe PaymentIntent for a booking. Returns clientSecret for the Stripe.js front-end.
bookingIdstringrequiredpaymentTypeenumfull or deposit. Defaults to deposit if the booking has a non-zero deposit_amount, otherwise full.
customerEmailstringOptional. Forwarded to Stripe receipt.
customerNamestringOptional. Forwarded to Stripe receipt (max 200 chars).
/api/payments/confirmRead-after-write check. The webhook is the source of truth; this endpoint exists for synchronous UX. Body: { bookingId }. Returns the booking's status and payment_status.
/api/payments/reportsAggregate payments by status. Returns rows of { status, c, total } where total is SUM(amount) in cents.
/api/payments/:idRead a single payment.
/api/payments/:id/refundRefund a payment. Body: { amount?, reason? }. amount is in MAJOR units (dollars); omit for a full refund. In test mode the database is updated directly; in live mode the Stripe refund is created.
amountnumberRefund amount in major units (dollars). Omit for a full refund.
reasonstringFree-text reason (max 500 chars).
Common errors
400 invalid_request— body validation failed (Zod). Details in details.fieldErrors.401 unauthorized— token missing, invalid, or wrong audience.403 tenant required / tenant not found— authed but no valid tenant context (check JWT tenantId).404 not_found— id doesn't exist or is on a different tenant.409 invalid_transition— tried to change booking status to one not allowed from current status (e.g. cancelled → completed).409 slot_taken— slot was just booked by another customer.409 slug_taken— business slug already exists.409 duplicate— staff user already attached to a staff_profile in this tenant.429 rate_limited— too many requests; check Retry-After.
Email hello@netwit.ca or call +1-604-206-8169. NetWit responds in 1 business day.