Lead webhook

The URL an outside platform posts new leads to, the fields it accepts, and what Gene App does with each one.

Where to change itFlowsInbound
On this page

Anything that can send an HTTP request can put leads into Gene App: a form builder, an ad platform, your own backend, or a connector like Albato, WebJack, Zapier or Make. There is nothing to install and no library to add.

You POST a lead to a URL, and Gene App treats it exactly like a lead from one of its own forms: deduped, assigned to an agent, placed in the pipeline, and answered by the AI.

This page is the developer reference. Inbound covers the same connection from the screen.

Getting your URL#

  1. Go to Settings > Lead Sources, the Webhooks tab.
  2. Click Create webhook.
  3. Name the connection after the platform sending the leads.

Gene App gives you a URL:

https://crm.gene.so/api/webhooks/inbound/<token>

The URL stays available afterwards in the connection's editor (Flows > Inbound, the Webhooks row), under Where to send leads.

Make one connection per platform. Two platforms pointed at the same URL share one set of statistics, field mappings and welcome messages, and you lose the ability to tell their leads apart.

Warning

The token in the URL is the credential. Anyone who has it can create contacts in your workspace, so treat it like a password and keep it server side.

Nothing else is required: no header, no key, no signature. That is deliberate, so a form action or a no-code connector can use it with no extra setup.

Until you finish the wizard, the connection is paused. A paused connection answers {"status":"setup"} and stores the payload so you can map its fields, but creates no contact and sends no message. Publishing makes it live.

The request#

POST https://crm.gene.so/api/webhooks/inbound/<token>
Content-Type: application/json

{
  "first_name": "Jane",
  "last_name": "Smith",
  "email": "[email protected]",
  "phone": "+971501234567",
  "tags": ["albato", "instagram-campaign"],
  "budget": "AED 2M - 3M",
  "timeline": "Within 3 months",
  "language": "Russian",
  "notes": "Asked about two bedroom units near the marina"
}

JSON is the format to send. Form encoding (application/x-www-form-urlencoded or multipart/form-data) is accepted as well, for platforms that cannot post JSON.

Every lead needs at least an email or a phone number. A payload with neither is refused, because there would be no way to reach the person and nothing to dedupe them on.

Fields Gene App recognises#

Field names are matched ignoring case, spaces and punctuation, so first_name, firstName and First Name are the same field.

SendAlso accepted asLands as
first_namefirstNameFirst name
last_namelastNameLast name
full_namefullName, nameSplit at the first space into first and last name
emailemail_address, e_mailEmail, lowercased
phonephone_number, mobile, mobile_number, tel, telephone, whatsapp, whatsapp_numberPhone
tagsTags on the contact. An array of strings. Gene 2.0 automation tags such as ai-agent-on or follow-ups-on are dropped. A dnd tag turns on Do not contact for the contact instead
custom_fieldscustomFieldsAn object. Every key inside it is kept on the contact

Send the phone in international format (+971501234567). A number written without a country code is resolved against the lead's own country first (a country answer on the form, such as "Egypt", "Egipto" or "Spain (+34)"), then the source's country, then your workspace's country. So a British lead who types 07956 500411 on a form that asks their country gets +44 7956 500411, even in a Spanish workspace.

A number that still has no country code after that, or that cannot exist for its country code (such as +1 followed by thirteen digits), is still saved but marked "Not on WhatsApp". No WhatsApp welcome goes out to it: when the lead gave an email, the welcome goes by email instead (when the source has Email after WhatsApp, or your workspace sends email from its own verified domain), otherwise it stops. Either way the conversation gets an internal note saying why, so your team can fix the number.

Four more fields are recognised from the wording of the question rather than an exact name, because ad platforms send questions as whole sentences:

Recognised fromFills
budget, price range, how much, spend, investment amountBudget, including the numeric range used by the Inbox filter
timeline, timeframe, how soon, when are you, move in, purchase withinTimeline
motive, purpose, buying for, reason for, property useMotive (Investment, Personal use, or Mixed use)
language, idioma, langue, sprache, taal, linguaThe lead's language, which routes them to an agent who speaks it

Everything else you send#

Nothing is thrown away. Any other field is kept on the contact as its own entry, shown in the contact's details and read by the AI when it writes the first message. So send the answers the lead actually gave.

Nested objects are flattened to dotted names, so {"property":{"city":"Dubai"}} is stored as property.city.

There are caps, so one verbose form cannot fill a contact with noise:

  • 40 fields kept per contact
  • 500 fields read per payload
  • six levels of nesting
  • each value truncated at 2,000 characters

Skip internal ids, tracking parameters and session data.

Ad click ids are the exception worth sending: fbclid, gclid, wbraid, gbraid, ttclid, msclkid, _fbc, _fbp and li_fat_id.

  • They are picked up under their usual spellings, or from a landing page URL you send.
  • They are kept as the lead's ad attribution, whatever the field mapping says.
  • The mapping screen lists them under Tracked automatically.

Campaign tags and the page address are worth sending too:

  • utm_source, utm_medium, utm_campaign, utm_term, utm_content and utm_id, under any spelling (utm_medium, Utm Medium, utmMedium).
  • The address of the page the form was filled in on, as page_url (or page, landing_page, form_url, source_url). UTM tags in that address are read as well.
  • They are saved on the contact as where the lead came from, so the AI knows the campaign, medium, source and page on every reply. They also stay among the form answers.

