Introduction
This page describes what webhooks you can receive from our backend, and how to validate that the webhook is indeed coming from our backend.
URL restrictions
Please make sure that the URLs you create are globally routable. Don't use private IP addresses since they won't be reachable.
Our webhook system doesn't follow redirects, so if your backend responds with a 3xx response, it won't be followed. Make sure to process the webhooks at the original provided address.
Authentication
We use ED25519 to sign all our requests. You can use the following public key to verify our requests: MCowBQYDK2VwAyEAkXeRyk6QO8yGBkuX5eDiLvdHOWZ8QaiadNJhFYwZfAk=. The key is in SPKI DER format.
Each webhook should reach your endpoints with the following headers:
X-Webhook-Id- The unique ID of the webhook. Useful for deduplicating webhooks, idempotency, or simply debuggingX-Timestamp- The Unix UTC timestamp (milliseconds) measured on our backend when the webhook was sentX-Webhook-Signature- The ED25519 signature of the webhook request.
The signature is generated based on the components of the request:
X-Timestamp- HTTP method in uppercase format. This is going to be
POSTfor our webhooks. - Request path, excluding the query string:
/users, /transactions/send - Query string, if present. Including the
?symbol:?amount=5&status=pending. This is useful if the webhook URL you provided to our backend contains a query string for some reason - HTTP body
Example
So for example, you add a webhook URL https://backend.com/my-webhook?source=inv. The query string is to demonstrate how signatures act, usually it's quite unexpected to have a query string as part of a webhook URL but it is supported. If our backend sent you a webhook fro the checkoutCompleted event, the signature would be calculated as follows:
- timestamp =
1780011704561 - method =
POST - path =
/my-webhook - query =
?source=inv - body =
{"type": "checkoutCompleted","data": {id: "123"}}
Signature = sign(`1780011704561POST/my-webhook?source=inv{"type": "checkoutCompleted","data": {id: "123"}}`);Example code
Below is an example verification process of our webhooks using Node.js, TypeScript, and Express.js. The gist lies in the verifySignature method.
import { createPublicKey, verify } from "crypto";
import express, { json, Request, Response } from "express";
import { URL } from "url";
const app = express();
const publicKey = createPublicKey({
key: Buffer.from(
"MCowBQYDK2VwAyEAkXeRyk6QO8yGBkuX5eDiLvdHOWZ8QaiadNJhFYwZfAk=",
"base64"
),
format: "der",
type: "spki",
});
function verifySignature(req: Request, body: Buffer) {
const timestamp = req.headers["x-timestamp"];
const signatureB64 = req.headers["x-webhook-signature"];
if (!signatureB64 || typeof signatureB64 !== "string") {
throw new Error("Signature is missing from the request");
}
// You can also validate the timestamp, for example to avoid old stray webhooks
if (!timestamp || typeof timestamp !== "string") {
throw new Error("Timestamp is missing from the request");
}
// Reconstruct the full URL
const host = req.headers["host"] ?? "";
const url = new URL(`${req.protocol}://${host}${req.originalUrl}`);
// Replicate the steps performed when generating the signature
const components = [timestamp, req.method, url.pathname, url.search].map(
Buffer.from
);
const valid = verify(
null,
Buffer.concat([...components, body]),
publicKey,
Buffer.from(signatureB64, "base64")
);
if (!valid) {
throw new Error("Invalid signature");
}
}
function webhookSignatureBodyVerifier(
req: Request,
res: Response,
buf: Buffer<ArrayBufferLike>
) {
try {
verifySignature(req, buf);
} catch (e) {
res.status(403).send({
error: (e as Error).message,
});
throw e;
}
}
app.post(
"/webhook",
json({
verify: webhookSignatureBodyVerifier,
}),
(req, res) => {
console.log(req.headers, req.body);
res.send({
ok: true,
});
}
);
app.listen(8000);Webhook types
Below are the type of webhooks our system sends. If you notice a type or a status enum that is not defined on this page, it can be located on a related API page.
checkoutCompleted
This webhook notification is sent when a checkout is successfully completed.
{
type: "checkoutCompleted";
data: {
id: string;
}
}checkoutExpired
This webhook notification is sent when a checkout expires before being completed.
{
type: "checkoutExpired";
data: {
id: string;
}
}transactionCreated
This webhook notification is sent when a new transaction is created.
{
type: "transactionCreated";
data: {
id: string;
}
}transactionUpdated
This webhook notification is sent when a transaction status or details are updated.
{
type: "transactionUpdated";
data: {
id: string;
rejectionReason?: string | null;
status: TransactionStatusDto;
};
}feeTransactionCreated
This webhook notification is sent when a fee transaction is created.
{
type: "feeTransactionCreated";
data: {
id: string;
}
}manualEtransferCreated
This webhook notification is sent when a manual e-transfer transaction is created.
{
type: "manualEtransferCreated";
data: {
id: string;
}
}manualEtransferUpdated
This webhook notification is sent when a manual e-transfer transaction is updated.
{
type: "manualEtransferUpdated";
data: {
id: string;
lastModifiedAt: number;
status: ManualEtransferStatusDto;
}
}paymentLinkCompleted
This webhook notification is sent when a payment link is completed.
{
type: "paymentLinkCompleted";
data: {
id: string;
}
}paymentLinkExpired
This webhook notification is sent when a payment link expires.
{
type: "paymentLinkExpired";
data: {
id: string;
}
}