Webhooks

Pay can be configured to send webhook events to notify your application any time an event happens on your transaction. Pay sends a response, via a HTTP request, to any endpoint URLs that you have provided us in the Pay dashboard.

Events

To listen to events, create a webhook in the Pay dashboard. Webhook URLs must start with https://.

EventDescription
purchase_completeA transaction is confirmed onchain.

Purchase Complete

Triggered when a transaction is confirmed onchain. This event provides information about the new status of the order and its transactionHash, as well as other relevant information.

Example Response:

{
"data": {
"buyWithFiatStatus": {
"intentId": "f4cf8ab7-bb62-4b3b-a180-70fc7d72446c",
"status": "ON_RAMP_TRANSFER_COMPLETED",
"toAddress": "0xebfb127320fcbe8e07e5a03a4bfb782219f4735b",
"quote": {
"createdAt": "2024-06-18T23:46:46.024Z",
"fromCurrency": {
"amountUnits": "279",
"amount": "2.79",
"currencySymbol": "USD",
"decimals": 2,
"amountUSDCents": 279
},
"fromCurrencyWithFees": {
"amountUnits": "294",
"amount": "2.94",
"currencySymbol": "USD",
"decimals": 2,
"amountUSDCents": 279
},
"onRampToken": {
"chainId": 137,
"tokenAddress": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"name": "Matic",
"symbol": "MATIC",
"decimals": 18,
"priceUSDCents": 54.797200000000004
},
"toToken": {
"chainId": 137,
"tokenAddress": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"name": "Matic",
"symbol": "MATIC",
"decimals": 18,
"priceUSDCents": 54.797200000000004
},
"estimatedOnRampAmountWei": "5000000000000000000",
"estimatedOnRampAmount": "5",
"estimatedToTokenAmount": "5",
"estimatedToTokenAmountWei": "5000000000000000000",
"estimatedDurationSeconds": 30
},
"source": {
"completedAt": "2024-06-18T23:49:00.347Z",
"amount": "5",
"amountWei": "5000000000000000000",
"token": {
"chainId": 137,
"tokenAddress": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"name": "Matic",
"symbol": "MATIC",
"decimals": 18,
"priceUSDCents": 54.797200000000004
},
"transactionHash": "0x4bb089f6a60b49235a817b52bf39bc078f1246df15731b85837526bb62cf4e70",
"explorerLink": "https://polygonscan.com/tx/0x4bb089f6a60b49235a817b52bf39bc078f1246df15731b85837526bb62cf4e70",
"amountUSDCents": 275
}
}
}
}

Since any outside origin can call your webhook endpoint, it is recommended to verify the webhook signature to ensure a request comes from your Pay instance.

Check the Signature

The payload body is signed with the webhook secret and provided in the X-Pay-Signature request header.

Get the webhook secret for your webhook endpoint from the dashboard.

This code example checks if the signature is valid:

const generateSignature = (
body: string,
timestamp: string,
secret: string,
): string => {
const payload = `${timestamp}.${body}`;
return crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
};
const isValidSignature = (
body: string,
timestamp: string,
signature: string,
secret: string,
): boolean => {
const expectedSignature = generateSignature(
body,
timestamp,
secret,
);
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signature),
);
};

Check the timestamp The event timestamp is provided in the X-Pay-Timestamp request header.

This code example checks if the event exceeds a given expiration duration:

export const isExpired = (
timestamp: string,
expirationInSeconds: number,
): boolean => {
const currentTime = Math.floor(Date.now() / 1000);
return currentTime - parseInt(timestamp) > expirationInSeconds;
};

Example webhook endpoint

This NodeJS code example listens for event notifications on the /webhook endpoint:

import express from "express";
import bodyParser from "body-parser";
import { isValidSignature, isExpired } from "./webhookHelper";
const app = express();
const port = 3000;
const WEBHOOK_SECRET = "<your_webhook_auth_secret>";
app.use(bodyParser.text());
app.post("/webhook", (req, res) => {
const signatureFromHeader = req.header("X-Pay-Signature");
const timestampFromHeader = req.header("X-Pay-Timestamp");
if (!signatureFromHeader || !timestampFromHeader) {
return res
.status(401)
.send("Missing signature or timestamp header");
}
if (
!isValidSignature(
req.body,
timestampFromHeader,
signatureFromHeader,
WEBHOOK_SECRET,
)
) {
return res.status(401).send("Invalid signature");
}
if (isExpired(timestampFromHeader, 300)) {
// Assuming expiration time is 5 minutes (300 seconds)
return res.status(401).send("Request has expired");
}
// Process the request
res.status(200).send("Webhook received!");
});
app.listen(port, () => {
console.log(`Server started on http://localhost:${port}`);
});