curl --request POST \
--url https://api.voice.mergenlab.ai/v1/customer-events \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"type": "payment",
"external_id": "LN-124",
"number": "+9198XXXXXX02",
"amount_micros": 3400000000,
"at": "2026-10-26T10:00:00+05:30",
"campaign_id": null
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
type: 'payment',
external_id: 'LN-124',
number: '+9198XXXXXX02',
amount_micros: 3400000000,
at: '2026-10-26T10:00:00+05:30',
campaign_id: null
})
};
fetch('https://api.voice.mergenlab.ai/v1/customer-events', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.voice.mergenlab.ai/v1/customer-events"
payload = {
"type": "payment",
"external_id": "LN-124",
"number": "+9198XXXXXX02",
"amount_micros": 3400000000,
"at": "2026-10-26T10:00:00+05:30",
"campaign_id": None
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"id": "cev_8f1e2d3c4b5a4968a7b6c5d4e3f2a1b0",
"type": "payment",
"external_id": "LN-124",
"number": "+9198XXXXXX02",
"amount_micros": 3400000000,
"at": "2026-10-26T04:30:00.000Z",
"campaign_id": null,
"matched": 1,
"records": [
{
"campaign_id": "cmp_7c1d2e3f4a5b4c6d8e9f0a1b2c3d4e5f",
"record_id": "rec_2b3c4d5e6f7a40718293a4b5c6d7e8f9",
"effect": "closed"
}
],
"duplicate": true,
"created_at": "2026-10-26T04:35:00.000Z"
}Report a payment or another customer event
Tells Mergen that something happened to a customer outside the calls: payment (paid), already_paid or
dispute_resolved. The event is matched to records by external_id, else by number, in the campaign named
by campaign_id, or else in every campaign of yours that may still call (draft, created, testing,
tested, running and paused). The answer lists every matched record in records with what happened to
it (effect).
A payment or already_paid closes every matched pending record (status: closed, result: paid;
effect: closed), and a call still waiting in the queue for it is withdrawn. A record that is on a call right
now is closed when the call ends (effect: pending_close). A record that was already finished is left as it
is (effect: none), and so is every record matched by a dispute_resolved event. For a payment,
amount_micros is added to the collected amount of ONE campaign: the one you name, else the most recently
started running or paused campaign that matched.
Idempotent on Idempotency-Key (or event_id when you send one): the same event again answers 200 and
changes nothing. Without either, an event equal to one received in the last 24 hours (same type,
external_id or number, at and amount_micros) is the same event.
curl --request POST \
--url https://api.voice.mergenlab.ai/v1/customer-events \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"type": "payment",
"external_id": "LN-124",
"number": "+9198XXXXXX02",
"amount_micros": 3400000000,
"at": "2026-10-26T10:00:00+05:30",
"campaign_id": null
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
type: 'payment',
external_id: 'LN-124',
number: '+9198XXXXXX02',
amount_micros: 3400000000,
at: '2026-10-26T10:00:00+05:30',
campaign_id: null
})
};
fetch('https://api.voice.mergenlab.ai/v1/customer-events', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.voice.mergenlab.ai/v1/customer-events"
payload = {
"type": "payment",
"external_id": "LN-124",
"number": "+9198XXXXXX02",
"amount_micros": 3400000000,
"at": "2026-10-26T10:00:00+05:30",
"campaign_id": None
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"id": "cev_8f1e2d3c4b5a4968a7b6c5d4e3f2a1b0",
"type": "payment",
"external_id": "LN-124",
"number": "+9198XXXXXX02",
"amount_micros": 3400000000,
"at": "2026-10-26T04:30:00.000Z",
"campaign_id": null,
"matched": 1,
"records": [
{
"campaign_id": "cmp_7c1d2e3f4a5b4c6d8e9f0a1b2c3d4e5f",
"record_id": "rec_2b3c4d5e6f7a40718293a4b5c6d7e8f9",
"effect": "closed"
}
],
"duplicate": true,
"created_at": "2026-10-26T04:35:00.000Z"
}Authorizations
Authorization: Bearer vg_live_<32 hex characters>. Keys are issued by Mergen admins and shown once.
Missing or invalid key: 401 unauthorized. Suspended account: 403 tenant_suspended.
Headers
Optional, 1 to 64 characters, one per event (your payment id works well). The same key again answers 200 and
changes nothing. Without it, event_id in the body deduplicates when sent.
1 - 64Body
Send external_id or number (or both). Unknown fields are refused (422).
payment, already_paid, dispute_resolved 128The amount paid (payment), INR micros.
x >= 0When it happened (default now).
Only this campaign's records, and the amount is credited to it (default every campaign that may still call).
Your id of the event; deduplicates like Idempotency-Key.
1 - 200Response
The same event again (same Idempotency-Key or event_id); nothing changed.
The event as recorded. records lists every record it matched, with effect: closed (the record was
pending and is now closed as paid), pending_close (the record is on a call and closes when the call ends) or
none (nothing changed, for example a finished record or an event that is not a payment). duplicate is
true when this is the same event again.
cev_ + 32 hex characters.
payment, already_paid, dispute_resolved E.164.
How many records the event matched.
x >= 0Show child attributes
Show child attributes