Skip to main content

Event types

M2M sends webhooks for the following events:
EventDescription
link.createdA magic link was created
link.openedUser opened a magic link
user.data_requestM2M needs additional user data for verification
operation.createdA money transfer operation was created
operation.cancelledA money transfer operation was cancelled
Sent when a magic link is created via the API. Use this to confirm link creation asynchronously and track the beginning of the link lifecycle.

Payload

{
  "event": "link.created",
  "timestamp": "2026-01-26T14:30:00.000Z",
  "data": {
    "linkId": "link_acme_a1b2c3d4e5f6",
    "referenceId": "user_12345",
    "url": "https://send.m2m.leapfinancial.com/link_acme_a1b2c3d4e5f6",
    "status": "created",
    "expiresAt": "2026-01-27T14:30:00.000Z",
    "createdAt": "2026-01-26T14:30:00.000Z"
  }
}
event
string
required
Always link.created.
timestamp
string
required
ISO 8601 timestamp when the webhook was generated.
data
object
required
Event payload.

Example handler

Node.js
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 a user opens a magic link for the first time. Use this to track engagement and prepare for potential data requests.

Payload

{
  "event": "link.opened",
  "timestamp": "2026-01-26T14:30:00.000Z",
  "data": {
    "linkId": "link_acme_a1b2c3d4e5f6",
    "referenceId": "user_12345",
    "openedAt": "2026-01-26T14:30:00.000Z",
    "deviceInfo": {
      "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)...",
      "platform": "iOS",
      "language": "en-US"
    }
  }
}
event
string
required
Always link.opened.
timestamp
string
required
ISO 8601 timestamp when the webhook was generated.
data
object
required
Event payload.

Example handler

Node.js
app.post('/webhooks/m2m', async (req, res) => {
  const { event, data } = req.body;
  
  if (event === 'link.opened') {
    console.log(`User ${data.referenceId} opened link ${data.linkId}`);
    
    // Update your records
    await db.users.update({
      where: { referenceId: data.referenceId },
      data: { lastLinkOpenedAt: data.openedAt }
    });
    
    // Prepare for potential data request
    await prepareUserDataForM2M(data.referenceId);
  }
  
  res.status(200).send('OK');
});

user.data_request

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.

Payload

{
  "event": "user.data_request",
  "timestamp": "2026-01-26T14:30:00.000Z",
  "data": {
    "dataRequestId": "dr_acme_x9y8z7w6v5u4",
    "referenceId": "user_12345",
    "linkId": "link_acme_a1b2c3d4e5f6",
    "requiredFields": [
      "name.secondName",
      "name.secondLastName",
      "idNumber",
      "idType",
      "dob"
    ],
    "replyEndpoint": "https://api.m2m.leapfinancial.com/partner/data-requests/dr_acme_x9y8z7w6v5u4",
    "expiresAt": "2026-01-26T15:30:00.000Z"
  }
}
event
string
required
Always user.data_request.
timestamp
string
required
ISO 8601 timestamp when the webhook was generated.
data
object
required
Event payload.

Responding to data requests

When you receive this webhook, you can provide the missing data by making a PUT request to the replyEndpoint:
cURL
curl -X PUT https://api.m2m.leapfinancial.com/partner/data-requests/dr_acme_x9y8z7w6v5u4 \
  -H "X-API-Key: m2m_live_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "referenceId": "user_12345",
    "userData": {
      "name": {
        "secondName": "Carlos",
        "secondLastName": "Lopez"
      },
      "idNumber": "GALO900515HDFRPN09",
      "idType": "CURP",
      "dob": "1990-05-15"
    }
  }'
You only need to provide the fields listed in requiredFields. Any additional fields you provide will be stored but aren’t required.

Response to your PUT request

{
  "success": true,
  "data": {
    "dataRequestId": "dr_acme_x9y8z7w6v5u4",
    "status": "fulfilled",
    "linkId": "link_acme_a1b2c3d4e5f6",
    "fulfilledAt": "2026-01-26T14:35:00.000Z",
    "message": "User data received and merged successfully"
  }
}

