Skip to main content

Overview

When a user opens a magic link and their data is incomplete for CIP verification, M2M sends a user.data_request webhook to your server. This gives you an opportunity to provide the missing data before M2M asks the user directly.
Data requests are non-blocking. The user continues through the widget while you fetch and send data. If you respond before they reach the CIP step, they skip data entry entirely.

The webhook

When M2M needs user data, you receive this webhook:
{
  "event": "user.data_request",
  "timestamp": "2026-01-26T14:30:00.000Z",
  "data": {
    "dataRequestId": "dr_acme_a1b2c3d4e5f6",
    "referenceId": "user_12345",
    "linkId": "link_acme_x9y8z7w6v5u4",
    "requiredFields": [
      "name.secondName",
      "name.secondLastName",
      "idNumber",
      "idType",
      "dob"
    ],
    "replyEndpoint": "https://api.m2m.leapfinancial.com/partner/data-requests/dr_acme_a1b2c3d4e5f6",
    "expiresAt": "2026-01-26T15:30:00.000Z"
  }
}

Key fields

FieldDescription
dataRequestIdUnique ID for this request - use when responding
referenceIdYour user identifier
requiredFieldsList of fields M2M needs
replyEndpointFull URL to send data to
expiresAtDeadline to respond (same as link expiration)

Possible required fields

FieldDescription
name.firstNameFirst name
name.secondNameMiddle name / second first name
name.lastNamePrimary surname
name.secondLastNameMaternal surname
idNumberGovernment ID number (e.g., CURP)
idTypeType of ID document
dobDate of birth

Responding to the request

Make a PUT request to the replyEndpoint with the user data:
curl -X PUT https://api.m2m.leapfinancial.com/partner/data-requests/dr_acme_a1b2c3d4e5f6 \
  -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"
    }
  }'

Request body

referenceId
string
required
Your user identifier. Must match the referenceId in the webhook.
userData
object
required
Object containing the requested fields. You only need to include fields from requiredFields, but you can include additional fields.

Success response

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

Error handling

Reference ID mismatch (400)

The referenceId in your request doesn’t match the data request:
{
  "success": false,
  "error": {
    "code": "REFERENCE_ID_MISMATCH",
    "message": "Reference ID does not match the data request"
  }
}

Data request not found (404)

The dataRequestId doesn’t exist:
{
  "success": false,
  "error": {
    "code": "DATA_REQUEST_NOT_FOUND",
    "message": "Data request not found"
  }
}

Data request expired (409)

The data request (and link) has expired:
{
  "success": false,
  "error": {
    "code": "DATA_REQUEST_EXPIRED",
    "message": "Data request has expired"
  }
}

Implementation guide

Complete webhook handler

Node.js
const express = require('express');
const crypto = require('crypto');

const app = express();
app.use('/webhooks/m2m', express.raw({ type: 'application/json' }));

const WEBHOOK_SECRET = process.env.M2M_WEBHOOK_SECRET;
const API_KEY = process.env.M2M_API_KEY;

// Verify webhook signature
function verifySignature(req) {
  const signature = req.headers['x-m2m-signature'];
  const timestamp = req.headers['x-m2m-timestamp'];
  const body = req.body.toString();
  
  const expected = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(`${timestamp}.${body}`)
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature.replace('sha256=', '')),
    Buffer.from(expected)
  );
}

// Fetch user data from your database
async function getUserData(referenceId, requiredFields) {
  const user = await db.users.findUnique({
    where: { id: referenceId }
  });
  
  if (!user) return null;
  
  const userData = {};
  
  // Map your fields to M2M fields
  if (requiredFields.includes('name.secondName') && user.middleName) {
    userData.name = userData.name || {};
    userData.name.secondName = user.middleName;
  }
  
  if (requiredFields.includes('name.secondLastName') && user.maternalSurname) {
    userData.name = userData.name || {};
    userData.name.secondLastName = user.maternalSurname;
  }
  
  if (requiredFields.includes('idNumber') && user.governmentId) {
    userData.idNumber = user.governmentId;
  }
  
  if (requiredFields.includes('idType') && user.idDocumentType) {
    userData.idType = user.idDocumentType;
  }
  
  if (requiredFields.includes('dob') && user.dateOfBirth) {
    userData.dob = user.dateOfBirth.toISOString().split('T')[0];
  }
  
  return userData;
}

