Step 3 of 5

Set up webhooks

Let zolar call your system instead of polling GET /leads.

Steps

  1. 1

    Pick an event

    lead:bought fires when your organisation buys a lead; lead:deleted fires when a lead you bought has its personal data erased (privacy request or retention expiry), so you can erase your copy. Every request carries the event in the x-zolar-event header.

  2. 2

    Create an endpoint

    Add a secret header, such as Authorization, so your system can verify the request comes from zolar.

    curl
    curl -X POST https://demo.api.lead.zolar.tech/api/public/v2/installer/webhooks \
      -H "Authorization: Bearer sk_[YOUR DEMO KEY]" \
      -H "Content-Type: application/json" \
      -d '{
        "event": "lead:bought",
        "url": "https://crm.example.com/zolar/leads",
        "headers": [
          { "name": "Authorization", "value": "Bearer [YOUR WEBHOOK SECRET]" }
        ]
      }'

    The response is the endpoint with its id; header values are never returned, only headerNames.

  3. 3

    Test it

    zolar sends a sample event matching the endpoint's event (a bought lead or a deletion) and returns the status and response body your system answered with.

    curl
    curl -X POST https://demo.api.lead.zolar.tech/api/public/v2/installer/webhooks/[WEBHOOK ID]/test \
      -H "Authorization: Bearer sk_[YOUR DEMO KEY]"
    
    # { "status": 200, "body": "ok" }

    Status 501 means your endpoint could not be reached; 555 means a custom body could not be filled.

  4. 4

    Manage endpoints

    These are the same endpoints as in the portal under Settings → Integrations.

    curl
    # List
    curl https://demo.api.lead.zolar.tech/api/public/v2/installer/webhooks \
      -H "Authorization: Bearer sk_[YOUR DEMO KEY]"
    
    # Replace (leave out "headers" to keep the stored ones; [] removes them)
    curl -X PUT https://demo.api.lead.zolar.tech/api/public/v2/installer/webhooks/[WEBHOOK ID] \
      -H "Authorization: Bearer sk_[YOUR DEMO KEY]" \
      -H "Content-Type: application/json" \
      -d '{ "event": "lead:bought", "url": "https://crm.example.com/zolar/v2" }'
    
    # Delete
    curl -X DELETE https://demo.api.lead.zolar.tech/api/public/v2/installer/webhooks/[WEBHOOK ID] \
      -H "Authorization: Bearer sk_[YOUR DEMO KEY]"

    PUT is a full replace: a left-out name, enabled or bodyTemplate returns to its default.

  5. 5

    Send a custom body

    Optional

    Set bodyTemplate to a JSON template if your system expects its own shape: a JSON object up to 16 KB, with {{ lead.firstName }} or {{ path | filter }} placeholders in string values. A value that is exactly one placeholder keeps its type; unknown variables or filters are rejected on save.

    All variables and filters are listed under Body template reference below; the portal's template editor (Custom body) shows the same list with a live preview. If a template can't be filled for a lead, nothing is sent and the run shows as failed under Recent runs.

Standard payload

Without a custom body, a lead:bought request carries this JSON; id is your leadOfferId.

lead:bought
{
  "id": "00000000-0000-4000-8000-000000000001",
  "salutation": "woman",
  "firstName": "Erika",
  "lastName": "Musterfrau",
  "email": "erika@example.org",
  "phone": "+4915100000000",
  "country": "DE",
  "city": "Musterstadt",
  "postalCode": "12345",
  "street": "Musterstraße 12",
  "properties": {
    "interest": { "pv": "interestNew", "battery": "interestNew" },
    "building": { "type": "residential", "isOwner": true }
  },
  "marketPlace": "My Marketplace"
}
lead:deleted
{
  "id": "00000000-0000-4000-8000-000000000001",
  "leadId": "00000000-0000-4000-8000-000000000002",
  "event": "lead:deleted",
  "reason": "gdpr",
  "deletedAt": "2026-10-23T09:30:00.000Z",
  "marketPlace": "My Marketplace"
}

reason is gdpr or retention.

Delivery and limits

  • A 2xx answer counts as delivered. There are no automatic retries: a request that fails, times out (10 s) or gets a non-2xx answer is not sent again. Redirects are not followed.
  • Failed runs show under the endpoint's Recent runs in Settings → Integrations, where Retry resends them by hand and Send past purchases delivers earlier purchases.
  • Up to 20 webhook endpoints per organisation.
  • Host, Content-Length, Transfer-Encoding, Connection and x-zolar-event can't be set as headers; each name only once.
  • The URL must use http or https on port 80 or 443 and point at a public host — not localhost or a private, loopback or link-local address.
  • Permissions: Read webhooks to list, Manage webhooks to create, replace, delete and test.

