Most WhatsApp API dashboards answer one question well and the rest badly: is it up? The screen that actually earns its place is not a chart of messages sent. It is a searchable log of every request, with the account, the error code and the thing you can do next. This post is what to build, in what order, and what each screen is really for.
#The one screen that matters
Everything a provider claims about reliability is a summary of the request log. Put the log first and the rest of the dashboard becomes optional.
A request log entry needs five things, and four of them are not optional:
- The route and method. Which endpoint, which verb.
- The status code. The one your code actually saw, including the 202.
- The account. Which linked number this was for, by name and id.
- The `x-request-id`. Search for it in your own logs and you can quote it to support.
- The timestamp and the duration. When, and how long it took.
{ "id": "req_8f2k1m9p4q", "createdAt": "2026-09-24T14:22:31.408Z", "method": "POST", "route": "/v1/messages", "status": 202, "durationMs": 38, "accountId": "k57a8m2x9d3f0q1wjh6ypc4n2d7s0vbr", "accountName": "Support line", "errorCode": null, "requestId": "req_8f2k1m9p4q"}A log without the account is a log you cannot use. Half of all integration questions are "which number was this from", and a row that does not say is a row that costs a support ticket.
The same is true of the project. If you run one key per customer, the log needs that project on the row too, because "which customer's number" is the first question in every platform support call. The projects and usage reference is where that scoping comes from.
#The second screen: accounts and their state
One row per linked number, and the state is the content. This is the screen a person opens when a customer says their bot stopped answering.
| column | why it is there |
|---|---|
| name and number | the human handle for the row |
| status | ready, disconnected, failed, and the rest |
disconnectReason | the diagnosis, when there is one |
| last connected | how long it has been down |
| proxy location | the country and city it exits from |
| pacing | whether it has been customised from the defaults |
The reason column is the one that earns the screen. A status of disconnected says nothing; temporary_ban says stop sending and wait, and logged_out says the device was removed from the phone. Same status, opposite response, so the reason has to be on the row.
What an operator does when a customer says the bot is down
#The third: webhooks and their deliveries
Sending is visible in the API. Receiving is not, because it happens on your server. A dashboard that shows only what you sent is blind to half the integration.
A webhook delivery view needs the endpoint, the event type, the attempt number, the response code and the time. When a customer says "the bot missed a message", this screen is the answer.
- Endpoint URL and the events it is subscribed to.
- Delivery attempts, with the 2xx or the failure.
- Which of the six attempts this was.
- The request id, so the same request appears in the request log.
#The fourth: usage, and the line that surprises people
Two numbers matter for a bill, and only one of them is messages.
- Connected accounts, and the band each one falls into.
- Proxy traffic, metered in bytes, and how much of it the pooled allowance covered.
Everything else on a usage screen is decoration. A dashboard that leads with "messages sent" is optimising for a number that is not billed.
| what to show | why |
|---|---|
| connected accounts | the only line that scales with customers |
| proxy bytes used, and the pool | where a media-heavy account's overage appears |
| messages, if at all | for a platform rebilling per customer |
#The fifth: the API key screen
Small screen, high stakes. It needs to answer one question: what does this key reach?
- The key's name and prefix, so you can tell two keys apart in a log.
- The scope: the whole organization, or one project.
- When it was last used, which is how you find a key nobody needs any more.
- A one-time reveal, for the organization's owner, from an encrypted copy.
Revoking takes effect on the next request, and there is no grace period to discover it was still in use. The last-used column is how you check before you press it.
#What to build first, in order
If you are building a dashboard for a WhatsApp integration, this is the order that pays.
- The request log. Nothing else works without it.
- Accounts and their state, with
disconnectReasonon the row. - Webhook deliveries, grouped by event.
- Keys, with scope and last used.
- Usage, accounts and proxy bytes.
Charts, funnels and message-volume graphs come after all five, and they may never come, because they answer questions about the past that a log answers better about a specific failure.
#The shapes a log should make easy
Four questions a log has to answer without anyone exporting a CSV.
- Which errors are we getting? Group by
error.code.rate_limitedandaccount_offlineneed different responses. - Is one number the problem? Filter by account. A failure on one account is an account problem; the same failure across all of them is you.
- What did I send that got `not_on_whatsapp`? The log has the body, so you can see the recipient.
- Which support ticket is this? Search the
x-request-idthe customer quoted.
- Filter by
- route, status, error code, account, project, time range
- Search by
- request id, account name, recipient
- Keep
- the request body, with secrets removed
- Never keep
- the full API key; store a hash and a prefix
#Why the request id is the thread
Every response carries x-request-id. A support conversation should be able to run on that one string: the person quotes it, you paste it into the log, and you see the request, the account, the response and the webhook deliveries it produced.
try {
await wuapi.messages.send({ accountId, to, text })
} catch (err) {
if (err instanceof WuapiError) {
logger.error({ code: err.code, message: err.message }, "send failed")
}
}This is the cheapest thing on the list and it shortens every support conversation you will ever have.
#What the screens owe each other
A dashboard is not five screens, it is one investigation seen at five zoom levels, and it is worth checking that each screen's output is the next screen's input.
| the question | the screen that answers it | what it hands to the next screen |
|---|---|---|
| a customer says the bot is down | accounts and their state | an account id and a disconnectReason |
| a send failed and nobody knows why | the request log | a request id and an error.code |
| an event did not arrive | webhook deliveries | an event id and an attempt number |
| the bill does not match expectations | usage | an account count and a byte count |
| a key may no longer be needed | keys | a last-used timestamp |
The chain that breaks most often is the first one. An account that dropped at 14:22 explains every failed send after 14:22, and finding that in thirty seconds is the difference between an outage and a support ticket queue. That is why disconnectReason belongs on the account row rather than behind a click.
#Two things a WhatsApp dashboard should never do
Both are common, both are wrong, and both come from copying a generic analytics dashboard.
Do not lead with a message-volume chart. Messages are not the unit you are billed on, and a chart of a number that is neither billed nor actionable is the screen a person scrolls past. The account count and the proxy bytes are the two that change a decision.
Do not show a green uptime badge and stop there. "99.98% uptime" tells a customer nothing they can act on. What they need is the last failed request, which account it was, and whether a message was lost. That is the request log, which is why it is first.
The underlying mistake is treating a dashboard as a report about the past rather than a tool for the next ten minutes. Everything in this post is chosen by asking: will this screen change what somebody does, and if not, why is it here.
#The four questions a dashboard is asked
Every screen in every product dashboard reduces to a small number of questions somebody actually typed. On a WhatsApp integration they are consistently these four, and each one has a screen that answers it without a spreadsheet.
- Is it up? Answered by the status page, not by this dashboard, and it deliberately shows a different thing: component health, not your integration.
- Is my number up? Answered by the accounts screen, and the answer is the status plus the reason.
- What just failed? Answered by the request log, filtered by error code and account.
- What is this costing me? Answered by the usage screen, in accounts and proxy bytes.
A fifth question arrives constantly and has no screen at all: whose number was this? That is a request for the log to carry the account and the project on every row, which is why those two fields are not optional in the entry above. Adding them costs nothing at write time and everything at read time.
#Alerts worth having, and alerts worth not having
Four alerts cover a WhatsApp integration properly, and the discipline is in what you leave out.
| alert | fires when | why it earns its place |
|---|---|---|
| an account disconnected | any disconnectReason | the only alert that predicts a support ticket |
a rate_limited run | three or more in a minute | a pace problem, visible before a customer complains |
| webhook deliveries failing | two consecutive attempts | your server, not WhatsApp, and it is silent from outside |
| an overage crossing the pool | a usage call reports it | a bill you can still explain |
What to leave out is the important half. Do not alert on a single failed send: account_offline during a reconnect is expected behaviour, and an alert for it teaches people to ignore alerts. Do not alert on every not_on_whatsapp: a number that is simply not on WhatsApp is a fact, not an incident. And do not alert on message volume, because volume is not something anybody decided to do.
if (event.type === "account.disconnected") {
await alertOnce({
key: `disconnected:${event.data.object.id}`,
title: `${event.data.object.name} is ${event.data.object.disconnectReason}`,
})
}Deduplicating on the account id matters more than the threshold. A reconnection loop can fire the same event eleven times, and eleven notifications is eleven reasons to turn notifications off.
#Retention, which is a product decision
The request log is the only screen here that grows without bound, so how long you keep it is worth deciding on purpose rather than by default.
Three questions decide it, and they are the same three that decide every log-retention policy.
- What is the longest a customer takes to report a problem? A month is generous for an integration and thin for an agency reselling it, because the person who notices may be your customer's customer.
- What does an investigation actually need? One request, its retries, and the events it produced. A failure that surfaces after a week was caused by something in the first hour, so the deep history is rarely what you open.
- What is it worth to be able to answer "prove it was sent"? Some disputes are settled by a log line and a timestamp, and that is worth keeping for a year for a fraction of the volume.
A workable default: keep everything for 30 days, keep failed requests and everything carrying a request id for a year, and aggregate the rest. The aggregation is where the volume goes, because a successful POST /v1/messages is the overwhelming majority of the log and the least interesting row in it.
Whatever you choose, keep the account and the error.code on the aggregate. A monthly count of failures per account is more useful than a monthly count of successes, and it is a fraction of the size.
#Questions people ask
What should a WhatsApp API dashboard show first?
The request log, with the route, the status code, the account, the duration and the `x-request-id` on every row. Almost every integration question is answered by one of those five fields, and a dashboard that leads with a chart of messages sent has put its least useful screen first.
How do I debug a failed WhatsApp message?
Start with the `x-request-id` from the failed response, find it in the request log, and read `error.code`. `rate_limited` means it waited past the queue timeout, `account_offline` means the account went down while it waited, and `send_failed` usually means the media URL refused the download.
Why is my WhatsApp number showing as disconnected?
Read `disconnectReason`, because the status alone is not a diagnosis. `logged_out` means the device was removed from the phone and will not come back on its own. `temporary_ban` means WhatsApp acted against the number and you should stop sending. Most other reasons reconnect by themselves after a network drop.
What should I log for a WhatsApp integration?
The request id, the route and method, the status, the account id, the error code and the duration, on every request. Keep the request body with secrets removed so you can see what was sent. Never store a full API key: a hash and a prefix are enough to identify it and to revoke it.
#Where to go next
Verifying a WhatsApp webhook signature covers the events that arrive on your server rather than in the log. The WhatsApp QR code is about the account.qr_code_issued case a dashboard has to render, and what a WhatsApp API costs is where the proxy bytes on the usage screen end up.