Agency Guide

Vouch has three levels of multi-tenancy: platformagencytenant → (brand →) location. This guide explains what an agency is, how the RBAC model enforces agency boundaries, and walks through setting up an agency's tenants, locations, and campaigns end to end.

If you want copy-pasteable requests rather than narrative, jump straight to DEMO_FLOWS.md. For the MCP/AI-assistant side of agency access, see MCP_INTEGRATION.md and MCP_SKILLS.md.

Contents

  1. What an agency is
  2. Roles and access model
  3. Setting up an agency
  4. Setting up a tenant under an agency
  5. Setting up locations
  6. Setting up campaigns (marketing actions)
  7. Cross-tenant reporting
  8. Two ways to act on a tenant as an agency admin
  9. MCP / AI-assistant access

1. What an agency is

An Agency is a reseller or multi-business account that owns one or more Tenants. Every tenant belongs to exactly one agency — direct self-serve signups are attached to a built-in "House Accounts" agency (slug: "house") so the data model never has to special-case "no agency."

This maps onto two real-world shapes:

Platform
 └─ Agency ("Acme Marketing Group")
     ├─ Tenant ("Riverside Dental")
     │   └─ Brand → Region (optional) → Location(s)
     ├─ Tenant ("Downtown Auto Group")
     │   └─ Brand → Location(s)
     └─ Tenant (…)

2. Roles and access model

Three roles matter for this guide (see packages/contracts/src/enums/index.ts for the full UserRole list):

Role Scope Can do
platform_admin platform Everything, across every agency and tenant. Creates agencies.
agency_admin agency (one RoleAssignment per agency administered) Full read/write on every tenant owned by their agency/agencies — create tenants, tenant users, locations, campaigns; cross-tenant rollups.
tenant_admin / tenant_user / etc. tenant Normal tenant-scoped access, unaware of the agency layer entirely.

The enforcement lives in services/api/src/middleware/auth.ts:

An agency admin who administers more than one agency must disambiguate with an explicit agencyId on the handful of endpoints that create new top-level resources (creating a tenant); everywhere else the tenant id itself is enough to resolve which agency is in play.

3. Setting up an agency

Only a platform_admin can create an agency — this is a one-time onboarding step per agency, done via /admin/agencies (Platform Admin console or API).

POST /admin/agencies
Authorization: Bearer <platform_admin_token>
Content-Type: application/json

{
  "name": "Acme Marketing Group",
  "slug": "acme-marketing",
  "adminEmail": "ops@acmemarketing.com",
  "adminName": "Jordan Lee"
}

Passing adminEmail provisions the first agency_admin in the same call — Vouch resolves or creates the user (Azure AD lookup, then B2B invite, then a local account with a password-setup email as a last resort) and grants them the agency_admin role scoped to the new agency. You can grant more admins later with POST /admin/agencies/:id/admins.

The response is an Agency object; keep its id — every subsequent call in this guide is scoped by it (implicitly, once the agency admin signs in).

4. Setting up a tenant under an agency

From here on, everything runs as the agency admin, not the platform admin — this is the self-service surface a real agency operator uses daily.

POST /agency/tenants
Authorization: Bearer <agency_admin_token>
Content-Type: application/json

{
  "name": "Riverside Dental",
  "slug": "riverside-dental",
  "plan": "growth",
  "brandName": "Riverside Dental",
  "brandSlug": "riverside-dental"
}

This creates the tenant and a default brand in one transaction (the brand is required before locations can be added). If the admin only administers one agency, agencyId can be omitted — it's required only when disambiguating between multiple agencies.

Other tenant operations available to the agency admin:

5. Setting up locations

Add the client's physical locations under the new tenant:

POST /agency/tenants/{tenantId}/locations
Authorization: Bearer <agency_admin_token>
Content-Type: application/json

{
  "name": "Riverside Dental — Main St",
  "addressLine1": "123 Main St",
  "city": "Riverside",
  "state": "CA",
  "postalCode": "92501",
  "phone": "+19515550100"
}

brandId is optional — omit it and the location is attached to the tenant's oldest active brand (usually the default one created in §4). Pass it explicitly for tenants with multiple brands.

For bulk onboarding (a client with dozens of locations), use the CSV import on the regular tenant-scoped endpoint instead — see §8 for why that's available to an agency admin too: POST /v1/locations/bulk with x-tenant-id set to the client's tenant.

6. Setting up campaigns (marketing actions)

Vouch's term for what's often called a "marketing action" elsewhere is a Campaign — a review-solicitation or messaging program with a type (one_time, event_triggered, scheduled_batch, drip, or continuous) and one or more channels (email, sms, whatsapp). Campaigns are built from Templates (the message content) and, for multi-step programs, Sequences. Individual sends are tracked as Solicitations.

POST /agency/tenants/{tenantId}/campaigns
Authorization: Bearer <agency_admin_token>
Content-Type: application/json

{
  "name": "Post-visit review request",
  "type": "event_triggered",
  "channels": ["sms", "email"],
  "locationId": "{locationId}"
}

This is the same shape as the regular POST /v1/campaigns endpoint — see §8 — surfaced under /agency/* for convenience so an agency operator doesn't need to juggle an x-tenant-id header for the most common setup action. Attach templates and (optionally) a sequence via the regular tenant-scoped /v1/templates and /v1/campaigns/:id/sequence endpoints once the campaign exists.

7. Cross-tenant reporting

Two rollup endpoints answer "show me this across every client I manage," which is the thing agencies need that a single tenant never does:

GET /agency/locations
GET /agency/campaigns

Both return every matching row across all tenants the agency owns, each annotated with its owning tenant: { id, name, slug }. Pass ?tenantId=… to scope either one down to a single client without switching headers.

8. Two ways to act on a tenant as an agency admin

There are two equally valid ways for an agency admin to act on a specific client tenant, and it's worth being explicit about when to use which:

  1. /agency/* convenience routes (this guide) — no x-tenant-id header, built for the handful of setup actions an agency operator does across many clients: create a tenant, add its first locations, stand up its first campaign, add its users. Small, curated surface.
  2. Regular /v1/* routes with x-tenant-id set to the client's tenant — the entire tenant-scoped API (templates, sequences, contacts, reviews, the AI assistant, analytics, integrations, SSO settings, everything in the API reference) is available this way. requireTenantScope grants accessScope: "agency" for any tenant the caller's agency owns, and requireWriteAccess treats that the same as a tenant admin — full read/write, not read-only.

In practice: use /agency/* for the initial client setup narrated in §§4–6, then switch to /v1/* + x-tenant-id for everything else (bulk CSV location import, template authoring, sequence building, reviewing the inbox, running analytics) — exactly as if you were a tenant admin logged into that specific client's workspace.

9. MCP / AI-assistant access

An agency admin who connects to the MCP server via OAuth (not an API key — API keys aren't tied to a user, so they can't be agency-scoped) gets a session that can call agency.list-tenants and pass an explicit tenantId to agency-aware skills (location.list, campaign.list, score.location, review.list today). This is read-only today; use the REST endpoints above for setup actions. Full details in MCP_INTEGRATION.md §3.4 and MCP_SKILLS.md.

See DEMO_FLOWS.md for a runnable script that combines everything in this guide — agency creation, tenant/location/campaign setup, cross-tenant reporting, and an MCP session — into one end-to-end walkthrough.