Skip to content

Automatic sending

Use this route when your system can send a message to MarsX the moment a lead is created or changed. It is the fastest route, and the one MarsX prefers.

Each lead event is one signed request over the internet. MarsX stores it, answers 202, and applies it a moment later. Your system also hosts three small read-only pages. MarsX reads them to find anything a request missed.

New to this? Start with Testing and going live. It sends one test lead in about 20 minutes.

Using an AI coding agent?

Paste this prompt into your agent. It points the agent at this documentation and sets the rules for the whole job. You can also read it as plain text at /agent-prompt.txt.

The sandbox (api-sandbox.marsx.media) opens when MarsX sends your connection details. The live address (api.marsx.media) opens at go-live.

Read the prompt
You are helping us connect our system to the MarsX Lead Intake API, so that our customer enquiries reach Concour. Treat these two documents as the only source of truth, and do not guess beyond them: https://docs.marsx.media/llms-full.txt and https://docs.marsx.media/openapi.yaml. If we have not yet received sandbox details from MarsX, stop after reading the documents and planning the work.

1. First ask me which route we use: automatic sending (each lead as it happens), automatic file upload (one Excel file each working day), or upload by staff (a person uploads the file, so no code is needed).
2. When MarsX has sent our sandbox connection details, build and test against the sandbox first, https://api-sandbox.marsx.media, with invented data only. Keep the base URL, connection reference and keys in configuration, so going live is a configuration change, not new code.
3. Sign every request exactly as the "Sign the request" section of the Automatic sending page describes, and prove your code against its worked examples before sending anything.
4. Only a 202 answer means MarsX accepted an event. Only a 201 answer means MarsX stored a file. Retry exactly as documented, with the same webhook-id.
5. Send withdrawals and deletions as soon as the customer asks. With automatic sending, send each as its own event straight away. With a file route, send a file within 24 hours of the request.
6. For automatic sending, also build the three read-only endpoints that MarsX calls to catch missed leads.
7. Keys come from MarsX through a one-time secure link. Ask me for them. Never hard-code them, never log them, and never log customer data.
8. Run every sandbox test case on the Testing and going live page, show me the results, and send them to MarsX as that page describes.
9. Go live only after MarsX confirms in writing that the sandbox tests passed and sends the production connection reference and keys through a new one-time secure link. Then switch the configuration to https://api.marsx.media, send the first real lead, and confirm with me and MarsX that it arrived. With automatic sending, use the status check. With a file route, MarsX emails the outcome. Sandbox keys never work in production, and production keys never go into the sandbox.

Ask me before making any assumption that the documentation does not settle.
Environment Address Use
Production https://api.marsx.media Live leads
Sandbox https://api-sandbox.marsx.media Testing with made-up data only. Events sent here never reach dealers

Every request goes to a connection: the link between your company and MarsX, for one brand. MarsX gives each connection its own reference, for example conn_exampleexample22. That reference is made up. MarsX issues a different reference, with its own keys, for the sandbox and for production. You do not choose it.

Item Value
Address POST https://api.marsx.media/v1/sources/{connection}/events
Sandbox https://api-sandbox.marsx.media. Made-up data only. Events sent here never reach dealers
One event for each request Yes. No batches
Content type application/json. A parameter such as charset=utf-8 is accepted and ignored. Any other type gets 415
Body UTF-8, no byte-order mark
Transport HTTPS only, TLS 1.2 or higher. MarsX never redirects, and a redirect counts as a failure
Timeout Set 15 to 30 seconds on your side, as Standard Webhooks recommends
Size A body of up to 32,768 bytes. A lead with every field filled and a 500-character note is about 4 KB

Sign on your server only. Never call this address from a browser or an app.

Sign on your server only. Never call this address from a browser or an app.

type data.record_action When to send
lead.new new A first enquiry
lead.update update Any later change to the lead. Send the full current state, a higher source_revision and the time of the change in updated_at
lead.withdraw withdraw The customer withdraws consent or asks to restrict processing
lead.delete delete The customer asks for erasure

Send a withdrawal or deletion at once. Never batch it. Handle withdrawal and deletion has the detail. The fields are in Lead fields. The signing steps are in Signing and worked examples.

