Introduction:
The client builds CRM software for property developers. Their customers ran a separate service on the side: it pulled leads out of Facebook ad accounts and dropped them into the CRM. It was run by an outside contractor — a studio with a name on the market.
At some point the contractor let everyone down. Fifty companies were left without a channel between their advertising and their sales teams. The ad money keeps going out, the leads keep coming in, and none of them reach the sales managers.
When the question came up of what to do with those customers, I took three days for a first version.
What follows is the interface of that very first version, which is still taking leads today. Company names, personal names and phone numbers are hidden in the screenshots: company and lead data is not shown.

How it went:
The first version looks nothing like what runs now. I took the simplest methods, the ones proven by time — essentially a hand-tuned algorithm. Plain PHP, no build step, a customer is created as a directory on disk. Make a folder — get a webhook address.
Sounds crude. But a customer gets connected in a minute, not through a database migration.



For the first few months the project looked like a closed admin panel for the rollout and support team. The customer had nothing but notifications in Telegram. That was the plan: first the leads simply had to move.
Along the way we closed gaps the previous contractor hadn’t touched for years. End-to-end campaign tagging, tags, sending rejection reasons back into advertising, choosing the CRM installation by country — the company has five of them.
The fire was put out. After that the service spent several months turning into what is running in production today — a simple engine pushed to the limit.

A lead in any shape. Everyone’s ad forms are different: the fields are named however the marketer decided to name them. If the name and the phone number sit in fields that make sense, the lead goes straight through. If not, a language model takes it apart.
The model runs on my own server. That is a matter of principle: a lead holds the name and phone number of a real person, and data like that must not leave the environment. There are no cloud services anywhere in this chain — from the ad account to the CRM, the lead is never shown to a third party.
If the model doesn’t manage either, there is a third stage — parsing the raw payload with regular expressions.


Delivery with nothing lost. A webhook takes the lead and puts it in a queue, and cron processes the queue once a minute. If the CRM doesn’t answer, the lead isn’t lost: retries go at a growing interval — 15 minutes, half an hour, an hour, three hours, eight. There is a separate backfill as well: if the outage happened before the fix, the missed leads arrive after the fact.
Duplicates were what I feared most. Calling a person twice is worse than not calling at all. Every lead carries a delivery mark — a retry sees it and passes by.

data as of 2 September 2026
57
connected companies
77
ad pages
3,423 lead forms
459,000+
leads processed
~1,950
leads a day
around 78,000 a month
100%
delivery with nothing lost
delivered successfully over the last week of observation
Result:
The service is in its seventh month. Fifty-seven companies, seventy-seven ad pages, three and a half thousand forms.
More than 459,000 leads have gone through it. Around two thousand a day. Over the last week of observation not a single one was lost.
The second version is live now: automatic billing, client portals with their own settings, centralised management.
Stack:
Backend — Laravel, PostgreSQL, queues on Redis. Frontend — Vue and TypeScript. Deployed with Docker.