> ## Documentation Index
> Fetch the complete documentation index at: https://docs.leapfinancial.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Data Requests

> Handle webhooks to provide missing user data and reduce friction

## 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.

```mermaid theme={null}
sequenceDiagram
    participant User
    participant Widget
    participant M2M
    participant Partner

    User->>Widget: Opens magic link
    Widget->>M2M: Claim link
    M2M->>M2M: Check data completeness
    
    alt Data incomplete
        M2M->>Partner: Webhook: user.data_request
        M2M-->>Widget: Continue (non-blocking)
        Partner->>Partner: Fetch user data
        Partner->>M2M: PUT /partner/data-requests/{id}
        M2M->>M2M: Merge data
    end
    
    User->>Widget: Reaches CIP step
    
    alt Data now complete
        Widget->>User: Skip data entry
    else Data still incomplete
        Widget->>User: Show data entry form
    end
```

<Info>
  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.
</Info>

## The webhook

When M2M needs user data, you receive this webhook:

```json theme={null}
{
  "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://m2m-backend-qa.up.railway.app/partner/data-requests/dr_acme_a1b2c3d4e5f6",
    "expiresAt": "2026-01-26T15:30:00.000Z"
  }
}
```

### Key fields

| Field            | Description                                      |
| ---------------- | ------------------------------------------------ |
| `dataRequestId`  | Unique ID for this request - use when responding |
| `referenceId`    | Your user identifier                             |
| `requiredFields` | List of fields M2M needs                         |
| `replyEndpoint`  | Full URL to send data to                         |
| `expiresAt`      | Deadline to respond (same as link expiration)    |

### Possible required fields

| Field                 | Description                       |
| --------------------- | --------------------------------- |
| `name.firstName`      | First name                        |
| `name.secondName`     | Middle name / second first name   |
| `name.lastName`       | Primary surname                   |
| `name.secondLastName` | Maternal surname                  |
| `idNumber`            | Government ID number (e.g., CURP) |
| `idType`              | Type of ID document               |
| `dob`                 | Date of birth                     |

## Responding to the request

Make a PUT request to the `replyEndpoint` with the user data:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X PUT https://m2m-backend-qa.up.railway.app/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"
      }
    }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch(
    'https://m2m-backend-qa.up.railway.app/partner/data-requests/dr_acme_a1b2c3d4e5f6',
    {
      method: 'PUT',
      headers: {
        'X-API-Key': 'm2m_live_your_api_key_here',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        referenceId: 'user_12345',
        userData: {
          name: {
            secondName: 'Carlos',
            secondLastName: 'Lopez'
          },
          idNumber: 'GALO900515HDFRPN09',
          idType: 'CURP',
          dob: '1990-05-15'
        }
      })
    }
  );

  const data = await response.json();
  ```

  ```python Python theme={null}
  import requests

  response = requests.put(
      'https://m2m-backend-qa.up.railway.app/partner/data-requests/dr_acme_a1b2c3d4e5f6',
      headers={
          'X-API-Key': 'm2m_live_your_api_key_here',
          'Content-Type': 'application/json'
      },
      json={
          'referenceId': 'user_12345',
          'userData': {
              'name': {
                  'secondName': 'Carlos',
                  'secondLastName': 'Lopez'
              },
              'idNumber': 'GALO900515HDFRPN09',
              'idType': 'CURP',
              'dob': '1990-05-15'
          }
      }
  )
  ```
</CodeGroup>

### Request body

<ParamField body="referenceId" type="string" required>
  Your user identifier. Must match the `referenceId` in the webhook.
</ParamField>

<ParamField body="userData" type="object" required>
  Object containing the requested fields. You only need to include fields from `requiredFields`, but you can include additional fields.

  <Expandable title="userData properties">
    <ParamField body="name" type="object">
      User's name fields.

      <Expandable title="name properties">
        <ParamField body="firstName" type="string">
          First name.
        </ParamField>

        <ParamField body="secondName" type="string">
          Middle name or second first name.
        </ParamField>

        <ParamField body="lastName" type="string">
          Primary surname.
        </ParamField>

        <ParamField body="secondLastName" type="string">
          Maternal surname.
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField body="idNumber" type="string">
      Government ID number.
    </ParamField>

    <ParamField body="idType" type="string">
      Type of ID document (e.g., `CURP`, `INE`, `PASSPORT`).
    </ParamField>

    <ParamField body="dob" type="string">
      Date of birth in `YYYY-MM-DD` format.
    </ParamField>
  </Expandable>
</ParamField>

### Success response

```json theme={null}
{
  "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:

```json theme={null}
{
  "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:

```json theme={null}
{
  "success": false,
  "error": {
    "code": "DATA_REQUEST_NOT_FOUND",
    "message": "Data request not found"
  }
}
```

### Data request expired (409)

The data request (and link) has expired:

```json theme={null}
{
  "success": false,
  "error": {
    "code": "DATA_REQUEST_EXPIRED",
    "message": "Data request has expired"
  }
}
```

## Implementation guide

### Complete webhook handler

```javascript Node.js theme={null}
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

<AccordionGroup>
  <Accordion title="Respond quickly">
    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
  </Accordion>

  <Accordion title="Handle partial data">
    If you don't have all requested fields, send what you have. Partial data still reduces friction:

    ```javascript theme={null}
    // 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);
    }
    ```
  </Accordion>

  <Accordion title="Validate data format">
    Ensure data matches expected formats before sending:

    * Dates: `YYYY-MM-DD` format
    * CURP: 18 characters, uppercase
    * Names: Use legal names, not nicknames
  </Accordion>

  <Accordion title="Log for debugging">
    Log webhook receipt and your responses for troubleshooting:

    ```javascript theme={null}
    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)
    });
    ```
  </Accordion>

  <Accordion title="Implement idempotency">
    Data requests are idempotent - you can respond multiple times safely. M2M only processes the first successful response.
  </Accordion>
</AccordionGroup>

## 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

<Note>
  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.
</Note>

## Testing

### Sandbox testing

1. Create a link in sandbox with incomplete data:
   ```json theme={null}
   {
     "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

<CardGroup cols={2}>
  <Card title="User Data Guide" icon="user" href="/m2m/integration/user-data">
    Understand the friction vs. integration trade-off.
  </Card>

  <Card title="Webhook Security" icon="shield" href="/m2m/webhooks/security">
    Implement secure webhook handling.
  </Card>
</CardGroup>
