Framework examples
Next.js (App Router)#
The client, server only:
TypeScriptlib/cash.ts
// lib/cash.ts (import this only from route handlers and other server code)
import { CallCash } from "callcash";
import { firestoreStore } from "callcash/firestore";
import { db } from "./firebase-admin";
export const cash = new CallCash({
gateways: {
yoco: { secretKey: process.env.YOCO_SECRET_KEY!, webhookSecret: process.env.YOCO_WEBHOOK_SECRET },
netcash: { serviceKey: process.env.NETCASH_PAYNOW_KEY!, test: process.env.NETCASH_TEST === "1" },
},
routes: { card: ["yoco", "netcash"], default: ["netcash"] },
store: firestoreStore(db),
returnUrl: `${process.env.APP_URL}/paid`,
});Start a payment:
TypeScriptapp/api/pay/route.ts
// app/api/pay/route.ts
import { NextResponse } from "next/server";
import { isCallCashError } from "callcash";
import { cash } from "@/lib/cash";
export async function POST(req: Request) {
const { orderId } = await req.json();
const order = await loadOrder(orderId); // your code: the price comes from your database, never the browser
try {
const p = await cash.payments.create({ amount: order.total, reference: order.id, email: order.email, metadata: { order: order.id } });
return NextResponse.json(p.checkout_url ? { url: p.checkout_url } : { form: p.checkout_form });
} catch (e) {
if (isCallCashError(e)) return NextResponse.json({ error: e.code }, { status: e.status });
throw e;
}
}In the browser, redirect to url, or post form (the postToNetcash function on the Netcash page).
The webhooks:
TypeScriptapp/api/yoco/webhook/route.ts
// app/api/yoco/webhook/route.ts
import { cash } from "@/lib/cash";
import { onCash } from "@/lib/on-cash";
export async function POST(req: Request) {
return (await cash.webhooks.handle("yoco", req, onCash)).response;
}TypeScriptapp/api/netcash/notify/route.ts
// app/api/netcash/notify/route.ts
import { cash } from "@/lib/cash";
import { onCash } from "@/lib/on-cash";
export async function POST(req: Request) {
return (await cash.webhooks.handle("netcash", req, onCash)).response;
}The handler:
TypeScriptlib/on-cash.ts
// lib/on-cash.ts
import type { CashEvent } from "callcash";
export async function onCash(event: CashEvent) {
if (event.type === "payment.paid") await markOrderPaid(event.reference); // a no-op if already paid
}The return page shows "confirming your payment" and reads the order's status from your database. It never marks anything paid.
Express#
webhooks.handle takes a standard Request. Give Express the raw body on the webhook routes and build one:
TypeScript
import express from "express";
import { cash } from "./cash.js";
import { onCash } from "./on-cash.js";
const app = express();
function toRequest(req: express.Request): Request {
const headers = new Headers();
for (const [k, v] of Object.entries(req.headers)) if (typeof v === "string") headers.set(k, v);
return new Request(`https://${req.get("host")}${req.originalUrl}`, { method: "POST", headers, body: req.body });
}
async function send(res: express.Response, response: Response) {
res.status(response.status).type("application/json").send(await response.text());
}
// raw body: the Yoco signature is checked against the exact bytes
app.post("/webhooks/yoco", express.raw({ type: "*/*" }), async (req, res) => {
await send(res, (await cash.webhooks.handle("yoco", toRequest(req), onCash)).response);
});
app.post("/webhooks/netcash", express.raw({ type: "*/*" }), async (req, res) => {
await send(res, (await cash.webhooks.handle("netcash", toRequest(req), onCash)).response);
});
app.post("/pay", express.json(), async (req, res) => {
const order = await loadOrder(req.body.orderId);
const p = await cash.payments.create({ amount: order.total, reference: order.id });
res.json(p.checkout_url ? { url: p.checkout_url } : { form: p.checkout_form });
});Register the webhook routes before any global express.json(), or the body will already be parsed and the Yoco signature will not match.