Payment Charge Events
Sent when a payment is received on a charge — whether it was created through the Checkout SDK, a payment link, or the create charge API. This covers both bank transfer and crypto payments.
This is a legacy payload format and has no eventType field (the x-100pay-event-type header is unknown). Identify it by the presence of chargeId together with type: "credit".
A charge with multiple payments (e.g. partial payments) produces one webhook per payment.
Payload
interface PaymentChargeWebhook {
type: "credit"; // 👈 identifies the payload together with chargeId
_id: string; // unique ID of this payment event
chargeId: string; // the charge this payment belongs to
reference: string;
eventRef: string; // stable dedupe reference (equals _id)
appId: string;
cryptoChargeId: string;
createdAt: string; // ISO 8601
data: {
from: string; // sender account / address
to: string; // receiving account / address
transaction_id: string;
status: "CONFIRMED" | "PENDING" | "FAILED";
timestamp: string;
value: {
local: { amount: string; currency: string };
crypto: { amount: number; currency: string };
};
metadata?: {
sessionId?: string;
provider?: string; // e.g. "safehaven"
originalAmount?: number;
fees?: number;
sessionCurrency?: string;
[key: string]: unknown;
};
charge: {
customer: { user_id?: string; name: string; email: string; phone: string };
billing: {
currency: string;
amount: string;
description: string;
country: string;
vat?: number;
pricing_type: "fixed_price" | "fixed_or_partial_price";
};
status: {
value: "paid" | "unpaid" | "overpaid" | "underpaid";
total_paid: number;
context: { status: string; value: number }; // over/underpaid delta
};
ref_id: string; // your reference, set when creating the charge
metadata?: Record<string, unknown>; // your metadata, echoed back
payments: unknown[]; // provider-specific payment records
charge_source: "external" | "payment_page";
call_back_url: string;
hosted_url?: string;
app_id: string;
userId: string;
createdAt: string;
};
};
}Example — Bank Transfer Payment
{
"type": "credit",
"_id": "665f1c2e8b6a4a0012a4d901",
"chargeId": "665f1b998b6a4a0012a4d8ff",
"reference": "AbCdEfGhIjKlMnOp",
"eventRef": "665f1c2e8b6a4a0012a4d901",
"data": {
"from": "0123456789",
"to": "9876543210",
"transaction_id": "100000000000000000000000000000",
"status": "CONFIRMED",
"timestamp": "2026-03-12T14:40:29.987Z",
"value": {
"local": { "amount": "50.00", "currency": "NGN" },
"crypto": { "amount": 50, "currency": "NGN" }
},
"metadata": {
"sessionId": "VIR_SESSION_0000000000000_XXXXXXXX",
"provider": "safehaven",
"originalAmount": 55,
"fees": 5,
"sessionCurrency": "NGN"
},
"charge": {
"customer": {
"user_id": "1",
"name": "Jane Smith",
"email": "jane.smith@example.com",
"phone": "08000000000"
},
"billing": {
"currency": "NGN",
"vat": 0,
"pricing_type": "fixed_price",
"amount": "50",
"description": "Funding my account",
"country": "NG"
},
"status": {
"context": { "status": "paid", "value": 50 },
"value": "paid",
"total_paid": 50
},
"ref_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"charge_source": "external",
"call_back_url": "https://yourapp.com/verify-order/",
"app_id": "62ee5dbfb029b7002d5b7453"
}
},
"appId": "62ee5dbfb029b7002d5b7453",
"cryptoChargeId": "665f1b998b6a4a0012a4d8fe",
"createdAt": "2026-03-12T14:40:29.998Z"
}For a crypto payment, data.value.crypto carries the on-chain amount and currency (e.g. { "amount": 10.5, "currency": "USDT" }), and data.from / data.to are blockchain addresses.
Handling
async function handlePaymentCharge(event: PaymentChargeWebhook) {
const { charge, value, status } = event.data;
if (status !== "CONFIRMED") return; // wait for confirmation
switch (charge.status.value) {
case "paid":
case "overpaid":
// Full amount received — fulfill the order keyed on charge.ref_id
await fulfillOrder(charge.ref_id, charge.status.total_paid);
break;
case "underpaid":
// Partial payment — charge.status.context.value holds the shortfall
await recordPartialPayment(charge.ref_id, charge.status.total_paid);
break;
}
}Check data.charge.status.value, not just data.status. data.status: "CONFIRMED" means this payment settled; charge.status.value tells you whether the charge as a whole is paid, underpaid, or overpaid — critical when pricing_type is fixed_or_partial_price.
Before fulfilling high-value orders, confirm the payment server-side with the verify endpoint using data.transaction_id.
Deduplication
eventRef(and thex-100pay-event-refheader) equals the unique_idof this payment event, so each payment in a multi-payment charge is a distinct delivery.- Use
x-100pay-delivery-idfor retry-safe idempotency.