Skip to content

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
text

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 ….

None of this is specific to one product. It is the standard remote-MCP flow — OAuth 2.1 with dynamic client registration (RFC 7591), PKCE, and audience-restricted tokens (RFC 8707) — so any conforming client connects the same way.

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

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.

ScopeWhat the assistant can reach
profileWho authorized, and which workspace
connections:readYour connections and their status, chats, message templates, number lookup
messages:readChat history, and the media of a received message
messages:sendSending — every message type below, plus the typing indicator
webhooks:readYour webhooks, the event catalogue, delivery attempts
webhooks:writeCreating, 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

ToolScopeConnection
get_workspaceprofileany
list_connectionsconnections:readany
check_whatsapp_numberconnections:readQR only
list_chatsconnections:readQR only
list_message_templatesconnections:readOfficial only
get_message_templateconnections:readOfficial 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

ToolScopeConnection
send_whatsapp_messagemessages:sendanycosts money
send_whatsapp_mediamessages:sendanycosts money
send_whatsapp_voice_notemessages:sendanycosts money
send_whatsapp_stickermessages:sendQR onlycosts money
send_whatsapp_templatemessages:sendOfficial onlycosts money
send_whatsapp_listmessages:sendQR onlycosts money
send_whatsapp_buttonsmessages:sendQR onlycosts money
set_typing_presencemessages:sendQR onlypaid plan
list_chat_historymessages:readany
download_message_mediamessages:readany
prepare_media_uploadmessages:sendanyopens 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

ToolScope
list_webhook_event_typeswebhooks:readthe catalogue, with a reliability note per event
list_webhookswebhooks:read
list_webhook_deliverieswebhooks:readwhat Pingo actually tried to deliver
create_webhookwebhooks:write
update_webhookwebhooks:writedestructive: the event list replaces, it does not merge
delete_webhookwebhooks:writedestructive

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.update and connection.update never 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.update and presence.update. list_webhook_event_types carries that truth per event, which is why an assistant should call it before creating a subscription. For connection status, poll list_connections instead.
  • A webhook with no connection linked never fires. create_webhook requires at least one connection id — pass every id if you want the whole workspace. The webhook is created active and list_webhook_deliveries comes 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 only5511999998888, no +, no spaces, no punctuation. For a number you have not messaged before, run check_whatsapp_number first and send the canonical id it returns rather than what the user typed.
  • Already-hosted media goes by public https URL; 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_upload returns a single-use upload URL: it uploads the file with one curl -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_media gives 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_template fills positional text variables only. A template whose parameterFormat is NAMED, 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.
  • delay is 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

TransportStreamable HTTP, stateless, POST only
Rate limit, MCP endpoint300 requests per minute, keyed by the credential rather than the IP
Rate limit, /v3/oauth/token120 per minute, per IP
Rate limit, /v3/oauth/authorize and /register15 per minute, per IP
Access tokenvalid for 1 hour
Refresh tokenvalid for 30 days, rotated on every use
Authorization codevalid for 60 seconds, single use
Message quota and text lengthwhatever your plan allows — the same limits the dashboard enforces
Media sizeby 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 requiredset_typing_presence only
Upload URLsingle 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 exposedWhy
Read or rotate a webhook's signing secretThat 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 templateIt 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 helpdeskOut 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.

SymptomWhat 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 workspaceThe 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 fireslist_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_token grant at registration. grant_types omitted means authorization_code alone, 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 a client_secret shown 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 resource parameter scopes the token to a path. A token minted for https://api.pingonotify.com/v3/mcp is accepted under /v3/mcp and 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.

© 2026 Pingo Notify. All rights reserved.

pingonotify.com ·Built with Nuxt and Scalar