paylod
MCP server

AI agents

MCP server

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

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

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

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

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

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

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

~/.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 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.

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

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

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"

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

ToolScopeWhat it does
create_apppaylod:apps.writeFirst-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_applicationpaylod:apps.writeAdd another application to an organization you already have.
get_callback_urlpaylod:apps.writeGet the hosted M-Pesa callback URL for an application — the URL you paste into the Daraja portal. Treat it as a secret.
set_credentialspaylod:credentials.writeStore or rotate the Daraja consumer key, secret, shortcode and passkey. Write-only — never read back.
mint_keypaylod:keys.mintMint a paylod API key (mp_test_… / mp_live_…) for your own backend. Returned once.
configure_webhookpaylod:webhooks.writeCreate or update a webhook endpoint so results are pushed to your server, signed.
list_webhookspaylod:webhooks.writeList the webhook endpoints configured for an application.
list_applicationspaylod:team.readList 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

ToolScopeWhat it does
request_stk_pushpaylod:payments.collectSend an STK Push to a customer's handset. Accepts an idempotency key, so a retrying agent never double-charges.
get_payment_statuspaylod:payments.readLook up one payment by id. If still pending, runs a live STK query and settles it on the spot.
generate_qrpaylod:payments.collectGenerate an M-Pesa QR code (base64 PNG). Stateless — no money moves.
register_c2bpaylod:payments.collectRegister C2B validation/confirmation URLs so paybill payments made outside your app reach you.
get_account_balancepaylod:payments.readQuery the M-Pesa account balance. Asynchronous — returns a queryId; the result arrives on your callback.
get_transaction_statuspaylod:payments.readQuery the status of an M-Pesa transaction by receipt. Asynchronous, same callback pattern.
payoutpaylod:payments.payoutSend money out (B2C). Irreversible on a live application. Requires the opt-in money-out scope.
reversalpaylod:payments.payoutReverse or refund a transaction. Requires the opt-in money-out scope.

Sandbox & offline

ToolScopeWhat it does
simulate_test_paymentpaylod:payments.simulateCreate a simulated collection in sandbox — no handset, no real money.
simulate_outcomepaylod:payments.simulateForce 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:

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