The fields of each event are in Lead fields. A lead.new needs only eight fields: partner_lead_id, record_action, created_at, customer_name, a phone or an email, source_channel, contact_consent (yes) and consent_time. What blocks a lead lists the rest.

MarsX follows the open standard Standard Webhooks 1.0.0: standardwebhooks.com. Libraries for many languages are on that site. Use one if you can.

A signature is a short piece of text that only the holder of a secret can produce. MarsX checks it to be sure that a request came from you and arrived unchanged.

Header Value
webhook-id A unique ID for the event. It matches ^[A-Za-z0-9_-]{1,128}$, so your own event UUID works. The same value on every retry of the same event. No full stops
webhook-timestamp The time of this attempt, in whole seconds since the Unix epoch (1 January 1970, UTC). It changes on every retry
webhook-signature One or more signatures, separated by a space. Each is v1,<base64>

Join three parts with full stops:

<webhook-id>.<webhook-timestamp>.<raw request body>

The body is the exact bytes you send. Do not parse and re-serialise it between signing and sending. A single extra space breaks the signature.

Every request is signed with HMAC v1. This is the only scheme to use.

Item v1
Algorithm HMAC-SHA256
Key One shared secret. Both sides hold it
Key display whsec_ followed by the base64 of 24 to 64 random bytes
Header value v1, followed by the base64 of the 32-byte digest

MarsX compares signatures in constant time.

MarsX rejects a request when webhook-timestamp is more than 300 seconds away from the MarsX clock, in either direction. Keep your servers on network time (NTP). On every retry, use a fresh webhook-timestamp and a fresh signature. The webhook-id stays the same.

  • The signature proves that the request came from you. If one character changes on the way, the signature no longer fits.
  • The timestamp stops someone from copying a real request and sending it again later. A copied request goes stale in five minutes.
  • The webhook-id lets MarsX recognise a request that arrives twice, for example after a lost answer. It stores nothing new. It never decides which version of a lead wins. The source_revision decides that.
  • One key for each connection and environment keeps the damage small if a key leaks. The sandbox key never works in production, and a key from one connection never works on another.
  • Signing happens on your server only. Anything in a browser or an app can be read by whoever uses it.

Use these made-up inputs to check your code before you send anything. They are called worked examples because every input and the expected result are given.

Input Value
Secret whsec_ZmljdGlvbmFsLXRlc3Qtc2VjcmV0LWRvLW5vdC11c2U=
Secret after you remove whsec_ and decode the base64 The 32 ASCII characters fictional-test-secret-do-not-use
webhook-id msg_test_0001
webhook-timestamp 1791100800 (4 October 2026, 15:00:00 at +07:00)
Body The single line below, 350 bytes, no line break at the end

Body:

body
{"type":"lead.withdraw","timestamp":"2026-10-04T15:00:00+07:00","data":{"partner_lead_id":"SRC-TEST-0001","source_revision":3,"record_action":"withdraw","created_at":"2026-10-01T09:15:00+07:00","updated_at":"2026-10-04T15:00:00+07:00","request_type":"withdraw_consent","request_time":"2026-10-04T14:59:30+07:00","request_reference":"PRIV-TEST-0001"}}

The string that is signed (the ID, a full stop, the timestamp, a full stop, then the body):

signed string
msg_test_0001.1791100800.{"type":"lead.withdraw","timestamp":"2026-10-04T15:00:00+07:00","data":{"partner_lead_id":"SRC-TEST-0001","source_revision":3,"record_action":"withdraw","created_at":"2026-10-01T09:15:00+07:00","updated_at":"2026-10-04T15:00:00+07:00","request_type":"withdraw_consent","request_time":"2026-10-04T14:59:30+07:00","request_reference":"PRIV-TEST-0001"}}

Expected result:

expected header
webhook-signature: v1,fzi3+AruxTHPp63y+tTDa97ZUOcbMHHhKNLnXyz/CSM=

Any HMAC-SHA256 implementation, given the decoded key and the signed string above, must produce the same 32 bytes, written in base64. These functions produce it:

import base64, hashlib, hmac
def sign_v1(secret: str, webhook_id: str, timestamp: str, raw_body: bytes) -> str:
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = webhook_id.encode() + b"." + timestamp.encode() + b"." + raw_body
digest = hmac.new(key, signed, hashlib.sha256).digest()
return "v1," + base64.b64encode(digest).decode()

