Docs · Account

Clients and portals (Agency).

The Agency tier turns Ciela into the operating layer for your client roster. Add each client you've sold a Library agent to, drop the per-client webhook URL into their n8n flow or Vapi assistant, and watch performance roll up automatically. Each client also gets a public branded portal at /portal/{slug} so they can see the ROI you're delivering without you having to assemble a deck every month.

Quick facts

Plan
Agency tier only
Per-client URL
/api/agent-callback/{slug}, 16 hex chars
Per-client secret
48 hex chars, sent as X-Webhook-Secret
Public portal
/portal/{slug}, read-only, no login
Idempotency
Send external_id and retries collapse to one row

Model

Three concepts to keep straight:

  • ClientAn end client of your agency, not a Ciela user. They never log in. You add them at /dashboard/clients with their name, primary contact, and monthly retainer. The server mints a unique slug + webhook secret for them on creation.
  • DeploymentOne Library agent you've installed for this client. Receptionist, lead reactivation, quote generator, missed-call text-back, or custom. The deployment is your bookkeeping record, the agent itself runs in the client's n8n + Vapi (not in Ciela). Status flips between live, paused, retired.
  • EventA performance datapoint POSTed by a deployed agent. Call answered, lead captured, appointment booked, message sent, message received, quote sent, or custom. Optional value_cents for revenue events drives the ROI rollup.

Walkthrough

01

Add the client

Click Add client at /dashboard/clients. Name is required. Primary contact, contact email, and monthly retainer are optional, all editable later.

On save you land on the detail page where the unique POST URL and webhook secret are already provisioned and visible. Copy both. Treat the secret like any production credential, if it leaks, delete the client and re-add to rotate.

02

Record the deployments

For every Library agent you install in this client's infrastructure, hit Record deployment. Pick the agent type, give it a name your client will recognize (eg "Acme Front Desk"), and add any setup notes (Vapi assistant id, n8n flow URL, GHL pipeline id) you want to find again later.

Deployments are pure bookkeeping, you can ship the agent to the client either way, but logging it here lets events attribute to a specific install on the rollup and the portal. If you retire an agent, set its status to retired rather than deleting, so historical metrics keep their attribution.

03

Wire the webhook into n8n + Vapi

In the deployed flow (n8n HTTP Request node, Vapi tool, or any other runtime), add a step that POSTs to the client-specific URL with the secret in the X-Webhook-Secret header. Wire it to fire on whatever moments matter for the agent: every call completed, every booking confirmed, every lead written to the CRM.

Minimum body:

POST /api/agent-callback/<slug>
X-Webhook-Secret: <secret>
Content-Type: application/json

{
  "event_type": "appointment_booked",
  "deployment_id": "<optional, from /dashboard/clients/[id]>",
  "external_id": "<optional, your idempotency key>",
  "value_cents": 20000,
  "metadata": { "customer": "Jane", "service": "Cleaning" }
}

