AI agents
MCP server
Let an AI agent manage and use your M-Pesa integration — a remote, OAuth-authenticated MCP endpoint. Nothing to install.
Nothing to install
paylod ships a remote MCP server. There is no npm package to add, no process to run, and no API key to paste into a config file. You give your AI client one URL; it discovers the authorization server, opens your browser once, and you approve exactly the capabilities you want. The agent gets a scoped token — never your Daraja credentials, never your paylod API key.
https://mcp.paylod.dev/mcpTransport: streamable-http. Auth: OAuth 2.1 (authorization code + PKCE, with Dynamic Client Registration) against https://paylod.dev/oauth. Any MCP client that speaks remote HTTP works — and stdio-only clients can bridge.
Add it to your client
Pick your client. Each one runs the OAuth flow for you on first connect: a browser tab opens, you sign in to paylod, you consent to scopes, done.
Claude Code
One command. Use --scope user to make it available across all your projects (local is the default; project writes a shared .mcp.json).
claude mcp add --transport http --scope user paylod https://mcp.paylod.dev/mcpThen run /mcp inside Claude Code and pick paylod to authenticate. After that, /mcp shows the server connected with its tools listed.
Prefer to commit the config? Put this in .mcp.json at the repo root. The type field is required — an entry with a url but no type is treated as stdio and skipped.
{
"mcpServers": {
"paylod": {
"type": "http",
"url": "https://mcp.paylod.dev/mcp"
}
}
}Claude Desktop
Remote servers are added in the UI, not in claude_desktop_config.json (that file is for local stdio servers). Go to Settings → Connectors → Add custom connector, paste the server URL, and click Connect to run the OAuth flow. Leave the advanced client-ID/secret fields blank — paylod supports Dynamic Client Registration, so your client registers itself.
Cursor
Add to ~/.cursor/mcp.json (global) or .cursor/mcp.json (per-project). Cursor keys remote servers off url. Open Settings → MCP and click the login prompt to authorize.
{
"mcpServers": {
"paylod": {
"url": "https://mcp.paylod.dev/mcp"
}
}
}VS Code
Add to .vscode/mcp.json. Note the top-level key is servers, not mcpServers. VS Code implements the MCP authorization spec including DCR, so it prompts you to sign in on first use.
{
"servers": {
"paylod": {
"type": "http",
"url": "https://mcp.paylod.dev/mcp"
}
}
}Windsurf
Add to ~/.codeium/windsurf/mcp_config.json. Windsurf uses serverUrl for remote servers and supports OAuth on Streamable HTTP.
{
"mcpServers": {
"paylod": {
"serverUrl": "https://mcp.paylod.dev/mcp"
}
}
}Codex CLI
Codex supports remote Streamable-HTTP MCP servers with OAuth. Add the block below to ~/.codex/config.toml, then run codex mcp login paylod.
[mcp_servers.paylod]
url = "https://mcp.paylod.dev/mcp"Fallback for stdio-only clients
If your client only speaks stdio, bridge it with mcp-remote — the standard stdio-to-HTTP proxy. It handles the OAuth flow (opening your browser, caching the token under ~/.mcp-auth) and needs no flags against paylod.
{
"mcpServers": {
"paylod": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://mcp.paylod.dev/mcp"]
}
}
}You almost certainly do not need this — Claude Code, Claude Desktop, Cursor, VS Code, Windsurf and Codex all speak remote HTTP natively.
First thing to try
decode_mpesa_error is pure and offline — it needs no scopes, no credentials, and touches no network. That makes it the ideal way to confirm the server is wired up before you grant anything. Ask your agent:
What does M-Pesa error 2001 mean?A good answer comes back with the cause (wrong PIN), the fix, and the message to show the customer — that is the tool, not the model guessing. Anyone who has stared at a bare 2001 in a Daraja response knows why this exists.
How the OAuth flow works
Your client hits the server URL with no token and gets a 401 carrying a WWW-Authenticate header that points at the protected-resource metadata. From there it discovers the authorization server at https://paylod.dev/oauth, registers itself dynamically, and opens your browser. You sign in to paylod and land on a consent screen listing exactly which capabilities the agent is asking for — one scope per capability, each with its own checkbox.
curl -i -X POST https://mcp.paylod.dev/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.paylod.dev/.well-known/oauth-protected-resource"The three high-risk scopes — payments.payout (send money out), credentials.write (write your Daraja keys) and keys.mint (mint API keys) — are unchecked by default. You have to deliberately turn them on. Leave them off and the agent cannot call payout, reversal, set_credentials or mint_key at all: the server rejects the call on the scope, so it is not a prompt you have to trust the model to respect. Grant only payments.simulate and the agent can build against the sandbox without ever being able to touch real money.
The resulting access token is bound to this server as its audience, held by your client, and never pasted into the conversation. Your Daraja consumer key and secret are write-only — no tool reads them back, so an agent can store them for you but can never exfiltrate them.
Tools
Every call is authorized by the OAuth access token and gated on the scope shown next to it — a token without the scope cannot invoke the tool.
Management — set the integration up
| Tool | Scope | What it does |
|---|---|---|
create_app | paylod:apps.write | First-run onboarding: create your organization and its first application (paybill or till). Returns the applicationId, your callback URL and a one-time API key. |
create_application | paylod:apps.write | Add another application to an organization you already have. |
get_callback_url | paylod:apps.write | Get the hosted M-Pesa callback URL for an application — the URL you paste into the Daraja portal. Treat it as a secret. |
set_credentials | paylod:credentials.write | Store or rotate the Daraja consumer key, secret, shortcode and passkey. Write-only — never read back. |
mint_key | paylod:keys.mint | Mint a paylod API key (mp_test_… / mp_live_…) for your own backend. Returned once. |
configure_webhook | paylod:webhooks.write | Create or update a webhook endpoint so results are pushed to your server, signed. |
list_webhooks | paylod:webhooks.write | List the webhook endpoints configured for an application. |
list_applications | paylod:team.read | List the applications you can access, with their environments and configuration state. |
authenticate | — (no scope) | Show who the token belongs to and which scopes were actually granted. |
Runtime — move and read money
| Tool | Scope | What it does |
|---|---|---|
request_stk_push | paylod:payments.collect | Send an STK Push to a customer's handset. Accepts an idempotency key, so a retrying agent never double-charges. |
get_payment_status | paylod:payments.read | Look up one payment by id. If still pending, runs a live STK query and settles it on the spot. |
generate_qr | paylod:payments.collect | Generate an M-Pesa QR code (base64 PNG). Stateless — no money moves. |
register_c2b | paylod:payments.collect | Register C2B validation/confirmation URLs so paybill payments made outside your app reach you. |
get_account_balance | paylod:payments.read | Query the M-Pesa account balance. Asynchronous — returns a queryId; the result arrives on your callback. |
get_transaction_status | paylod:payments.read | Query the status of an M-Pesa transaction by receipt. Asynchronous, same callback pattern. |
payout | paylod:payments.payout | Send money out (B2C). Irreversible on a live application. Requires the opt-in money-out scope. |
reversal | paylod:payments.payout | Reverse or refund a transaction. Requires the opt-in money-out scope. |
Sandbox & offline
| Tool | Scope | What it does |
|---|---|---|
simulate_test_payment | paylod:payments.simulate | Create a simulated collection in sandbox — no handset, no real money. |
simulate_outcome | paylod:payments.simulate | Force a simulated payment to succeed or fail, so the agent can exercise your failure paths. |
decode_mpesa_error | — (no scope) | Decode any Safaricom result code or Daraja error into cause, fix, and a customer-facing message. Pure and offline. |
The same codes decode_mpesa_error returns are browsable in the error reference.
What an agent can actually do
The point of the management tools is that an agent can take you from nothing to a working, live M-Pesa integration without you opening a dashboard. Connect the server, then ask:
Set up M-Pesa for my shop "Acme Ltd". Here are my Daraja sandbox
credentials: consumer key …, secret …, shortcode 174379, passkey ….
Create the app, store them, mint me a test API key, give me my
callback URL, then send a 10 KES STK push to 0712345678 and tell me
when it settles.The agent calls create_app → set_credentials → mint_key → get_callback_url → request_stk_push, then polls get_payment_status until the payment settles. If it fails, it calls decode_mpesa_error and tells you in plain English that the customer cancelled the prompt (1032) or the handset timed out (1037). You hand it a phone number; it hands you back an API key and a receipt.
The one step the agent cannot do for you is paste that callback URL into the Safaricom Daraja portal — Safaricom has no API for it. Everything else, including the webhook that pushes results to your own server (configure_webhook), it can do.
Everything the agent does over MCP is the same platform your backend talks to over HTTP at /functions/v1 — see the API reference. MCP is a second front door, not a separate system.