> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fingopay.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive asynchronous events, verify signatures, and handle retries

## Overview

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

### Registering webhook URLs

* Provide a **per-request** <code>webhookUrl</code> (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:

```http theme={"dark"}
X-Fingo-Signature: t=<unix>, v1=<hex_hmac>
X-Fingo-Event-Id: evt_...
```

Compute <code>v1 = hex(hmac\_sha256(secret, t + "." + raw\_body))</code> and require timestamp <code>t</code> within 5 minutes.

<CodeGroup>
  ```javascript Node.js (Express) theme={"dark"}
  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.toString('utf8')}`
    const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex')
    return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'))
  }

  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)
  })
  ```

  ```python Python (Django) theme={"dark"}
  import hmac, hashlib, time
  from django.http import HttpResponse, HttpResponseBadRequest

  def verify_signature(secret: str, timestamp: str, raw_body: bytes, signature: str) -> bool:
      payload = f"{timestamp}.{raw_body.decode('utf-8')}".encode('utf-8')
      expected = hmac.new(secret.encode('utf-8'), payload, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, signature)

  def fingo_webhook(request):
      sig = request.headers.get('X-Fingo-Signature', '')
      evt_id = request.headers.get('X-Fingo-Event-Id')
      try:
          t = sig.split(',')[0].split('=')[1]
          v1 = sig.split(',')[1].split('=')[1]
      except Exception:
          return HttpResponseBadRequest('bad_signature_header')

      if abs(int(time.time()) - int(t)) > 300:
          return HttpResponseBadRequest('timestamp_out_of_window')

      ok = verify_signature(os.environ['FINGO_WEBHOOK_SECRET'], t, request.body, v1)
      if not ok:
          return HttpResponseBadRequest('invalid_signature')

      # process event
      return HttpResponse(status=200)
  ```
</CodeGroup>

### 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 <code>X-Fingo-Event-Id</code>.

### Event types

* <code>transaction.created</code>
* <code>transaction.processing</code>
* <code>transaction.succeeded</code>
* <code>transaction.failed</code>
* <code>transaction.creation\_failed</code>
* <code>transaction.reversed</code>

<Info>
  `transaction.*` events apply to all transaction flows, including collections and payouts.
</Info>

<Card title="Webhook Event Reference" icon="webhook" href="/models/webhook-events">
  View detailed webhook event schemas and examples for transaction.\* event types
</Card>

### Example payloads

#### C2B (M-Pesa Charge) Webhooks

<Tabs>
  <Tab title="Transaction Succeeded">
    Sent when the customer successfully completes the STK Push payment.

    <ResponseExample>
      ```json transaction.succeeded theme={"dark"}
      {
        "id": "evt_k8m2x9p4lq7n",
        "type": "transaction.succeeded",
        "created": 1736697600,
        "data": {
          "id": "txn_01j7b6f9p5y9h",
          "merchantTransactionId": "mtx_123",
          "status": "completed",
          "message": "The service was accepted successfully",
          "amount": 10000,
          "currency": "KES",
          "type": "charge",
          "paymentMethod": "mobile_money",
          "chargedPhone": "254712345678",
          "narration": "Invoice #1234",
          "processor": "mpesa",
          "processorReference": "ODI31ABC123XYZ",
          "createdAt": "2025-01-12T14:30:00.000Z",
          "updatedAt": "2025-01-12T14:30:45.000Z",
          "metadata": {
            "orderId": "ORD-987",
            "source": "api"
          }
        }
      }
      ```
    </ResponseExample>
  </Tab>

  <Tab title="Transaction Failed - User Cancelled">
    Sent when the customer cancels, times out, or the payment fails for any reason.

    <ResponseExample>
      ```json transaction.failed theme={"dark"}
      {
        "id": "evt_p3q7r2s5tw8y",
        "type": "transaction.failed",
        "created": 1736697800,
        "data": {
          "id": "txn_01j7b8x2m4n6k",
          "merchantTransactionId": "mtx_456",
          "status": "failed",
          "message": "Request cancelled by user",
          "amount": 5000,
          "currency": "KES",
          "type": "charge",
          "paymentMethod": "mobile_money",
          "chargedPhone": "254722334455",
          "narration": "Order payment",
          "processor": "mpesa",
          "processorReference": "QWE78DEF456GHI",
          "createdAt": "2025-01-12T15:00:00.000Z",
          "updatedAt": "2025-01-12T15:01:30.000Z",
          "metadata": {
            "orderId": "ORD-654",
            "source": "api"
          }
        }
      }
      ```
    </ResponseExample>
  </Tab>

  <Tab title="Transaction Failed - Creation Error">
    Sent when the transaction fails during creation (e.g., shortcode not found, account not configured).

    <ResponseExample>
      ```json transaction.failed theme={"dark"}
      {
        "id": "evt_z9x8w7v6ut5s",
        "type": "transaction.failed",
        "created": 1736698000,
        "data": {
          "id": "txn_01j7c2a4b6c8d",
          "merchantTransactionId": "mtx_789",
          "status": "failed",
          "message": "Shortcode not found",
          "amount": 15000,
          "currency": "KES",
          "type": "charge",
          "paymentMethod": "mobile_money",
          "processor": "mpesa",
          "createdAt": "2025-01-12T15:30:00.000Z",
          "updatedAt": "2025-01-12T15:30:00.000Z",
          "metadata": {
            "reason": "Shortcode not found"
          }
        }
      }
      ```
    </ResponseExample>
  </Tab>
</Tabs>

#### Payout transaction webhooks

<Tabs>
  <Tab title="Creation Failed - Business Logic Error">
    Sent when transaction creation fails due to non-retriable errors like insufficient balance, duplicate transaction, or account not found.

    <ResponseExample>
      ```json transaction.creation_failed theme={"dark"}
      {
        "id": "evt_k8x9m2y4abc1",
        "type": "transaction.creation_failed",
        "created": 1737043200,
        "data": {
          "id": "txn_abc123xyz789",
          "merchantTransactionId": "order_12345",
          "status": "failed",
          "error": {
            "code": "INSUFFICIENT_BALANCE",
            "message": "Insufficient balance on payout account"
          },
          "amount": 100000,
          "currency": "KES",
          "type": "payment",
          "paymentMethod": "mobile_money",
          "processor": "mpesa",
          "createdAt": "2026-01-16T12:00:00.000Z",
          "updatedAt": "2026-01-16T12:00:00.000Z"
        }
      }
      ```
    </ResponseExample>

    <AccordionGroup>
      <Accordion title="Other error code examples">
        <ResponseExample>
          ```json DUPLICATE_TRANSACTION theme={"dark"}
          {
            "error": {
              "code": "DUPLICATE_TRANSACTION",
              "message": "Duplicate merchantTransactionId: order_12345"
            }
          }
          ```
        </ResponseExample>

        <ResponseExample>
          ```json ACCOUNT_NOT_FOUND theme={"dark"}
          {
            "error": {
              "code": "ACCOUNT_NOT_FOUND",
              "message": "Debit account not found"
            }
          }
          ```
        </ResponseExample>

        <ResponseExample>
          ```json NO_PAYOUT_ACCOUNT theme={"dark"}
          {
            "error": {
              "code": "NO_PAYOUT_ACCOUNT",
              "message": "No payout account configured"
            }
          }
          ```
        </ResponseExample>
      </Accordion>
    </AccordionGroup>
  </Tab>

  <Tab title="Transaction Succeeded (Payout)">
    Sent when a payout transaction completes successfully.

    <ResponseExample>
      ```json transaction.succeeded theme={"dark"}
      {
        "id": "evt_p7m3n5q2def4",
        "type": "transaction.succeeded",
        "created": 1737043260,
        "data": {
          "object": {
            "id": "txn_abc123xyz789",
            "merchantTransactionId": "order_12345",
            "status": "completed",
            "message": "B2C transfer completed",
            "amount": 100000,
            "currency": "KES",
            "type": "payment",
            "paymentMethod": "mobile_money",
            "chargedPhone": "+254712345678",
            "narration": "Salary payment for January",
            "processor": "mpesa",
            "processorReference": "AG_20260116_1234567890abcdef",
            "externalReference": "B2C_XYZ123ABC",
            "createdAt": "2026-01-16T12:00:00.000Z",
            "updatedAt": "2026-01-16T12:01:00.000Z",
            "metadata": {
              "employeeId": "EMP-001",
              "source": "api"
            }
          }
        }
      }
      ```
    </ResponseExample>
  </Tab>

  <Tab title="Transaction Failed (Payout)">
    Sent when a payout transaction fails at provider level.

    <ResponseExample>
      ```json transaction.failed theme={"dark"}
      {
        "id": "evt_f4k2j8r9ghi5",
        "type": "transaction.failed",
        "created": 1737043260,
        "data": {
          "object": {
            "id": "txn_abc123xyz789",
            "merchantTransactionId": "order_12345",
            "status": "failed",
            "message": "The initiator information is invalid.",
            "amount": 100000,
            "currency": "KES",
            "type": "payment",
            "paymentMethod": "mobile_money",
            "chargedPhone": "+254712345678",
            "narration": "Salary payment for January",
            "processor": "mpesa",
            "processorReference": "AG_20260116_1234567890abcdef",
            "createdAt": "2026-01-16T12:00:00.000Z",
            "updatedAt": "2026-01-16T12:01:00.000Z",
            "metadata": {
              "employeeId": "EMP-001",
              "source": "api",
              "error": {
                "reason": "The initiator information is invalid."
              }
            }
          }
        }
      }
      ```
    </ResponseExample>
  </Tab>
</Tabs>
