# Fortpesa — AI agent integration skill > Raw skill for AI coding agents. Fetch this URL and follow it to integrate M-Pesa payments via the Fortpesa REST API into a user's app. Human-readable docs: https://fortpesa.co.ke/quickstart ## What Fortpesa is A multi-tenant M-Pesa collection platform for Kenyan businesses. Merchants collect via STK Push (customer's phone gets an M-Pesa prompt) or shareable payment links. Customer money settles **directly to the merchant's M-Pesa account** — Fortpesa never holds funds. Platform fees are prepaid from a wallet and deducted per successful transaction. - Base URL: `https://fortpesa.co.ke/api/v1` - Currency: `KES` only - Full API reference: https://fortpesa.co.ke/api-reference/stk ## Before you write code — prerequisites The merchant needs (all from their Fortpesa dashboard, not your code): 1. **API key** — Settings & API keys → `pb_live_…`. Never hardcode; read from env (`FORTPESA_API_KEY`). 2. **Channel ID** — Channels page, integer `id` column (the local integer, not any provider-side UUID). Every STK request must include it. 3. **Wallet topped up** — platform fees are prepaid; empty wallet = HTTP 402. ## Hard conventions (do not get these wrong) | Rule | Detail | |---|---| | **Amounts** | Whole KES integers. `150` = KES 150. No cents, no floats, no minor units at the API edge. | | **Phone** | `254XXXXXXXXX` (12 digits, no `+`, no leading 0). `07…` and `+254…` are rejected. | | **channel_id** | Required integer on every STK request. No fallback. | | **Idempotency-Key** | Optional header (8–128 chars `[A-Za-z0-9._:-]`) on STK create. Same key = same transaction returned (201 + `Idempotency-Replayed: true`), no second prompt. Generate one per logical payment intent. | | **Errors** | Envelope: `{"error": {"code": "...", "message": "..."}}`. Map by `code`, not HTTP status alone. | ## Integration checklist (execute in order) 1. Read `FORTPESA_API_KEY` and a channel ID from the user / env. 2. Confirm wallet balance (`GET /wallet/balance`) — optional but catches 402 early. 3. Send STK Push (`POST /payments/stk`) with amount, phone, channel_id, account reference. 4. Poll `GET /transactions/{uuid}` every ~3s until terminal (`success` / `failed` / `cancelled` / `expired`), max ~2.5 min. 5. Optionally register a webhook URL for async notification (preferred for production). 6. On webhook receipt: verify HMAC, then re-fetch the transaction via GET before trusting the payload. ## STK Push ```http POST /api/v1/payments/stk Authorization: Bearer pb_live_... Content-Type: application/json Idempotency-Key: ord-1042-8f3a { "amount": 150, "phone": "254712345678", "channel_id": 42, "account_reference": "ORDER1042", "description": "Order 1042" } ``` Response `201`: ```json { "data": { "uuid": "550e8400-e29b-41d4-a716-446655440000", "status": "pending", "amount": 150, "platform_fee": 6, "phone": "254712345678", "account_reference": "ORDER1042", "provider_transaction_id": "..." } } ``` Field limits: `account_reference` ≤ 12 chars, `description` ≤ 13 chars. Truncate if needed. ## Transaction status ```http GET /api/v1/transactions/{uuid} Authorization: Bearer pb_live_... ``` Non-terminal transactions are **live-reconfirmed against the provider** on each GET (rate-limited to once per 10s per transaction). Safe to poll. Terminal rows are served from the DB. List endpoint: `GET /api/v1/transactions` (filters: `status`, `from`, `to`, `per_page`). ## Webhooks Fortpesa POSTs signed events to your webhook URL on every status change. ### Signature ``` sha256 = HMAC-SHA256("{timestamp}.{raw_body}", WEBHOOK_SECRET) Header: X-Fortpesa-Signature: sha256={hex_digest} Header: X-Fortpesa-Timestamp: {unix_seconds} ``` Verification steps (never skip): 1. Read the **raw request body** — do not re-serialize parsed JSON (key order / whitespace changes the HMAC). 2. Reject if `|now - timestamp| > 300` seconds (5-minute replay window). 3. Compute HMAC over `"{timestamp}.{raw_body}"` with your signing secret (`whsec_…`). 4. Compare with `hash_equals` (constant-time). 5. On valid signature, **re-fetch** `GET /api/v1/transactions/{uuid}` and trust only the GET result — the callback body is untrusted input. ### Event payload (shape) ```json { "event": "transaction.updated", "data": { "uuid": "...", "status": "success", "amount": 150, "platform_fee": 6, "account_reference": "ORDER1042", "provider_transaction_id": "..." } } ``` Delivery retries: up to 5 attempts over ~4 minutes. Respond `2xx` quickly; do heavy work in a background job. Reference implementations: https://fortpesa.co.ke/webhooks ## Payment links (no-code path) For simple “pay me” flows, create a payment link in the dashboard and share `https://fortpesa.co.ke/pay/{uuid}`. Supports fixed or open amounts. Useful when the agent is building a static site or the user does not want to handle API keys in code. To embed it on a website (modal button or inline iframe, with optional prefilled amount/phone), load `https://fortpesa.co.ke/paylink.js` and call `Fortpesa.pay('{uuid}', options)`, or use `