The timestamp in this vector is far from today. A live MarsX address would reject it. Use the vector for offline checks of your signing code only.

During a key change, send both signatures, separated by a space. MarsX accepts either.

webhook-signature: v1,<signature with the old key> v1,<signature with the new key>
  • Signing a re-serialised body instead of the bytes that go on the wire.
  • Reusing an old webhook-timestamp on a retry.
  • Putting a full stop inside webhook-id.
  • Using the sandbox key in production.
  • Leaving off the v1, prefix in the header.

Only 202 confirms acceptance. 202 means MarsX has the event safely stored. It does not mean the lead has changed yet.

Status What it means What you do
202 Stored and queued. The body says accepted or duplicate. Done. Do not resend.
400 The body is not valid JSON, the event type is unknown, record_action does not match type, or a field is missing or not valid. Fix it. Do not retry the same request.
401 The signature is missing or wrong, the timestamp is outside the window, or the URL or environment is wrong. All give the same answer. Check the key, the clock, the URL and the environment. Do not retry the same request.
413 The body is over 32,768 bytes, or over the transport cap of the route. Fix it. Do not retry.
415 The content type is not application/json. Fix it. Do not retry.
429 Too many requests. The Retry-After header says when to try again. Wait at least Retry-After, then retry the same event.
502, 503, 504, other 5xx, a timeout or a dropped connection A temporary fault. The event may already be stored. Slow down. Wait at least Retry-After if present. Retry the same event.

“Do not retry” means no automatic retry loop. The event is still owed. After you fix the cause, send it again with the same webhook-id, a fresh timestamp and a fresh signature. A rejected withdrawal or deletion matters most. Do not drop it.

A gateway can fail after MarsX saves an event, so a timeout does not mean the event was lost. MarsX removes repeats by webhook-id, so a retry is always safe: a repeat gets 202 with result duplicate and is not processed twice.

On every retry:

  • keep the same webhook-id and the same body
  • use a fresh webhook-timestamp and a fresh signature
  • wait at least Retry-After when the answer carries it
  • add random jitter to every delay, so many failures do not retry at the same moment

Use the example schedule from Standard Webhooks (exponential backoff over several days). Retry for at least 3 days, which is 10 attempts:

Attempt Delay before it Time since the first try
1 Immediately 00:00:00
2 5 seconds 00:00:05
3 5 minutes 00:05:05
4 30 minutes 00:35:05
5 2 hours 02:35:05
6 5 hours 07:35:05
7 10 hours 17:35:05
8 14 hours 31:35:05
9 20 hours 51:35:05
10 24 hours 75:35:05

Do not delay a withdrawal or deletion because an earlier event for another lead is still retrying.

After the last attempt, keep the event visible to your operators so they can re-send it. Also email the MarsX technical contact (support@marsx.media) with the connection, the webhook-id and the event type. The email carries no customer data. The catch-up checks below still find the lead later.

MarsX stores each event and answers within 10 seconds. It then processes the event from a queue, so normal bursts never need throttling. Keep each connection at or below 100 event requests per 10 seconds.

MarsX may answer any request with 429, 502, 503 or 504. Treat each as a signal to slow down. Wait at least the Retry-After value when present, and otherwise continue your normal retry schedule. None of these answers confirms acceptance, and neither does a timeout or a dropped connection.

When a customer asks you to stop using their data, tell MarsX at once.

The customer request_type Event (automatic sending) record_action (file)
Withdraws consent withdraw_consent lead.withdraw withdraw
Asks to restrict processing restriction lead.withdraw withdraw
Asks for erasure erasure lead.delete delete

MarsX stores request_type unchanged in its privacy request register. In release 1, a restriction and a withdrawal set the same marker.

A removal event carries identity and request fields only. It must not carry customer contact, interest or profile fields.

lead.withdraw
{
"type": "lead.withdraw",
"timestamp": "2026-10-04T15:00:00+07:00",
"data": {
"partner_lead_id": "SRC-TEST-0001",
"source_revision": 3,
"record_action": "withdraw",
"created_at": "2026-10-01T09:15:00+07:00",
"updated_at": "2026-10-04T15:00:00+07:00",
"request_type": "withdraw_consent",
"request_time": "2026-10-04T14:59:30+07:00",
"request_reference": "PRIV-TEST-0001"
}
}

