Skip to main content
Socialift
CRM integration

Integrating Instagram, Lead Ads and Telegram with amoCRM and Bitrix24

To get Instagram Lead Ads or Telegram enquiries into amoCRM or Bitrix24 automatically, your own backend sits in the middle. It receives the platform webhook, verifies the signature, fetches the lead, normalises the phone to +998 format and writes to the CRM API. Socialift has no ready-made connector for these CRMs: the SDK covers the platform side, the CRM calls live in your code.

Updated:

How do you connect Instagram to amoCRM or Bitrix24?

There are two routes. One is the CRM's own channel integration (for example Bitrix24 Open Channels), which suits chat conversations. The other is your own backend: Meta webhook → signature check → fetch lead → normalise → CRM API. When you need ad attribution, your own duplicate rules and routing, the second route gives full control.

The Bitrix24 help center lists Instagram and Telegram among the channels you can connect to Open Channels, and amoCRM offers a Chats API for integrators. Those bring conversations into the CRM, but writing the Lead Ads campaign, ad and form IDs into the fields you choose usually takes your own code.

Socialift has no ready-made connector for amoCRM, Bitrix24 or any other CRM. Socialift is a Python SDK: it verifies webhook signatures, splits events, fetches the lead from the Graph API and normalises form answers. Below are the 20–40 lines that write the result into a CRM.

Architecture: from webhook to CRM record in 6 steps

Each step is independent, so a failure in one does not lose data.

  1. Wire up the webhook

    Subscribe your Meta app to the leadgen field and install the app on every Page (SDK: fb.subscribe_page(page_id, page_token, fields=["leadgen"])). For a Telegram bot: set_webhook(url, secret_token=...).

  2. Verify the signature

    Meta requests carry X-Hub-Signature-256: verify_request(app_secret, body, headers). Telegram does not sign the body — verify_telegram_request(headers, secret_token) compares the secret string in the header.

  3. Split events and deduplicate

    parse_events(payload) yields WebhookEvent objects. Store event.key in a unique column: Meta may deliver the same event again.

  4. Fetch the lead

    The leadgen webhook carries no form answers — only leadgen_id, page_id, form_id, ad_id and a timestamp. Get the full record with fb.lead(leadgen_id, page_token).

  5. Normalise

    normalize(lead.field_data) maps arbitrarily named fields to full_name, phone, email. Finish the phone as E.164 (+998...) yourself.

  6. Write to the CRM from a queue

    Return 200 to the webhook immediately and hand the CRM call to a background worker: search for a duplicate by phone, then create. Retry 429/503 with a delay.

Webhook receiver (aiohttp)

The handler only verifies, deduplicates and enqueues — the heavy work happens later.

webhook.py
import json, os
from aiohttp import web
from socialift.webhooks import verify_request, parse_events

APP_SECRET = os.environ["META_APP_SECRET"]

async def meta_webhook(request: web.Request) -> web.Response:
    body = await request.read()  # raw bytes — the signature covers these
    if not verify_request(APP_SECRET, body, request.headers):
        return web.Response(status=403)
    for event in parse_events(json.loads(body)):
        if event.field == "leadgen" and await mark_seen(event.key):
            await queue.put({"leadgen_id": event.leadgen_id, "page_id": event.page_id})
    return web.Response(text="ok")

mark_seen is your function: INSERT ... ON CONFLICT DO NOTHING, returning True for a new row. queue can be Redis, RabbitMQ or a database table.

Fetching the lead and normalising the phone to +998

The SDK's normalize() strips spaces and dashes and keeps +, but it does not add a country code.

lead.py
import re
from socialift import Facebook
from socialift.leads import normalize

def to_e164_uz(raw: str | None) -> str | None:
    digits = re.sub(r"\D", "", raw or "")
    if digits.startswith("00"):
        digits = digits[2:]
    if len(digits) == 12 and digits.startswith("998"):
        return "+" + digits
    if len(digits) == 9:  # 90 123 45 67
        return "+998" + digits
    return None  # route to manual review

async def load_lead(fb: Facebook, leadgen_id: str, page_token: str) -> dict:
    lead = await fb.lead(leadgen_id, page_token)
    person = normalize(lead.field_data)
    return {"lead_id": lead.id, "name": person.full_name, "email": person.email,
            "phone": to_e164_uz(person.phone), "phone_raw": person.phone,
            "form_id": lead.form_id, "ad_id": lead.ad_id,
            "campaign": lead.campaign_name, "organic": lead.is_organic}

