Yoco
Yoco takes card payments. callCash opens a Yoco checkout, sends your customer to Yoco's page, and believes the payment only once a signed Yoco event confirms it.
What you need#
- A Yoco account with online payments switched on.
- Your secret key from the Yoco portal.
sk_test_keys run in test mode; any other key runs live. - A webhook registered with Yoco that points at your Yoco webhook route, and the
whsec_secret Yoco gives you for it. Yoco's own documentation shows how to register one. - A return URL for customers to land on afterwards.
Configure#
export const cash = new CallCash({
gateways: {
yoco: { secretKey: process.env.YOCO_SECRET_KEY!, webhookSecret: process.env.YOCO_WEBHOOK_SECRET },
},
store,
returnUrl: "https://example.co.za/paid",
});Without returnUrl (or return_url on the call), creating a Yoco payment throws config_invalid. Without webhookSecret, Yoco events are refused with config_invalid, because they cannot be trusted.
Taking a payment#
const payment = await cash.payments.create({ amount: 250, reference: "order_1042", gateway: "yoco" });
// redirect the customer to payment.checkout_urlWhat callCash sends Yoco:
- The amount in cents, in ZAR.
- An
Idempotency-Keyequal to the payment id, so a retry after a dropped connection cannot open a second checkout. metadata.callcashset to the payment id, plusmetadata.reference. Your ownmetadatagoes along too, but cannot overwrite those two.- Three return addresses built from your
returnUrl, with?status=success|cancelled|failed&reference=…&payment=…added.
If Yoco answers 5xx or 429, callCash tries up to three times, then throws gateway_down (retryable) so routing can try your next gateway.
The return URL is not proof#
A customer arriving at ?status=success tells you nothing you can trust: anyone can type that address. Show a "we're confirming your payment" page there, and fulfil the order only in your webhook handler.
The webhook route#
// app/api/yoco/webhook/route.ts
import { cash } from "@/lib/cash";
import { onCash } from "@/lib/on-cash";
export async function POST(req: Request) {
const r = await cash.webhooks.handle("yoco", req, onCash);
return r.response;
}What callCash checks before your handler runs:
- The
webhook-signatureheader matches an HMAC-SHA256 of the event, using yourwhsec_secret. - The event is no more than three minutes old (Yoco's recommendation), so a captured event cannot be replayed later.
- The event's mode matches your key, so a test event never reaches a live deployment, or the other way round.
- For a payment, the status is
succeeded, the currency is ZAR and the amount matches. A different amount givespayment.mismatch, and the payment is not marked paid.
An event without metadata.callcash is not a callCash payment: handle returns handled: false, so older code can still process it.
Yoco events callCash understands: payment.succeeded, payment.failed, refund.succeeded, refund.failed.
Refunds#
Yoco refunds only work on payments made with live keys. With test keys, refund() throws refund_unsupported. See Refunds.
Test it#
Use sk_test_ keys and Yoco's test cards. Payments made this way have mode: "test".