request_reference is your own reference for the request. It must match ^[A-Za-z0-9._-]{1,64}$. Together with the connection and the lead ID, it lets MarsX recognise a repeat of the same request.

Send the event as soon as the customer asks. Never batch it. Never hold it behind another event that is still retrying. Keep a record of each request and its request_reference.

  • MarsX restricts the lead on receipt. When MarsX stores a lead.withdraw or lead.delete, it places a pending restriction on the lead before the background worker runs. No salesperson can see or contact the lead in between. This holds even when MarsX has not seen the lead before.
  • MarsX applies privacy events first, before other events in its queue.
  • The revision number does not block it. MarsX applies a withdrawal or deletion even when its source_revision is lower than the one held.
  • A deletion after a withdrawal is allowed. Use a new request_reference. An erasure after a withdrawal is a new request, not a conflict.
  • The marker stays. After a withdrawal or deletion, the privacy marker stays until an approved privacy process lifts it. A later new or update for that lead is set aside for the privacy operator. A source file or event cannot lift a marker.
  • After an erasure, MarsX keeps minimal suppression data only: the lead ID, the marker and the request details.

A 400, 413 or 415 stores no lead data. MarsX keeps a restricted rejection record with the reason, the instance reference, the connection, the event type and the lead ID if it could be read. The record holds no customer data. A refused withdrawal or deletion also alerts the MarsX privacy operator.

You still have to act.

  1. Read the detail and the errors in the answer. See Errors.
  2. Fix the cause.
  3. Send the event again with the same webhook-id, a fresh timestamp and a fresh signature.
  4. Check the outcome with the status call, if you use it.

Do not drop a rejected privacy event.

An event goes through two stages, in this order.

  1. At the door. This is the answer to your POST. MarsX checks the signature, the timestamp and the shape of the data. It stores the event in an inbox and answers 202. The answer is accepted, or duplicate when MarsX has already received this webhook-id on this connection. webhook-id removes repeated deliveries and does nothing else.
  2. After storage. A background worker applies the event under the rules below. The POST answer does not report the outcome. You can read it through the status call.

The answer to the POST tells you that MarsX holds the event safely. It does not tell you what the worker decided.

What you see What it means What to do
202 is lost on the way back The event is stored. You do not know it Retry with the same webhook-id. MarsX answers 202 with result duplicate and stores nothing new
429, 502, 503, 504 or a timeout Not a confirmation. The event may already be stored Wait, at least Retry-After, with backoff and jitter. Retry with the same webhook-id and body, a fresh timestamp and a fresh signature
400, 401, 413 or 415 No lead data was stored. MarsX recorded the rejection Fix the cause, then send the event again

The worker uses source_revision to decide the order of events. Arrival order, webhook-id and webhook-timestamp do not matter. The worker may process events in a different order from the order they arrived.

Situation What the worker does Processing state
new or update with a higher source_revision than the stored one Replaces all fields you own with the full state in the event applied
update or new for a lead MarsX does not know Applies it as the first state of that lead applied
new or update with the same revision and identical content Nothing. A safe re-send under a new webhook-id ignored_duplicate_content
new or update with the same revision and different content Applies neither version. Sets the event aside and alerts MarsX quarantined, reason revision_conflict
new or update with a lower revision Logs it and ignores it ignored_stale_revision
withdraw or delete, at any revision Applies it even when the revision is lower than the stored one. Turns the pending restriction into a privacy marker and restricts the personal data applied
delete after an earlier withdraw of the same lead, with a new request_reference Applies it. An erasure after a withdrawal is a new request, not a conflict applied
withdraw or delete that repeats an earlier privacy request (same request_reference) with identical content Nothing ignored_duplicate_content
withdraw or delete that repeats an earlier privacy request with different content Sets it aside for the privacy operator quarantined, reason privacy_request_conflict
new or update after a privacy marker exists Sets it aside for the privacy operator, whatever the revision. Personal data cannot be restored this way quarantined, reason privacy_marker_set
  1. An event is stored. Its state is queued. A withdraw or delete sets a pending restriction at this moment.
  2. The worker takes it. It becomes applied (a higher revision, or a withdraw or delete at any revision), ignored_stale_revision (a lower revision), ignored_duplicate_content (same revision, same content) or quarantined (a conflict, or an update after a privacy marker).
  3. Each of these end states is final.
  • source_revision must go up with every change to a lead. It must never be reused for different content. On a lead.new you may leave it out, and MarsX uses 1. For a change, always send a higher revision number and the time of the change.
  • MarsX applies privacy events before other events in its queue.
  • A new event for a lead that MarsX already knows is never ignored just because the lead is known. The rules above judge it.
  • Withdrawals and deletions restrict the lead on receipt, before the worker runs. No salesperson can see or contact the lead in between.