// Send data to M2M
async function respondToDataRequest(replyEndpoint, referenceId, userData) {
  const response = await fetch(replyEndpoint, {
    method: 'PUT',
    headers: {
      'X-API-Key': API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ referenceId, userData })
  });
  
  return response.json();
}

// Webhook handler
app.post('/webhooks/m2m', async (req, res) => {
  // Verify signature
  if (!verifySignature(req)) {
    return res.status(401).send('Invalid signature');
  }
  
  const event = JSON.parse(req.body);
  
  // Acknowledge immediately
  res.status(200).send('OK');
  
  // Handle data request asynchronously
  if (event.event === 'user.data_request') {
    try {
      const { referenceId, requiredFields, replyEndpoint } = event.data;
      
      // Fetch user data
      const userData = await getUserData(referenceId, requiredFields);
      
      if (userData && Object.keys(userData).length > 0) {
        // Send to M2M
        const result = await respondToDataRequest(
          replyEndpoint,
          referenceId,
          userData
        );
        
        console.log('Data request fulfilled:', result);
      } else {
        console.log('No data available for user:', referenceId);
      }
    } catch (error) {
      console.error('Error handling data request:', error);
    }
  }
});

app.listen(3000);

Best practices

The faster you respond, the smoother the user experience. Aim to respond within a few seconds:
  • Pre-fetch user data when you create links
  • Use a fast database with indexed queries
  • Consider caching frequently accessed data
If you don’t have all requested fields, send what you have. Partial data still reduces friction:
// Even if you only have some fields, send them
const userData = {};

if (user.middleName) {
  userData.name = { secondName: user.middleName };
}

if (Object.keys(userData).length > 0) {
  await respondToDataRequest(endpoint, referenceId, userData);
}
Ensure data matches expected formats before sending:
  • Dates: YYYY-MM-DD format
  • CURP: 18 characters, uppercase
  • Names: Use legal names, not nicknames
Log webhook receipt and your responses for troubleshooting:
console.log('Received data request:', {
  dataRequestId: event.data.dataRequestId,
  referenceId: event.data.referenceId,
  requiredFields: event.data.requiredFields
});

console.log('Sending response:', {
  dataRequestId: event.data.dataRequestId,
  fieldsProvided: Object.keys(userData)
});
Data requests are idempotent - you can respond multiple times safely. M2M only processes the first successful response.

What if you don’t respond?

If you don’t respond (or can’t provide data), M2M gracefully falls back:
  1. User reaches the CIP step in the widget
  2. M2M shows a form for the missing fields
  3. User enters the data manually
  4. Transaction continues normally
Not responding doesn’t break the flow - it just increases friction for the user. Implement data requests when you’re ready, but don’t let it block your initial integration.

Testing

Sandbox testing

  1. Create a link in sandbox with incomplete data:
    {
      "referenceId": "test_user_123",
      "userData": {
        "name": { "firstName": "Test" }
      }
    }
    
  2. Configure your webhook endpoint in Partner Portal
  3. Open the link in your browser
  4. Verify you receive the user.data_request webhook
  5. Send a response and verify it’s accepted
  6. Check that the widget shows pre-filled data

Testing without webhooks

If you’re not ready to implement webhooks:
  1. Create a link with minimal data
  2. Open it and complete the full CIP flow manually
  3. Understand what users experience
  4. Use this to prioritize which data to provide upfront

Next steps

User Data Guide

Understand the friction vs. integration trade-off.

Webhook Security

Implement secure webhook handling.