AI agents
MCP server
Kawaa speaks the Model Context Protocol, so an AI agent can verify addresses, watch a bulk job and read the results without anyone writing an integration. One URL: sign in, or use an API key.
Connect
The endpoint is https://api.kawaa.com/mcp. Authenticate with the same API key you would use for the REST API, as an HTTP bearer token. Create one in Settings → API keys.
It uses the current stable MCP protocol (2026-07-28) over Streamable HTTP, with compatibility for deployed 2024/2025 clients. There is no vendor-specific server: every tab below reaches the same endpoint and the same tool schemas.
export KAWAA_API_KEY=ev_your_key_here
claude mcp add --transport http --scope user kawaa https://api.kawaa.com/mcp \
--header "Authorization: Bearer $KAWAA_API_KEY"
# Check it connected
claude mcp listexport KAWAA_API_KEY=ev_your_key_here
codex mcp add kawaa \
--url https://api.kawaa.com/mcp \
--bearer-token-env-var KAWAA_API_KEY
# Check it connected
codex mcp list// ~/.cursor/mcp.json: prefer this over .cursor/mcp.json inside a
// repository, which puts the key somewhere a commit can publish it.
// Cursor interpolates ${env:...}, so the key stays in your environment.
{
"mcpServers": {
"kawaa": {
"url": "https://api.kawaa.com/mcp",
"headers": {
"Authorization": "Bearer ${env:KAWAA_API_KEY}"
}
}
}
}
// Cursor will not load a newly configured server until you approve it,
// and the approval is per project. Run this once in each:
// cursor-agent mcp enable kawaaexport KAWAA_API_KEY=ev_your_key_here
copilot mcp add --transport http \
--header "Authorization: Bearer $KAWAA_API_KEY" \
kawaa https://api.kawaa.com/mcp
# Check the saved server
copilot mcp get kawaaexport KAWAA_API_KEY=ev_your_key_here
grok mcp add --transport http kawaa https://api.kawaa.com/mcp \
--header "Authorization: Bearer $KAWAA_API_KEY"
# Check it connected
grok mcp doctor// ~/.gemini/config/mcp_config.json
{
"mcpServers": {
"kawaa": {
"serverUrl": "https://api.kawaa.com/mcp",
"headers": {
"Authorization": "Bearer ev_your_key_here"
}
}
}
}export KAWAA_API_KEY=ev_your_key_here
# ${KAWAA_API_KEY} stays a reference; OpenClaw expands it when it connects,
# so the key never lands in the config file.
openclaw mcp set kawaa '{"url":"https://api.kawaa.com/mcp","transport":"streamable-http","headers":{"Authorization":"Bearer ${KAWAA_API_KEY}"}}'
# Check it connected
openclaw mcp probe kawaaOr sign in instead of pasting a key
A client that supports MCP’s OAuth sign-in needs no key at all. Add the server with just the URL; the client finds Kawaa’s sign-in from the endpoint, opens a Kawaa page where you approve it, and receives a short-lived token it refreshes on its own. We have run this end to end with Claude Code and Codex:
claude mcp add --transport http --scope user kawaa https://api.kawaa.com/mcp
# Opens Kawaa's consent screen in your browser; approve, and you're connected
claude mcp login kawaacodex mcp add kawaa --url https://api.kawaa.com/mcp
# Opens Kawaa's consent screen in your browser; approve, and you're connected
codex mcp login kawaa- You see who is asking and for what. The approval page names the app (and says whether a domain vouches for that name or the app chose it itself), lists each permission it wants, and lets you untick any of them. By default an agent asks to verify, follow its jobs, read results and see usage.
- Some things are never on offer. A connected app can never change your plan or payment details, manage your team, change your password or keys, or delete the account, whatever it asks for.
- You can end it at any time. Settings → AI agents lists each connected app with what it may do and when it was last used. Disconnecting stops it on its next call. A connection left unused for 30 days ends on its own, and changing or resetting your password, or locking the account, disconnects every app.
- A copied token is cut off, and so is the app. Each refresh token the app is given works once. If the answer to a refresh gets lost, or the app sends several refreshes at once, it can repeat that refresh up to 5 times within 60 seconds. Any other reuse means someone else holds a copy, so Kawaa ends the connection at once for everyone holding it, and the app has to be connected again.
Using a key? Give the agent one of its own
Create a separate key for each agent rather than reusing the one your application holds. Two things follow from that, and both matter the first time you want to know what the agent has been doing:
- You can see its activity on its own. The dashboard’s analytics page reports verifications, API calls and last-used per key, so you can tell whether an agent is busy and when it last ran. Credit spend is not broken down that way: the ledger is per account.
- You can cut it off on its own. Revoking one key from Settings → API keys stops the agent on its next call and leaves your other integrations untouched.
Treat the key the way you treat a password: prefer an environment variable over a value pasted into a config file that might end up in a repository.
What the agent can do
Twelve tools, designed around what someone actually asks an agent to do rather than around the shape of the API. Seven of them read, four verify, and one stops a running job.
Five carry the protocol’s readOnlyHint, which is what a client uses to decide what may run without asking you. Two of the reads deliberately do not, because that hint is about consequence rather than price: get_email_history spends 0.5 credits, and get_job_export_link is free but writes the whole result set to storage behind an hour-long link that anyone holding it can read. Neither changes a record; both deserve the same pause as spending money. cancel_job is the one tool marked destructiveHint, so a client should ask you before it runs.
Reading
| Tool | Cost | What it does |
|---|---|---|
get_account | Free | Plan, credit balance, usage against the current allowance, the API rate limit your key is held to (your plan's, unless a white-label provider's limit or a cap on the key itself is lower), and `limits_plan`: the plan whose limits apply, which for a white-label sub-account is its provider's plan. |
get_usage | Free | Credits, API calls, verifications and bulk jobs against the current allowance. Read `credits.allowance_basis` before calling it monthly: only `billing_period` resets. A Free plan, a white-label sub-account and a paid plan with no billing period on file are not reset on a schedule, so their figures are cumulative. |
list_jobs | Free | Your verification jobs, newest first. |
get_job | Free | One job's progress and verdict counts. |
get_job_results | Free | A job's per-address verdicts, a page at a time, filterable by status. |
get_job_export_link | Free | A time-limited link to a large job’s results, rather than fifty pages of them. Says whether the file is the complete set. |
get_email_history | 0.5 credits | Engagement your own account's tracking pixels and links have recorded for an address. |
Verifying
These spend credits. Each one says its price in its own description, so the agent knows before it calls.
| Tool | Cost | What it does |
|---|---|---|
verify_email | 1 credit (0.5 cached) | Verify one address and return the verdict. |
verify_emails | ~1 per address | Submit a list as a job. How many at once depends on your plan. |
find_business_email | 5 credits | Work out a work address from a name and a company domain. Starter plan and up; a lower plan is refused before anything is charged. |
check_domain_setup | 10 credits (0 cached) | SPF, DKIM, DMARC, MX and blocklist check for a domain. A repeat for the same domain and sending IP within 24 hours costs 0 credits, unless refresh is true. |
Stopping a job
Free, and it gives credits back. It is still marked destructive: a stopped batch cannot be resumed, so the work it abandons has to be submitted again.
| Tool | Cost | What it does |
|---|---|---|
cancel_job | Free | Stop a running batch. Addresses already checked stay, stay billed and stay readable; the credits held for the rest are refunded. Needs the verification:write scope. |
Nothing here can change your account
Apart from stopping a running batch, an agent cannot change anything: there is no tool to change a plan, buy credits, invite a teammate, rotate a key or delete data. Those stay in the REST API, for a person with the right credential.
Try it
Once the server is connected, ask in plain language. The agent picks the tools.
Check how many Kawaa credits I have left.
Verify sales@example.com and tell me whether it is safe to send to.
Clean the list in subscribers.csv: verify every address, then tell me
which ones to remove and which need a human decision.
Why is mail from example.com landing in spam?The last two are also built-in prompts, clean_email_list and diagnose_deliverability, which walk the agent through the whole workflow, including budgeting, paging the results, and waiting for a bulk job properly instead of resubmitting it.
A prompt has to be invoked, not described
Typing the sentence does not load the prompt. In Claude Code the server’s prompts appear to you as slash commands under /mcp, not to the model as something it can choose, so asking in plain language gets you the tools and the server’s connection-time instructions, which is usually enough, but not the step-by-step the prompt carries. Run /mcp and pick clean_email_list when you want that.
Clients differ here, and it is a client behaviour, not a server one: check how your client lists an MCP server’s prompts before relying on one.
You do not need to prompt-engineer this
The server tells the agent, at connection time, that verification costs credits, which tools are free, and that catch_all and unknown are not verdicts to act on mechanically. Each tool states its own price and its own arguments. There is nothing to paste into a system prompt, and nothing that will quietly go stale in one.
The one line worth adding to your repository’s AGENTS.md or CLAUDE.md is the fact that it exists, so an agent looks for it:
Email verification, list hygiene and deliverability checks are available
through the `kawaa` MCP server. Use it rather than writing HTTP calls.Give the key only what the agent needs
A key can be created with a list of scopes, and a scoped key is refused (with a 403 naming the scope it needed) for anything outside them. An agent doing list hygiene has no reason to reach billing, the team or the audit log, so do not hand it a key that can.
curl -X POST https://api.kawaa.com/v1/api-keys \
-H "Authorization: Bearer $KAWAA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Claude Code agent",
"scopes": ["verification:write", "verification:read", "account:read", "analytics:read"]
}'That list is what GET /v1/api-keys publishes as recommended_agent_scopes, and it covers every tool except check_domain_setup. Add deliverability:write if the agent should run domain checks too. Omit scopes entirely and the key is unrestricted. In the dashboard, the key form’s AI agent choice creates this same key, and Full access creates an unrestricted one.
Scopes are chosen when the key is created
They cannot be changed afterwards. To narrow an agent’s access, create a new key with the scopes you want and revoke the old one. Revocation takes effect on the agent’s next call.
Keeping a lid on what it spends
An agent can call an API thousands of times faster than a person can. Three things keep that from turning into a surprise on your bill.
A ceiling you set
verify_emails takes a max_credits argument. If the list would cost more, nothing is submitted and nothing is charged.
A prompt retry is free
All five charging tools send an idempotency key and hand it back, so a retry that reuses it returns the original result rather than buying it again, for as long as the receipt lasts, which is a day for get_email_history and a month for check_domain_setup and find_business_email, and at least 30 days for the two verification tools. With one caveat worth knowing: when a timeout loses the response it loses the key with it, and the key the server derives instead is bucketed by a fixed ten-minute window, so a retry that crosses the boundary gets a different key and can charge again. Retry promptly, or pass your own key.
Mistakes cost nothing
A malformed address, an oversized batch or a list over your ceiling is refused before the request is sent, not after it is paid for.
Your plan’s rate limits apply to MCP exactly as they do to the REST API, because every tool call is a REST call made with your key. The protocol chatter around them (connecting, listing the tools, reading the reference documents) is answered inside the server and uses no credits. When it carries your key, the server checks the key with the API once and reuses the answer for a minute, so connecting uses at most one request of your rate limit. MCP also gives each key a budget of its own: 30 requests every 10 seconds and 120 a minute, whoever else shares your network address.
Reading the results properly
The server publishes a resource, kawaa://reference/verification-statuses, that explains what each verdict means, in particular the three that are easiest to misread.
- catch_all means the domain accepts mail for every recipient, so nothing could be learned about that particular mailbox. It is not a yes and not a no.
- unknown means the check could not conclude. It is often something temporary worth re-checking later.
- risky covers several unrelated situations, and
sub_statussays which.
Agents read that resource before interpreting results, and the built-in prompts tell them to hand these cases to you rather than deciding silently. The full status reference says the same thing for humans.
When something goes wrong
A failed tool call comes back structured, so the agent knows whether to retry, to wait, to fix its arguments, or to ask you for something.
{
"error": {
"kind": "payment_required",
"code": "INSUFFICIENT_CREDITS",
"message": "Insufficient credits. Required: 500, Available: 12.",
"retryable": false,
"next_step": "Top up at https://app.kawaa.com/settings/billing, then retry. Nothing was charged for this call.",
"request_id": "b2c1…"
}
}That is the tool result’s structuredContent, so the discriminator is at structuredContent.error.kind rather than at the top level. The result’s one text block carries it too, for clients that show their model only text: the human-readable message and remedy first, then this object as JSON. Successful results are shaped the same way: a one-line summary, then the full result as JSON.
kind is the thing to branch on, and code is the same error code the REST API returns, so an MCP failure and an API log entry read the same.
What it does not do
- No streaming. Responses are JSON. There is no server-sent-events stream and no server-initiated messages, because nothing in the tool set needs them.
- No sessions. The server is stateless; every request stands on its own.
- Browser-based MCP clients may be blocked. The endpoint’s CORS policy, and that of the sign-in endpoints, allows Kawaa’s own origins. Command-line and editor clients, and clients that connect from a server, are unaffected.
Everything the agent can reach through MCP is also available through the REST API, and anything MCP does not cover is there.