Webhooks (Callbacks)

Centiwise sends an HTTP POST request to your server_callback_url whenever a transaction status changes. Webhooks are the primary way to learn the final outcome of processing transactions.

Callback Payload

{
  "status":         "success",
  "orderid":        "CW-987654",
  "client_orderid": "ORD-001",
  "amount":         1000,
  "currency":       "KES"
}

The payload mirrors the standard API response structure. The status field will be success, error, or declined.


Endpoint Requirements

Your server_callback_url must meet these requirements:

RequirementDetail
ProtocolHTTPS with a valid TLS certificate (TLS 1.2 or higher)
Method acceptedPOST
ResponseReturn HTTP 200 within 10 seconds
IdempotencyHandle duplicate deliveries — the same client_orderid may arrive more than once
Publicly accessibleThe URL must be reachable from the internet (no localhost or private IPs)

Recommended Implementation Pattern

Acknowledge immediately, process asynchronously. Never perform slow operations (database writes, third-party calls) before returning the 200. Queue the payload and process it in the background.

Node.js (Express)

const express = require('express');
const app     = express();

app.use(express.json());

app.post('/callback', async (req, res) => {
  // 1. Acknowledge immediately
  res.sendStatus(200);

  // 2. Process asynchronously
  const { status, client_orderid, orderid, amount, currency } = req.body;

  try {
    // Idempotency check — skip if already processed
    const existing = await db.orders.findOne({ client_orderid });
    if (existing?.status === 'paid') return;

    if (status === 'success') {
      await db.orders.update(client_orderid, { status: 'paid', gateway_id: orderid });
      await fulfillOrder(client_orderid);
    } else {
      await db.orders.update(client_orderid, { status: 'failed' });
    }
  } catch (err) {
    console.error('Callback processing error:', err);
    // Log and alert — Centiwise will retry
  }
});

Python (Flask)

from flask import Flask, request, jsonify
import threading

app = Flask(__name__)

@app.route('/callback', methods=['POST'])
def callback():
    payload = request.get_json()
    # Acknowledge immediately
    thread = threading.Thread(target=process_callback, args=(payload,))
    thread.start()
    return '', 200

def process_callback(payload):
    client_orderid = payload.get('client_orderid')
    status         = payload.get('status')

    # Idempotency check
    order = db.orders.find_one(client_orderid)
    if order and order['status'] == 'paid':
        return

    if status == 'success':
        db.orders.update(client_orderid, {'status': 'paid'})
        fulfill_order(client_orderid)
    else:
        db.orders.update(client_orderid, {'status': 'failed'})

Idempotency

Centiwise may deliver the same callback more than once (network retries, provider re-notifications). Your handler must be idempotent:

// Before processing, check if this order was already finalised
const order = await db.orders.findOne({ client_orderid });

if (order && ['paid', 'failed'].includes(order.status)) {
  // Already processed — do nothing
  return;
}

// Safe to process

Testing Your Callback Endpoint

Use a tool like ngrok to expose a local server during development:

ngrok http 3000
# Gives you a public URL like https://abc123.ngrok.io

# Set your server_callback_url to:
# https://abc123.ngrok.io/callback

Troubleshooting

IssueLikely CauseFix
Callback not receivedURL not publicly accessibleCheck firewall rules and ensure the URL is reachable from the internet
Callback received but no 200Error in your handler before the responseMove res.sendStatus(200) to the very first line of your handler
Duplicate callbacks processedMissing idempotency checkCheck client_orderid against your database before processing
SSL errorInvalid or self-signed certificateUse a valid certificate from a trusted CA (Let's Encrypt is free)


Did this page help you?