API Reference
Log In
API Reference

Webhooks

📘

In a nutshell

Webhooks let you set up a notification system that automatically sends you updates when certain actions happen via the Pandascrow API — like payments, escrows, or account changes.

Introduction

Most times when you hit an API, you expect an instant response — success or failure, right away. But in some cases (like payment confirmations or long-running processes), that’s not always possible.

To avoid timeouts, we might return a pending response while the request continues processing in the background.

That’s where webhooks come in.

Webhooks let us notify your server once the final state of an event is known, like when a transaction is completed, a payment fails, or an escrow is updated. By setting up a webhook URL, your system stays in sync without needing to constantly poll our API.

Create a Webhook URL

<?php
// Set content type
header('Content-Type: application/json');

// Capture raw request body
$input = file_get_contents("php://input");
$event = json_decode($input, true);

// 1️⃣ Basic validation
if (!$event || !isset($event['event']) || !isset($event['data'])) {
    http_response_code(400);
    echo json_encode([
        'status' => false,
        'message' => 'Invalid payload structure'
    ]);
    exit;
}

// 2️⃣ Header checks
$receivedSignature = $_SERVER['HTTP_X_PANDASCROW_SIGNATURE'] ?? '';
$appKey            = $_SERVER['HTTP_X_PANDASCROW_APP'] ?? '';

// 3️⃣ Fetch your expected secret (this should match what Pandascrow uses)
$expectedSecret = 'sk_live_your_own_secret_key_here'; // You must replace this securely

// 4️⃣ Signature validation
$calculatedSignature = hash_hmac('sha256', json_encode([
    'event'     => $event['event'],
    'data'      => $event['data'],
    'timestamp' => $event['timestamp']
]), $expectedSecret);

if (!hash_equals($calculatedSignature, $receivedSignature)) {
    http_response_code(401);
    echo json_encode([
        'status' => false,
        'message' => 'Invalid signature'
    ]);
    exit;
}

// ✅ If passed, process the event
$eventType = $event['event'];
$payload   = $event['data'];

// You can now handle the event here...

// Respond to Pandascrow
http_response_code(200);
echo json_encode([
    'status' => true,
    'message' => 'Webhook received and processed successfully'
]);
// server.js
const express = require('express');
const crypto = require('crypto');

const app = express();
const PORT = 3000;

// Use raw body for signature verification
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString();
  }
}));

// Your secret key from Pandascrow dashboard
const EXPECTED_SECRET = 'sk_live_your_secret_key_here';

// Webhook endpoint
app.post('/webhook', (req, res) => {
  try {
    // Get headers
    const signature = req.headers['x-pandascrow-signature'];
    const appKey = req.headers['x-pandascrow-app'];
    
    // Get raw body (already saved via middleware)
    const rawBody = req.rawBody;
    const event = JSON.parse(rawBody);

    // 1️⃣ Basic validation
    if (!event || !event.event || !event.data) {
      return res.status(400).json({
        status: false,
        message: 'Invalid payload structure'
      });
    }

    // 2️⃣ Signature validation
    if (!signature) {
      return res.status(401).json({
        status: false,
        message: 'Missing signature'
      });
    }

    // Calculate expected signature
    // Pandascrow signs: event + data + timestamp
    const dataToSign = {
      event: event.event,
      data: event.data,
      timestamp: event.timestamp
    };

    const calculatedSignature = crypto
      .createHmac('sha256', EXPECTED_SECRET)
      .update(JSON.stringify(dataToSign))
      .digest('hex');

    // Compare signatures (timing-safe)
    if (!crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(calculatedSignature)
    )) {
      return res.status(401).json({
        status: false,
        message: 'Invalid signature'
      });
    }

    // 3️⃣ Process the event
    const eventType = event.event;
    const payload = event.data;

    console.log(`Received ${eventType} event:`);
    console.log(JSON.stringify(payload, null, 2));

    // Handle different event types
    switch(eventType) {
      case 'escrow.created':
        // New escrow created
        handleEscrowCreated(payload);
        break;
        
      default:
        console.log(`Unhandled event type: ${eventType}`);
    }

    // 4️⃣ Respond to Pandascrow
    res.status(200).json({
      status: true,
      message: 'Webhook received and processed successfully'
    });

  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).json({
      status: false,
      message: 'Internal server error'
    });
  }
});

