Tenant detail
The tenant detail page is at https://booking.netwit.ca/admin/tenant?id=xxx where xxx is the tenant ID. You can reach it from the tenants list by clicking Open on any row, or by pasting a tenant ID directly into the URL.
Sections
The page is one long scroll with eight sections, in order:
- Profile — name, slug, business type, custom domain, widget color, address, contact
- Owner — the user who created the tenant, with email and last login
- Members — every user in
tenant_membersfor this tenant, with role and active flag - Services — the tenant's service catalog (count + recent items)
- Staff — staff profiles for the tenant (count + recent)
- Customers — count of customer profiles linked to this tenant
- Bookings — the most recent 10 bookings with status and amount
- Payments — count and last 30-day total (no PII surfaced)
- AI activity — count of AI actions in the last 7 days
- Audit — the 50 most recent
audit_logentries whose target is this tenant
GET /api/admin/tenants/:id response returns profile, users, counts (bookings / customers / services / staff), the 10 most recent bookings, and the 50 most recent audit rows. The console page renders those plus the AI activity rollup. Owner, payments, and AI activity are derived in the UI from other endpoints; they are not in the :id response yet.Action: Impersonate
Impersonation opens a read-only session as the tenant owner. It exists so you can see exactly what the owner sees when a customer reports a bug. The rules:
- You can read every page the owner can read: dashboard, services, staff, customers, bookings, settings, AI log.
- You cannot write. The UI greys out every action button. The API refuses writes that originate from an impersonation token.
- Every page you open during an impersonation session writes an
admin.tenant.impersonaterow toaudit_logwith the impersonated tenant ID, the page you opened, and a timestamp. - Impersonation sessions expire after 1 hour. After that, you have to re-enter the reason and re-launch.
- You must enter a reason in the prompt before the session starts. The reason is stored in the audit row.
Action: Suspend
Suspension blocks all bookings immediately. Specifically:
- The tenant's public booking page (
https://booking.netwit.ca/book?slug=...) returns503with a "temporarily unavailable" page. - The widget on the tenant's own domain (if any) shows the same unavailable state.
- Existing customers trying to book a new slot are stopped at the calendar step.
- Existing confirmed bookings are not cancelled — they remain on the books and the staff still shows up. Suspend does not touch existing data; it only blocks new bookings.
- The owner gets an email within ~1 minute explaining the suspension and the reason you provided.
- Internal API keys (if any) keep working so the tenant's existing integrations don't break.
How to suspend
curl -X PATCH https://booking-api.netwit.ca/api/admin/tenants/$TENANT_ID \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"isSuspended": true,
"suspendedReason": "Chargeback fraud - investigating",
"reason": "Suspended by alex@netwit.ca after 2x chargebacks"
}'Action: Restore
Restoration is the inverse of suspension. Set isSuspended: false:
curl -X PATCH https://booking-api.netwit.ca/api/admin/tenants/$TENANT_ID \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"isSuspended": false,
"reason": "Chargebacks resolved, customer agreed to update payment method"
}'The tenant's public booking page is back online within a few seconds (the next request from any customer triggers cache invalidation).
Action: Plan change
You can set the plan to one of three values: free, pro, or enterprise. Billing is not yet integrated — the change takes effect immediately but no charge is made. The owner is expected to be billed manually based on the new plan; the finance team reconciles by reading the audit log.
curl -X PATCH https://booking-api.netwit.ca/api/admin/tenants/$TENANT_ID \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "plan": "pro", "reason": "Manual upgrade per sales agreement" }'You can also extend a trial by setting trialEndsAt:
curl -X PATCH https://booking-api.netwit.ca/api/admin/tenants/$TENANT_ID \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "trialEndsAt": "2026-09-30T23:59:59Z", "reason": "Trial extended 30 days" }'What you can change on this page
| Field | How | What it does |
|---|---|---|
| isSuspended | PATCH | Flip the soft-suspend flag. Triggers an email to the owner. |
| suspendedReason | PATCH | Free-text reason, max 500 chars. Stored alongside the suspend action. |
| plan | PATCH | Set to free, pro, or enterprise. Writes tenant.plan_changed to audit. |
| trialEndsAt | PATCH | ISO-8601 string or null. Extends or ends the trial. |
| reason | PATCH | Free-text, max 500 chars. The 'why' you did this. Always required. |
isSuspended, suspendedReason, plan, trialEndsAt, and reason. Profile edits must go through the tenant owner, or directly in D1 if it's a data correction (always with a D1 audit note + a row in audit_log).API
/api/admin/tenants/:idReturns the full tenant record plus members, counts, recent bookings, and the 50 most recent audit rows for this tenant.
/api/admin/tenants/:idMutate the tenant. Accepts isSuspended, suspendedReason, plan, trialEndsAt, reason. Writes a before/after diff to audit_log.
GET response shape
{
"data": {
"id": "abc123…",
"slug": "demo-salon",
"businessName": "Demo Salon",
"businessType": "salon",
"email": "owner@demosalon.com",
"phone": "+16045551234",
"plan": "pro",
"planStatus": "active",
"isActive": true,
"isSuspended": false,
"suspendedReason": null,
"suspendedAt": null,
"trialEndsAt": null,
"createdAt": "2026-06-12T18:34:00Z",
"customDomain": "book.demosalon.com",
"widgetColor": "#6366F1",
"description": "Full-service hair and nails.",
"addressLine1": "123 Main St",
"city": "Vancouver",
"country": "Canada",
"users": [
{
"id": "u_xyz",
"email": "owner@demosalon.com",
"role": "owner",
"firstName": "Alex",
"lastName": "Doe",
"isActive": true,
"createdAt": "2026-06-12T18:34:00Z"
}
],
"counts": {
"bookings": 412,
"customers": 184,
"services": 14,
"staff": 6
},
"recentBookings": [
{
"id": "b_123",
"startDatetime": "2026-07-25T16:00:00Z",
"status": 'confirmed',
"totalAmount": 4500,
"currency": "CAD"
}
],
"audit": [
{
"id": "al_001",
"action": "tenant.plan_changed",
"actorId": "sa_42",
"targetId": "abc123…",
"reason": "Manual upgrade per sales agreement",
"createdAt": "2026-07-20T14:11:00Z"
}
]
}
}PATCH request body
isSuspendedbooleanSet to true to suspend, false to restore. Setting true stamps suspended_at; setting false clears it.
suspendedReasonstring | nullFree-text, max 500 chars. Visible to the owner in the suspension email.
planenum: free | pro | enterpriseNew plan value. Triggers a tenant.plan_changed audit row.
trialEndsAtISO-8601 string | nullSet or clear the trial end. Omit to leave unchanged.
reasonstringYour "why". Max 500 chars. Stored verbatim in audit_log.reason. Always include one.
Audit action names written by PATCH
| Trigger | action value |
|---|---|
| isSuspended: true | tenant.suspended |
| isSuspended: false | tenant.restored |
| plan change (any value) | tenant.plan_changed |
| suspendedReason only (no other field) | tenant.updated |
Things to do before you suspend
- Read the last 5 audit rows for this tenant. Is the reason already in there?
- Check if there are bookings in the next 24 hours. If yes, the staff and customer will show up to a closed door. Either call them or move the bookings first.
- Look at the charges in the last 30 days. If the tenant is suspended for fraud, you'll want to flag the Stripe account separately.
- Write a clear, specific
reason. "Violation of ToS" is not useful. "2 chargebacks for services never rendered" is.
Email hello@netwit.ca or call +1-604-206-8169. NetWit responds in 1 business day.