Skip to main content

Receiving Webhooks

When an event fires, Merdeka sends a POST request to your configured endpoint with a JSON payload. This page covers how to verify authenticity, understand the payload, and handle failures.


Verifying Requests​

During webhook creation, you define a header name and secret value. Merdeka includes this header in every delivery so your endpoint can confirm the request is authentic.

POST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
X-Webhook-Secret: your-configured-secret

{ "eventSlug": "lead.created", ... }
info

The header name and secret are entirely up to you — whatever you set during webhook creation is what Merdeka sends verbatim.

const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhooks/merdeka', (req, res) => {
const secret = req.headers['x-webhook-secret'];

if (secret !== process.env.MERDEKA_WEBHOOK_SECRET) {
return res.status(401).json({ error: 'Unauthorized' });
}

const { eventSlug, entityType, entityId, data } = req.body;

// Respond immediately, process async
res.status(200).json({ received: true });

// Handle event asynchronously
processEvent({ eventSlug, entityType, entityId, data });
});

Retry Behavior​

If your endpoint fails to respond with a 2xx status, Merdeka retries automatically:

Max attempts3 (initial + 2 retries)
BackoffExponential — ~1s, then ~2s
Timeout15 seconds per attempt

A delivery is considered failed when:

  • HTTP response is non-2xx (e.g., 404, 500)
  • No response within 15 seconds
  • Network error (DNS failure, connection refused)
warning

After all 3 attempts exhaust, the delivery is permanently marked as failed. Monitor failures via Delivery Logs in the Backoffice.


Payload Structure​

Every delivery body follows this envelope:

{
"eventSlug": "project.updated",
"entityType": "project",
"entityId": "4a51e919-9d74-4b8e-94e7-0c99ac879c53",
"occurredAt": "2026-07-30T09:17:48.872Z",
"data": { ... }
}
FieldTypeDescription
eventSlugstringEvent identifier (e.g., lead.created)
entityTypestring"lead" or "project"
entityIdstringUUID of the affected entity
occurredAtstringISO 8601 timestamp of when the event fired
dataobjectEvent-specific payload — contents vary by event type

Notes:

  • If you configured data filters, only selected fields appear in data.
  • If you set a custom body with meta, it merges into data.meta.

Event Payloads​

Below are full sample payloads for each event type. These represent the unfiltered payload (no data filters applied).

lead.created​

Fires when a new lead is created.

View full payload
{
"eventSlug": "lead.created",
"entityType": "lead",
"entityId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"occurredAt": "2026-07-30T09:17:48.872Z",
"data": {
"lead": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"leadIncrementId": 42,
"leadSource": { "id": "src-1", "name": "Web" },
"leadStatus": { "id": "status-1", "name": "New" },
"leadType": { "id": "type-1", "name": "Residential" },
"sdrChannel": { "id": "ch-1", "name": "Inbound" },
"companyBrand": { "id": "brand-1", "name": "SunPower" },
"unqualifiedReason": null,
"previousLeadSource": null,
"leadOwner": {
"id": "user-1",
"firstName": "Greta",
"lastName": "Knowles",
"email": "greta@example.com",
"phone": "+1234567890"
},
"contact": {
"id": "contact-1",
"firstName": "Alexandre",
"lastName": "Fry",
"email": "alex@example.com",
"phone1": "+1987654321",
"phone2": null,
"contactSource": { "id": "csrc-1", "name": "Website Form" },
"contactSourceDetail": { "id": "csd-1", "name": "Landing Page A" },
"account": {
"id": "acct-1",
"address1": "123 Solar Lane",
"address2": null,
"city": "Austin",
"stateId": "TX",
"stateCode": "TX",
"zipCode": "78701",
"country": "US",
"propertyType": { "id": "pt-1", "name": "Single Family" },
"roofType": { "id": "rt-1", "name": "Composition Shingle" },
"state": { "id": "TX", "name": "Texas" }
}
},
"salesforceExternalIds": []
},
"project": null
}
}

lead.updated​

Fires when a lead is updated (status change, owner reassignment, contact info edit, etc.).

View full payload
{
"eventSlug": "lead.updated",
"entityType": "lead",
"entityId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"occurredAt": "2026-07-30T10:22:15.441Z",
"data": {
"lead": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"leadIncrementId": 42,
"leadSource": { "id": "src-1", "name": "Web" },
"leadStatus": { "id": "status-2", "name": "Qualified" },
"leadType": { "id": "type-1", "name": "Residential" },
"sdrChannel": { "id": "ch-1", "name": "Inbound" },
"companyBrand": { "id": "brand-1", "name": "SunPower" },
"unqualifiedReason": null,
"previousLeadSource": { "id": "src-3", "name": "Referral" },
"leadOwner": {
"id": "user-2",
"firstName": "Marcus",
"lastName": "Chen",
"email": "marcus@example.com",
"phone": "+1555000111"
},
"contact": {
"id": "contact-1",
"firstName": "Alexandre",
"lastName": "Fry",
"email": "alex@example.com",
"phone1": "+1987654321",
"phone2": "+1987654322",
"contactSource": { "id": "csrc-1", "name": "Website Form" },
"contactSourceDetail": { "id": "csd-1", "name": "Landing Page A" },
"account": {
"id": "acct-1",
"address1": "123 Solar Lane",
"address2": "Suite B",
"city": "Austin",
"stateId": "TX",
"stateCode": "TX",
"zipCode": "78701",
"country": "US",
"propertyType": { "id": "pt-1", "name": "Single Family" },
"roofType": { "id": "rt-1", "name": "Composition Shingle" },
"state": { "id": "TX", "name": "Texas" }
}
},
"salesforceExternalIds": [
{
"epcExternalId": "sf-ext-001",
"projectId": "proj-1",
"projectName": "Fry Residence Solar"
}
]
},
"project": {
"id": "proj-1",
"incrementId": 7,
"name": "Fry Residence Solar",
"projectStatusId": "ps-2",
"projectStatusName": "In Progress"
}
}
}

