Customer Sync
Use this API to convert retailer rows and shop rows from Excel into Mulltiply customer records. A retailer comes from the Customer Sheet, and each matching shop row becomes a shop under that retailer.
At A Glance
/v2/retailers/sync-data| Area | Detail |
|---|---|
| Purpose | Sync retailer/customer data and shop/branch data from Excel. |
| Content type | application/json |
| Authentication | x-api-key request header |
| Request body | Array of retailer objects |
| Primary key | Retailer syncId, mapped from Sheet 1 No |
| Shop link | Sheet 2 Customer_No must match Sheet 1 No |
The Word source contains a concrete API key example. The public documentation
uses YOUR_API_KEY instead so an implementation team knows where the key goes
without exposing a real credential.
Source Workbook
The integration reads two sheets from the Excel file.
| Sheet | Purpose | Key columns |
|---|---|---|
| Sheet 1 | Retailer / Customer data | No, Contact, Name, Mobile_Phone_No, Phone_No, Search_Name, address fields, GST fields |
| Sheet 2 | Shop / Branch data | Customer_No, Code, Name, address fields |
The script converts rows from these sheets into the API payload before sending them to Mulltiply.
Authentication And Headers
Send the request as JSON and include the issued API key.
Content-Type: application/json
x-api-key: YOUR_API_KEY
Request Body
The endpoint expects an array. Each item in the array is one retailer.
- JSON Body
- cURL
[
{
"name": "Demo Retailer",
"firstName": "Demo",
"lastName": "Retailer",
"email": "",
"phoneCountryCode": "+91",
"phone": "9999901001",
"syncId": "RET-1001",
"note": "",
"tags": ["DEMO_TAG"],
"taxExempt": false,
"createdAt": "2026-03-09T07:46:56Z",
"updatedAt": "2026-03-09T11:41:27Z",
"outStandingAmount": "1500",
"shops": [],
"billingAddress": {
"syncId": null,
"address1": "Sample Address Line 1",
"address2": "",
"city": "Sample City",
"province": "Sample State",
"provinceCode": "WB",
"country": "India",
"countryCode": "IN",
"zip": "000001",
"phoneCountryCode": "+91",
"phone": "9999901001",
"company": "",
"firstName": "Demo",
"lastName": "Retailer",
"isDefault": true,
"isBillingAddress": true
}
}
]
curl --request POST \
--url 'https://api.mulltiply.org/v2/retailers/sync-data' \
--header 'Content-Type: application/json' \
--header 'x-api-key: YOUR_API_KEY' \
--data '[{"name":"Demo Retailer","firstName":"Demo","lastName":"Retailer","phoneCountryCode":"+91","phone":"9999901001","syncId":"RET-1001","tags":["DEMO_TAG"],"taxExempt":false,"outStandingAmount":"1500","shops":[]}]'
Retailer Mapping
Retailer fields come from Sheet 1, also called the Customer Sheet.
| API field | Excel column | Rule |
|---|---|---|
name | Contact or Name | Retailer name. |
firstName | Contact or Name | Retailer first name. |
lastName | Name_2 | Retailer last name. |
phone | Mobile_Phone_No or Phone_No | Processed through the phone rules below. |
syncId | No | Unique retailer identifier. |
tags | Search_Name | Sent as a one-item tags array when present. |
createdAt | Generated automatically | Current timestamp. |
updatedAt | Generated automatically | Current timestamp. |
outStandingAmount | Balance_LCY | Stored at the top level of the retailer payload. |
Phone Number Rules
The integration chooses the retailer phone in this order:
- Read
Mobile_Phone_No. - If it contains multiple numbers separated by
/, use the first number. - If the selected number has more than 10 digits, keep the last 10 digits.
- If the result is invalid, try
Phone_No. - If
Phone_Nois also invalid, send an empty phone value.
Example:
Mobile_Phone_No = 9999901001/9999901002
Final phone = 9999901001
Shop Matching
Shops are read from Sheet 2 and attached to a retailer when this condition is true:
Sheet2.Customer_No = Sheet1.No
When a match exists, each matching Sheet 2 row becomes a separate shop under the retailer.
Shop Mapping
| API field | Excel column | Rule |
|---|---|---|
syncId | Customer_No + / + Code | Unique shop identifier. |
customerSyncId | Customer_No | Links the shop back to the retailer. |
shopName | Name | Shop name. |
city | City | Shop city. |
state | State_Code | Shop state. |
pincode | Post_Code | Shop postal code. |
gstNumber | Sheet 1 GST_Registration_No | GST number for the retailer/shop. |
outStandingAmount | Sheet 1 Balance_LCY | Used for the primary or first shop when applicable. |
Shop syncId example:
Customer_No = RET-1001
Code = SHOP-001
syncId = RET-1001/SHOP-001
Address Mapping
Retailer address fields are used to generate billing and shipping addresses.
| API field | Excel column |
|---|---|
address1 | Address |
address2 | Address_2 |
city | City |
province | State_Code |
country | Country_Region_Code |
zip | Post_Code |
phone | Phone_No |
Shop Creation Logic
Case 1: Retailer Has No Matching Shops
If the retailer does not have matching rows in Sheet 2, the integration creates one shop automatically from the retailer row.
| Shop field | Value used |
|---|---|
| Shop name | Retailer name |
| City | Retailer city |
| GST | Retailer GST |
Two addresses are generated from the retailer address fields:
- Billing address
- Shipping address
Case 2: Retailer Has Matching Shops
If Sheet 2 contains one or more rows where Customer_No matches the retailer
No, each matching row becomes a separate shop. In this case:
- Shop address comes from Sheet 2.
- Billing address still comes from Sheet 1.
- The shop remains linked to the retailer through
customerSyncId.
Tags
Retailer tags come from Search_Name.
Search_Name = DEMO_TAG
Result:
{
"tags": ["DEMO_TAG"]
}
Normalized Final Payload
The example below is normalized as valid JSON and uses the documented shop
syncId rule.
[
{
"name": "Demo Retailer",
"firstName": "Demo",
"lastName": "Retailer",
"email": "",
"phoneCountryCode": "+91",
"phone": "9999901001",
"syncId": "RET-1001",
"note": "",
"tags": ["DEMO_TAG"],
"taxExempt": false,
"createdAt": "2026-03-09T07:46:56Z",
"updatedAt": "2026-03-09T11:41:27Z",
"outStandingAmount": "1500",
"billingAddress": {
"syncId": null,
"address1": "Sample Address Line 1",
"address2": "",
"city": "Sample City",
"province": "Sample State",
"provinceCode": "WB",
"country": "India",
"countryCode": "IN",
"zip": "000001",
"phoneCountryCode": "+91",
"phone": "9999901001",
"company": "",
"firstName": "Demo",
"lastName": "Retailer",
"isDefault": true,
"isBillingAddress": true
},
"shops": [
{
"syncId": "RET-1001/SHOP-001",
"customerSyncId": "RET-1001",
"shopName": "Demo Shop",
"outStandingAmount": "1500",
"shopAddress": "",
"city": "Sample City",
"state": "Sample State",
"pincode": "000001",
"documentType": "GST",
"gstNumber": "22AAAAA0000A1Z5",
"identityAttachments": [],
"shopPhotos": [],
"isPrimary": true,
"shippingAddress": {
"syncId": null,
"address1": "Sample Address Line 1",
"address2": "",
"addressType": "SHIPPING_ADDRESS",
"city": "Sample City",
"province": "Sample State",
"provinceCode": "WB",
"country": "India",
"countryCode": "IN",
"zip": "000001",
"phoneCountryCode": "+91",
"phone": "9999901001",
"company": "",
"firstName": "Demo",
"lastName": "Retailer"
}
}
]
}
]
Excel Column Checklist
Use this checklist before running the sync.
| Excel column | Sheet | Used for |
|---|---|---|
No | Sheet 1 | Retailer unique identifier |
Contact | Sheet 1 | Retailer first name |
Name | Sheet 1 | Retailer name |
Name_2 | Sheet 1 | Retailer last name |
Mobile_Phone_No | Sheet 1 | Primary retailer phone |
Phone_No | Sheet 1 | Backup retailer phone |
Search_Name | Sheet 1 | Retailer tag |
Balance_LCY | Sheet 1 | Outstanding amount |
Address | Sheet 1 | Retailer address line 1 |
Address_2 | Sheet 1 | Retailer address line 2 |
City | Sheet 1 | Retailer city |
State_Code | Sheet 1 | Retailer state |
Country_Region_Code | Sheet 1 | Retailer country |
Post_Code | Sheet 1 | Retailer postal code |
GST_Registration_No | Sheet 1 | GST number |
Customer_No | Sheet 2 | Links shop to retailer |
Code | Sheet 2 | Shop identifier |
Name | Sheet 2 | Shop name |
Address | Sheet 2 | Shop address line 1 |
Address_2 | Sheet 2 | Shop address line 2 |
City | Sheet 2 | Shop city |
State_Code | Sheet 2 | Shop state |
Country_Region_Code | Sheet 2 | Shop country |
Post_Code | Sheet 2 | Shop postal code |
Responses
All Rows Valid
{
"count": 3,
"message": "All rows are being synced. You will be notified once done."
}
Some Rows Invalid, Up To 20 Errors
{
"count": 5,
"message": "Some rows had validation errors and were not processed. Please check the nonProcessableRows field for details.",
"nonProcessableRows": [
{
"row": 2,
"errors": ["Invalid phone number"]
},
{
"row": 4,
"errors": ["Missing shop name"]
}
]
}
Some Rows Invalid, More Than 20 Errors
When more than 20 rows fail validation, the response only returns the first 20 non-processable rows. Complete details can be checked in the bulk upload logs.
{
"count": 50,
"message": "Only showing first 20 of 32 non-processable rows. Please check the nonProcessableRows field for details. You can also check the bulk upload log for complete details.",
"nonProcessableRows": [
{
"row": 1,
"errors": ["Invalid GST number"]
},
{
"row": 2,
"errors": ["Missing address"]
}
]
}
All Rows Invalid
{
"count": 0,
"message": "All rows had validation errors and were not processed. Please check the nonProcessableRows field for details.",
"nonProcessableRows": [
{
"row": 1,
"errors": ["Missing retailer name"]
},
{
"row": 2,
"errors": ["Invalid document type"]
}
]
}
Response Field Notes
| Field | Meaning |
|---|---|
count | Total number of rows received in the request. |
nonProcessableRows | Present only when validation errors exist. |
nonProcessableRows[].row | Row number that failed validation. |
nonProcessableRows[].errors | Validation messages for that row. |
Implementation Checklist
- Confirm Sheet 1 has one row per retailer.
- Confirm Sheet 2 shop rows use
Customer_Novalues that exist in Sheet 1No. - Confirm every retailer has a stable
Novalue. - Confirm phone values are clean or can be normalized by the phone rules.
- Confirm
Balance_LCYis available when outstanding amount is required. - Confirm address columns are present for billing and shipping address creation.
- Send a JSON array, even when syncing one retailer.