How a lead is turned into an HTTP request, and how the response is judged. Written for anyone wiring Targenix to an API we do not ship a built-in adapter for.
Most destinations — Telegram, Google Sheets, Bitrix24, Kommo, HubSpot, Salesforce and the Uzbek CPA networks — already have adapters. A custom destination is the escape hatch: you describe the request, we fill it with lead data and decide whether the answer counts as delivered.
That second half is where the interesting decisions live, so this page spends most of its time there.
A destination is a URL, a method, headers and a body. Bodies come in three content types, and the one you pick decides how the body is described:
| Content type | Body is |
|---|---|
json | A JSON template. Values may contain {{variable}} tokens. |
form-urlencoded | A list of key/value pairs. |
multipart | The same key/value list, sent as form-data. |
Headers accept the same tokens, which is what makes
Authorization: Bearer {{SECRET:api_key}} work without hard-coding a
credential into a template.
Every lead is expanded into a flat string context before the template is
rendered. These keys are always present — missing values are empty strings, never
null or the literal word undefined:
| Token | Value |
|---|---|
{{name}} | Full name. {{full_name}} is an alias. |
{{phone}} | Normalised phone. {{phone_number}} is an alias. |
{{email}} | Email, when the form collected one. |
{{lead_id}} | Meta's leadgen_id for ad leads. |
{{external_ref}} | Stable external reference. Webhook leads carry their own; ad leads fall back to lead_id. Use this one for a partner's external_id — it is a safe drop-in and it survives both lead sources. |
{{page_id}}, {{page_name}} | Source page. |
{{form_id}}, {{form_name}} | Source form. |
{{platform}} | Where the lead came from. |
{{created_time}} | ISO-8601 timestamp. |
{{is_organic}} | Tri-state: "true", "false", or empty when unknown. A missing flag is never coerced to false. |
Every question on the form is also available under its own key, and a form question wins over a fixed key of the same name — the answer a human typed is more specific than a value we derived.
People type things into a phone field. Not a number with a stray space — a number with an order note attached, a greeting in front of it, or a name after it. Shipping that raw breaks partners in unhelpful ways: one partner database rejects the row outright when a Cyrillic character reaches a phone column, and the error it returns says nothing about phones.
"+998 90 123 45 67" → +998901234567
"901234567 Buyurtma" → 901234567
"Салом 901234567" → 901234567
So {{phone}} is always reduced to a leading + (only
if the original had one) plus digits. Clean numbers pass through unchanged. If
you need the raw text, read the form question directly instead of
{{phone}}.
{{upper(name)}}
{{concat(name, " — ", form_name)}}
{{if(is_organic == "true", "organic", "paid")}}
An unknown variable and an unknown function both resolve to an empty string rather than raising. A template typo therefore produces a request the partner can validate and reject with a real message, instead of a stack trace on our side that tells you nothing about which field was wrong.
{{SECRET:key}} tokens are resolved before ordinary
variables, so a mixed string works in one pass:
Bearer {{SECRET:api_key}} for {{name}}
The order is not cosmetic. Variable expansion matches any {{…}},
so if it ran first it would treat SECRET:api_key as an unknown
variable and silently erase the credential.
Four success conditions, evaluated against the response:
| Condition | Delivered when |
|---|---|
http_2xx | Status is 2xx. The default, and http_200 is an alias kept for older configurations. |
json_field | A named JSON field equals an expected value — you supply the field name and the value. |
ok_true | ok === true in the response body. |
If you can use anything other than http_2xx, do. A large share of
lead-receiving APIs answer 200 and put the refusal in the body:
HTTP/1.1 200 OK
{"ok": false, "error": "duplicate phone"}
Count status codes and your delivery rate reads 99% while the operator's phone stays quiet. This is the single most common way a lead pipeline lies to the person running it.
When the success condition fails we pull the human-readable reason out of the
body rather than storing HTTP 200, because the reason is what tells
you whether anything can be done. Then it is classified, and the classification
decides everything that happens next:
A body-level rejection with an unfamiliar message is treated as infrastructure — retried, not discarded. Declaring the phrases that mean "permanently no" moves those into the validation class and stops the pointless retries. Defaulting to retry rather than to discard is deliberate: an unnecessary retry costs a request, and a wrongly discarded lead costs a customer.
None of the above is exotic. It is simply what a lead pipeline turns into after enough deliveries, and most of it is invisible until something has already gone wrong quietly for a week. Publishing it is cheaper than everyone rediscovering it.
The destinations that already have adapters are listed at targenix.uz/integrations. Sending leads from your own landing page instead is covered in the inbound webhook reference. The service is free, with the reasoning at targenix.uz/pricing.