Agency Guide
Vouch has three levels of multi-tenancy: platform → agency → tenant → (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
- What an agency is
- Roles and access model
- Setting up an agency
- Setting up a tenant under an agency
- Setting up locations
- Setting up campaigns (marketing actions)
- Cross-tenant reporting
- Two ways to act on a tenant as an agency admin
- 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:
- A marketing agency managing review/reputation programs for many independent client businesses, each client being its own tenant with its own brand, locations, contacts, and campaigns — invisible to each other, visible in aggregate to the agency.
- A franchisor or multi-brand holding company where each brand or franchise group is its own tenant, and a small central team (the "agency" in Vouch's model) needs to onboard new tenants and see rollup reporting across all of them.
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:
requireTenantScopegrants access to a givenx-tenant-idthree ways, in precedence order: platform (any tenant) → tenant (a direct tenant-scoped role) → agency (the caller isagency_adminof the agency that owns that tenant). This is why an agency admin can call almost any regular/v1/*endpoint for a client's tenant just by settingx-tenant-id— see §8.requireAgencyAdminguards the/agency/*router — every route there requires the caller to administer at least one agency (or be aplatform_admin), and populatesreq.agencyIdswith the set they administer.requireWriteAccesstreatsaccessScope === "agency"the same as"platform"— an agency admin acting on an owned tenant is a full writer, not read-only.
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:
GET /agency/tenants— list every tenant the agency owns.GET /agency/tenants/:id— tenant detail, including its brands.PATCH /agency/tenants/:id— rename, change plan, or pause (isActive: false) the tenant.GET /agency/tenants/:id/users/POST /agency/tenants/:id/users— list or add tenant users (e.g. the client's own staff logging into Vouch directly).
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:
/agency/*convenience routes (this guide) — nox-tenant-idheader, 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.- Regular
/v1/*routes withx-tenant-idset 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.requireTenantScopegrantsaccessScope: "agency"for any tenant the caller's agency owns, andrequireWriteAccesstreats 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.