paylod
MCP server

AI agents

MCP server: M-Pesa for AI agents

.md

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.

Server URL
https://mcp.paylod.dev/mcp

Transport: 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.

Terminal
claude mcp add --transport http --scope user paylod https://mcp.paylod.dev/mcp

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

.mcp.json
{
  "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.

~/.cursor/mcp.json
{
  "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.

.vscode/mcp.json
{
  "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.

~/.codeium/windsurf/mcp_config.json
{
  "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.

~/.codex/config.toml
[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.

mcp-remote bridge
{
  "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:

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

The 401 that starts it all
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

ToolScopeWhat it does
create_apppaylod:apps.writeUse 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_applicationpaylod:apps.writeAdd another application to an organization that you already have.
get_callback_urlpaylod:apps.writeGet the hosted M-Pesa callback URL for an application. You paste this URL into the Daraja portal. Treat the URL as a secret.
set_credentialspaylod:credentials.writeStore or rotate the Daraja consumer key, secret, shortcode and passkey. These values are write-only. No tool reads them back.
mint_keypaylod:keys.mintMint a paylod API key (mp_test_… / mp_live_…) for your own backend. The tool returns the key one time only.
list_keyspaylod:keys.mintList the API keys of an application. The tool returns the prefixes only. It never returns the secret.
revoke_keypaylod:keys.mintRevoke an API key by id.
configure_webhookpaylod:webhooks.writeCreate or update a webhook endpoint. paylod then sends signed results to your server.
list_webhookspaylod:webhooks.writeList the webhook endpoints that an application uses.
list_applicationspaylod:team.readList 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

ToolScopeWhat it does
request_stk_pushpaylod:payments.collectSend 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_statuspaylod:payments.readRead one payment by id. If the payment is still pending, the tool reads the live STK status and settles the payment immediately.
generate_qrpaylod:payments.collectGenerate an M-Pesa QR code (base64 PNG). The tool is stateless and moves no money.
register_c2bpaylod:payments.collectRegister the C2B validation URL and confirmation URL. Paybill payments made outside your app then reach you.
get_account_balancepaylod:payments.readRead the M-Pesa account balance. The tool is asynchronous. It returns a queryId, and the result arrives on your callback.
get_transaction_statuspaylod:payments.readRead the status of an M-Pesa transaction by receipt. The tool is asynchronous and uses the same callback pattern.
payoutpaylod:payments.payoutSend money out (B2C). On a live application you cannot reverse this action. The tool needs the opt-in money-out scope.
reversalpaylod:payments.payoutReverse or refund a transaction. The tool needs the opt-in money-out scope.

Sandbox & offline

ToolScopeWhat it does
simulate_test_paymentpaylod:payments.simulateCreate a simulated collection in the sandbox. The tool uses no handset and no real money.
simulate_outcomepaylod:payments.simulateForce 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.

  1. Check for an existing applist_applications. Any result means that you are onboarded.
  2. Create the application. A new user calls create_app, which creates the organization and its first application. A user who already has an organization calls create_application instead, because create_app returns a 409 already onboarded. Do not pick the wrong tool here. This is the most common first-call mistake.
  3. Store the Daraja credentialsset_credentials. The tool needs product, which is the kind of shortcode, paybill or till. product is not the Daraja product name. For a till, also pass partyB, the till number that receives the funds. Make sure that partyB differs from shortcode, or Daraja rejects the push with 2002. paylod encrypts the credentials at rest and keeps them write-only, and no tool reads them back.
  4. Get the callback URLget_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 random cbk_… token in the path authenticates the callback from Safaricom.
  5. Mint an API keymint_key (mp_test_… for sandbox, mp_live_… for production). paylod shows the key one time only. Your server puts this key in PAYLOD_API_KEY. The MCP server itself never uses an API key.
  6. Register a webhook if you want oneconfigure_webhook. You can skip this step and poll instead. paylod records every payment in both cases.
  7. Write the application code with the SDK. Run npm install @paylod/node. Then use new Paylod(process.env.PAYLOD_API_KEY!) and await paylod.collectAndWait({ amount, phone }). Show outcome.message, and offer a retry only when outcome.retryable is true. Do not write a fetch wrapper, a poll loop, or a callback handler. See Quickstart and the Node SDK.
  8. Test the failure paths → use simulate_test_payment with simulate_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:

Prompt
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_appset_credentialsmint_keyget_callback_urlrequest_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.