MCP server
Connect an AI assistant to your workspace — how to authorize it, what it can do, and every tool it gets.
Pingo Notify speaks MCP (Model Context Protocol), so an AI assistant can operate a workspace directly: list connections, send WhatsApp messages, read a conversation, manage webhooks. You approve what it may do once, on a consent screen, and it never handles an API key.
Connect
Add one URL to the client, as a custom or remote connector:
https://api.pingonotify.com/v3/mcp
Everything after that is the protocol doing its job:
The client discovers the server
Its first call arrives with no credential and gets a 401 carrying WWW-Authenticate: Bearer …, resource_metadata="…/.well-known/oauth-protected-resource/v3/mcp". That document names the authorization server and, deliberately, only the scopes this server actually uses.
It registers itself
POST /v3/oauth/register. Registration needs no paperwork: an AI connector registers as a public client — no secret, PKCE instead.
You authorize
The client opens /v3/oauth/authorize. You sign in, choose the workspace, and approve the scopes it asked for. This is the only step with a person in it.
It gets a token
POST /v3/oauth/token exchanges the code, and from there every call carries Authorization: Bearer ….
Only an Owner or an Admin can authorize an application for a workspace. The consent screen offers you only the workspaces where your role allows it; a Manager or an Agent cannot connect an assistant to the team's workspace. Authorizing without picking a workspace binds the token to your personal account instead.
Connecting itself needs no plan, and it keeps working on an expired subscription — but sending does not. See Limits.
What the assistant gets
One workspace, fixed at consent
Everything the assistant sees belongs to the workspace you picked while authorizing. There is no tool to switch workspaces and no header it can send to change one: a request that tries answers 403 — This access token is bound to a different workspace; re-authorize to switch. A second workspace means a second authorization.
Scope is a ceiling, never a grant
The effective permission of a token is the intersection of the scopes you approved and what you can do in that workspace. Approving messages:send does not let an assistant send if your own role cannot; a scope only ever narrows.
| Scope | What the assistant can reach |
|---|---|
profile | Who authorized, and which workspace |
connections:read | Your connections and their status, chats, message templates, number lookup |
messages:read | Chat history, and the media of a received message |
messages:send | Sending — every message type below, plus the typing indicator |
webhooks:read | Your webhooks, the event catalogue, delivery attempts |
webhooks:write | Creating, editing and deleting webhooks |
You can always approve less than what was asked. Re-approving with fewer scopes revokes the tokens already issued to that integration, so the screen and reality never disagree.
connections:write may appear on the consent screen as well, because it also satisfies "read a connection". No tool in this catalogue needs it — approving connections:read alone is enough.
It knows its own limits before it tries
The handshake carries a paragraph assembled for that token: the tools this access can use, and the ones it cannot, by name. An assistant that already knows it lacks webhooks:write tells you so instead of spending a turn discovering it.
Tools out of scope stay listed on purpose. Hiding them would turn a refusal that names the missing scope into an unhelpful "unknown tool".
The tools
Twenty-three tools. The cut is by domain, not by route — the helpdesk is out of scope by product decision.
Orientation
| Tool | Scope | Connection |
|---|---|---|
get_workspace | profile | any |
list_connections | connections:read | any |
check_whatsapp_number | connections:read | QR only |
list_chats | connections:read | QR only |
list_message_templates | connections:read | Official only |
get_message_template | connections:read | Official only |
Start with list_connections, always. Every other tool needs a connectionId, and it is the tool that reveals the connection's type — which decides whether a sending tool works at all. It reports canSend too, which is true only while the connection is open.
check_whatsapp_number takes up to 50 numbers at once and returns the canonical id of each. get_workspace tells the assistant which scopes it actually holds.
Messaging
| Tool | Scope | Connection | |
|---|---|---|---|
send_whatsapp_message | messages:send | any | costs money |
send_whatsapp_media | messages:send | any | costs money |
send_whatsapp_voice_note | messages:send | any | costs money |
send_whatsapp_sticker | messages:send | QR only | costs money |
send_whatsapp_template | messages:send | Official only | costs money |
send_whatsapp_list | messages:send | QR only | costs money |
send_whatsapp_buttons | messages:send | QR only | costs money |
set_typing_presence | messages:send | QR only | paid plan |
list_chat_history | messages:read | any | — |
download_message_media | messages:read | any | — |
prepare_media_upload | messages:send | any | opens a send |
There is one tool per shape of message rather than one tool with a type field. A single schema with seven optional branches lets a model assemble a combination WhatsApp rejects — and that error arrives after the send was charged.
Webhooks
| Tool | Scope | |
|---|---|---|
list_webhook_event_types | webhooks:read | the catalogue, with a reliability note per event |
list_webhooks | webhooks:read | — |
list_webhook_deliveries | webhooks:read | what Pingo actually tried to deliver |
create_webhook | webhooks:write | — |
update_webhook | webhooks:write | destructive: the event list replaces, it does not merge |
delete_webhook | webhooks:write | destructive |
Rules that fail in silence
Worth reading before you let an assistant loose: ignoring these produces no error at all, just a result nobody gets.
The connection type decides what is possible. A QR connection sends stickers, lists, buttons and the typing indicator, checks whether a number exists on WhatsApp, and is the only kind that feeds a webhook with the full set of events. An Official connection (Meta's Cloud API) sends approved templates — the only way to start a conversation outside the 24-hour window — and does none of the above. A list or a set of buttons sent through the official channel arrives as plain text, with no buttons, and nobody gets an error.
- An official connection feeds a webhook less. Inbound messages (
messages.upsert) and delivery updates (messages.update) do arrive there;send.message,messages.delete,messages.edited,presence.updateandconnection.updatenever do. Subscribing to them on an official connection produces silence, not a rejection. - Two events are accepted at registration and never delivered on any connection:
connection.updateandpresence.update.list_webhook_event_typescarries that truth per event, which is why an assistant should call it before creating a subscription. For connection status, polllist_connectionsinstead. - A webhook with no connection linked never fires.
create_webhookrequires at least one connection id — pass every id if you want the whole workspace. The webhook is created active andlist_webhook_deliveriescomes back empty, which reads exactly like "wrong event". - Your endpoint will see the same event more than once. Delivery times out at 10 seconds and is retried up to three times, one row logged per attempt. Make the endpoint idempotent; an endpoint that answers slower than 10 seconds guarantees duplicates.
- Phone numbers travel as international digits only —
5511999998888, no+, no spaces, no punctuation. For a number you have not messaged before, runcheck_whatsapp_numberfirst and send the canonical id it returns rather than what the user typed. - Already-hosted media goes by public
httpsURL; a local file goes by upload. Base64 in the argument is not accepted, and that is not fussiness: once an image reached the assistant as an image, it is no longer a string it can hand back — and when the bytes do exist, writing them would consume the whole reply (1 MB of file is ~1.4 million characters). For a file on the assistant's machine,prepare_media_uploadreturns a single-use upload URL: it uploads the file with onecurl -F 'file=@path'and the message is sent in that same request, without the bytes ever crossing its context. With no terminal available, the same URL lets you upload the file yourself. - The media type is a choice, and the contact sees different things. An image shows in the bubble with the caption underneath; a video becomes a player with a thumbnail and plays inside the conversation; an audio is an attachment, with a play button and the file name beside it; a voice note is the round waveform bubble with 1x/1.5x/2x playback — what people mean by "send an audio"; a document is the card with an icon, file name and size that the contact downloads to open. Anything that is not an image, a video or an audio can only be a document. The assistant declares this type when it asks for the upload URL (or in
mediatype, when sending by URL) — it is not guessed from the file extension, precisely so a video does not arrive as a file card. - Sending costs the workspace money and cannot be undone. The server instructions tell the assistant to confirm with you before sending, unless you already asked for that send in the conversation.
download_message_mediagives up quietly on a big file. Above roughly 750 KB it answers with no content and a note telling the assistant to open the file in Pingo — a success, not an error. A client that only checks for failure will carry on with nothing in hand.send_whatsapp_templatefills positional text variables only. A template whoseparameterFormatisNAMED, or that has a media header or a dynamic button, cannot be filled through this tool — that one is the HTTP API. Meta rejects the whole send when the name, language or parameter count do not match.delayis in milliseconds and stops at 20000. It blocks on the WhatsApp side; higher values time the request out after the message was accepted — an error for the assistant, a delivered message for the contact.
Limits
| Transport | Streamable HTTP, stateless, POST only |
| Rate limit, MCP endpoint | 300 requests per minute, keyed by the credential rather than the IP |
Rate limit, /v3/oauth/token | 120 per minute, per IP |
Rate limit, /v3/oauth/authorize and /register | 15 per minute, per IP |
| Access token | valid for 1 hour |
| Refresh token | valid for 30 days, rotated on every use |
| Authorization code | valid for 60 seconds, single use |
| Message quota and text length | whatever your plan allows — the same limits the dashboard enforces |
| Media size | by URL, WhatsApp's own ceiling — Pingo has no bytes to measure and does not apply the plan's limit. By upload (prepare_media_upload), your plan's per-media-type limit applies |
| Paid plan required | set_typing_presence only |
| Upload URL | single use, valid for 10 minutes |
The MCP bucket is keyed by credential on purpose. A remote MCP client is not called by your browser: the calls come from the infrastructure hosting the model, out of a handful of egress IPs. Keyed by IP, one workspace would throttle another.
The MCP endpoint answers POST only. GET and DELETE return 405 with Allow: POST. The server is stateless, so there is no session for a streamed GET to receive; a client that opened one would hold a socket that never gets a message.
What it deliberately will not do
| Not exposed | Why |
|---|---|
| Read or rotate a webhook's signing secret | That secret is what proves an event came from Pingo. In a tool it becomes the ability to forge signed events against your endpoint. Rotating it breaks verification in production the instant after, and nothing in a chat can update the other side. |
| Create, edit or delete a message template | It is connection configuration, not messaging, and a rejected template counts against your WhatsApp Business quality rating. Reading templates stays available — without it, sending one is impossible. |
| Anything in the helpdesk | Out of scope by product decision. |
The first two live in the dashboard, where there is a person.
When something goes wrong
A tool that fails comes back as tool content, readable, not as a protocol exception — so the assistant sees the reason and can correct course: ask you for another scope, warn you, try a different tool. Unexpected failures never leak detail; the stack and any database message stay in the log.
| Symptom | What it means |
|---|---|
"This integration was not granted permission to use <tool>" | The token lacks a scope. The refusal names which ones; re-authorize the connector and approve them. If it repeats, check that your own role in the workspace has that permission — the token cannot exceed you. |
| "This workspace has no active plan, so it cannot send messages" | Reading keeps working; sending does not. |
| "…reached its plan limit of N messages for the period" | The quota, not the request. Retrying will not help. |
| 403 on every call, mentioning another workspace | The token is bound to the workspace chosen at consent. Re-authorize to switch. |
401 with error="invalid_token" | Expired, revoked, or issued for a different resource. The WWW-Authenticate header says which, and points at the metadata document to start over. |
| My webhook never fires | list_webhook_deliveries says whether Pingo ever tried. An empty list is a subscription problem — wrong event, or an official connection. A list of 4xx/5xx is your endpoint. |
Building your own client
If you are writing the client rather than using one, four details save an afternoon:
- Ask for the
refresh_tokengrant at registration.grant_typesomitted meansauthorization_codealone, and then no refresh token is ever issued — your connector stops working an hour later and needs a human again. - Send
token_endpoint_auth_method: "none"to be a public client. Omitting the field creates a confidential client instead, which gets aclient_secretshown exactly once and must send it on every token call. - The first request needs
Accept: application/json, text/event-stream. The MCP transport requires it, even though this server always answers JSON. - The
resourceparameter scopes the token to a path. A token minted forhttps://api.pingonotify.com/v3/mcpis accepted under/v3/mcpand rejected with 401 everywhere else in the API. Ask for the resource you actually intend to call.
prompt=consent forces the screen even when a live consent already covers the request; prompt=none never shows it and answers consent_required or login_required instead. Otherwise a repeat authorization can complete silently — but only when exactly one existing consent covers the scopes asked for, and your role still allows it.
The authorization server describes itself at /.well-known/oauth-authorization-server, and this endpoint's own metadata — including the exact list of scopes it uses — at /.well-known/oauth-protected-resource/v3/mcp. Both are cached for five minutes and need no credential. For the credential model behind the rest of the API, see Authentication.