project.created​

Fires when a new project is created.

View full payload
{
"eventSlug": "project.created",
"entityType": "project",
"entityId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"occurredAt": "2026-07-30T11:05:33.219Z",
"data": {
"project": {
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"name": "Fry Residence Solar",
"incrementId": "PRJ-0042",
"epcId": "epc-1",
"companyId": "company-1",
"currentStageId": "stage-1",
"salesRepId": "user-2",
"createdAt": "2026-07-30T11:05:33.219Z",
"updatedAt": "2026-07-30T11:05:33.219Z",
"epc": { "id": "epc-1", "name": "Tynergy" },
"territory": { "id": "terr-1", "name": "Texas South", "code": "TX-S" },
"stage": { "id": "stage-1", "name": "Site Survey", "order": 1 },
"salesRep": {
"id": "user-2",
"firstName": "Marcus",
"lastName": "Chen",
"email": "marcus@example.com",
"phone": "+1555000111"
},
"customer": {
"id": "contact-1",
"firstName": "Alexandre",
"lastName": "Fry",
"address": "123 Solar Lane",
"city": "Austin",
"zipCode": "78701",
"phone": "+1987654321",
"email": "alex@example.com"
}
},
"projectDetail": {
"id": "pd-1",
"address": "123 Solar Lane",
"city": "Austin",
"zipCode": "78701",
"stateId": "TX",
"systemSize": "8.5 kW",
"leadSource": "Web",
"projectStatusId": "ps-1",
"saleDate": null,
"installedAt": null,
"ptoCompletedAt": null,
"territory": { "id": "terr-1", "name": "Texas South" }
},
"timeline": []
}
}

project.updated​

Fires when a project is updated (stage change, sale date set, installation completed, etc.).

View full payload
{
"eventSlug": "project.updated",
"entityType": "project",
"entityId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"occurredAt": "2026-08-01T14:30:12.887Z",
"data": {
"project": {
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"name": "Fry Residence Solar",
"incrementId": "PRJ-0042",
"epcId": "epc-1",
"companyId": "company-1",
"currentStageId": "stage-3",
"salesRepId": "user-2",
"createdAt": "2026-07-30T11:05:33.219Z",
"updatedAt": "2026-08-01T14:30:12.887Z",
"epc": { "id": "epc-1", "name": "Tynergy" },
"territory": { "id": "terr-1", "name": "Texas South", "code": "TX-S" },
"stage": { "id": "stage-3", "name": "Installation", "order": 3 },
"salesRep": {
"id": "user-2",
"firstName": "Marcus",
"lastName": "Chen",
"email": "marcus@example.com",
"phone": "+1555000111"
},
"customer": {
"id": "contact-1",
"firstName": "Alexandre",
"lastName": "Fry",
"address": "123 Solar Lane",
"city": "Austin",
"zipCode": "78701",
"phone": "+1987654321",
"email": "alex@example.com"
}
},
"projectDetail": {
"id": "pd-1",
"address": "123 Solar Lane",
"city": "Austin",
"zipCode": "78701",
"stateId": "TX",
"systemSize": "8.5 kW",
"leadSource": "Web",
"projectStatusId": "ps-2",
"saleDate": "2026-07-31T00:00:00.000Z",
"installedAt": null,
"ptoCompletedAt": null,
"territory": { "id": "terr-1", "name": "Texas South" }
},
"timeline": [
{
"id": "tl-1",
"name": "Site Survey Completed",
"date": "2026-07-31T00:00:00.000Z",
"timelineCategoryId": "tlcat-1"
},
{
"id": "tl-2",
"name": "Permit Approved",
"date": "2026-08-01T00:00:00.000Z",
"timelineCategoryId": "tlcat-2"
}
]
}
}

Best Practices​

PracticeWhy
Respond with 200 immediatelyProcessing > 15s triggers a retry. Do heavy work async.
Handle duplicatesUse entityId + occurredAt as idempotency key — retries can cause duplicate delivery.
Use data filtersReduce payload size by selecting only the fields you need.
Monitor delivery logsCatch failures early in the Backoffice.
Keep your secret safeStore it in environment variables, never commit to source control.