Webhooks
CashOver does not expose open endpoints or offer backend SDKs. Instead, we provide a robust webhook-based integration system for validating payments.
Webhooks are the official and recommended way to securely confirm payment status.
Step 1: Add Your Webhook
- Open your CashOver merchant dashboard.
- Navigate to Manage Account → Webhooks.
- On this page, click Add Webhook and enter the endpoint URL you want CashOver to notify.
Once added:
- Each webhook will be assigned a unique
id
andsecret
. - These
id
s are passed to the SDK when initiating a payment. - The
secret
is used to validate the authenticity of requests.
Your endpoint must support POST requests. Ensure it is accessible and able to respond within 3-5 seconds.
Step 2: Implement the Webhook Endpoint
When a payment event is triggered, CashOver will send a POST request to your webhook URL with event data.
Supported events:
Event | Description |
---|---|
transactionSuccessful | Payment was completed successfully |
transactionRefunded | A transaction was refunded |
No event is sent for failed payments, as no transaction entity is created.
Below is a secure Node.js (Firebase Functions) implementation for handling webhooks:
{
import { z } from "zod";
import * as admin from "firebase-admin";
import * as functions from "firebase-functions";
import * as crypto from "node:crypto";
admin.initializeApp();
admin.firestore().settings({ ignoreUndefinedProperties: true });
const WEBHOOK_SECRET = "h10y2t-Z2D5PHv8K4lLbN6dU-dH8Gvfm"; // securely stored in env
export enum OperationStatus {
pending = "pending",
successful = "successful",
failed = "failed",
canceled = "canceled",
}
export enum WebhookEvent {
TransactionRefunded = "transactionRefunded",
TransactionSuccessful = "transactionSuccessful",
}
const orderDataScheme = z.object({
items: z.array(z.object({
amount: z.number().min(0),
quantity: z.number().min(1),
description: z.string().min(1).optional(),
})),
orderId: z.string().min(1),
totalAmount: z.number().min(0),
orderStatus: z.enum(["pending", "cancelled", "refunded", "delivered", "returned"]),
paymentStatus: z.nativeEnum(OperationStatus),
refunded: z.boolean().optional(),
});
export const updatePaymentStatus = functions.https.onRequest(async (request, response) => {
try {
const signatureHeader = request.header("X-Signature");
const timestampHeader = request.header("X-Signature-Timestamp");
if (!signatureHeader || !timestampHeader) {
response.status(400).json({ error: "Missing signature headers" });
return;
}
const [tPart, v1Part] = signatureHeader.split(",");
const timestamp = parseInt(tPart.split("=")[1]);
const receivedSignature = v1Part.split("=")[1];
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > 300) {
response.status(400).json({ error: "Timestamp too old" });
return;
}
const rawBody = JSON.stringify(request.body);
const expectedSignature = crypto.createHmac("sha256", WEBHOOK_SECRET)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(receivedSignature, "hex"), Buffer.from(expectedSignature, "hex"))) {
response.status(403).json({ error: "Invalid signature" });
return;
}
const { event, status, refunded, metadata } = request.body;
const orderId = metadata?.orderId;
const orderDoc = admin.firestore().collection("Orders").doc(orderId);
const orderSnapshot = await orderDoc.get();
const orderData = orderDataScheme.parse(orderSnapshot.data());
if (event === WebhookEvent.TransactionRefunded) {
orderData.refunded = refunded ?? false;
}
if (event === WebhookEvent.TransactionSuccessful) {
orderData.paymentStatus = status ?? OperationStatus.successful;
orderData.orderStatus = orderData.paymentStatus === OperationStatus.successful ? "delivered" : "pending";
}
await orderDoc.update(orderData);
response.json(orderData).send();
} catch (e) {
console.error(e);
response.status(500).json({ error: "Unable to update payment status" }).send();
}
});
}
This is a sample json response for what you will receive from CashOver:
Successful Transaction
{
"operationType": "transaction",
"createdAt": {
"_seconds": 1752697534,
"_nanoseconds": 183000000
},
"operationId": "77f42f1d-9ac0-4e62-8dd7-d1062d13232d",
"amount": 1207000,
"recordedBalance": 4760725,
"transactionType": "fiat",
"transactionRole": "recipient",
"refunded": false,
"metadata": {
"orderId": "3afc33e2-3bda-4483-8445-9c0ea710cacb",
"platform": "dropOver",
"storeUserName": "pizza.rush",
"storeName": "Pizza Rush"
},
"senderUserName": "test.username",
"senderName": "Test Name",
"amountReceived": 1147850,
"destinationCountry": "LB",
"fees": [
{
"flatFee": 0,
"percentageFee": 0,
"totalAmount": 0,
"feeSource": "operation"
},
{
"flatFee": 0,
"percentageFee": 0.05,
"totalAmount": 59150,
"feeSource": "dropOverPlatform"
}
],
"senderOperationId": "ca907002-3390-4b09-bcdb-b1d57f8e1358",
"currency": "LBP",
"originCountry": "LB",
"event": "transactionSuccessful"
}
Refunded Transaction
{
"operationType": "transaction",
"createdAt": {
"_seconds": 1752697534,
"_nanoseconds": 183000000
},
"operationId": "77f42f1d-9ac0-4e62-8dd7-d1062d13232d",
"amount": 1207000,
"recordedBalance": 4760725,
"transactionType": "fiat",
"transactionRole": "recipient",
"metadata": {
"orderId": "3afc33e2-3bda-4483-8445-9c0ea710cacb",
"platform": "dropOver",
"storeUserName": "pizza.rush",
"storeName": "Pizza Rush"
},
"senderUserName": "test.username",
"senderName": "Test Name",
"amountReceived": 1147850,
"destinationCountry": "LB",
"fees": [
{
"flatFee": 0,
"percentageFee": 0,
"totalAmount": 0,
"feeSource": "operation"
},
{
"flatFee": 0,
"percentageFee": 0.05,
"totalAmount": 59150,
"feeSource": "dropOverPlatform"
}
],
"senderOperationId": "ca907002-3390-4b09-bcdb-b1d57f8e1358",
"currency": "LBP",
"originCountry": "LB",
"refundedAt": {
"_seconds": 1752697738,
"_nanoseconds": 382000000
},
"refunded": true,
"updatedAt": {
"_seconds": 1752697738,
"_nanoseconds": 382000000
},
"event": "transactionRefunded"
}
Webhook Reliability and Security
- CashOver guarantees webhook delivery within 60 seconds.
- We retry up to 3 times in case of failure.
- Merchants must handle replay protection by validating timestamps.
- Reject requests older than 5 minutes.
- Make sure your endpoint responds within 3 to 5 seconds to avoid user delays.
- Our queue has a 60-second timeout, so your endpoint must respond within this duration.
Integration Flow
All webhook events are signed with HMAC-SHA256. Use the
secret
to verify.
You're Ready to Go!
Use webhooks to track real-time payments reliably. This setup ensures your system remains consistent, secure, and aligned with CashOver’s instant payment model.