Automatic file upload
Use this route when your system can make the daily Excel file and upload it by itself. Your system sends one file each working day. It signs the upload so that MarsX knows the file came from you.
If people on your team upload the file by hand, use Upload by staff instead. Both routes use the same file and the same rules. MarsX shows a connection that sends files as file-fed, never as real time.
How it works
Section titled “How it works”- Your system exports one Excel file each working day, in the MarsX template. It holds one row for each lead event.
- Your system uploads the file with one signed
PUTrequest. - MarsX stores the file and answers
201. It checks every row and emails you the result.
Make the file
Section titled “Make the file”Use the Excel template, version 1.2. The template columns page lists every column. The lead fields page gives the format of each one.
- Put one row for each lead event. Use the dropdown arrow wherever a cell has one. In a file, MarsX accepts either the dropdown label or its code.
- Include only the leads that changed since your previous file. A lead that is missing from a file means no change. It never means deletion.
- A
neworupdaterow carries the lead’s full current state. A blank cell means that field is now empty. - A
withdrawordeleterow carries only sections A and H. Leave every customer, interest, location, source, consent and AI column blank. source_revisiongoes up with every change to a lead. Never reuse a number for different content. For a change, always send a higher revision number and the time of the change. On a new lead you may leave both blank, and MarsX uses revision 1 and the enquiry time.- A new lead needs
partner_lead_id,record_action,created_at,customer_name, a phone or an email,source_channel,contact_consent(Yes) andconsent_time. A lead without the model or the city is accepted and marked incomplete. - Write every date and time with its offset from UTC. In the file, use
+07:00. - Add privacy rows for every withdrawal and deletion request you received since the previous file, including leads you no longer hold.
Rules for the File info sheet:
| Item | System name | What to enter |
|---|---|---|
| Template version | template_version |
Leave as it is. A file on another version is rejected |
| Partner code | partner_code |
The short code MarsX gave your source, in capital letters |
| File number | file_sequence |
Six digits. Goes up by one with every file, for example 000123 |
| Date and time of export | exported_at |
When you made the file, for example 2026-10-01T14:30:00+07:00 |
| Number of lead rows | row_count |
The number of filled rows on the Leads sheet. A wrong count rejects the whole file as incomplete |
Name the file
Section titled “Name the file”<partner_code>_<YYYY-MM-DD>_<file_sequence>.xlsxFor example, EXAMPLE_2026-10-01_000123.xlsx. The file_sequence goes up by one with every file. MarsX flags a gap.
Check before you upload
Section titled “Check before you upload”row_countmatches the number of rows.- The file has no macros, no formulas, no merged cells and no external links. MarsX rejects a file that has any of them.
- The file is within the limits in the table below.
Sign and upload the file
Section titled “Sign and upload the file”Your system signs the file bytes the same way it signs an event. Sign the request explains the signing and gives worked examples to prove your code.
| Item | Value |
|---|---|
| Address | PUT https://api.marsx.media/v1/sources/{connection}/files/{file_name} |
| Sandbox | https://api-sandbox.marsx.media |
| Body | The file bytes |
| Content type | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
webhook-id |
The file name without .xlsx |
webhook-timestamp |
The time of this attempt, in whole seconds since 1 January 1970 (UTC) |
webhook-signature |
The signature over <webhook-id>.<webhook-timestamp>.<file bytes> |
| Success | 201, after MarsX has stored the file safely |
A repeat of the same file is answered 201 with result duplicate. Nothing changes.
SFTP is not offered. Never send a file by email or WhatsApp.
import { createHmac } from "node:crypto";import { readFileSync } from "node:fs";import { basename } from "node:path";
const host = "https://api-sandbox.marsx.media";const connection = process.env.MARSX_CONNECTION; // for example conn_exampleexample22const secret = process.env.MARSX_SIGNING_SECRET; // starts with whsec_const path = process.argv[2]; // for example EXAMPLE_2026-10-01_000123.xlsx
const fileName = basename(path);const body = readFileSync(path);const id = fileName.replace(/\.xlsx$/, "");const timestamp = String(Math.floor(Date.now() / 1000));
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");const signature = "v1," + createHmac("sha256", key).update(`${id}.${timestamp}.`).update(body).digest("base64");
const response = await fetch(`${host}/v1/sources/${connection}/files/${fileName}`, { method: "PUT", headers: { "content-type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "webhook-id": id, "webhook-timestamp": timestamp, "webhook-signature": signature, }, body,});console.log(response.status, await response.text());import base64, hashlib, hmac, os, sys, timeimport urllib.error, urllib.request
host = "https://api-sandbox.marsx.media"connection = os.environ["MARSX_CONNECTION"] # for example conn_exampleexample22secret = os.environ["MARSX_SIGNING_SECRET"] # starts with whsec_path = sys.argv[1] # for example EXAMPLE_2026-10-01_000123.xlsx
file_name = os.path.basename(path)with open(path, "rb") as f: body = f.read()webhook_id = file_name.removesuffix(".xlsx")timestamp = str(int(time.time()))
key = base64.b64decode(secret.removeprefix("whsec_"))signed = webhook_id.encode() + b"." + timestamp.encode() + b"." + bodysignature = "v1," + base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
request = urllib.request.Request( f"{host}/v1/sources/{connection}/files/{file_name}", data=body, method="PUT", headers={ "content-type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "webhook-id": webhook_id, "webhook-timestamp": timestamp, "webhook-signature": signature, },)try: with urllib.request.urlopen(request, timeout=60) as response: print(response.status, response.read().decode())except urllib.error.HTTPError as error: print(error.code, error.read().decode())Limits
Section titled “Limits”| Limit | Value |
|---|---|
Transport cap of this route, answered 413 before authentication |
10 MiB (10,485,760 bytes) |
| File size on disk, after authentication | 10 MiB |
| Size when expanded | 100 MiB |
| Rows | 10,000 |
| Characters in a cell | 500 |
A file over a limit is rejected whole. Other paths have a 1 MiB transport cap. See Limits.
Read the answers
Section titled “Read the answers”| Status | Meaning | What you do |
|---|---|---|
201 |
MarsX stored the file safely | Done. Wait for the emailed result |
400 |
The request is not valid | Fix it. Do not retry the same request |
401 |
The signature is missing or wrong, the clock is off by more than 300 seconds, or the URL or environment is wrong | Check the key, the clock, the URL and the environment |
413 |
The file is over the limit of this route | Make the file smaller. Do not retry |
429 |
Too many requests. Retry-After says when to try again |
Wait at least that long, then upload again |
502, 503, 504, a timeout or a dropped connection |
A temporary fault | Wait, then upload again with the same webhook-id |
See Errors for the full list.
Schedule and rules
Section titled “Schedule and rules”| Rule | Detail |
|---|---|
| Schedule | One file each working day by 09:00 WIB. Also a file within 24 hours of any privacy request, even if it holds only privacy rows. |
| File sequence | Goes up by one with every file. MarsX flags a gap. |
| Row count | A mismatch rejects the whole file as truncated. A wrong template_version also rejects it. |
| Content | Only the leads that changed since the previous file. |
| Invalid row | Set aside with a reason. Valid rows in the same file still load. |
| Not accepted | Macros, formulas, merged cells and links. MarsX rejects the whole file. |
| Each connection | Has its own upload and its own file sequence. MarsX processes the files of each connection separately. |
| Before activation | Send a made-up sample file. |
When a file is missing or partial
Section titled “When a file is missing or partial”MarsX finds a missing or partial file from the file sequence and the row count. Then:
- MarsX tells you.
- You send that file again, unchanged, with its privacy rows, within 24 hours of the notice.
- A later file does not replace the missing one. It carries only the privacy requests since the file before it.
- Until the gap closes, the connection stays restricted and is shown as having a file gap.
- Sending the same file twice changes nothing.
Read the outcome
Section titled “Read the outcome”MarsX emails the result of each file to your technical contact. The result is one of three: accepted, rejected with the reason, or a gap in the sequence.
The full privacy list
Section titled “The full privacy list”When MarsX asks for it, send a full list of the leads on your connection that are currently withdrawn, restricted or erased, including leads you no longer hold. Send it as a template file with only withdraw and delete rows, named <partner_code>_<YYYY-MM-DD>_privacy-list.xlsx, by the same upload.
Every file also carries withdraw and delete rows for each privacy request since the previous file. See Send a withdrawal or deletion for what MarsX does with them.
Never do this
Section titled “Never do this”Never send a lead file by email or WhatsApp
MarsX does not accept it. Use the signed upload or the staff upload account.
Never edit or retype a file after export
Upload the file exactly as your system produced it. Do not re-key a lead.
Never send identity or money documents
No KTP or family-card numbers, income, financing documents or full chat transcripts. No AI guesses about affordability or location.
Never send status or sales outcomes
No status, assignment, follow-up, test drives, sales or SPK numbers. Dealers record these in MarsX Dashboard.
Never reuse a revision number for different content
The number must rise with every change. Reuse sets the event aside as a conflict.
Test before you go live
Section titled “Test before you go live”Run the upload checks with made-up files.