If your platform uses field names Gene App does not recognise, open the connection after the first payload arrives and map each field by hand. Gene App also proposes a mapping from the first payload it sees.

What comes back#

Gene App answers as soon as the lead is safely stored, then dedupes, assigns and messages it in the background, usually within seconds. So a sender with a short timeout gets its answer well inside it.

StatusBodyMeaning
202{"status":"accepted","id":"..."}Received and stored. The contact is created (or matched to one you already have) and routed straight after. id is the delivery's reference, not a contact id
200{"status":"duplicate","repeatInquiry":false}A retry of a delivery already received in the last 24 hours (see below). Not processed again, so the lead gets no second message. Carries the contactId once the first delivery has been processed
200{"status":"setup"}The connection is still paused. Payload stored for mapping, nothing created
400{"error":"Either email or phone is required"}Nothing to reach the lead on
404{"error":"Invalid webhook token"}Wrong URL, or the connection was deleted
429{"error":"rate_limited"}Website form connections only: more than 20 submissions a minute from one address, or 120 a minute in total. Wait the seconds in the Retry-After header
500{"error":"Internal error"}Our side. Retry

Read any 2xx as success. On the rare occasion the background queue is unavailable, Gene App processes the lead before answering instead, and the answer is the older form: 201 {"status":"created","contactId":"..."} for a new contact, or 200 {"status":"duplicate","contactId":"..."} for somebody already in the workspace.

Retry on a 500 or a timeout, not on a 400 or a 404. Retrying is safe:

  • Send your own id for the inquiry as external_id (or externalId). A second delivery with the same external_id within 24 hours is answered as a duplicate and not processed again.
  • With no id of your own, add "retried": true to a re-send. It is matched to an earlier delivery within 24 hours from the same email or phone, about the same project or listing when both name one (project_id, project_url, property_id, listing_id and similar), and not processed again.
  • A re-send with neither is handled as a new inquiry from the same person: deduped onto their contact, not duplicated, but followed up as a repeat inquiry.

The last ten payloads a connection received are kept and shown in its field mapping, so a failing integration is diagnosed from what actually arrived rather than from guesswork.

What happens to the lead#

  1. Deduped, on phone first and then email. A lead whose email matches a contact with the same number written another way (01000842046 against +20 100 084 2046) lands on that contact. A shared mailbox such as info@ or sales@ with a different number is a different person: they get their own contact, without the email, which stays with the contact that has it. An existing contact is treated as a repeat inquiry:
    • the new answers merge into the record they already have; an empty first or last name is filled from the form, a name already there is never overwritten
    • the inquiry joins their timeline
    • if Gene has already messaged them, the AI follows it up in the conversation already running instead of opening a second one, with a short message that names the form they filled in and uses their answers. On email it goes in the same thread, under the conversation's "Re:" subject, and anything your automations sent before is credited to "we", never to a named teammate
    • if Gene has never messaged them (an imported or hand-added contact, say), they get the source's welcome flow, the same first message a new lead gets
  2. Assigned by your rules, including language. See Routing.
  3. Placed in the pipeline, at Contacting or at whatever stage the connection sets, with any tags you sent. Gene App adds no tags of its own: the connection is recorded on the contact as its lead source, not as a tag.
  4. Answered by the AI, unless Gene AI is switched off for that connection.
    • By default, the first message goes out after a random delay of up to five minutes, so the reply does not read as automated.
    • What it says, and whether it waits longer than that, is set per connection under Welcome flow, covered on Nurturing.

Which channel the AI writes on:

  • WhatsApp, when the contact has a phone number.
  • Email, when there is only an email address.
  • Telegram, only to a person who has already messaged your Telegram bot. Telegram does not allow a bot to open a conversation with somebody who has not started one.

Whatever the lead replies, on any channel, arrives in the Inbox and the AI carries the conversation on from there.

Setting it up in a connector#

The steps are the same in Albato, WebJack, Zapier and Make, and in anything else with a generic HTTP action:

  1. Trigger on a new lead in the platform that holds them.
  2. Add an HTTP request (or webhook) action. Method POST, the URL from Gene App, body JSON.
  3. Map their fields onto first_name, last_name, email and phone. Pass qualification answers straight through under their own names.
  4. Run it once with a real lead. The wizard picks it up and shows its fields to map, then you publish.
    • If you closed the wizard first, open the connection under Flows > Inbound. Its Field mapping tab and a Publish button are there.
  5. Switch the scenario on.

To check the connection by hand before wiring anything:

curl -X POST https://crm.gene.so/api/webhooks/inbound/<token> \
  -H 'Content-Type: application/json' \
  -d '{"first_name":"Test","phone":"+971501234567","notes":"webhook check"}'
Warning

Use a phone number you control. A live connection answers a test lead like any other, which means a real WhatsApp message.

When to use the API instead#

The webhook is for leads arriving. To read contacts back out, move deals along the pipeline, or keep another database in step, create a key under Settings > API.

The two are not interchangeable. A contact created through the API is filed quietly, on the assumption that a sync should not message anybody. The webhook is the path that starts a conversation.