<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Samandar Xusenov</title>
    <description>The latest articles on DEV Community by Samandar Xusenov (@targenix).</description>
    <link>https://dev.to/targenix</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4101311%2F63b920fd-d792-4eed-ac23-feae3b6c9fd8.jpg</url>
      <title>DEV Community: Samandar Xusenov</title>
      <link>https://dev.to/targenix</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/targenix"/>
    <language>en</language>
    <item>
      <title>Shipping an Instagram DM automation engine without breaking Meta's rules: 6 lessons</title>
      <dc:creator>Samandar Xusenov</dc:creator>
      <pubDate>Mon, 28 Sep 2026 09:21:05 +0000</pubDate>
      <link>https://dev.to/targenix/shipping-an-instagram-dm-automation-engine-without-breaking-metas-rules-6-lessons-1db8</link>
      <guid>https://dev.to/targenix/shipping-an-instagram-dm-automation-engine-without-breaking-metas-rules-6-lessons-1db8</guid>
      <description>&lt;p&gt;We run &lt;a href="https://targenix.uz/en" rel="noopener noreferrer"&gt;Targenix&lt;/a&gt;, a free automation platform for businesses in Uzbekistan that sell through Facebook and Instagram ads. This year we replaced our hard-coded "reply to a comment, then ask for a phone number" logic with a real flow engine: 9 triggers (ad comment, DM, story reply, story mention, ad click, ig.me link, Ice Breakers, persistent menu, button), 17 step types, and an AI reply step for everything the flow does not cover.&lt;/p&gt;

&lt;p&gt;The engine itself was the easy part. The hard part was the platform rules and the edge cases they create. Here is what we would tell anyone building on the Instagram Messaging API and Messenger Platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The first reply to a comment is not a normal message
&lt;/h2&gt;

&lt;p&gt;When someone comments under an ad, you are allowed to send them &lt;strong&gt;one private reply&lt;/strong&gt; about that comment. It is addressed to the comment, not to the person. You do not know the customer's messaging ID (PSID/IGSID) until the Send API returns it in its response.&lt;/p&gt;

&lt;p&gt;That changes your data model. We had assumed "a conversation starts with a user id", so a run was keyed on the user before anything was sent. For comment flows that is impossible. The fix: send the private reply first, read the recipient id from the response, and only then open the run.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. There are two windows, and they are not the same window
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A private reply to a comment is allowed for &lt;strong&gt;7 days&lt;/strong&gt; after the comment.&lt;/li&gt;
&lt;li&gt;After that, every message is governed by the standard &lt;strong&gt;24-hour window&lt;/strong&gt; that starts at the customer's last message.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our first version applied the 24-hour check to the private reply as well. Every comment flow older than a day died on its first step, silently. If you model "can I send now?" as a single function, give it the reason you are sending, not just the timestamp.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. "Waiting for the customer" must be woken by the customer
&lt;/h2&gt;

&lt;p&gt;A flow often greets the customer and then waits for their answer. We implemented the wait as a state (&lt;code&gt;awaiting first inbound message&lt;/code&gt;) and, out of habit, attached it to the scheduler that handles timed waits.&lt;/p&gt;

&lt;p&gt;A timer can never know that the customer replied. The result was a flow that said hello once and then stayed silent forever. The inbound webhook has to resume the run. Timers are only for "no answer after N hours".&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Run the new engine in shadow before it talks to anyone
&lt;/h2&gt;

&lt;p&gt;We did not switch engines at once. For every page in the pilot, the new engine processed each event and recorded what it &lt;em&gt;would&lt;/em&gt; have sent, while the old code kept answering customers. We compared the two decisions for 48 hours, fixed the differences, then enabled real sending page by page.&lt;/p&gt;

&lt;p&gt;One practical detail: write the shadow decisions to the database, not to logs. Our logs went to stdout, and a deploy recreates the container. The first comparison window disappeared with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. You cannot test comment replies from your own account
&lt;/h2&gt;

&lt;p&gt;A page cannot send a private reply to a person who has a role on that page. Meta returns error &lt;strong&gt;10903&lt;/strong&gt; ("This user can't reply to this activity"). The owner commenting on their own ad is the most natural test, and it never works.&lt;/p&gt;

&lt;p&gt;Keep a role-less test account for end-to-end checks, and do not read 10903 in production as "the engine is broken".&lt;/p&gt;

&lt;h2&gt;
  
  
  6. AI is a step, not the whole flow
&lt;/h2&gt;

