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
Field Description 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
Field Description 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
Your user identifier. Must match the referenceId in the webhook.
Object containing the requested fields. You only need to include fields from requiredFields, but you can include additional fields. User’s name fields. Middle name or second first name.
Type of ID document (e.g., CURP, INE, PASSPORT).
Date of birth in YYYY-MM-DD format.
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
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 );
}
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:
User reaches the CIP step in the widget
M2M shows a form for the missing fields
User enters the data manually
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
Create a link in sandbox with incomplete data:
{
"referenceId" : "test_user_123" ,
"userData" : {
"name" : { "firstName" : "Test" }
}
}
Configure your webhook endpoint in Partner Portal
Open the link in your browser
Verify you receive the user.data_request webhook
Send a response and verify it’s accepted
Check that the widget shows pre-filled data
Testing without webhooks
If you’re not ready to implement webhooks:
Create a link with minimal data
Open it and complete the full CIP flow manually
Understand what users experience
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.