// Event handlers
function handleEscrowCreated(payload) {
  // payload contains: escrowId, amount, currency, buyerEmail, sellerEmail, etc.
  console.log('New escrow created:', payload.escrowId);
  // Update your database, send notifications, etc.
}

// Start server
app.listen(PORT, () => {
  console.log(`Webhook server running on http://localhost:${PORT}`);
  console.log(`Webhook endpoint: http://localhost:${PORT}/webhook`);
});

📩 Receiving & Acknowledging Webhooks

When your webhook URL receives an event from Pandascrow, your system should do two things:

  • Parse the event payload
  • Acknowledge receipt by responding with a status true meaning (OK)

⚠️ If we don't receive a True (OK), we’ll assume delivery failed and retry for up to 72 hours.

🔁 Retry Behavior

  • Live Mode
    • First 4 retries: every 3 minutes
    • Afterwards, once every hour for the next 72 hours
  • Test Mode
    • Retries occur hourly for up to 72 hours

🛡️ Verifying Event Origin

Because your webhook URL is publicly accessible, it’s critical to confirm that incoming events are genuinely from Pandascrow, not spoofed by third parties.

You can do this in two ways:

  • Signature Validation: Each event includes a signature header you can verify using your secret key. This ensures the event hasn’t been tampered with.
  • IP Whitelisting: Only accept requests coming from Pandascrow’s IP ranges.

Implementing both is highly recommended for production systems.

Supported events