An Uzbek number is 9 digits after +998. Never drop a number that fails the format: put phone_raw in a CRM note and let a manager handle it.

amoCRM vs Bitrix24 API: what you write with

amoCRM vs Bitrix24 API: what you write with — Capability matrix by platform
amoCRM (API v4)Bitrix24 (REST)
AccessOAuth 2.0; long-lived token for private integrationsInbound webhook (secret in the URL) or an OAuth app
Token lifetimeDocs example shows expires_in 86400 (24 h); refresh token lasts 3 months and can be exchanged onceOAuth: access 1 hour, refresh 180 days; a webhook runs with its creator's permissions
Create a leadPOST /api/v4/leads/complex — deal + contact + company in one call, up to 50crm.item.add (entityTypeId: 1); crm.lead.add still works but development has stopped
Phone fieldcustom_fields_values: field_code PHONE, enum_code WORK/MOBfm: typeId PHONE, valueType WORK/MOBILE
Duplicate searchGET /api/v4/contacts?query=...; duplicate control in complex (if enabled for the integration)crm.duplicate.findbycomm — PHONE/EMAIL, up to 20 values per call
Rate limit7 requests/s per integration, 50 requests/s per account; 429 when exceeded, 403 block on repeated violationsLeaky bucket: 2 requests/s, blocks at 50 (Enterprise: 5/s, 250); 503 QUERY_LIMIT_EXCEEDED
Errors401 unauthorised, 400 invalid dataerror and error_description in the body; bad webhook — 401 INVALID_CREDENTIALS

Values from the official amoCRM and Bitrix24 docs as of 2026-09-17. Limits change — re-check before launch.

amoCRM: deal and contact in one request

leads/complex creates the deal, the contact and the phone together. request_id comes back in the response — tie it to the lead ID.

amocrm.py
import os, aiohttp

AMO = f"https://{os.environ['AMO_SUBDOMAIN']}.amocrm.ru/api/v4"
HEADERS = {"Authorization": f"Bearer {os.environ['AMO_ACCESS_TOKEN']}"}

async def amo_create(session: aiohttp.ClientSession, lead: dict) -> dict:
    payload = [{
        "name": f"Lead Ads: {lead['campaign'] or lead['form_id']}",
        "request_id": lead["lead_id"],
        "_embedded": {
            "tags": [{"name": "instagram-lead-ads"}],
            "contacts": [{
                "first_name": lead["name"] or "Lead",
                "custom_fields_values": [{"field_code": "PHONE",
                    "values": [{"enum_code": "MOB", "value": lead["phone"]}]}],
            }],
        },
    }]
    async with session.post(f"{AMO}/leads/complex", json=payload, headers=HEADERS) as r:
        if r.status == 429:
            raise RetryLater(r.status)
        r.raise_for_status()
        return (await r.json())[0]  # id, contact_id, merged

merged: true in the response means amoCRM duplicate control found an existing contact and merged into it. If duplicate control is off, search first with GET /api/v4/contacts?query=<phone>.

Attribution fits well in a note: POST /api/v4/leads/{id}/notes with note_type: "common" and the form, ad and campaign in params.text.

Bitrix24: inbound webhook and crm.item.add

The webhook URL is the credential: keep it in an environment variable and never ship it in frontend code.

bitrix24.py
import os, aiohttp

B24 = os.environ["B24_WEBHOOK_URL"]  # https://<portal>/rest/<user_id>/<secret>/

async def b24(session: aiohttp.ClientSession, method: str, params: dict):
    async with session.post(f"{B24}{method}.json", json=params) as r:
        data = await r.json()
    if "error" in data:  # QUERY_LIMIT_EXCEEDED -> retry later
        raise B24Error(data["error"], data.get("error_description"))
    return data["result"]

async def b24_create(session: aiohttp.ClientSession, lead: dict) -> int:
    dup = await b24(session, "crm.duplicate.findbycomm",
                    {"entity_type": "LEAD", "type": "PHONE", "values": [lead["phone"]]})
    if isinstance(dup, dict) and dup.get("LEAD"):
        return dup["LEAD"][0]
    res = await b24(session, "crm.item.add", {"entityTypeId": 1, "fields": {
        "title": f"Lead Ads: {lead['campaign'] or lead['form_id']}",
        "name": lead["name"],
        "utmCampaign": lead["campaign"],
        "sourceDescription": f"form={lead['form_id']} ad={lead['ad_id']}",
        "fm": [{"typeId": "PHONE", "valueType": "MOBILE", "value": lead["phone"]}],
    }})
    return res["item"]["id"]