Send external_id if the runtime might retry (n8n's retry-on-fail, Vapi tool retries). Duplicate POSTs with the same external_id return { ok: true, duplicate: true } and collapse to one row, so you never double-count.

Send deployment_id to attribute the event to a specific agent install. Useful when one client has two receptionists running side by side, otherwise leave it null and the event still rolls up at the client level.

04

Watch metrics roll up

Back at /dashboard/clients the topline shows active clients and total events in the last 30 days. Each client row shows their own 30-day event count, live deployments, and the retainer amount you set for them (a per-client note, not rolled up across the roster).

Open a client to see the per-type breakdown, the latest events with metadata, attributed value (sum of value_cents), and the deployment list with per-agent statuses you can flip in place.

05

Share the branded portal

Hit Open client portal on the detail page, or send your client the URL at /portal/{slug}. It's public, read-only, no login. They see the last 30 days of events, the agents running for them, recent activity, and any attributed revenue.

Ciela's brand is nowhere on the portal. It renders under your agency's brand fields (name, logo, color) from your profile, with per-client logo and color overrides if you set them. Set yours under your account settings before sending the first portal link.

What you see on the roster

The top of /dashboard/clients shows two rollup cards across every client at once, so you can answer "how is my book of business doing?" without opening a client.

  • Active clientsCount of clients with status = active. Total roster count (including paused + churned) shows below.
  • Events (30d)Webhook events received across every deployment in the last 30 days. The denominator behind whether your AI agency is actually delivering volume to clients.

Each row in the roster shows that client's own retainer amount (a self-reported number you set per client, for your own bookkeeping), live vs total deployments, and 30-day event count. Retainer is per-client context, not rolled up across the roster, because the number is operator-typed and shouldn't be confused with billing-grade MRR. Click any row to open the per-client detail.

Set your agency brand

The public portal at /portal/{slug} renders under your agency's brand, not Ciela's. Set this once under account settings, Agency Brand tab and every client portal you share from then on inherits it.

  • Agency nameRendered in the portal footer as "Report by <Agency name>". Leave blank for a generic "Performance report" footer.
  • LogoUpload directly from your machine, square 256x256 works fine. Falls back as the header image when a client has no per-client logo of their own. JPG, PNG, WebP, SVG, or GIF up to 2 MB.
  • Brand colorAny CSS color (hex, rgb, hsl, named). Drives the accent on metric numbers, agent cards, and the activity feed inside the portal. Defaults to champagne (#d4a574) if unset.

Per-client overrides: each client row has its own optional logo_url and brand_color. If set, the portal uses the client's override for the avatar and accent; if null, it falls back to your agency-level brand. Use overrides when a client wants their own brand on the report (eg an enterprise client who insists on their own logo). Otherwise leave them null and let the agency brand carry through.

Filtering the detail view

The per-client detail page is where you have monthly review conversations. Three controls narrow what you're looking at:

  • Date rangePill toggle at the top: 7 days, 30 days, 90 days. Drives the metrics rollup card (Events count, type breakdown, attributed value). The activity feed itself always shows newest first regardless of range, so you can still see what just happened.
  • Event-type chipsClick Calls · 22, Appointments · 21, etc. to narrow the activity feed to just that event type. Counts on the chips reflect the current date range. Only types that actually have at least one event in the window appear, no empty filters.
  • Per-agent chipsSurface only when a client has more than one deployment. Click an agent name to scope the feed to events that deployment posted (events without deployment_id attribution are excluded when this filter is active).
  • Load 50 moreThe feed starts with the newest 50. Load 50 more paginates backwards using an occurred_at cursor. Works with filters applied: paginated pages respect the current chip selection.
  • Clear filtersA single chip appears when any type or per-agent filter is active. One click resets both back to All / All agents.

Monthly summary (the portal hero)

The detail page also drives the narrative that opens the public portal. Claude reads the client's event ledger + active deployments for a period you pick, writes a 60-120 word summary in the agency voice, and hands it back to you to edit before publishing. The end client opens /portal/{slug} and sees the story you approved sitting above the metrics, not a raw activity log. Saved summaries replace the old activity-feed approach to monthly reporting.

  • Pick a periodDropdown next to Generate summary: This month, Last month, Last 30 days, Last 90 days. Drives which events Claude reads. The period label (eg "January 2026") is rendered on the portal next to the summary so the client knows what window they're looking at.
  • GenerateOne Haiku call, a few cents per generate. Claude reads the events for the chosen period plus the previous window of the same length so it can speak to trend ("calls trended up vs the prior month"). Returns a one-or-two-paragraph draft in plain prose, no markdown, no em dashes, no platform names. You see it as an editable text field, not auto-published.
  • EditThe draft is operator-editable. Tighten the wording, swap a number, change the closing sentence, anything. The agency voice is yours to set, Claude just gives you a starting point that's grounded in the actual ledger so you're not staring at a blank box.
  • PublishSaves summary_text + summary_period_label to the client row and stamps summary_generated_at server-side. From that moment, anyone with the portal URL sees it at the top of the report. Until you publish, the portal renders without a summary, just the metrics carry the page.
  • RegenerateGenerating a new summary does not auto-replace the published one, you have to edit + publish again. So the client never sees a draft you didn't sign off on. The detail page shows a 'Generated X days ago' hint next to the saved summary so you know when it's time to refresh.
  • ClearEmpty the text field and save to wipe the summary off the portal. The portal renders again without the hero, metrics only. Useful when a client period is too soft to summarize, or when you're between months.

What Claude is told NOT to do: mention Ciela, n8n, or Vapi (the client doesn't care about your stack), use em dashes or markdown, speculate about reasons for changes the data can't prove, or sign off with a name (the portal renders your agency name separately).

What Claude does: lead with the headline metric, name 2-3 specific event counts, include attributed dollar value if non-zero, and close with a forward-looking sentence grounded in what was observed. The voice is third-person factual ("Your AI receptionist answered 47 calls this month"), not first-person agency-speak.

Lifecycle and statuses

Clients and deployments each have their own status field. Use them to keep historical metrics intact while reflecting current reality.

  • Client: activeRetainer is being paid, agents are deployed and running. Counts toward the roster's Active clients rollup.
  • Client: pausedTemporary hold (client took a break, frozen for a season, mid-pivot). Stays on the roster, drops out of the Active count. Set this when you want metrics history preserved but no longer want the client counted as live.
  • Client: churnedRelationship ended. Stays on the roster as historical record. Don't delete the client unless you also want to lose every event ever posted for them, the cascade is aggressive on purpose to keep tables clean.
  • Deployment: liveAgent is currently running in the client's infrastructure and posting events. Default on creation.
  • Deployment: pausedTemporarily off (maintenance, awaiting a client config change). Events you POST while paused still land in client_events, the status is informational. Flip back to live when you resume.
  • Deployment: retiredPermanently decommissioned. Flip to this instead of deleting the row when an agent install ends, otherwise the events that posted under it lose their attribution (the deployment_id FK is ON DELETE SET NULL, so the events themselves survive but the link is gone).

Event types

  • call_answeredAgent picked up an inbound call (Vapi assistant or n8n handoff).
  • call_completedCall wrapped up. Use this for end-of-call metrics if your runtime distinguishes pick-up from completion.
  • lead_capturedA new lead got written to the CRM. Add metadata to identify source (form, missed-call recovery, etc.).
  • appointment_bookedCalendar event created. Add value_cents if you want it to show in the ROI rollup.
  • message_sentOutbound text or chat message dispatched by the deployed agent.
  • message_receivedInbound message handled by the deployed agent.
  • quote_sentQuote / estimate delivered to a prospect. value_cents = the quoted amount if you want to track pipeline.
  • customAnything else. Put a descriptive label in metadata so the activity feed reads sensibly.

Per-client URL + secret model

Every client gets a separate slug + secret. Compromising one client's secret does not affect any other client. Posting to client A's URL with client B's secret returns 404, indistinguishable from a typo on the slug, so blind guessing yields no signal.

Slugs are 16 hex chars, secrets are 48 hex chars, both generated server-side at creation time and stored on the client row. They don't expire. If a secret leaks today the fastest recovery is to delete the client (which cascades deployments + events) and re-add, both URL and secret regenerate. A dedicated "Rotate secret" button is on the roadmap once enough usage justifies it.

Privacy notes for the portal

  • What the client seesTheir name, your agency brand, aggregate counts by event type, attributed value, the deployments running for them, and per-event metadata you chose to send. Nothing about your other clients, nothing about your retainer.
  • Metadata is visibleAnything you put in the metadata object on the webhook event shows up on the portal feed. Don't post PII you don't want the client to see. The portal is effectively a public URL guarded only by the slug's non-enumerability.
  • AuthenticationNone. The slug is the secret. Treat the portal URL like a shared Google Doc link, share it deliberately. If you need to revoke a portal you delete the client (or wait for the planned slug-rotation feature).

Common failure modes

401 Missing X-Webhook-Secret: the runtime sent the POST without the header. Some no-code tools strip custom headers, in that case put the secret in the JSON body as "secret" instead.

404 Not found: slug or secret is wrong. We collapse "wrong slug" and "wrong secret" into the same response so attackers can't tell which factor failed. Copy both fresh from the client detail page.

400 Invalid event_type: the body did not include one of the eight allowed values. Use custom for anything that doesn't fit.

No events showing up: confirm the POST returned { ok: true } in the runtime's logs. Successful posts always include an event_id in the response. If you see duplicate: true, that event already landed under the same external_id.

Need help?

Stuck wiring the webhook from n8n or Vapi? Ask Ciela for a copy-paste config for your specific stack, or write to support@ciela.ai and we'll jump on the connection with you.

Ask Ciela

Keep reading