The answer to a POST shows intake only. To see what the worker decided, make one read-only call. It is optional. Leads flow without it.

GET https://api.marsx.media/v1/sources/{connection}/events/{webhook_id}
{
"webhook_id": "msg_test_0001",
"type": "lead.withdraw",
"partner_lead_id": "SRC-TEST-0001",
"source_revision": 3,
"processing_state": "applied",
"received_at": "2026-10-04T15:00:01+07:00",
"processed_at": "2026-10-04T15:00:03+07:00"
}
processing_state Meaning
queued Stored and waiting for the worker. processed_at is null. Ask again later.
applied The worker applied the event.
ignored_stale_revision A new or update with a lower revision than the one held. Logged and ignored.
ignored_duplicate_content The same revision with the same content was already applied, or an identical privacy request was repeated. Nothing changed.
quarantined Stored but not applied. A reason says why. MarsX reviews it.

Rules for this call:

  • It is read-only. It returns the state of one event and nothing else. It carries no customer data.
  • It uses OAuth 2.0 client credentials, issued by MarsX. The token address is https://api.marsx.media/oauth/token (sandbox: https://api-sandbox.marsx.media/oauth/token). The scope is events:read. Client authentication is HTTP Basic. Tokens last 3600 seconds.
  • You get one client for each connection and environment. A token from another connection fails.
  • 404 means MarsX has no event with that webhook-id for your connection. Either MarsX never stored it, so send it again, or the status is older than 90 days. MarsX keeps status and webhook-id de-duplication for 90 days.
  • Keep status requests at or below 60 per 60 seconds for each connection. Read the status when you need it. Do not poll every event in a tight loop.

The reason values are revision_conflict, privacy_marker_set and privacy_request_conflict. A quarantined event needs no resend. For a revision_conflict, find out why one revision number carried two different contents.

Warnings. The response can carry an optional warnings array. It is present only when MarsX applied the event and something in it was not used. The lead was accepted. Each item has a code and a reason:

"warnings": [{ "code": "dealer_code_ignored", "reason": "unknown" }]

The code dealer_code_ignored means MarsX ignored the preferred_dealer_code and routed the lead as if no code had been sent. The reason is unknown (the code is not on the MarsX dealer list) or not_active (the code names an outlet that takes no car sales lead). Nothing needs resending. Treat an unknown code or reason as informational.

A send can fail without anyone noticing. So MarsX checks your side on a schedule, and only reads. Nothing is written to your system. This is the safety net. It is not a second way to deliver leads.

Endpoint Purpose MarsX reads it
GET /v1/changes?cursor=&limit= Lead revisions that changed Every 15 minutes
GET /v1/privacy-events?cursor=&limit= Every withdrawal, deletion and restriction Every 15 minutes
GET /v1/leads/{partner_lead_id} The full current state of one lead When MarsX finds a lead it lacks or holds at an older revision

You may choose your own path prefix and host name. The names, query parameters and response shapes stay as in the API reference.

Requirement Detail
Replay window Both feeds reach back at least 90 days
Page size Default at least 100. Maximum at least 500. MarsX asks for limit=500 and accepts fewer
Request budget At least 60 requests per minute for each client on each endpoint. MarsX stays at or below 1 request per second and honours Retry-After
Transport HTTPS, TLS 1.2 or higher, no redirects. The sandbox host differs from the production host
Authentication OAuth 2.0 client credentials, a read-only scope, tokens of 300 to 86,400 seconds, HTTP Basic client authentication

MarsX has no fixed addresses, so an IP allow-list does not work. The identity of a MarsX call is its OAuth client. Each call also carries User-Agent: MarsX-Reconciler/1, Accept: application/json and Authorization: Bearer <access_token>.

