AI

How to build a per-client ad lead dashboard with Claude Code

The earlier guide on building with AI shows the context the agent needs. This one is a closed project: an internal dashboard that shows, per client, how many ad leads arrived and how many became sales. The architecture is small (webhook, database, one page) and fits an afternoon with Claude Code, as long as you split the work into verifiable prompts. The ZapZap MCP server comes in for queries only, in read-only mode, and your own code receives the events. ZapZap is an unofficial solution: WhatsApp may limit or block numbers, and the dashboard should show each instance's status.

Published on · 4 min read

This article is also available in: Português · Español

What you will build and the architecture

Four pieces: an endpoint that receives the instances' webhook, a table linking each instance to a client, a leads table with status and value, and a page with the per-client query. The events come from ZapZap, the sale goes back through tracking/conversion, and the dashboard only reads your database. Avoid letting the code agent query the API on every page view.

For secure receiving, follow the guide on idempotent webhooks. For measurement, see measuring CPL with the conversion API. Here the focus is on orchestrating the agent.

Step 1: project instructions and the read-only MCP

Start with the project instructions file, with the base URL, the variable names and the rule to use only operations from the OpenAPI. Then install the MCP server. On 11/11/2026 the zapzap-mcp package does not appear on npm (npm view returned 404), so installation means cloning the github.com/zapzapapi/zapzap-mcp repository and building it. Recheck with npm view before using npx.

Register it with ZAPZAP_READ_ONLY=true, which exposes only the read tools (list instances, check status, query messages and webhooks). That way the agent queries and does not send a message by mistake. The server does not receive messages: receiving is the webhook in the next step.

Claude Code: project instructions

# CLAUDE.md of the dashboard project (instructions for the agent)
- Stack: Node.js + Express + SQLite (or Postgres) + one HTML page.
- ZapZap API: base https://api.zapzapapi.com/api/v1, headers x-api-key and x-api-secret,
  read from ZAPZAP_API_KEY and ZAPZAP_API_SECRET. Never print or commit them.
- Spec: https://zapzapapi.com/openapi/zapzap-openapi.pt.json  Summary: https://zapzapapi.com/llms.txt
- Use only operations present in the spec. If something does not exist, say so.
- The zapzap MCP server is READ ONLY (ZAPZAP_READ_ONLY=true). Never ask to send messages.
- Webhooks: validate X-ZapZap-Signature on the raw body, dedupe by event_id or delivery_id.

Install the MCP server read-only

# The zapzap-mcp package is not on npm yet (npm view zapzap-mcp returned 404 on 2026-11-11),
# so install from the repository. Check "npm view zapzap-mcp" again before using npx.
git clone https://github.com/zapzapapi/zapzap-mcp.git
cd zapzap-mcp && npm install && npm run build

# Register it in Claude Code in read-only mode (no send tools are exposed)
claude mcp add zapzap \
  -e ZAPZAP_API_KEY=your-api-key -e ZAPZAP_API_SECRET=your-api-secret -e ZAPZAP_READ_ONLY=true \
  -- node /full/path/to/zapzap-mcp/dist/src/index.js

Step 2: the database schema and the page query

Ask the agent for the schema below, which maps instance_id to a client (the key the event always carries) and stores the lead by phone. Mapping by the envelope's instance_id is sturdier than relying on extra fields. The instance's metadata serves as a cross-check. The query at the end feeds the page.

SQL: minimal schema and page query

-- Minimal schema for the per-client lead dashboard
CREATE TABLE clients (id text PRIMARY KEY, name text NOT NULL);
CREATE TABLE instances (instance_id text PRIMARY KEY, client_id text NOT NULL REFERENCES clients(id));
CREATE TABLE leads (
  id integer PRIMARY KEY AUTOINCREMENT,
  client_id text NOT NULL, instance_id text NOT NULL,
  phone text NOT NULL, first_message text, first_seen_at text NOT NULL,
  status text NOT NULL DEFAULT 'open',          -- open | qualified | scheduled | won
  sale_value real, UNIQUE (instance_id, phone));
CREATE TABLE webhook_events (
  instance_id text NOT NULL, dedupe_key text NOT NULL, received_at text NOT NULL,
  PRIMARY KEY (instance_id, dedupe_key));

-- The page query: leads by client in the last 30 days
SELECT c.name, COUNT(*) AS leads,
       SUM(l.status = 'won') AS sales, COALESCE(SUM(l.sale_value), 0) AS revenue
FROM leads l JOIN clients c ON c.id = l.client_id
WHERE l.first_seen_at >= date('now', '-30 days')
GROUP BY c.id ORDER BY leads DESC;

Step 3: the prompts, one at a time, with a definition of done

Prompt 1: "Create the POST /webhook endpoint with express.raw, validate X-ZapZap-Signature (HMAC SHA-256 over t.body, 5-minute tolerance), store the event_id or delivery_id key in webhook_events and answer 200. Write tests for a valid, expired and tampered signature." Prompt 2: "Use the MCP to list the instances and fill the instances table; show me what you will write before writing." Prompt 3: "Record each first received message as a lead, unique per instance and phone. Before modeling, save the first real payload and show it to me." That last request keeps the agent from inventing the shape of the data field.

Prompt 4: "Create the /clientes page with the query, ordered by leads, and an instance status column." Prompt 5: "Call POST /{instanceId}/tracking/conversion when I mark a sale in the dashboard and handle 404 and 409 as documented." At every step, require the agent to cite the OpenAPI operation it used.

Step 4: review, security and what comes next

Review three things before going live: secrets only in environment variables, never in the repository or the prompt; end-customer text treated as untrusted input (the MCP reads messages and they may contain instructions); and a login on the page, because it shows phone numbers. Keep read-only mode, and only think about enabling sending after a volume lock and an allow-list.

Evolve in order: a number health dashboard, then the monthly report (automating reports), then the client portal link. For the general context, see the AI agents page and AI agencies.

Quick summary

  • A per-client dashboard fits in four pieces: webhook, instance-to-client map, leads table and a page.
  • Install the MCP by cloning the repository (the package was not on npm on 11/11/2026) and use ZAPZAP_READ_ONLY=true.
  • Map by the envelope's instance_id, and use metadata as a cross-check.
  • Ask the agent for the first real payload before modeling the data field.
  • Review secrets, untrusted input and login before publishing.

Also read

Frequently asked questions

Do I need the MCP to build the dashboard?

No. The dashboard works with just the webhook and the OpenAPI. The MCP helps the agent query instances and status during development.

Does the MCP receive customers' messages?

No. According to the server's README it does not receive incoming messages: configure the webhook for that.

Why ZAPZAP_READ_ONLY=true?

With it only the read tools are exposed, and the agent cannot send a message or change the instance.

Which database should I use?

Any SQL database. The guide's schema uses SQLite for simplicity; for several simultaneous users, prefer Postgres.

Does the dashboard replace ZapZap's client portal?

No. The portal is the screen the client sees with their brand; the guide's dashboard is internal to the agency, with the view of all clients.

Start the dashboard with one instance

Create the account, connect a number and point the webhook at your project. R$15 per active number per month, with no per-message fee. When you verify your phone there is a welcome credit, subject to the current campaign.