Platform operators

Tenant detail

Platform operatorsTenant 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_members for 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_log entries whose target is this tenant
Some sections are surfaced in the API; some are only on the API roadmap.
The current 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.impersonate row to audit_log with 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=...) returns 503 with 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

FieldHowWhat it does
isSuspendedPATCHFlip the soft-suspend flag. Triggers an email to the owner.
suspendedReasonPATCHFree-text reason, max 500 chars. Stored alongside the suspend action.
planPATCHSet to free, pro, or enterprise. Writes tenant.plan_changed to audit.
trialEndsAtPATCHISO-8601 string or null. Extends or ends the trial.
reasonPATCHFree-text, max 500 chars. The 'why' you did this. Always required.
You cannot edit the tenant's name, slug, email, or address from here.
The PATCH schema only accepts 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

GET/api/admin/tenants/:id

Returns the full tenant record plus members, counts, recent bookings, and the 50 most recent audit rows for this tenant.

PATCH/api/admin/tenants/:id

Mutate 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

isSuspendedboolean

Set to true to suspend, false to restore. Setting true stamps suspended_at; setting false clears it.

suspendedReasonstring | null

Free-text, max 500 chars. Visible to the owner in the suspension email.

planenum: free | pro | enterprise

New plan value. Triggers a tenant.plan_changed audit row.

trialEndsAtISO-8601 string | null

Set or clear the trial end. Omit to leave unchanged.

reasonstring

Your "why". Max 500 chars. Stored verbatim in audit_log.reason. Always include one.

Audit action names written by PATCH

Triggeraction value
isSuspended: truetenant.suspended
isSuspended: falsetenant.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.
Need a human?

Email hello@netwit.ca or call +1-604-206-8169. NetWit responds in 1 business day.