Skip to main content

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

  1. Open your CashOver merchant dashboard.
  2. Navigate to Manage Account → Webhooks.
  3. 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 and secret.
  • These ids 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:

EventDescription
transactionSuccessfulPayment was completed successfully
transactionRefundedA 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.