AI agents
MCP server: M-Pesa for AI agents
Let an AI agent manage and use your M-Pesa integration — a remote, OAuth-authenticated MCP endpoint. Nothing to install.
Nothing to install
paylod supplies a remote MCP server. You add no npm package. You run no process. You paste no API key into a config file. You give your AI client one URL. The client then finds the authorization server and opens your browser one time. You approve the exact capabilities that you want. The agent gets a scoped token. The agent never gets your Daraja credentials and never gets 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 supports remote HTTP works. A stdio-only client can use a bridge.
The server is open source: github.com/mosesmrima/paylod-mcp.
Add it to your client
Pick your client. Each client runs the OAuth flow for you on the first connection. A browser tab opens. You sign in to paylod. You consent to the scopes.
Claude Code
Run one command. Use --scope user to make the server available in all of your projects. The default scope is local. The project scope writes a shared .mcp.json.
claude mcp add --transport http --scope user paylod https://mcp.paylod.dev/mcpThen run /mcp in Claude Code and pick paylod to authenticate. After that, /mcp shows the server as connected and shows its tools.
You can also commit the config. To do this, put the block below in .mcp.json at the repo root. Always include the type field. Claude Code treats an entry that has a url but no type as stdio, and skips that entry.
{
"mcpServers": {
"paylod": {
"type": "http",
"url": "https://mcp.paylod.dev/mcp"
}
}
}Claude Desktop
Add remote servers in the UI, not in claude_desktop_config.json. That file is only for local stdio servers. Go to Settings → Connectors → Add custom connector. Paste the server URL. Click Connect to run the OAuth flow. Leave the advanced client-ID and secret fields blank. paylod supports Dynamic Client Registration, so your client registers itself.
Cursor
Add the block below to ~/.cursor/mcp.json for all projects, or to .cursor/mcp.json for one project. Cursor identifies a remote server by the url field. Open Settings → MCP. Click the login prompt to authorize.
{
"mcpServers": {
"paylod": {
"url": "https://mcp.paylod.dev/mcp"
}
}
}VS Code
Add the block below to .vscode/mcp.json. Note that the top-level key is servers, not mcpServers. VS Code implements the MCP authorization spec, and it includes DCR. VS Code therefore prompts you to sign in the first time that you use the server.
{
"servers": {
"paylod": {
"type": "http",
"url": "https://mcp.paylod.dev/mcp"
}
}
}Windsurf
Add the block below to ~/.codeium/windsurf/mcp_config.json. Windsurf uses serverUrl for a remote server. Windsurf also 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 supports only stdio, use mcp-remote as a bridge. mcp-remote is the standard stdio-to-HTTP proxy. It runs the OAuth flow for you. It opens your browser and it caches the token in ~/.mcp-auth. It needs no flags for paylod.
{
"mcpServers": {
"paylod": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://mcp.paylod.dev/mcp"]
}
}
}Most users do not need this bridge. Claude Code, Claude Desktop, Cursor, VS Code, Windsurf and Codex all support remote HTTP directly.
First thing to try
decode_mpesa_error is pure and offline. It needs no scopes and no credentials, and it uses no network. Use this tool to confirm that the server is set correctly before you grant any scope. Ask your agent:
What does M-Pesa error 2001 mean?A good answer gives the cause (wrong PIN), the fix, and the message to show the customer. The tool supplies that answer. The model does not guess it. A Daraja response gives you only the bare code 2001, and this tool explains the code.
How the OAuth flow works
Your client calls the server URL with no token. The server returns a 401 with a WWW-Authenticate header. That header points to the protected-resource metadata. From the metadata, the client finds the authorization server at https://paylod.dev/oauth. The client registers itself dynamically and opens your browser. You sign in to paylod. paylod then shows a consent screen. The consent screen lists each capability that the agent requests, as one scope per capability 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"Three scopes are high-risk: payments.payout (send money out), credentials.write (write your Daraja keys) and keys.mint (mint API keys). The consent screen puts these three scopes in their own flagged group, so you always see them. The consent screen shows every requested scope as checked by default. Untick each scope that you do not want to grant. paylod grants only the scopes that stay ticked.
Untick the three high-risk scopes to stop the agent from calling payout, reversal, set_credentials or mint_key. The server rejects each such call on the scope. The server does not depend on the model to obey an instruction. If you approve only payments.simulate, the agent can build against the sandbox and cannot move real money.
paylod binds the access token to this server as its audience. Your client holds the token. The token never appears in 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 cannot extract them.
Tools
The OAuth access token authorizes every call. Each tool also needs the scope that the table shows next to it. A token without that scope cannot call the tool.
Management — set the integration up
| Tool | Scope | What it does |
|---|---|---|
create_app | paylod:apps.write | Use this tool on your first run. It creates your organization and its first application (paybill or till). It returns the applicationId, your callback URL and a one-time API key. |
create_application | paylod:apps.write | Add another application to an organization that you already have. |
get_callback_url | paylod:apps.write | Get the hosted M-Pesa callback URL for an application. You paste this URL into the Daraja portal. Treat the URL as a secret. |
set_credentials | paylod:credentials.write | Store or rotate the Daraja consumer key, secret, shortcode and passkey. These values are write-only. No tool reads them back. |
mint_key | paylod:keys.mint | Mint a paylod API key (mp_test_… / mp_live_…) for your own backend. The tool returns the key one time only. |
list_keys | paylod:keys.mint | List the API keys of an application. The tool returns the prefixes only. It never returns the secret. |
revoke_key | paylod:keys.mint | Revoke an API key by id. |
configure_webhook | paylod:webhooks.write | Create or update a webhook endpoint. paylod then sends signed results to your server. |
list_webhooks | paylod:webhooks.write | List the webhook endpoints that an application uses. |
list_applications | paylod:team.read | List the applications that you can access. The tool also gives their environments and their configuration state. |
authenticate | — (no scope) | Show the owner of the token. Show also the scopes that paylod granted. |
Runtime — move and read money
| Tool | Scope | What it does |
|---|---|---|
request_stk_push | paylod:payments.collect | Send an STK Push to the handset of a customer. The tool accepts one idempotency key for each payment attempt. Duplicate deliveries of that attempt therefore become a single charge. If the call stops part way, the key is spent and the tool returns 409 indeterminate. In that case paylod cannot prove that no debit occurred. Read get_payment_status first, then retry with a new key. |
get_payment_status | paylod:payments.read | Read one payment by id. If the payment is still pending, the tool reads the live STK status and settles the payment immediately. |
generate_qr | paylod:payments.collect | Generate an M-Pesa QR code (base64 PNG). The tool is stateless and moves no money. |
register_c2b | paylod:payments.collect | Register the C2B validation URL and confirmation URL. Paybill payments made outside your app then reach you. |
get_account_balance | paylod:payments.read | Read the M-Pesa account balance. The tool is asynchronous. It returns a queryId, and the result arrives on your callback. |
get_transaction_status | paylod:payments.read | Read the status of an M-Pesa transaction by receipt. The tool is asynchronous and uses the same callback pattern. |
payout | paylod:payments.payout | Send money out (B2C). On a live application you cannot reverse this action. The tool needs the opt-in money-out scope. |
reversal | paylod:payments.payout | Reverse or refund a transaction. The tool needs the opt-in money-out scope. |
Sandbox & offline
| Tool | Scope | What it does |
|---|---|---|
simulate_test_payment | paylod:payments.simulate | Create a simulated collection in the sandbox. The tool uses no handset and no real money. |
simulate_outcome | paylod:payments.simulate | Force a simulated payment to succeed or to fail. The agent can then test your failure paths. |
decode_mpesa_error | — (no scope) | Decode any Safaricom result code or Daraja error. The tool returns the cause, the fix, and a message for the customer. It is pure and offline. |
get_docs | — (no scope) | Get paylod documentation by topic. The tool is pure and local. It needs no scope, no token and no network. |
The error reference lists the same codes that decode_mpesa_error returns.
Set an integration up with the tools, in order
Use the tools to set the integration up and to inspect it. Your application does not call these tools at runtime. At runtime your application calls the Node SDK. Call the tools in this order.
- Check for an existing app →
list_applications. Any result means that you are onboarded. - Create the application. A new user calls
create_app, which creates the organization and its first application. A user who already has an organization callscreate_applicationinstead, becausecreate_appreturns a409 already onboarded. Do not pick the wrong tool here. This is the most common first-call mistake. - Store the Daraja credentials →
set_credentials. The tool needsproduct, which is the kind of shortcode,paybillortill.productis not the Daraja product name. For a till, also passpartyB, the till number that receives the funds. Make sure thatpartyBdiffers fromshortcode, or Daraja rejects the push with2002. paylod encrypts the credentials at rest and keeps them write-only, and no tool reads them back. - Get the callback URL →
get_callback_url. Then paste the URL into the Safaricom Daraja portal as the CallbackURL / ResultURL / ValidationURL / ConfirmationURL. Do not skip this step, because no payment resolves without it. No agent can do this step for you, because Safaricom has no API for it. Treat the URL as a secret. The randomcbk_…token in the path authenticates the callback from Safaricom. - Mint an API key →
mint_key(mp_test_…for sandbox,mp_live_…for production). paylod shows the key one time only. Your server puts this key inPAYLOD_API_KEY. The MCP server itself never uses an API key. - Register a webhook if you want one →
configure_webhook. You can skip this step and poll instead. paylod records every payment in both cases. - Write the application code with the SDK. Run
npm install @paylod/node. Then usenew Paylod(process.env.PAYLOD_API_KEY!)andawait paylod.collectAndWait({ amount, phone }). Showoutcome.message, and offer a retry only whenoutcome.retryableis true. Do not write a fetch wrapper, a poll loop, or a callback handler. See Quickstart and the Node SDK. - Test the failure paths → use
simulate_test_paymentwithsimulate_outcome. Together they drive success, cancellation, a wrong PIN or a timeout through the real settlement path. They need no handset.
request_stk_push and get_payment_status let an agent test the loop from end to end. They also let a person say "charge this number 10 bob". Do not ship these two tools as your runtime integration. If you do, you rebuild the SDK badly.
Do not treat result code 4999 as a failed payment. 4999 means that the payment is pending, because the customer must still enter the PIN. 4999 is not retryable. A second request_stk_push sends a second prompt and can charge the customer two times. Poll get_payment_status instead.
What an agent can actually do
The management tools let an agent build a complete live M-Pesa integration for you. You do not open 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. The agent then polls get_payment_status until the payment settles. If the payment fails, the agent calls decode_mpesa_error. The agent then tells you in simple words that the customer cancelled the prompt (1032) or that the handset timed out (1037). You give the agent a phone number. The agent gives you an API key and a receipt.
The agent cannot paste the callback URL into the Safaricom Daraja portal, because Safaricom has no API for it. This is the only step that you must do yourself. The agent can do every other step. This includes configure_webhook, which sets the webhook endpoint that receives results on your own server.
The agent uses the same platform over MCP that your backend uses over HTTP at /functions/v1. See the API reference. MCP is a second front door. It is not a separate system.