What the three endpoints return

GET /v1/changes lists the leads that changed:

{
"items": [
{ "partner_lead_id": "SRC-TEST-0001", "source_revision": 2, "updated_at": "2026-10-04T10:20:00+07:00" },
{ "partner_lead_id": "SRC-TEST-0002", "source_revision": 1, "updated_at": "2026-10-04T10:20:00+07:00" }
],
"next_cursor": "opaque-cursor-example-0002",
"has_more": true
}

GET /v1/privacy-events lists every withdrawal, deletion and restriction. Include leads you no longer hold. Items carry no customer contact, interest or profile data.

{
"items": [
{
"partner_lead_id": "SRC-TEST-0001",
"request_type": "withdraw_consent",
"request_time": "2026-10-04T14:59:30+07:00",
"request_reference": "PRIV-TEST-0001"
},
{
"partner_lead_id": "SRC-TEST-0007",
"request_type": "erasure",
"request_time": "2026-10-04T15:30:00+07:00",
"request_reference": "PRIV-TEST-0002"
}
],
"next_cursor": "opaque-cursor-example-0102",
"has_more": false
}

GET /v1/leads/{partner_lead_id} returns what you would send as the latest event for that lead, in the same data shape as the automatic sending events.

Lead state at your end Answer
Live 200 with the full current state. record_action is new or update
Withdrawn or restricted 200 with the withdrawal form: identity and privacy request fields only
Erased and no longer held 404. MarsX finds the erasure in privacy-events. This 404 applies only to an authenticated caller
Cursor rules

A cursor is a marker that tells an API where the last page stopped.

  • next_cursor is opaque. MarsX stores it and sends it back unchanged. Return it on every page, including an empty page.
  • Pages come oldest first. The order must stay stable when several changes share the same updated_at. One way: order by updated_at, then by a unique, increasing sequence number that your database assigns when the change is committed.
  • A change must never appear behind a cursor that you have already handed out.
  • Leave out cursor to start at the oldest item you hold.
  • A cursor older than the replay window gets 410.
An expired cursor fails closed
  • If the privacy-events cursor has expired, MarsX cannot prove that it has seen every withdrawal and deletion. MarsX keeps the leads of that connection restricted for contact until recovery is complete and proven.
  • Starting again from the oldest item you still hold does not prove completeness. A deletion older than the replay window would be missed.
  • So you must be able to supply, when MarsX asks, a full list of the lead IDs of the connection that are currently withdrawn, restricted or erased, including IDs 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, through the file upload. MarsX lifts the restriction only when recovery is proven.
  • For changes, an expired cursor means MarsX starts again from the oldest item held and re-reads the leads it holds. It shows the connection as not verified for completeness until you have resent any lead that fell in the gap. A resend is a normal push.

MarsX authenticates to your endpoints with OAuth 2.0 client credentials (RFC 6749 section 4.4). The token request looks like this:

POST <your token URL>
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <base64(client_id:client_secret)>
grant_type=client_credentials&scope=leads:read

The answer is {"access_token":"...","token_type":"Bearer","expires_in":3600}. Every read call carries Authorization: Bearer <access_token>. MarsX asks for a new token after a 401 or when expires_in has passed.

You issue one client for each connection and environment to MarsX, through the secure key exchange. The scope allows reading only, and you name it. The token address uses HTTPS. MarsX accepts a static key with an IP allow-list only as a recorded exception.

Publish one OpenAPI 3.1 document with a webhooks section for the events and paths for the three endpoints above. The contract file is a ready base. Keep webhooks and the three paths. Remove the two paths that MarsX hosts: the event status call and the file upload.

Also provide a sandbox with made-up data only. The sandbox must document one cursor value that always returns 410, so MarsX can run the expired-cursor test.

A signing key should change from time to time, and at once if you think someone else has seen it. You can change a key without dropping events. It applies to automatic sending and to the automatic file upload.