Bitrix24 recommends detecting errors by the error field in the body rather than the HTTP status — errors inside a batch come back with 200.

If the portal runs simple CRM mode there are no leads: a created lead turns into a deal immediately. Check the mode with crm.settings.mode.get. Valid sourceId values come from crm.status.list (ENTITY_ID = SOURCE).

Attribution: where Lead Ads fields go in the CRM

The SDK's Lead object returns form and ad identifiers. Keep them — otherwise you cannot tell which campaign produced a sale.

Attribution: where Lead Ads fields go in the CRM — Capability matrix by platform
Lead fieldamoCRMBitrix24
form_idTag or custom field; in a notesourceDescription
ad_id, ad_nameCustom field or noteutmContent or comments
campaign_id, campaign_nameDeal name and tagutmCampaign
is_organicTag (organic / paid)sourceId (a source configured in the portal)
lead.idrequest_id and notecomments or custom field

Need the ad set, campaign objective and targeting? fb.ad_context(ad_id, token) returns them; without ads_management it returns an empty dict and never blocks the lead flow.

How do you send Telegram bot enquiries to a CRM?

Telegram bot webhooks are not signed: the string you pass to set_webhook(url, secret_token=...) comes back on every request in the X-Telegram-Bot-Api-Secret-Token header. Check it with verify_telegram_request(headers, secret_token).

The official way to ask for a phone number is a request_contact keyboard button: when pressed, the user's number arrives as a contact message (private chats only). Normalise contact.phone_number to +998 exactly as for Lead Ads and pass it to the same amo_create or b24_create.

Use update_id, or chat ID plus message ID, as the dedupe key. When the same customer messages the bot several times, adding a note to the existing lead is usually better than creating a new one.

Duplicates, retries and reliability

  • Two-level dedupe: event.key at the webhook level (unique column), phone search at the CRM level.
  • amoCRM 429 and Bitrix24 QUERY_LIMIT_EXCEEDED are transient: retry with exponential backoff and jitter.
  • amoCRM blocks the account (403) after repeated limit violations — cap worker concurrency, stop and alert on 403.
  • amoCRM refresh tokens are single-use: persist the new access/refresh pair in one transaction.
  • Backfill any period the webhook missed with fb.leads(form_id, page_token, since=...).
  • Never log phones, emails or tokens; treat the Bitrix24 webhook URL as a secret.
  • Don't drop a number you couldn't normalise — keep the raw value in a note for a manager.

← Back to blog

Frequently asked questions

Does Socialift have a ready-made amoCRM or Bitrix24 connector?

No. Socialift is a Python SDK and dashboard for developers. It covers the Instagram, Facebook, Telegram and other platform side; you write the code that calls the CRM API in your own backend. The samples in this article are the core of that code.

Does the Lead Ads webhook include the customer's phone number?

No. The leadgen webhook only sends identifiers: leadgen_id, page_id, form_id, ad_id and a timestamp. Form answers are fetched with a separate Graph API call — fb.lead(leadgen_id, page_token) in the SDK.

Should I use crm.lead.add or crm.item.add in Bitrix24?

For a new integration, crm.item.add with entityTypeId: 1. The Bitrix24 docs state that development of crm.lead.add has stopped and recommend crm.item.add. Phones are passed in crm.item.add through the fm array.

What are the amoCRM API rate limits?

According to the docs, no more than 7 requests per second per integration and up to 50 requests per second for the whole account. Exceeding them returns 429; repeated violations block the account with 403. Sending no more than 50 deals per request is recommended.

What does QUERY_LIMIT_EXCEEDED mean in Bitrix24?

It is the request-intensity limit: when the counter passes its threshold, Bitrix24 returns 503 with QUERY_LIMIT_EXCEEDED. On regular plans the counter drains at 2 per second and blocks at 50; on Enterprise it is 5 and 250. The fix is a queue and a short delayed retry.

Sources

  1. amoCRM — Leads API, complex creation
  2. amoCRM — API recommendations and limits
  3. amoCRM — OAuth 2.0 step by step
  4. Bitrix24 — crm.item.add
  5. Bitrix24 — REST API limits
  6. Meta — Retrieving Leads

Related pages