Body template reference

Templates may be up to 16 KB. Below an object or array of lead.properties marked "any path below it too", paths that are not listed still work; they are filled with the lead's value, or null.

Variables — lead:bought
VariableTypeValuesExample
leadOffer.idstring—c1a2…
lead.idstring—9f8e…
lead.salutationenum or nullman, woman, diverseman
lead.firstNamestring—Jan
lead.lastNamestring—de Vries
lead.emailstring—jan@example.com
lead.phonestring—+31612345678
lead.streetstring—Kerkstraat 12a
lead.streetNamestring or null—Kerkstraat
lead.houseNumberstring or null—12
lead.houseNumberExtensionstring or null—a
lead.postalCodestring—1234 AB
lead.citystring—Utrecht
lead.countrystring—NL
lead.languagestring or null—nl
lead.commentstring or null—Call after 18:00
lead.propertiesobject—{ "interest": … }
lead.properties.interestobject
any path below it too
—{ "pv": "interestNew" }
lead.properties.interest.pvenum or nullexisting, interestNew, interestUpgrade, service, repairinterestNew
lead.properties.interest.batteryenum or nullexisting, interestNew, interestUpgrade, service, repairinterestNew
lead.properties.interest.heatPumpenum or nullexisting, interestNew, interestUpgrade, service, repairinterestNew
lead.properties.interest.evenum or nullexisting, interestNew, interestUpgrade, service, repairinterestNew
lead.properties.interest.acenum or nullexisting, interestNew, interestUpgrade, service, repairinterestNew
lead.properties.interest.wallboxenum or nullexisting, interestNew, interestUpgrade, service, repairinterestNew
lead.properties.interest.balconyPvenum or nullexisting, interestNew, interestUpgrade, service, repairinterestNew
lead.properties.interest.floorInsulationenum or nullexisting, interestNew, interestUpgrade, service, repairinterestNew
lead.properties.interest.roofInsulationenum or nullexisting, interestNew, interestUpgrade, service, repairinterestNew
lead.properties.interest.cavityWallInsulationenum or nullexisting, interestNew, interestUpgrade, service, repairinterestNew
lead.properties.interest.timeHorizonMonthsobject or null
any path below it too
—{ "from": 0, "to": 3 }
lead.properties.buildingobject
any path below it too
—{ "type": "residential" }
lead.properties.building.typestring or null—residential
lead.properties.building.isOwnerboolean or null—true
lead.properties.building.ownershipTypestring or null—owner
lead.properties.building.builtYearstring or null—before_2019
lead.properties.roofobject or null
any path below it too
—{ "material": "brick" }
lead.properties.roof.materialstring or null—brick
lead.properties.roof.lastRenovationstring or null—after_1990
lead.properties.roof.facesarray or null
any path below it too
—[{ "tilt": 30 }]
lead.properties.energyConsumptionobject or null
any path below it too
—{ "amount": 3500 }
lead.properties.energyConsumption.amountnumber or null—3500
lead.properties.energyConsumption.profilestring or null—h0
lead.properties.availabilityobject or null
any path below it too
—{ "anyTime": true }
lead.properties.availability.anyTimeboolean or null—true
lead.properties.availability.daytimearray or null
any path below it too
—["morning"]
lead.properties.availability.weekdaysarray or null
any path below it too
—["Mo", "Tu"]
marketplace.namestring—zolar NL
event.typestring—lead:bought
event.boughtAtstring—2026-09-18T09:30:00.000Z
Variables — lead:deleted (IDs only, the personal data is erased)
VariableTypeValuesExample
leadOffer.idstring—c1a2…
lead.idstring—9f8e…
marketplace.namestring—zolar NL
event.typestring—lead:deleted
event.reasonenumgdpr, retentiongdpr
event.deletedAtstring—2026-09-20T09:30:00.000Z
Filters — run left to right
FilterArgumentExample
number—"12" → 12
upper—"nl" → "NL"
lower—"NL" → "nl"
trim—" a " → "a"
default:""requirednull → "x"
map:a=X,*=Zrequiredman → M (map:man=M,woman=V,*=O)
join:", "optional["a","b"] → "a, b"
truncate:20requiredfirst 20 chars
NextErrors