app.post('/webhooks/m2m', async (req, res) => { const { event, data } = req.body; if (event === 'link.created') { console.log(`Link ${data.linkId} created for user ${data.referenceId}`); // Store the link in your system await db.links.create({ linkId: data.linkId, referenceId: data.referenceId, url: data.url, expiresAt: data.expiresAt, createdAt: data.createdAt }); } res.status(200).send('OK');});
Sent when M2M needs additional user data to complete identity verification (CIP). This happens when the user data provided during link creation is incomplete.
This webhook gives you an opportunity to provide user data before M2M asks the user directly. Responding quickly improves user experience.
If you don’t respond to the data request (or can’t provide the data), M2M will prompt the user to enter the missing information directly in the widget. This increases friction but ensures the transaction can still complete.See Data Requests for more details on this flow.
Sent when a user completes a money transfer operation through the widget. Use this to record transactions in your system and trigger downstream processes.
app.post('/webhooks/m2m', async (req, res) => { const { event, data } = req.body; if (event === 'operation.created') { console.log(`Operation ${data.operationId} created for user ${data.referenceId}`); // Record the operation in your system await db.operations.create({ operationId: data.operationId, referenceId: data.referenceId, sendAmount: data.amount.send, receiveAmount: data.amount.receive, fee: data.amount.fee, status: data.status, createdAt: data.createdAt }); // Notify your team or trigger downstream processes await notifyTeam('New M2M operation', data); } res.status(200).send('OK');});
Sent when a user cancels a money transfer operation from the widget. Use this to update your records, release any held resources, and keep your system in sync.
If you’ve already begun processing the operation on your side (e.g., provisional balance holds), make sure your cancellation handler reverses that state.
Here’s a complete webhook handler that processes all event types:
Node.js
const express = require('express');const crypto = require('crypto');const app = express();// Use raw body for signature verificationapp.use('/webhooks/m2m', express.raw({ type: 'application/json' }));app.post('/webhooks/m2m', async (req, res) => { // Verify signature const signature = req.headers['x-m2m-signature']; const timestamp = req.headers['x-m2m-timestamp']; if (!verifySignature(req.body.toString(), signature, timestamp)) { return res.status(401).send('Invalid signature'); } // Parse the body const { event, data } = JSON.parse(req.body); // Acknowledge immediately res.status(200).send('OK'); // Process asynchronously try { switch (event) { case 'link.created': await handleLinkCreated(data); break; case 'link.opened': await handleLinkOpened(data); break; case 'user.data_request': await handleDataRequest(data); break; case 'operation.created': await handleOperationCreated(data); break; case 'operation.cancelled': await handleOperationCancelled(data); break; default: console.log(`Unknown event: ${event}`); } } catch (error) { console.error(`Error processing ${event}:`, error); // Don't throw - we already responded 200 }});function verifySignature(payload, signature, timestamp) { const secret = process.env.M2M_WEBHOOK_SECRET; const expected = crypto .createHmac('sha256', secret) .update(`${timestamp}.${payload}`) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature.replace('sha256=', '')), Buffer.from(expected) );}async function handleLinkCreated(data) { console.log(`Link created: ${data.linkId} for user ${data.referenceId}`); // Your logic here - store the link, send it to the user, etc.}async function handleLinkOpened(data) { console.log(`Link opened: ${data.linkId} by user ${data.referenceId}`); // Your logic here}async function handleDataRequest(data) { console.log(`Data request: ${data.dataRequestId} for user ${data.referenceId}`); // Your logic here - fetch and send user data}async function handleOperationCreated(data) { console.log(`Operation created: ${data.operationId} for user ${data.referenceId}`); // Your logic here - record the operation, notify team, etc.}async function handleOperationCancelled(data) { console.log(`Operation cancelled: ${data.operationId} for user ${data.referenceId}`); // Your logic here - update status, release held resources, etc.}app.listen(3000);