Paymish Loading Docs Preparing guides, collections, and live API references...
Paymish Docs

Webhooks

Setup

  1. Go to Dashboard → Settings → Webhooks.
  2. Add a publicly accessible HTTPS endpoint such as https://yourapp.com/webhooks/paymish.
  3. Select the events you want to receive and save the endpoint.
  4. Copy the generated Webhook Secret and store it securely for signature verification.
Important: never trust incoming webhook payloads until the signature is verified.

Event Types

Event
Description
transaction.successful
A payment or transfer completed successfully.
transaction.failed
A payment or transfer attempt failed.
transfer.pending
A transfer has been queued and is awaiting processing.
account.verified
A user verification check was completed successfully.
wallet.funded
A wallet received new funds.

Payload Structure

Every webhook includes a consistent outer envelope. The nested data object changes depending on the event type.

Example payload - transaction.successful
{
  "event": "transaction.successful",
  "timestamp": "2024-06-01T14:22:10Z",
  "data": {
    "id": "txn_abc123",
    "amount": 5000,
    "currency": "GHS",
    "reference": "ref_xyz789",
    "status": "success",
    "customer": {
      "email": "customer@example.com"
    }
  }
}

Verification

Paymish signs every webhook using HMAC-SHA256. Verify the signature before you parse or process the body.

Python (Django)
import hmac
import hashlib
from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt
import json

WEBHOOK_SECRET = "your_webhook_secret"

@csrf_exempt
def paymish_webhook(request):
    signature = request.headers.get("X-Paymish-Signature", "")
    payload = request.body

    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        payload,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(expected, signature):
        return HttpResponseBadRequest("Invalid signature")

    event = json.loads(payload)
    event_type = event.get("event")

    if event_type == "transaction.successful":
        pass
    elif event_type == "transaction.failed":
        pass

    return HttpResponse(status=200)
Node.js (Express)
const crypto = require("crypto");
const express = require("express");
const app = express();

const WEBHOOK_SECRET = "your_webhook_secret";

app.post("/webhooks/paymish", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-paymish-signature"];
  const expected = crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(req.body)
    .digest("hex");

  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
    return res.status(400).send("Invalid signature");
  }

  const event = JSON.parse(req.body);

  switch (event.event) {
    case "transaction.successful":
      break;
    case "transaction.failed":
      break;
  }

  res.status(200).send("OK");
});
PHP
<?php
$webhookSecret = "your_webhook_secret";
$payload = file_get_contents("php://input");
$signature = $_SERVER["HTTP_X_PAYMISH_SIGNATURE"] ?? "";

$expected = hash_hmac("sha256", $payload, $webhookSecret);
if (!hash_equals($expected, $signature)) {
    http_response_code(400);
    exit("Invalid signature");
}

$event = json_decode($payload, true);
switch ($event["event"]) {
    case "transaction.successful":
        break;
    case "transaction.failed":
        break;
}

http_response_code(200);
echo "OK";

Best Practices

  • Verify the X-Paymish-Signature header before processing any payload.
  • Return 200 quickly and offload heavier work to a queue or background worker.
  • Handle duplicate events idempotently using a stable event identifier.
  • Log failed processing attempts and retry them safely.
  • Accept webhook traffic over HTTPS only.