{
    "event": "escrow.paid",
    "data": {
        "escrow_id": 2,
        "escrow_type": "onetime",
        "escrow_data": {
            "_id": 2,
            "escrow_type": "onetime",
            "initiator_role": "seller",
            "title": "Purchase #123456729 from Store ABC",
            "description": "Purchase 20 Ton of metal",
            "currency": "NGN",
            "amount": "350.00",
            "amount_pay": "350.00",
            "amount_receive": "341.25",
            "inspection_period": 1,
            "no_reviews": 1,
            "delivery_date": "2025-11-06",
            "how_dispute_is_handled": "platform",
            "who_pay_fees": "seller",
            "dispute_window": 5,
            "initiator_id": "a49882d5-041c-49f7-9bee-83ef86aa9b90",
            "receiver_id": "291a18b4-f074-42f5-998d-7f2e2a18d96f",
            "broker_id": null,
            "prd_url": "",
            "acceptance_criteria": "Hello world, this is an acceptance criteria xoxo",
            "deposit_option": "full",
            "callback_url": "app.wiserlance.com\/success",
            "partner_escrow_fee": "0.00",
            "status": "funded",
            "created_at": "2026-02-16 15:52:24",
            "updated_at": "2026-02-16 16:05:38"
        },
        "transaction": {
            "_id": 2,
            "escrow_id": 2,
            "user_id": "a49882d5-041c-49f7-9bee-83ef86aa9b90",
            "milestone_id": null,
            "type": "fund",
            "amount": "350.00",
            "amount_pay": "350.00",
            "amount_receive": "341.25",
            "currency": "NGN",
            "status": "success",
            "payment_url": "https:\/\/checkout.paystack.com\/w05phx8ia6y3eaj",
            "provider": "paystack",
            "transaction_ref": "fjjqemac4g",
            "domain": "test",
            "channel": "card",
            "ip_address": "127.0.0.1",
            "authorization_code": "AUTH_1e3rnb81by",
            "bin": "408408",
            "last4": "4081",
            "exp_month": "12",
            "exp_year": "2030",
            "card_type": "visa ",
            "bank": "TEST BANK",
            "country_code": "NG",
            "signature": "SIG_0jfBFo9zdvDO8gEFOX9w",
            "cust_name": "Pandascrow Buyer",
            "cust_email": "[email protected]",
            "created_at": "2026-02-16 15:52:30",
            "updated_at": "2026-02-16 16:07:23"
        },
        "buyer": {
            "_id": 4,
            "escrow_id": 2,
            "user_id": "291a18b4-f074-42f5-998d-7f2e2a18d96f",
            "name": "Pandascrow Buyer",
            "email": "[email protected]",
            "phone": "+2348098765432",
            "role": "receiver",
            "joined_at": "2026-02-16 15:52:24"
        },
        "seller": {
            "_id": 3,
            "escrow_id": 2,
            "user_id": "a49882d5-041c-49f7-9bee-83ef86aa9b90",
            "name": "Precious Tom",
            "email": "[email protected]",
            "phone": null,
            "role": "initiator",
            "joined_at": "2026-02-16 15:52:24"
        },
        "gateway_data": {
            "event": "charge.success",
            "data": {
                "id": 5844280067,
                "domain": "test",
                "status": "success",
                "reference": "fjjqemac4g",
                "amount": 35000,
                "message": null,
                "gateway_response": "Successful",
                "paid_at": "2026-02-16T15:53:00.000Z",
                "created_at": "2026-02-16T15:52:30.000Z",
                "channel": "card",
                "currency": "NGN",
                "ip_address": "105.116.9.111",
                "metadata": {
                    "escrow_id": "2",
                    "user_id": "a49882d5-041c-49f7-9bee-83ef86aa9b90",
                    "provider": "paystack"
                },
                "fees_breakdown": null,
                "log": null,
                "fees": 525,
                "fees_split": null,
                "authorization": {
                    "authorization_code": "AUTH_1e3rnb81by",
                    "bin": "408408",
                    "last4": "4081",
                    "exp_month": "12",
                    "exp_year": "2030",
                    "channel": "card",
                    "card_type": "visa ",
                    "bank": "TEST BANK",
                    "country_code": "NG",
                    "brand": "visa",
                    "reusable": true,
                    "signature": "SIG_0jfBFo9zdvDO8gEFOX9w",
                    "account_name": null,
                    "receiver_bank_account_number": null,
                    "receiver_bank": null
                },
                "customer": {
                    "id": 317040401,
                    "first_name": null,
                    "last_name": null,
                    "email": "[email protected]",
                    "customer_code": "CUS_730cys763yiibdn",
                    "phone": null,
                    "metadata": null,
                    "risk_action": "default",
                    "international_format_phone": null
                },
                "plan": [],
                "subaccount": [],
                "split": [],
                "order_id": null,
                "paidAt": "2026-02-16T15:53:00.000Z",
                "requested_amount": 35000,
                "pos_transaction_data": null,
                "source": {
                    "type": "api",
                    "source": "merchant_api",
                    "entry_point": "transaction_initialize",
                    "identifier": null
                }
            }
        },
        "recurring_data": null
    },
    "timestamp": 1771258051
}
{
    "event": "escrow.completed",
    "data": {
        "event": "escrow.completed",
        "escrow_id": 1,
        "escrow_data": {
            "_id": 1,
            "title": "Payment for Buyer Offer- Aluminium Beverage Cans",
            "description": "Secure payment for Buyer Offer- Aluminium Beverage Cans in Product by Lotanna",
            "currency": "NGN",
            "amount": "2000.00",
            "amount_pay": "2130.00",
            "amount_receive": "2000.00",
            "status": "completed",
            "escrow_type": "onetime",
            "initiator_role": "broker",
            "created_at": "2026-05-21 09:44:45",
            "completed_at": "2026-05-21 11:17:49",
            "broker_id": "aa5488cf-0c0b-4a77-af8a-afa5beacb2b0"
        },
        "transaction": {
            "transaction_ref": "lp2huy4f01",
            "amount": "2000.00",
            "currency": "NGN",
            "provider": "paystack",
            "status": "success",
            "completed_at": "2026-05-21 11:17:44"
        },
        "participants": {
            "buyer": {
                "user_id": "6811a04b-4f9c-491c-b89a-1c5c9f94672c",
                "name": "Micah Tom",
                "email": "[email protected]"
            },
            "seller": {
                "user_id": "2a692c94-cc6a-4fdd-873d-bbeed77ee274",
                "name": "Kendrick Lamar",
                "email": "[email protected]"
            },
            "broker": {
                "user_id": "aa5488cf-0c0b-4a77-af8a-afa5beacb2b0",
                "name": "Precious Tom",
                "email": "[email protected]"
            }
        },
        "completed_by": {
            "user_id": null,
            "name": null,
            "email": null,
            "role": null
        },
        "is_broker_escrow": true
    },
    "timestamp": 1779362269
}
{
    "event": "invoice.paid",
    "data": {
        "event": "invoice.paid",
        "invoice_id": 3,
        "transaction_id": "pa60xufb0b",
        "invoice_data": {
            "_id": 3,
            "invoice_number": "INV-6A0DBECB16A59",
            "uuid": "aa5488cf-0c0b-4a77-af8a-afa5beacb2b0",
            "client_id": 1,
            "currency": "NGN",
            "subtotal": "6200.00",
            "tax": "0.00",
            "total": "6200.00",
            "status": "paid",
            "delivery_method": "email",
            "payment_method": "bank_trasfer",
            "notes": "Thank you for your business.",
            "due_date": "2025-07-01",
            "sent_at": "2026-05-20 14:01:47",
            "created_at": "2026-05-20 14:01:47",
            "updated_at": "2026-05-20 14:02:37"
        },
        "wallet_update": {
            "_id": 1,
            "uuid": "aa5488cf-0c0b-4a77-af8a-afa5beacb2b0",
            "currency": "NGN",
            "balance": "12400.00",
            "tier": 1,
            "metadata": null,
            "created_at": "2026-05-20 10:13:18",
            "updated_at": "2026-05-20 14:02:37"
        },
        "payer": {
            "_id": 1,
            "owner_uuid": "aa5488cf-0c0b-4a77-af8a-afa5beacb2b0",
            "full_name": "Pandascrow HQ",
            "email": "[email protected]",
            "billing_address": "15 New Haven Crescent, NTA, Port Harcourt",
            "phone": "08021325996",
            "company_name": "Pandascrow",
            "currency": "NGN",
            "created_at": "2026-05-20 11:44:38",
            "updated_at": "2026-05-20 13:56:18"
        },
        "webhook_raw": {
            "event": "charge.success",
            "data": {
                "id": 6167317014,
                "domain": "test",
                "status": "success",
                "reference": "pa60xufb0b",
                "amount": 623100,
                "message": null,
                "gateway_response": "Successful",
                "paid_at": "2026-05-20T14:02:03.000Z",
                "created_at": "2026-05-20T14:01:48.000Z",
                "channel": "card",
                "currency": "NGN",
                "ip_address": "105.113.40.117",
                "metadata": 0,
                "fees_breakdown": null,
                "log": null,
                "fees": 19347,
                "fees_split": null,
                "authorization": {
                    "authorization_code": "AUTH_w0a5qgqybp",
                    "bin": "408408",
                    "last4": "4081",
                    "exp_month": "12",
                    "exp_year": "2030",
                    "channel": "card",
                    "card_type": "visa ",
                    "bank": "TEST BANK",
                    "country_code": "NG",
                    "brand": "visa",
                    "reusable": true,
                    "signature": "SIG_0jfBFo9zdvDO8gEFOX9w",
                    "account_name": null,
                    "receiver_bank_account_number": null,
                    "receiver_bank": null
                },
                "customer": {
                    "id": 161136929,
                    "first_name": null,
                    "last_name": null,
                    "email": "[email protected]",
                    "customer_code": "CUS_84p6sd2spqx02tg",
                    "phone": null,
                    "metadata": null,
                    "risk_action": "default",
                    "international_format_phone": null
                },
                "plan": [],
                "subaccount": [],
                "split": [],
                "order_id": null,
                "paidAt": "2026-05-20T14:02:03.000Z",
                "requested_amount": 623100,
                "pos_transaction_data": null,
                "source": {
                    "type": "api",
                    "source": "merchant_api",
                    "entry_point": "transaction_initialize",
                    "identifier": null
                }
            }
        },
        "paid_at": "2026-05-20 14:02:39"
    },
    "timestamp": 1779285759
}
{
  "event": "wallet.deposit.success",
  "uuid": "merchant-uuid-here",
  "amount": 25000,
  "currency": "NGN",
  "transaction_ref": "DVA_20260606222716_959",
  "nipsessionid": "999169231016134100460496227401",
  "sender_name": "John Doe",
  "account_number": "5030000013",
  "memo": "DVA Payment from John Doe to 5030000013",
  "dva": {
    "record_uuid": "dva-record-uuid",
    "reference": "DVA_20260606222716_959",
    "account_name": "Acme Store",
    "amount_type": "EXACT",
    "expected_amount": 25000,
    "total_funded_amount": 25000,
    "funding_count": 1,
    "expiry_date": "2026-06-07 11:30:00"
  }
}