When a form platform has no ready integration — a custom-built site, an internal system, a back-office script — you can post leads to KO directly. This is the Webhook: Lead Intake connection.
Only the contact is required. Everything else improves what the reports can tell you.
The request
URL: https://webhook.kodata.pro/{hash32} — issued in the connection settings.
Method: POST
Content type: application/json or multipart/form-data
Any field may be omitted except the contact. Phone numbers are accepted in any shape:
| Form | Example |
|---|---|
| Formatted | +358 40 123 4567 |
| Digits | 358401234567 |
With extension (; separator) | 358401234567;5555 |
| SIP address | u100@example.com |
Phone and email can each hold several values — pass an array.
Minimal request
{
"contact": {
"phone": "+358 40 123 4567"
}
}
This creates a lead with no source. It will appear in reports as unattributed, which is the right answer when the source is genuinely unknown — but if the visitor came through your site, pass the touch identifier below and it will not have to be.
Minimal request with attribution
{
"contact": {
"phone": "+358 40 123 4567"
},
"touch": {
"touch_id": 123
}
}
touch_id comes from the _ko_touch_id cookie, which the KO tracking script sets on your
pages. Reading that cookie and passing it here is the whole of attribution: it is what
connects this lead to the campaign that produced the visit.
Full request
{
"contact": {
"name": "Firstname Lastname",
"phone": "+358 40 123 4567",
"email": "person@example.com"
},
"lead": {
"amount": 1000,
"currency": "EUR",
"profile": {
"comment": "Call me tomorrow",
"i-agree": "on"
}
},
"touch": {
"touch_id": "123456",
"communication": "site",
"action": "lead",
"site_url": "https://example.com/path?utm_source=newsletter",
"ref_url": "https://referrer.example/path",
"user_device": "desktop",
"user_browser": "firefox",
"user_ip": "127.0.0.1"
}
}
Contact fields also accept messenger handles: whatsapp, telegram, viber,
facebook, instagram, skype.
lead.profile carries the form’s own fields. Either an object of name: value pairs,
or an array of {name, label, value} objects when you want the human-readable label
preserved:
"profile": [
{ "name": "comment", "label": "Comment", "value": "Call me tomorrow" }
]
touch field values:
| Field | Values |
|---|---|
communication | the channel: site, chat, chatbot, messenger, widget, quiz, leadform, call, email, sms, manager, payment, app, webinar, fishing, billboard, radio, book, unknown |
action | what happened: for a lead, lead; of the rest, view, impression, click, paid, subscribe, unsubscribe apply to a webhook |
user_device | desktop, tablet, mobile, tv, unknown |
user_browser | stored as sent — use the common lowercase name: chrome, firefox, safari, edge, opera |
A value outside these lists is not rejected, but it is not understood either: channel and action feed source detection, and an invented value files the lead under an unknown channel.
touch additionally accepts the UTM fields (utm_source, utm_medium, utm_campaign,
utm_content, utm_term) and client identifiers (client_id_ko, client_id_ga,
client_id_fb) when you have them.
The response
JSON, HTTP 200.
{
"contact_state": "ok",
"contact_id": 111,
"lead_id": 222,
"crm_service": "HubSpot",
"crm_contact_id": 333,
"crm_lead_id": 444
}
The submission succeeded when contact_id is not 0. Check that field specifically:
contact_state and crm_service are only returned when the lead reached a CRM connected to
the project — without a CRM connection the response is just the identifiers.
contact_state: "double" means the contact already exists in KO or in your CRM and was
not created again — the contact_id returned is the existing one. This is a normal
outcome, not an error: treat it as success.
Errors
An error comes back with the same 200, with two fields instead of the identifiers:
{
"error_code": 1,
"error_message": "putContact: Contact not created. Check request."
}
error_code: 1 — the request was not accepted (empty or unreadable contact);
error_code: 2 — putContact: Ignored by filter, the lead was dropped by an intake rule
configured on the connection.
Test mode
Add a test parameter to get a simulated response without creating anything. The value
determines the numbers you get back, so you can distinguish one test from another:
test=1 returns contact_id: 1 with contact_state: "double", test=2 returns
contact_id: 2 with contact_state: "ok", and so on.
{
"test": 2,
"contact": { "name": "Firstname Lastname", "phone": "+358 40 123 4567" }
}
{
"contact_state": "ok",
"contact_id": 2,
"lead_id": 2,
"crm_contact_id": 20,
"crm_lead_id": 20,
"crm_service": "Test"
}
Reading lead status back
Disabled by default — enable it in the connection settings under Permissions, with the Allow reading lead status checkbox; the deal amount is a second checkbox of its own, Show the deal amount.
If you want status changes pushed rather than polled, write to technical support: postbacks (lead created, lead changed, deal won, deal closed) are set up separately and arrive at your endpoint on their own.
Same URL, GET or POST. You can look up leads by lead_id or by crm_lead_id, at most
100 per request. Only leads submitted through this same webhook are visible, and deleted
leads are absent from the response.
{
"method": "getLeads",
"filter": {
"lead_id": [1, 2]
}
}
{
"leads": [
{ "lead_id": 1, "crm_lead_id": 10, "state": "process", "amount": 0, "currency": "" },
{ "lead_id": 2, "crm_lead_id": 20, "state": "success", "amount": 1000, "currency": "EUR" }
]
}
state is one of process, success, fail. Which fields come back depends on the
permissions granted in the connection settings — by default only the status is exposed,
and the deal amount is not.