Example handler

Node.js
app.post('/webhooks/m2m', async (req, res) => {
  const { event, data } = req.body;
  
  if (event === 'user.data_request') {
    // Acknowledge immediately
    res.status(200).send('OK');
    
    // Fetch user data from your system
    const user = await db.users.findUnique({
      where: { referenceId: data.referenceId }
    });
    
    if (!user) {
      console.log(`User ${data.referenceId} not found`);
      return;
    }
    
    // Build the response with requested fields
    const userData = {};
    
    if (data.requiredFields.includes('name.secondName')) {
      userData.name = userData.name || {};
      userData.name.secondName = user.middleName;
    }
    
    if (data.requiredFields.includes('idNumber')) {
      userData.idNumber = user.governmentId;
    }
    
    if (data.requiredFields.includes('dob')) {
      userData.dob = user.dateOfBirth;
    }
    
    // Send the data to M2M
    await fetch(data.replyEndpoint, {
      method: 'PUT',
      headers: {
        'X-API-Key': process.env.M2M_API_KEY,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        referenceId: data.referenceId,
        userData
      })
    });
    
    return;
  }
  
  res.status(200).send('OK');
});

What happens if you don’t respond?

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.

operation.created

Sent when a user completes a money transfer operation through the widget. Use this to record transactions in your system and trigger downstream processes.

Payload

{
  "event": "operation.created",
  "timestamp": "2026-01-26T14:30:00.000Z",
  "data": {
    "operationId": "op_a1b2c3d4e5f6",
    "linkId": "link_acme_a1b2c3d4e5f6",
    "referenceId": "user_12345",
    "userId": "usr_x9y8z7w6v5u4",
    "status": "created",
    "funding": {
      "method": "card",
      "last4": "4242",
      "bankName": "visa"
    },
    "payout": {
      "method": "bank_account"
    },
    "amount": {
      "send": 100.00,
      "receive": 1850.50,
      "rate": 18.505,
      "fee": 3.99,
      "currency": {
        "send": "USD",
        "receive": "MXN"
      }
    },
    "createdAt": "2026-01-26T14:30:00.000Z"
  }
}
event
string
required
Always operation.created.
timestamp
string
required
ISO 8601 timestamp when the webhook was generated.
data
object
required
Event payload.

Example handler

Node.js
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');
});

operation.cancelled

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.

Payload

{
  "event": "operation.cancelled",
  "timestamp": "2026-01-26T14:35:00.000Z",
  "data": {
    "operationId": "op_a1b2c3d4e5f6",
    "linkId": "link_acme_a1b2c3d4e5f6",
    "referenceId": "user_12345",
    "userId": "usr_x9y8z7w6v5u4",
    "status": "cancelled",
    "cancellationReason": "user_requested",
    "cancelledAt": "2026-01-26T14:35:00.000Z"
  }
}
event
string
required
Always operation.cancelled.
timestamp
string
required
ISO 8601 timestamp when the webhook was generated.
data
object
required
Event payload.

Example handler

Node.js
app.post('/webhooks/m2m', async (req, res) => {
  const { event, data } = req.body;
  
  if (event === 'operation.cancelled') {
    console.log(`Operation ${data.operationId} cancelled for user ${data.referenceId}`);
    
    // Update the operation status in your system
    await db.operations.update({
      where: { operationId: data.operationId },
      data: {
        status: 'cancelled',
        cancellationReason: data.cancellationReason,
        cancelledAt: data.cancelledAt
      }
    });
    
    // Release any held resources
    await releaseHeldFunds(data.operationId);
  }
  
  res.status(200).send('OK');
});

Handling webhooks

Complete example

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 verification
app.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);

Next steps

Webhook Security

Learn how to verify webhook signatures.

Data Requests

Deep dive into the data request flow.