&lt;p&gt;A pure AI chat drifts. A pure rule-based flow breaks the moment the customer asks something unexpected ("do you deliver to Samarkand?"). What worked for us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deterministic steps handle the path we care about: answer the price, show photos, &lt;strong&gt;ask for a phone number&lt;/strong&gt;, create a lead in the CRM or Telegram group.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;AI reply&lt;/strong&gt; step catches everything else, grounded in the ad text and the facts the business entered (prices, delivery terms, working hours). Then the flow returns to its goal.&lt;/li&gt;
&lt;li&gt;Complaints and complex questions go to a &lt;strong&gt;human operator&lt;/strong&gt; instead of looping.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each public comment reply is generated separately, so a hundred "price?" comments do not get a hundred identical answers. Repeated template text is what both users and platforms read as spam.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we did not build
&lt;/h2&gt;

&lt;p&gt;No broadcasts, no "we miss you" messages days later, no messages to people who never wrote. Conversations start with the customer and stay inside the window. Keeping messaging permissions is worth more than any single campaign.&lt;/p&gt;




&lt;p&gt;If you are solving the same problem for a Facebook or Instagram business, Targenix does all of the above without code and is free (one plan, 0 UZS, AI included): &lt;a href="https://targenix.uz/en" rel="noopener noreferrer"&gt;targenix.uz/en&lt;/a&gt;. Questions about the Messenger Platform edge cases are welcome in the comments.&lt;/p&gt;

</description>
      <category>instagram</category>
      <category>automation</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How Uzbek businesses turn Instagram lead ads into sales: automating 3 million leads without code</title>
      <dc:creator>Samandar Xusenov</dc:creator>
      <pubDate>Sun, 27 Sep 2026 06:04:06 +0000</pubDate>
      <link>https://dev.to/targenix/how-uzbek-businesses-turn-instagram-lead-ads-into-sales-automating-3-million-leads-without-code-hn1</link>
      <guid>https://dev.to/targenix/how-uzbek-businesses-turn-instagram-lead-ads-into-sales-automating-3-million-leads-without-code-hn1</guid>
      <description>&lt;p&gt;In Uzbekistan, and across much of Central Asia, paid social is Instagram. When we looked at the leads flowing through &lt;a href="https://targenix.uz" rel="noopener noreferrer"&gt;Targenix&lt;/a&gt;, the platform we build in Tashkent, the pattern was hard to miss: the overwhelming majority come from Instagram placements, not the Facebook feed, and almost every lead leaves a phone number instead of an email.&lt;/p&gt;

&lt;p&gt;That one fact shapes everything about how a local business should handle leads. Email-first tools built for the US market quietly fail here: there is no email to nurture, and a lead that waits an hour for a call is usually gone. This article is about the system we built around that reality, and what it looks like after roughly &lt;strong&gt;3 million leads, 1 million+ deliveries and close to 200 active businesses&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: a lead is worth the most in its first minute
&lt;/h2&gt;

&lt;p&gt;A Facebook or Instagram lead form collects a name and a phone number in two taps. Then, by default, the lead sits in Meta's Leads Center until someone downloads a CSV. For a shop selling a product through a call center, or a clinic booking consultations, every minute of that delay costs conversions.&lt;/p&gt;

&lt;p&gt;So the first job is boring but decisive: &lt;strong&gt;get the lead to the person who will call it, in under a second.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Lead routing: one lead, many destinations
&lt;/h2&gt;

