FuturePayNG Logo
FuturePayNGv1.0.0
FuturePayNG Invoice Payment API v1.0.0

Webhooks & Cryptographic Security

Receive instant, tamper-proof payment notifications directly on your server. Verify authentic events using HMAC-SHA256 signatures and fulfill orders with zero fraud risk.

webhook1. Webhooks Overview

Real-Time Settlement Notifications

When a customer completes a QR code payment, FuturePayNG immediately sends an HTTP POST request to the webhookUrl configured in your invoice or merchant dashboard.

boltSub-Second Delivery

Webhooks are dispatched within 350ms of network biometric debit confirmation.

verified_userSigned with HMAC

Every request carries a cryptographic hash in X-FuturePay-Signature.

replayExponential Retries

Automatic retries for up to 24 hours if your server does not respond with HTTP 200.

verified_user2. HMAC-SHA256 Signature Verification

How to Verify Webhook Signatures

FuturePayNG signs the webhook payload using your secret key and includes the signature in the X-FuturePay-Signature header in this format:

X-FuturePay-Signature: t=1730901234,v1=5257186dbcdf537d1ab502ca62983f064947190479c62070c8514c054ff46b46

Verification Steps:

  1. Extract the timestamp t and signature v1 from the header.
  2. Construct the payload to sign by concatenating {timestamp}.{raw_request_body}.
  3. Compute the HMAC-SHA256 hash using your FUTUREPAY_WEBHOOK_SECRET.
  4. Compare using a constant-time comparison function (e.g., hash_equals()).
HMAC-SHA256
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use App\Models\Order;

class FuturePayWebhookController extends Controller
{
    public function handle(Request $request)
    {
        $signatureHeader = $request->header('X-FuturePay-Signature');
        $secret = config('services.futurepay.webhook_secret');
        $rawPayload = $request->getContent();

        if (!$signatureHeader || !$secret) {
            return response()->json(['error' => 'Missing signature or secret'], 401);
        }

        // Parse t=... and v1=...
        parse_str(str_replace(',', '&', $signatureHeader), $parts);
        $timestamp = $parts['t'] ?? '';
        $receivedSignature = $parts['v1'] ?? '';

        // Prevent replay attacks (reject payloads older than 5 minutes)
        if (abs(time() - (int)$timestamp) > 300) {
            return response()->json(['error' => 'Timestamp expired'], 400);
        }

        // Compute expected signature
        $signedPayload = $timestamp . '.' . $rawPayload;
        $expectedSignature = hash_hmac('sha256', $signedPayload, $secret);

        // Constant-time comparison
        if (!hash_equals($expectedSignature, $receivedSignature)) {
            Log::warning('FuturePay webhook HMAC mismatch');
            return response()->json(['error' => 'Invalid signature'], 403);
        }

        $event = $request->json()->all();

        // Handle invoice.paid event
        if (($event['event'] ?? '') === 'invoice.paid') {
            $invoiceRef = $event['data']['invoiceReference'];
            $orderRef = $event['data']['orderReference'];

            $order = Order::where('reference', $orderRef)->first();
            if ($order && $order->status !== 'paid') {
                $order->update([
                    'status' => 'paid',
                    'futurepay_invoice_ref' => $invoiceRef,
                    'paid_at' => now(),
                ]);
            }
        }

        return response()->json(['status' => 'acknowledged'], 200);
    }
}
Security Tool

HMAC-SHA256 Signature Calculator

Verify signature calculation before implementing your backend webhook receiver.

Calculated Header Value:
Click 'Compute Signature' to generate HMAC value...
securityTiming-Attack Safe Verification Rule:

Always use timing-safe comparison (hash_equals($expected, $received) in PHP or crypto.timingSafeEqual() in Node.js) instead of === to prevent timing attacks.

data_object4. Webhook Event Payloads

Sample JSON Payloads

Event: invoice.paidJSON Payload
{
  "event": "invoice.paid",
  "id": "evt_0981240129",
  "timestamp": 1730901234,
  "data": {
    "invoiceReference": "FP-INV-89421-XYZ",
    "orderReference": "ORDER-NG-89421",
    "amount": 25000,
    "currency": "NGN",
    "status": "paid",
    "customer": {
      "email": "customer@domain.ng",
      "phone": "+2348012345678"
    },
    "paymentDetails": {
      "channel": "qr_biometric",
      "futurepayWalletId": "fp_wal_908123",
      "networkReference": "NIP-FP-9901823901"
    },
    "metadata": {
      "cartId": "cart_09812"
    },
    "paidAt": "2026-11-06T09:18:24.000Z"
  }
}
Event: invoice.cancelledJSON Payload
{
  "event": "invoice.cancelled",
  "id": "evt_0981240999",
  "timestamp": 1730901500,
  "data": {
    "invoiceReference": "FP-INV-89421-XYZ",
    "orderReference": "ORDER-NG-89421",
    "amount": 25000,
    "currency": "NGN",
    "status": "cancelled",
    "reason": "customer_abandoned",
    "cancelledAt": "2026-11-06T09:30:00.000Z"
  }
}
replay5. Retries & Idempotency

Delivery Guarantees & Idempotency Rules

FuturePayNG guarantees at-least-once delivery. In rare situations (such as network timeouts), your server might receive the same webhook event more than once.

Idempotent Handling Best Practice:
  • Always check if your internal order status is already paid before processing stock deductions or shipping.
  • Always respond immediately with an HTTP 200 OK within 5 seconds to prevent retries.
  • Perform long-running background jobs (e.g. sending customer confirmation emails) asynchronously after returning the 200 OK status.