During a short overlap, you sign every request with both the old and the new key. You send both signatures in the same header, separated by a space. MarsX accepts either one. When the overlap ends, MarsX retires the old key.

  • You have one key for each connection and each environment. The sandbox key never works in production. A key from one connection never works on another.
  • Keys are exchanged through the one-time secure key exchange. They never travel in an email or a WhatsApp message.
  • The overlap lasts at most 7 days. MarsX retires the old key 7 days after it activates the new one, or earlier when you confirm in writing.
  • On suspected exposure, MarsX retires the old key at once. Tell the technical contact immediately.
  1. Ask for a new key. Write to your technical contact. Say which connection and which environment.

  2. Receive the new key through the secure key exchange. MarsX activates it for your connection.

  3. Sign with both keys. For every request, make one signature with the old key and one with the new key.

  4. Send both in the header, separated by a space:

    webhook-signature: v1,<signature with the old key> v1,<signature with the new key>
  5. Check that events are accepted with the old key alone, with the new key alone, and with both. Test this in the sandbox first.

  6. Switch to the new key alone once the overlap ends or when MarsX confirms the old key is retired. You may confirm in writing that you are ready, which lets MarsX retire the old key earlier.

Each signature is v1,<base64>. The scheme is explained in Signing and worked examples.

Test P12 in the sandbox checks rotation: it sends a request with the old key, with the new key and with both, and expects 202 accepted each time, with no event lost. See Test in the sandbox.

Rule Detail
Transport HTTPS only, TLS 1.2 or higher. No redirects
Key exchange The issuer of a key or client credential shares it through a one-time, expiring secure share. Credentials never travel in an email or a WhatsApp message. Sandbox and production use separate shares
Lead files Never by email or WhatsApp. Use the signed upload or the upload by staff account
Replay protection A signed timestamp with a 300 second window, and webhook-id de-duplication
Privacy events Sent as soon as the customer asks. Never batched. Replayable for at least 90 days. On a file route, the 24 hour file rule applies
After a withdrawal or deletion The privacy marker stays until an approved privacy process lifts it. A later new or update for that lead is set aside for the privacy operator. A source file or event cannot lift a marker
Removal events Carry identity and request fields only. No contact, interest or profile data
After an erasure MarsX keeps minimal suppression data only: the lead ID, the marker and the request details
Consent evidence Every new and update carries contact_consent (yes) and consent_time. consent_reference, notice_version and consent_purpose are optional. A consent reference points to the real record, not to a yes or no value
Logging by you Do not write secrets or full request bodies to logs. Do not put personal data in URLs or in error messages
Logging by MarsX Metadata only: connection, event type, webhook-id, instance, status and timing. Never field values, request bodies, signatures or tokens
Browsers Never call the MarsX endpoint from a browser or an app. Sign on your server only
Write-back None. MarsX never writes anything back to your system
Errors application/problem+json. They never echo personal data

Concour is the only working surface for the brand and its dealers. Your data is a feed. If the same leads also appear in your own dashboard, dealers could work two statuses and do the work twice. So:

  • Leads that you also show in your own dashboard must be read-only there, or carry no status field. This is a condition of the connection.
  • You never send status, assignment, follow-up, test drives, sales or SPK numbers to MarsX.
  • Any status in your own dashboard is not authoritative. It does not feed MarsX state, workflow or reports.

MarsX owns assignment, workflow status, follow-up, appointments and outcomes. An event from you never overwrites them. MarsX never sends any field back to you.

ai_intent_label and ai_summary are optional. MarsX stores them apart from customer facts, and they never decide access or routing. Do not send AI guesses about affordability or location.

  • Never call MarsX from a browser or an app

    Your signing key must stay on your server. Anything in a browser can be read by its user.

  • Never reuse an old timestamp or signature on a retry

    Keep the same webhook-id. Make a fresh timestamp and a fresh signature every time.

  • Never treat anything but 202 as confirmation

    A timeout, 429, 502, 503 or 504 does not confirm anything. Retry the same event.

  • Never log secrets, bodies or personal data

    Keep keys and full request bodies out of logs. Keep personal data out of URLs and error messages.

  • Never reuse a revision number for different content

    The number must rise with every change. Reuse sets the event aside as a conflict.

  • Never send status or sales outcomes

    No status, assignment, follow-up, test drives, sales or SPK numbers. Dealers record these in Concour.

  • Never drop a rejected withdrawal or deletion

    Fix the cause and send it again. Do not batch these events or hold them for later.

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

Run the checks in Testing and going live. That page also lists the go-live steps.