&lt;p&gt;Targenix connects to a Facebook Page once and then delivers every new lead from every form on it to wherever the team works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CRMs:&lt;/strong&gt; Bitrix24, amoCRM/Kommo, Pipedrive, HubSpot, Salesforce, Zoho&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Messengers and sheets:&lt;/strong&gt; Telegram chats, Google Sheets, Notion, Asana, Todoist&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email and outreach:&lt;/strong&gt; MailerLite, SendPulse, ActiveCampaign, Kit, Instantly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;National CPA and e-commerce platforms&lt;/strong&gt; used in Uzbekistan — in total 50+ international and national platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One lead can go to several destinations at once (say, the CRM &lt;em&gt;and&lt;/em&gt; the sales team's Telegram group). Failed deliveries retry automatically, every delivery is logged, and a &lt;em&gt;Test lead&lt;/em&gt; button proves a new connection works before real traffic hits it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Automation like ManyChat — for comments and DMs
&lt;/h2&gt;

&lt;p&gt;The second place leads leak is the comment section. People ask "narxi qancha?" ("how much?") under an ad and never get an answer.&lt;/p&gt;

&lt;p&gt;Targenix has a visual flow builder for exactly this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;9 triggers&lt;/strong&gt; — a comment, a DM, a story reply or mention, an ad click, an &lt;code&gt;ig.me&lt;/code&gt; / &lt;code&gt;m.me&lt;/code&gt; ref link, Ice Breakers, the persistent menu, a button tap&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;17 step types&lt;/strong&gt; — reply to the comment publicly, send a DM with buttons or a carousel, ask for a phone number and save it, tag the contact, wait, branch on conditions, hand off to a human, reply with AI, or turn the conversation into a lead that flows into the CRM like any form lead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ready-made templates&lt;/strong&gt; such as "answer anyone who asks the price" and "send the catalog carousel"&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;test mode&lt;/strong&gt; on the canvas that lights up the path a message would take — nothing is sent while you experiment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The platform has handled around 100,000 comments and messages this way, and moderation rules can hide comments that leak phone numbers or turn negative.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Landing pages — no code, or all the code you want
&lt;/h2&gt;

&lt;p&gt;Not every campaign should use an instant form. For those that send traffic to a page, Targenix builds the page too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;200+ ready-made templates&lt;/strong&gt; grouped by niche, each in several looks&lt;/li&gt;
&lt;li&gt;blocks for offers, countdown timers, reviews, FAQs and order forms — and a &lt;strong&gt;custom HTML block&lt;/strong&gt; when you want full control&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta Pixel plus server-side Conversions API&lt;/strong&gt; out of the box (deduplicated), GA4, Yandex Metrica and TikTok Pixel&lt;/li&gt;
&lt;li&gt;your own domain with free SSL, publishing, revisions and one-click rollback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Form submissions on a landing page become leads in the same pipeline, so they reach the CRM exactly like Instagram form leads.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Keeping ad spend honest
&lt;/h2&gt;

&lt;p&gt;Routing leads is half the job; the other half is not overpaying for them. &lt;strong&gt;CPL Guard&lt;/strong&gt; pauses a campaign automatically when its cost per lead runs above your limit, or when it spends without producing leads. Profit digests arrive in Telegram, and campaigns can even be created and launched from the Telegram bot. Conversions API events are sent back to Meta when a lead turns into a sale, so the ad algorithm learns from real outcomes rather than form fills.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we'd tell any business running lead ads
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Measure time-to-call, not just cost per lead.&lt;/strong&gt; A cheap lead that waits an hour is expensive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design for phone numbers.&lt;/strong&gt; In markets like Uzbekistan, email nurture is not your funnel; Telegram and the phone are.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer the comments.&lt;/strong&gt; Every unanswered "how much?" is a lead you paid for and lost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send real outcomes back to Meta.&lt;/strong&gt; Conversions API with sale events beats optimizing for form submissions.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Targenix is &lt;strong&gt;completely free&lt;/strong&gt; — no card, no subscription, no hidden fees — and works in Uzbek, Russian and English. Connect a Facebook Page at &lt;a href="https://targenix.uz" rel="noopener noreferrer"&gt;targenix.uz&lt;/a&gt; and the first lead will be in your CRM or Telegram in about two minutes.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Samandar Xusenov, founder of Targenix&lt;/em&gt;&lt;/p&gt;

</description>
      <category>marketing</category>
      <category>automation</category>
      <category>saas</category>
      <category>nocode</category>
    </item>
    <item>
      <title>A campaign that pauses itself before you check your phone</title>
      <dc:creator>Samandar Xusenov</dc:creator>
      <pubDate>Thu, 03 Sep 2026 14:00:41 +0000</pubDate>
      <link>https://dev.to/targenix/a-campaign-that-pauses-itself-before-you-check-your-phone-1kcl</link>
      <guid>https://dev.to/targenix/a-campaign-that-pauses-itself-before-you-check-your-phone-1kcl</guid>
      <description>&lt;p&gt;Every performance marketer has woken up to a campaign that spent all night at triple the target cost per lead. The dashboard was right there. Nobody was looking at it at 3am. Here is what we built after the third time that happened, and the two design decisions that made it trustworthy enough to actually leave switched on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode nobody budgets for
&lt;/h2&gt;

&lt;p&gt;Cost per lead does not drift gradually most of the time. It steps — a competitor enters the auction, an audience saturates, a creative fatigues — and the step can happen in an hour. A human checking dashboards once a day catches it a day late, which in ad spend terms is not a delay, it is a bill.&lt;/p&gt;

&lt;p&gt;The obvious fix is an alert. The better fix is an action, because an alert at 3am gets read at 9am, and the six hours in between are exactly the six hours that mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "pause the campaign" actually requires
&lt;/h2&gt;

&lt;p&gt;The mechanism: on an hourly sweep, check every active campaign that spent today, compare its cost per lead against a target, and pause the ones that broke it. Two details decide whether marketers actually leave it running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A minimum-leads gate.&lt;/strong&gt; One unlucky expensive lead in the first hour of a new campaign should not kill it. The rule only arms once enough leads exist for the cost-per-lead number to mean something. Without this gate, the feature pauses good campaigns on noise, and the first false pause is usually the last time anyone trusts it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A separate zero-lead spend cap.&lt;/strong&gt; A campaign that spends money and produces zero leads has an undefined cost per lead — you cannot compare "undefined" against a target, so the rule above can never fire on it. That is precisely the campaign most worth stopping. So a second, independent check exists purely for spend with no leads at all, because the CPL rule structurally cannot catch it.&lt;/p&gt;

&lt;p&gt;Miss either of these and the feature is either too trigger-happy to trust or blind to the worst case it exists for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope it per Business Manager, not globally
&lt;/h2&gt;

&lt;p&gt;A target CPL that makes sense for one product line is nonsense for another running in the same ad account. Scoping the rule per Business Manager (or per campaign group) lets the target mean something specific instead of something average.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tell the human what happened and why
&lt;/h2&gt;

&lt;p&gt;A pause with no explanation reads as a bug. Ours sends a message naming the campaign and the number that triggered it — "CPL hit $X against a target of $Y" or "spent $X with zero leads." That single line is the difference between a marketer trusting the automation and disabling it the first time it surprises them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;p&gt;It is not bid optimisation, and it does not try to fix the campaign — only to stop it from bleeding further while a human decides what changed. Auto-resume is deliberately not part of it: a human should look at &lt;em&gt;why&lt;/em&gt; before spend resumes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that made us trust it enough to ship
&lt;/h2&gt;

&lt;p&gt;We run it same-day only — it evaluates today's spend against today's leads, not a rolling average. That is a real limitation, and stating it plainly turned out to matter more than hiding it. A guard rail that oversells its own reliability gets disabled the first time it is wrong in a way nobody warned you about.&lt;/p&gt;




&lt;p&gt;This runs in production at &lt;a href="https://targenix.uz" rel="noopener noreferrer"&gt;Targenix&lt;/a&gt;, free lead automation for Facebook and Instagram advertisers, as part of the free plan.&lt;/p&gt;

</description>
      <category>marketing</category>
      <category>ppc</category>
      <category>automation</category>
    </item>
    <item>
      <title>Your cost per lead is falling and your sales are flat. That is the algorithm working.</title>
      <dc:creator>Samandar Xusenov</dc:creator>
      <pubDate>Tue, 01 Sep 2026 11:59:31 +0000</pubDate>
      <link>https://dev.to/targenix/your-cost-per-lead-is-falling-and-your-sales-are-flat-that-is-the-algorithm-working-2oa2</link>
      <guid>https://dev.to/targenix/your-cost-per-lead-is-falling-and-your-sales-are-flat-that-is-the-algorithm-working-2oa2</guid>
      <description>&lt;p&gt;A lead campaign gets cheaper every week. Cost per lead down 40%. Sales, unchanged.&lt;/p&gt;

&lt;p&gt;The instinct is to look for a bug — bad tracking, a broken CRM sync, an operator not calling back. Usually there is none. The account is working exactly as instructed.&lt;/p&gt;

&lt;p&gt;Facebook optimises toward whatever you tell it success looks like. If the only thing you ever report back is "a form was submitted", it will go and find the people most likely to submit forms cheaply. Those people exist in large numbers. Very few of them buy anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signal you are not sending
&lt;/h2&gt;

&lt;p&gt;Ad platforms are guessing most of the time. Cookies are gone or blocked, click ids get dropped, email matching is probabilistic, attribution windows are a polite fiction. Everyone has made peace with that.&lt;/p&gt;

&lt;p&gt;Lead Ads are the exception, and it is worth appreciating how unusual it is.&lt;/p&gt;

&lt;p&gt;Every submission arrives with a &lt;code&gt;leadgen_id&lt;/code&gt;. When you later send a server-side conversion event carrying that same id, the join is &lt;strong&gt;deterministic&lt;/strong&gt;. Not "we think this hashed email is probably that user" — literally the identifier the platform handed you, given back. No cookie, no pixel in a browser, no identity resolution.&lt;/p&gt;

&lt;p&gt;Almost nobody running lead ads uses it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two events, and only one of them is interesting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Lead received.&lt;/strong&gt; Fire when the lead lands and passes validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Status changed.&lt;/strong&gt; Fire when the order actually moves — confirmed, delivered, cancelled.&lt;/p&gt;

&lt;p&gt;Teams ship the first one, see no change, and conclude the integration does not work. Of course it does not: &lt;em&gt;"a lead arrived"&lt;/em&gt; is the one fact the platform already had. You have told it nothing.&lt;/p&gt;

&lt;p&gt;The second event is the whole point, and it is inconvenient in a specific way — it arrives days later, because that is how long it takes a human to answer a phone, confirm an order and have a courier deliver it. An integration that only handles same-request events cannot express it. You need somewhere to write the outcome when it eventually shows up, and something that walks those outcomes and sends them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make it idempotent twice
&lt;/h2&gt;

&lt;p&gt;Status transitions repeat. Dispatchers re-scan. Order events get written twice by two paths that both thought they were first. Send a conversion twice and you have quietly doubled a number an optimiser is now steering on.&lt;/p&gt;

&lt;p&gt;The pattern that has held up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;dedupe_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lead_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="n"&gt;deterministic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;no&lt;/span&gt; &lt;span class="n"&gt;timestamps&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put a UNIQUE constraint on it in your own table, &lt;strong&gt;and&lt;/strong&gt; hand the same value to the platform as the event id. Now suppression exists in two independent systems: your insert fails on the second attempt, and if one ever slips past, the platform discards it as a duplicate event. Two boring guards beat one clever one.&lt;/p&gt;

&lt;p&gt;Deriving the key from anything time-based defeats the entire thing. It has to be a pure function of what happened, not of when you noticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The multi-tenant trap
&lt;/h2&gt;

&lt;p&gt;A dataset belongs to one ad account. Send an event about a lead belonging to a different account and you get no error, no warning, no failed response — the data is simply ignored.&lt;/p&gt;

&lt;p&gt;This is the worst possible failure mode: everything looks healthy, dashboards show events flowing, and none of them teach anything. If you run this for multiple advertisers, scope every send to the owning account's own dataset with the owner's own credential. A shared or default credential will produce a system that looks fine and does nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to expect, and how to not misread it
&lt;/h2&gt;

&lt;p&gt;Slowly. The optimiser needs a meaningful number of conversion events to learn from, and that is bounded by real order volume and real fulfilment time — not by your deploy.&lt;/p&gt;

&lt;p&gt;Then the shape changes rather than a single number:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost per lead goes up.&lt;/strong&gt; Fewer cheap form-fillers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Number of leads goes down.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost per order goes down&lt;/strong&gt;, which is the only one that was ever paying anyone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If cost per lead is still the metric on the wall at that point, this integration will look like it broke the account. Change the metric before you ship the feature, or you will roll back something that was working.&lt;/p&gt;

&lt;p&gt;That is the same lesson as everywhere else in this work: the number worth reporting is the one you would defend, not the one that looks best.&lt;/p&gt;




&lt;p&gt;We build this at &lt;a href="https://targenix.uz/" rel="noopener noreferrer"&gt;Targenix&lt;/a&gt; — free Facebook and Instagram lead automation for Uzbekistan. The Conversions API mechanics are written up in more detail &lt;a href="https://xuseyinoff.github.io/targenix-webhook-docs/conversions-api/" rel="noopener noreferrer"&gt;here&lt;/a&gt;, the whole platform is mapped &lt;a href="https://xuseyinoff.github.io/targenix-webhook-docs/platform/" rel="noopener noreferrer"&gt;here&lt;/a&gt;, and the destination list is at &lt;a href="https://targenix.uz/integrations" rel="noopener noreferrer"&gt;targenix.uz/integrations&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>marketing</category>
      <category>api</category>
      <category>architecture</category>
      <category>analytics</category>
    </item>
    <item>
      <title>The leads that never touch your form: turning ad comments into pipeline</title>
      <dc:creator>Samandar Xusenov</dc:creator>
      <pubDate>Tue, 01 Sep 2026 11:53:06 +0000</pubDate>
      <link>https://dev.to/targenix/the-leads-that-never-touch-your-form-turning-ad-comments-into-pipeline-1n41</link>
      <guid>https://dev.to/targenix/the-leads-that-never-touch-your-form-turning-ad-comments-into-pipeline-1n41</guid>
      <description>&lt;p&gt;Everyone building on Facebook Lead Ads instruments the form. The form fires a webhook, the webhook writes a lead, the lead goes to a CRM. Solved problem.&lt;/p&gt;

&lt;p&gt;Then you look at the ad itself and find thirty comments saying &lt;em&gt;"narxi qancha?"&lt;/em&gt; — how much is it — and eleven people who typed their phone number directly under the post. None of them touched the form. None of them exist in your pipeline. They raised their hand in public and your system did not notice.&lt;/p&gt;

&lt;p&gt;We route leads for advertisers in Uzbekistan, mostly physical goods sold on price and speed of callback. Here is what we learned building the comment path.&lt;/p&gt;

&lt;h2&gt;
  
  
  A comment is not a lead until three things are true
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It has to be reachable.&lt;/strong&gt; Extracting a phone number sounds like a regex problem and mostly is, until you see what people actually type. Numbers with a greeting glued to the front. Numbers with an order note after them. Numbers in Cyrillic-adjacent formatting. Local numbers with and without the country code, sometimes both in one thread. Normalise to something a partner API will accept before it leaves you — a compacted &lt;code&gt;+&lt;/code&gt; and digits — and keep the raw text separately, because the raw text is often where the product they want is written.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It has to be attributable.&lt;/strong&gt; A form lead arrives with a form id and a page id. A comment arrives attached to an ad. If you want cost-per-lead to mean anything afterwards, resolve the comment back to the campaign that paid for it at capture time, not later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It has to be deduplicated across sources.&lt;/strong&gt; The same person very often comments &lt;em&gt;and&lt;/em&gt; fills the form, ten minutes apart. If your unique constraint is on the form's lead id, the comment version sails straight past it and your operator calls the same person twice. Dedupe on the normalised phone within a window, not only on the platform's identifier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replying is where it stops being an engineering problem
&lt;/h2&gt;

&lt;p&gt;The obvious move is an auto-reply. The obvious auto-reply is &lt;em&gt;"Assalomu alaykum! Telefon raqamingizni qoldiring"&lt;/em&gt; under all thirty comments, which reads exactly like what it is and performs accordingly.&lt;/p&gt;

&lt;p&gt;What worked was narrower: read the product and price out of &lt;strong&gt;that specific ad's own caption&lt;/strong&gt;, answer the actual question, and ask for the number as the last clause rather than the first. One reply per comment, never a second one, and off by default per page so it is a decision rather than a default.&lt;/p&gt;

&lt;p&gt;The failure mode to design against is not a bad reply. It is a &lt;em&gt;plausible&lt;/em&gt; reply that invents a price. If the caption does not state one, the reply must not either.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody warns you about
&lt;/h2&gt;

&lt;p&gt;Your comment section is a list of people who publicly announced they want to buy, with contact details attached, on a competitor's screen as easily as yours.&lt;/p&gt;

&lt;p&gt;In a market where several sellers run near-identical creative for the same product, that is not hypothetical. So phone-bearing comments can be hidden automatically — visible to you, invisible to everyone else — and abusive ones removed. The lead still reaches you. It just stops being a shared resource.&lt;/p&gt;

&lt;p&gt;This is the sort of feature you only think of after an advertiser explains, patiently, why they stopped replying to comments at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permissions, and asking for them at the right time
&lt;/h2&gt;

&lt;p&gt;Comment access needs Facebook permissions that lead retrieval does not. The instinct is to bundle everything into one consent screen at signup. Do not: you will ask for four extra scopes from every user, most of whom will never use the feature, and lower your connect rate for all of them.&lt;/p&gt;

&lt;p&gt;Request them in a second, separate flow triggered when someone actually turns the feature on. Facebook shows only the new scopes on re-consent, so the ask stays small. And treat "does this token carry the scopes" as a live fact you check, not a flag you store — users revoke things, and a stored boolean will happily lie to you for weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was it worth building
&lt;/h2&gt;

&lt;p&gt;Yes, and not for the reason we expected. The volume matters, but the bigger effect is that comment leads arrive &lt;em&gt;earlier&lt;/em&gt; in the buying decision than form leads. Someone asking the price in public is often still comparing. Someone filling a form has usually already decided — sometimes on a competitor.&lt;/p&gt;




&lt;p&gt;We build this at &lt;a href="https://targenix.uz/" rel="noopener noreferrer"&gt;Targenix&lt;/a&gt; — free Facebook and Instagram lead automation for Uzbekistan: form leads, comment leads and your own landing page, routed to 50+ destinations including CRMs, Telegram, Google Sheets and local CPA networks. There is a written map of the whole platform &lt;a href="https://xuseyinoff.github.io/targenix-webhook-docs/platform/" rel="noopener noreferrer"&gt;here&lt;/a&gt;, the integration list is at &lt;a href="https://targenix.uz/integrations" rel="noopener noreferrer"&gt;targenix.uz/integrations&lt;/a&gt;, and it is genuinely free — the reasoning is at &lt;a href="https://targenix.uz/pricing" rel="noopener noreferrer"&gt;targenix.uz/pricing&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>startup</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Why a 200 OK is not proof of delivery: 4 lessons from routing 100k+ webhook deliveries</title>
      <dc:creator>Samandar Xusenov</dc:creator>
      <pubDate>Mon, 31 Aug 2026 06:31:36 +0000</pubDate>
      <link>https://dev.to/targenix/why-a-200-ok-is-not-proof-of-delivery-4-lessons-from-routing-100k-webhook-deliveries-2ob9</link>
      <guid>https://dev.to/targenix/why-a-200-ok-is-not-proof-of-delivery-4-lessons-from-routing-100k-webhook-deliveries-2ob9</guid>
      <description>&lt;p&gt;I maintain a service that takes Facebook and Instagram Lead Ads and delivers each new lead, in real time, to wherever the business actually works — a CRM, Telegram, Google Sheets, a CPA network, or a plain webhook. Over 50 destination types, all of them somebody else's API, all of them free to break at 3am.&lt;/p&gt;

&lt;p&gt;The naive version of this is one HTTP POST. The real version is about 80% error handling, and almost every line of that 80% exists because something failed silently in production first.&lt;/p&gt;

&lt;p&gt;Here are the four lessons that cost me the most.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A 200 OK is not proof of delivery
&lt;/h2&gt;

&lt;p&gt;This one still stings.&lt;/p&gt;

&lt;p&gt;We had an integration reporting a ~99% success rate while users complained that rows were missing. The destination returned &lt;code&gt;200 OK&lt;/code&gt; every single time. It was also writing &lt;strong&gt;empty rows&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The cause was a field mapping that pointed at a form field's &lt;em&gt;label&lt;/em&gt; instead of its &lt;em&gt;value&lt;/em&gt;. We sent a well-formed request full of empty strings, and the API cheerfully accepted it.&lt;/p&gt;

&lt;p&gt;The fix was to stop trusting the status code and start reading the response body. For Google Sheets, the append call echoes back an &lt;code&gt;updatedRange&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"updates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"updatedRange"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sheet1!A2:C2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"updatedRows"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that range is missing, or the row count is zero, it was not a delivery — no matter what the status line said. Every adapter now has a "what does success actually look like in the body?" check, not just &lt;code&gt;res.ok&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; for any write API, find the field in the response that proves the write happened, and assert on that.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Retries have to survive an outage, not a blip
&lt;/h2&gt;

&lt;p&gt;Most retry examples you find online do 3 attempts with exponential backoff over ~30 seconds. That handles a dropped packet. It does not handle a CRM that is down for six hours, which is completely normal.&lt;/p&gt;

&lt;p&gt;We separate failures into two classes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Validation failures&lt;/strong&gt; — duplicate, blacklisted, malformed phone number. Retrying is pointless; the payload will never become valid. Fail once, record why, move on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure failures&lt;/strong&gt; — timeouts, 5xx, DNS, connection reset. These are worth retrying for a long time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Infra retries now stretch across roughly 26 hours with widening backoff. An overnight outage stops being lost revenue and becomes a delay.&lt;/p&gt;

&lt;p&gt;The important part is that this split is decided &lt;em&gt;at the point of failure&lt;/em&gt;, by inspecting the error, not by a global retry count. Getting that classification wrong in either direction is expensive: retry a validation error forever and you burn the queue; give up on a 503 after 30 seconds and you lose the lead.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Circuit breakers belong per destination, not globally
&lt;/h2&gt;

&lt;p&gt;The first breaker I wrote was global. One dead endpoint slowed everyone down, because the queue kept marching into the same wall.&lt;/p&gt;

&lt;p&gt;Breakers are now keyed per destination. But that raised a subtler question: &lt;strong&gt;when does a breaker close again?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A breaker that opens because a remote host is unreachable is telling you something about the &lt;em&gt;network&lt;/em&gt;. A breaker that opens because the API returned &lt;code&gt;401&lt;/code&gt; is telling you something about &lt;em&gt;one user's credentials&lt;/em&gt;. Those need completely different recovery:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Network-class breakers can heal on their own, and they can also be healed by evidence — if any other tenant successfully reaches that same host, the network is clearly fine, so parked breakers can be moved from OPEN to HALF_OPEN immediately instead of waiting out a timer.&lt;/li&gt;
&lt;li&gt;Auth-class failures should never silently retry into a lockout. They should flip the connection's health to "broken" and tell the human, with the specific reason.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conflating those two was one of our longest-lived bugs. A transient &lt;code&gt;502&lt;/code&gt; from one provider once flipped a perfectly good connection to &lt;code&gt;error&lt;/code&gt; and quietly stopped delivering. Now transient failures leave health &lt;em&gt;unchanged&lt;/em&gt;, and only a provider-rejected credential marks a connection broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Report your success rate honestly
&lt;/h2&gt;

&lt;p&gt;Our dashboard used to show a delivery-rate gauge computed as &lt;code&gt;delivered / total&lt;/code&gt;. It looked bad, and unfairly so — because &lt;code&gt;total&lt;/code&gt; included duplicates we intentionally blocked, blacklisted numbers, and phone numbers that were never valid.&lt;/p&gt;

&lt;p&gt;Those are the system working, not failing.&lt;/p&gt;

&lt;p&gt;So validation-class outcomes are now excluded from the denominator. The gauge answers a real question — "of the leads that &lt;em&gt;could&lt;/em&gt; be delivered, how many were?" — instead of a meaningless one.&lt;/p&gt;

&lt;p&gt;This is worth saying out loud because the temptation runs the other way. It is very easy to build a metric that makes your product look good and tells your user nothing. A number your user cannot act on is decoration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boring conclusion
&lt;/h2&gt;

&lt;p&gt;Nothing here is clever. It is all just refusing to believe optimistic signals: the status code, the retry count, the global breaker, the flattering percentage.&lt;/p&gt;

&lt;p&gt;If you are building anything that hands data to somebody else's API, I would spend your first week of hardening on lesson 1. It is the one that hides the longest.&lt;/p&gt;




&lt;p&gt;I work on &lt;a href="https://targenix.uz" rel="noopener noreferrer"&gt;Targenix&lt;/a&gt;, which does this for Facebook and Instagram lead ads. The list of destinations is &lt;a href="https://targenix.uz/integrations" rel="noopener noreferrer"&gt;here&lt;/a&gt; if you are curious what 50+ different failure modes looks like in practice.&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>node</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Routing Facebook &amp; Instagram leads to any CRM in real time (and why we made it free)</title>
      <dc:creator>Samandar Xusenov</dc:creator>
      <pubDate>Sun, 30 Aug 2026 11:23:31 +0000</pubDate>
      <link>https://dev.to/targenix/routing-facebook-instagram-leads-to-any-crm-in-real-time-and-why-we-made-it-free-4pka</link>
      <guid>https://dev.to/targenix/routing-facebook-instagram-leads-to-any-crm-in-real-time-and-why-we-made-it-free-4pka</guid>
      <description>&lt;p&gt;If you run Facebook or Instagram Lead Ads, you know the gap: a customer fills in the form, and then the lead just sits inside Meta's Ads Manager. By the time someone exports the CSV, it's hours later and the lead is cold.&lt;/p&gt;

&lt;p&gt;We built &lt;a href="https://targenix.uz" rel="noopener noreferrer"&gt;Targenix&lt;/a&gt; to close that gap for businesses in Uzbekistan — deliver every Lead Ads lead to wherever the team actually works, in real time. This post is a quick tour of how it works, and why we decided to make lead forwarding completely free.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: leads that arrive too late
&lt;/h2&gt;

&lt;p&gt;Meta Lead Ads are excellent at &lt;em&gt;collecting&lt;/em&gt; leads and poor at &lt;em&gt;delivering&lt;/em&gt; them. The native options are a manual CSV export or a raw webhook you have to build and babysit yourself. Small teams and solo marketers don't have an engineer on call, so leads pile up and conversion drops the longer nobody calls back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;

&lt;p&gt;The core is boring on purpose — boring is reliable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Webhook ingestion.&lt;/strong&gt; Subscribe to Meta's &lt;code&gt;leadgen&lt;/code&gt; webhook. Every submitted form fires an event with a &lt;code&gt;leadgen_id&lt;/code&gt;; we fetch the full field set from the Graph API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A queue, not a cron.&lt;/strong&gt; Each lead becomes a job. That gives back-pressure, ordering, and somewhere safe to retry — instead of a giant loop that dies halfway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adapters per destination.&lt;/strong&gt; One small adapter per target: CRMs (Bitrix24, Kommo, amoCRM, Salesforce, Notion, Monday), Telegram, Google Sheets, and local CPA networks. Adding a destination means writing one adapter, not touching the core.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency + retries.&lt;/strong&gt; Dedupe by &lt;code&gt;leadgen_id&lt;/code&gt; so a webhook re-delivery never double-sends. Failed deliveries retry with exponential backoff and a circuit breaker per connection, so one flaky CRM doesn't stall the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversions API.&lt;/strong&gt; The same lead is sent back to Meta via the Conversions API (keyed by &lt;code&gt;leadgen_id&lt;/code&gt;) so the ad algorithm optimizes on real conversions, not just form-fills.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A few things we learned the hard way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Validate before you count.&lt;/strong&gt; Duplicates, blacklisted numbers and malformed phones should never inflate your "delivered" rate. We separate real delivery failures from validation drops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health has to be truthful.&lt;/strong&gt; A transient &lt;code&gt;502&lt;/code&gt; from a messenger API should not flip a connection to a permanent error state and silently drop leads. Transient == unchanged; only a provider &lt;em&gt;rejecting&lt;/em&gt; credentials means "reconnect".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attribution is a denominator problem.&lt;/strong&gt; Spend ÷ &lt;em&gt;captured&lt;/em&gt; leads and spend ÷ &lt;em&gt;delivered&lt;/em&gt; leads are different numbers, and mixing them makes your cost-per-lead lie.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why free?
&lt;/h2&gt;

&lt;p&gt;The honest answer: forwarding a lead costs us very little, and charging per lead punishes exactly the small businesses we want using it. So lead routing and the core features stay free — no subscription, no card. We'd rather win the market on being genuinely useful and monetize higher up the stack later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you're running Lead Ads and tired of exporting CSVs, you can connect a page and pick a destination in a few minutes: &lt;a href="https://targenix.uz" rel="noopener noreferrer"&gt;targenix.uz&lt;/a&gt;. The full list of destinations lives at &lt;a href="https://targenix.uz/integrations" rel="noopener noreferrer"&gt;targenix.uz/integrations&lt;/a&gt;, and if you're curious whether it's really free, that's spelled out at &lt;a href="https://targenix.uz/pricing" rel="noopener noreferrer"&gt;targenix.uz/pricing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about the pipeline in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>saas</category>
      <category>marketing</category>
    </item>
  </channel>
</rss>
