{
  "id": "evt_01j7b7a6r8h3x",
  "type": "transaction.succeeded",
  "created": 1723717200,
  "data": {
    "object": {
      "id": "txn_01j7b6f9p5y9h",
      "merchantTransactionId": "mtx_123",
      "status": "completed",
      "message": "Payment completed",
      "amount": 10000,
      "currency": "KES",
      "type": "charge",
      "paymentMethod": "mobile_money",
      "chargedPhone": "+254712345678",
      "narration": "Invoice #1234",
      "processor": "mpesa",
      "processorReference": "ODI31...",
      "createdAt": "2025-08-15T12:00:00Z",
      "updatedAt": "2025-08-15T12:02:12Z",
      "metadata": {"orderId": "ORD-987"}
    }
  }
}

Overview

Fingo Pay delivers asynchronous events (e.g., final transaction results) via HTTP POST to your endpoint.

Registering webhook URLs

  • Provide a per-request webhookUrl (as in C2B/B2C endpoints), and/or
  • Configure an account-level default webhook with our team (recommended for reliability).

Security — Signature verification

We sign every webhook with an HMAC SHA‑256 signature using your webhook secret. Headers:
X-Fingo-Signature: t=<unix>, v1=<hex_hmac>
X-Fingo-Event-Id: evt_...
Compute v1 = hex(hmac_sha256(secret, t + ”.” + raw_body)) and require timestamp t within 5 minutes.
import crypto from 'node:crypto'
import express from 'express'

const app = express()
app.use(express.raw({ type: 'application/json' }))

function verifySignature(secret, timestamp, rawBody, signature) {
  const payload = `${timestamp}.${rawBody}`
  const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex')
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
}

app.post('/webhooks/fingo', (req, res) => {
  const sig = req.header('X-Fingo-Signature') || ''
  const evtId = req.header('X-Fingo-Event-Id')
  const t = sig.split(',')[0]?.split('=')[1]
  const v1 = sig.split(',')[1]?.split('=')[1]
  if (!t || !v1) return res.status(400).end()

  const fiveMinutes = 5 * 60
  if (Math.abs(Math.floor(Date.now()/1000) - Number(t)) > fiveMinutes) {
    return res.status(400).send('timestamp_out_of_window')
  }

  const ok = verifySignature(process.env.FINGO_WEBHOOK_SECRET, t, req.body, v1)
  if (!ok) return res.status(400).send('invalid_signature')

  const event = JSON.parse(req.body)
  // handle event.type
  res.sendStatus(200)
})

Delivery & retries

  • Expect 2xx within 10 seconds.
  • Retries use exponential backoff for up to 24 hours.
  • Each event is delivered at least once; de‑duplicate using X-Fingo-Event-Id.

Event types

  • transaction.created
  • transaction.processing
  • transaction.succeeded
  • transaction.failed
  • transaction.reversed
  • payout.succeeded
  • payout.failed
  • balance.updated

Example payload

{
  "id": "evt_01j7b7a6r8h3x",
  "type": "transaction.succeeded",
  "created": 1723717200,
  "data": {
    "object": {
      "id": "txn_01j7b6f9p5y9h",
      "merchantTransactionId": "mtx_123",
      "status": "completed",
      "message": "Payment completed",
      "amount": 10000,
      "currency": "KES",
      "type": "charge",
      "paymentMethod": "mobile_money",
      "chargedPhone": "+254712345678",
      "narration": "Invoice #1234",
      "processor": "mpesa",
      "processorReference": "ODI31...",
      "createdAt": "2025-08-15T12:00:00Z",
      "updatedAt": "2025-08-15T12:02:12Z",
      "metadata": {"orderId": "ORD-987"}
    }
  }
}