uCheckeruChecker
Blog/Verification
For developersApril 6, 202613 min read

Email Validation in CRM: Bitrix24, amoCRM, and HubSpot Integration

Webhook on contact creation, bulk validation before sending, automatic segmentation by result. Working examples in PHP, Python, and Node.js.

A CRM stores contacts. An email validator checks addresses. Each tool works fine on its own. Together, they close the gap that lets invalid addresses, spam traps, and disposable inboxes pile up in your database.

This article covers specific integrations for Bitrix24, amoCRM, and HubSpot: architecture, code, and the rough edges you will actually hit. No abstract diagrams, just things you can implement today.

Why validate email inside a CRM

The typical sequence without validation: a sales rep enters a contact, marketing sends a campaign a week later, the message hits a nonexistent address, and your ESP logs a hard bounce. Twenty addresses like that and your domain reputation starts to slip. Fifty and your mail goes to the spam folder for everyone, including the real subscribers.

The problem compounds when contacts flow from multiple sources: a web form, an Excel import, manual entry by a rep, a LinkedIn scrape. Each source brings its own share of bad data. Forms get typos and disposable addresses. Excel files carry stale contacts. Manual entry produces simple mistakes. Without validation at the point of entry, the CRM turns into a collector of broken addresses.

The fix is to check the address the moment a contact is created or updated. Not once a month, not before a campaign but right away. That cuts off the bad data before it reaches your segments and sending lists.

General architecture: CRM plus validation API

Regardless of which CRM you use, the pattern is the same. Three integration points cover every scenario:

1.

Webhook on contact creation

The CRM fires an event to your server. Your server calls the validation API, gets the result, and writes the status back to the CRM. Round-trip latency is typically 2 to 5 seconds.

2.

Bulk validation of an existing database

Export contacts via the CRM API, submit the address list for bulk validation, then write statuses back once the job finishes. Use this for the initial cleanup.

3.

Scheduled re-validation

A cron job once a month picks contacts not checked in 30 days and re-validates them. Inboxes get deactivated: an address valid in January may not exist in March.

For storing results, create a custom field in the CRM, for example email_validation_status with options: valid, invalid, risky, unknown. Add a separate email_validated_at date field. That lets you build segments: “valid addresses only” for campaigns, “risky” for manual review by a rep.

Bitrix24: inbound webhook plus REST API

Bitrix24 supports outgoing webhooks on CRM events. The one you need is ONCRMCONTACTADD, which fires when a new contact is created. For updates, use ONCRMCONTACTUPDATE.

First, create the custom fields in Bitrix24: go to CRM → Settings → Custom Fields, add UF_EMAIL_VALID (list type) and UF_EMAIL_CHECKED (date type). Note the field IDs, you will need them in the code.

PHP webhook handler

Bitrix24 sends a POST request with the contact ID. Your server fetches the email via REST API, submits it for validation, then writes the result back.

PHPbitrix24-webhook-handler.php
<?php
// Webhook endpoint: POST /webhooks/bitrix24-contact

$contactId = $_POST['data']['FIELDS']['ID'] ?? null;
if (!$contactId) { http_response_code(400); exit; }

$bitrixWebhook = getenv('BITRIX24_WEBHOOK_URL');
// e.g. https://your-domain.bitrix24.ru/rest/1/abc123xyz/

// 1. Fetch contact email from Bitrix24
$contact = json_decode(file_get_contents(
    $bitrixWebhook . "crm.contact.get?ID=" . $contactId
), true)['result'];

$email = $contact['EMAIL'][0]['VALUE'] ?? null;
if (!$email) exit;

// 2. Validate via uChecker API
$ch = curl_init('https://api.uchecker.net/api/v1/validate/single');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'x-api-key: ' . getenv('UCHECKER_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['email' => $email]),
    CURLOPT_RETURNTRANSFER => true,
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);

$taskId = $result['task_id'];

// 3. Poll for result (in production use webhook_url instead)
sleep(5);
$ch = curl_init("https://api.uchecker.net/api/v1/tasks/$taskId");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => [
        'x-api-key: ' . getenv('UCHECKER_API_KEY'),
    ],
    CURLOPT_RETURNTRANSFER => true,
]);
$task = json_decode(curl_exec($ch), true);
curl_close($ch);

$status = $task['result']['status'] ?? 'unknown';
// Map to your custom field values
$statusMap = [
    'deliverable'   => 'valid',
    'undeliverable' => 'invalid',
    'risky'         => 'risky',
    'unknown'       => 'unknown',
];

// 4. Write result back to Bitrix24
file_get_contents($bitrixWebhook . "crm.contact.update?" . http_build_query([
    'ID'     => $contactId,
    'FIELDS' => [
        'UF_EMAIL_VALID'   => $statusMap[$status] ?? 'unknown',
        'UF_EMAIL_CHECKED' => date('Y-m-d'),
    ],
]));
!

In production, replace sleep(5) with a webhook_url callback. Pass webhook_url in the validation request and uChecker will POST the result to that URL when the check finishes. This avoids polling and keeps the handler thread free.

For on-premise Bitrix24 (self-hosted), the logic is identical, only the webhook URL differs. Cloud and self-hosted versions use the same REST API. If you manage multiple portals, store each webhook URL in config and select it by a source parameter in the request.

amoCRM: Digital Pipeline plus webhook

amoCRM exposes contact events through webhooks. To configure one: Settings → Integrations → create an integration with type “External handler” and point it at your server URL. amoCRM will POST when a contact is added or updated.

One thing to watch: amoCRM uses OAuth 2.0. For a server integration you need a long-lived token via the client_credentials flow, or periodic refresh via refresh_token. Store tokens in a database and refresh them automatically.

Python webhook handler

Pythonamocrm_webhook.py
import os, requests
from flask import Flask, request, jsonify

app = Flask(__name__)

UCHECKER_KEY = os.environ["UCHECKER_API_KEY"]
AMO_TOKEN    = os.environ["AMOCRM_ACCESS_TOKEN"]
AMO_DOMAIN   = os.environ["AMOCRM_DOMAIN"]  # e.g. mycompany

def validate_email(email: str) -> dict:
    """Send email to uChecker, return validation result."""
    resp = requests.post(
        "https://api.uchecker.net/api/v1/validate/single",
        headers={"x-api-key": UCHECKER_KEY},
        json={"email": email},
    )
    task_id = resp.json()["task_id"]

    # Poll until done (use webhook_url in production)
    import time
    for _ in range(10):
        time.sleep(3)
        r = requests.get(
            f"https://api.uchecker.net/api/v1/tasks/{task_id}",
            headers={"x-api-key": UCHECKER_KEY},
        )
        data = r.json()
        if data.get("status") == "completed":
            return data["result"]
    return {"status": "unknown"}

def update_amo_contact(contact_id: int, validation_status: str):
    """Write validation result back to amoCRM custom field."""
    requests.patch(
        f"https://{AMO_DOMAIN}.amocrm.ru/api/v4/contacts/{contact_id}",
        headers={
            "Authorization": f"Bearer {AMO_TOKEN}",
            "Content-Type": "application/json",
        },
        json={
            "custom_fields_values": [
                {
                    "field_id": 123456,  # your custom field ID
                    "values": [{"value": validation_status}],
                }
            ]
        },
    )

@app.route("/webhooks/amocrm", methods=["POST"])
def handle_amocrm_webhook():
    payload = request.json or {}
    contacts = payload.get("contacts", {}).get("add", [])
    contacts += payload.get("contacts", {}).get("update", [])

    for contact in contacts:
        contact_id = contact["id"]
        # Fetch full contact to get email
        r = requests.get(
            f"https://{AMO_DOMAIN}.amocrm.ru/api/v4/contacts/{contact_id}",
            headers={"Authorization": f"Bearer {AMO_TOKEN}"},
        )
        fields = r.json().get("custom_fields_values", [])
        email = None
        for f in fields:
            if f["field_code"] == "EMAIL":
                email = f["values"][0]["value"]
                break

        if not email:
            continue

        result = validate_email(email)
        status = result.get("status", "unknown")
        update_amo_contact(contact_id, status)

    return jsonify({"ok": True})

Note the field_id: amoCRM assigns a numeric ID to each custom field. Retrieve it via GET /api/v4/contacts/custom_fields, then replace the placeholder 123456 with the real value.

If contacts arrive in bursts, move validation into a task queue: Celery, RQ, or similar. Your webhook handler must respond within 200 to 300 milliseconds, or amoCRM will stop delivering events to it.

HubSpot: Workflows plus custom coded action

HubSpot gives you two options. First: Workflows with a custom coded action (Operations Hub Professional). Second: a webhook subscription via the API. For most teams, Workflows are easier, you get a visual editor, built-in logging, and automatic retries on failure.

Create a workflow with the trigger “Contact is created.” Add a “Custom coded action” step. Inside it, a Node.js script calls the uChecker API and writes the result to a contact property.

Custom coded action (Node.js)

Node.jshubspot-workflow-action.js
// HubSpot Custom Coded Action
// Input: contact email (configured in workflow)
// Secrets: UCHECKER_API_KEY (add in workflow settings)

const hubspot = require("@hubspot/api-client");
const axios = require("axios");

exports.main = async (event, callback) => {
  const email = event.inputFields["email"];
  if (!email) return callback({ outputFields: {} });

  const apiKey = process.env.UCHECKER_API_KEY;

  // 1. Submit for validation
  const { data: task } = await axios.post(
    "https://api.uchecker.net/api/v1/validate/single",
    { email },
    { headers: { "x-api-key": apiKey } }
  );

  // 2. Poll for result
  let result = null;
  for (let i = 0; i < 10; i++) {
    await new Promise((r) => setTimeout(r, 3000));
    const { data } = await axios.get(
      `https://api.uchecker.net/api/v1/tasks/${task.task_id}`,
      { headers: { "x-api-key": apiKey } }
    );
    if (data.status === "completed") {
      result = data.result;
      break;
    }
  }

  const status = result?.status || "unknown";

  // 3. Update contact property
  const hsClient = new hubspot.Client({
    accessToken: process.env.HUBSPOT_TOKEN,
  });

  await hsClient.crm.contacts.basicApi.update(
    event.object.objectId,
    {
      properties: {
        email_validation_status: status,
        email_validated_date: new Date().toISOString().split("T")[0],
      },
    }
  );

  callback({ outputFields: { validation_status: status } });
};

Before running this, create the contact properties in HubSpot: Settings → Properties → Create property. Use type Single-line text for email_validation_status and Date for email_validated_date. The internal names must match exactly what the code references.

If you don't have Operations Hub, use an external server with the HubSpot webhook subscription API. Subscribe to the contact.creation event and handle it like the Bitrix24 and amoCRM examples above. Webhook subscription requires a challenge verification: HubSpot sends a GET request with a parameter, and your server must return that value in the response.

Bulk cleanup of an existing database

A webhook integration covers new contacts going forward. But what about the thousands of addresses already in the CRM? Those need a one-time bulk validation.

The process works the same for any CRM: export contacts via the API, collect email addresses into a list, submit them for bulk validation, wait for the job to finish, then update statuses back. Here is a universal script for that.

Pythonbulk_validate_crm.py
import os, time, requests

API_KEY = os.environ["UCHECKER_API_KEY"]
BASE    = "https://api.uchecker.net"
HEADERS = {"x-api-key": API_KEY, "Content-Type": "application/json"}

def bulk_validate(emails: list[str]) -> dict:
    """Submit bulk validation and wait for results."""
    # 1. Create bulk task
    resp = requests.post(
        f"{BASE}/api/v1/validate/bulk",
        headers=HEADERS,
        json={"emails": emails},
    )
    task_id = resp.json()["task_id"]
    print(f"Bulk task created: {task_id}, emails: {len(emails)}")

    # 2. Poll until complete
    while True:
        time.sleep(10)
        r = requests.get(f"{BASE}/api/v1/tasks/{task_id}", headers=HEADERS)
        data = r.json()
        if data["status"] == "completed":
            return data["results"]  # {email: status, ...}
        if data["status"] == "failed":
            raise RuntimeError(f"Task {task_id} failed")
        print(f"  status: {data['status']}, progress: {data.get('progress', '?')}")

# Usage example:
# contacts = fetch_contacts_from_crm()
# emails = [c["email"] for c in contacts if c.get("email")]
# results = bulk_validate(emails)
# for contact in contacts:
#     status = results.get(contact["email"], "unknown")
#     update_crm_contact(contact["id"], status)

For large databases (over 10,000 addresses), split into batches. The uChecker bulk API accepts up to 50,000 addresses per request, but write status updates back to the CRM in batches of 100 to 500 contacts. That keeps you within the CRM's API rate limits and avoids a flood of concurrent requests.

Bulk validation results map naturally to segments. Contacts with status invalid: remove or flag as “do not send.” Status risky: send only transactional or high-priority messages. Valid: full send.

Error handling and edge cases

Any integration between two external systems will hit edge cases. Here is what actually breaks and how to handle it.

Validation API timeout

uChecker responds within 2 to 5 seconds for a single validation. If there is no response after 15 seconds, set the status to unknown and add the contact to a retry queue. Do not block the contact from being created in the CRM.

CRM API rate limits

Bitrix24 allows 2 requests per second per webhook. amoCRM allows 7 per second. HubSpot private apps get 100 requests per 10 seconds. During bulk updates, use batch endpoints where available and add delays between requests.

Duplicate contacts

The same email address can appear on multiple contacts. Cache validation results on your server (Redis, SQLite) for 24 hours. If the same address arrives again, use the cached result instead of spending an API credit.

Contacts without an email

Not every CRM contact has an email address. Check that the field is present before submitting to the API. This is obvious, but it is where roughly half of all handlers fail in practice.

Log every step: incoming webhook, API request to the validator, result, CRM update. When debugging an integration between two systems, logs save hours. Keep them for at least 7 days.

Implementation checklist

Whatever CRM you are working with, the sequence is the same. Here are the steps that actually work.

1.Create custom fields in the CRM: validation_status and validated_at
2.Get a uChecker API key at app.uchecker.net
3.Deploy the webhook handler on your server (PHP, Python, or Node.js)
4.Configure the outgoing webhook in the CRM for the contact creation event
5.Test on 5 to 10 contacts: create them manually, verify the status gets written
6.Add error handling: timeouts, retries, result caching
7.Run the bulk validation on the existing database
8.Set up a cron job for monthly re-validation
9.Create a CRM segment that filters to contacts with status valid
10.Set up monitoring: an alert if the webhook handler stops responding

From API key to a working webhook handler takes 2 to 4 hours for an experienced developer. Bulk-cleaning a database of 10,000 contacts adds another 30 to 40 minutes, including writing statuses back to the CRM.

Email validation in a CRM is not a one-time cleanup. It is a process: checking on entry, re-validating on a schedule, segmenting by status. Addresses expire, and a result from three months ago guarantees nothing.

The result of the integration is easy to measure: bounce rate before and after. If you were at 5 to 8% and it drops below 2%, the integration is working. If bounce rate climbs again two to three months later, scheduled re-validation has probably broken. One metric, no ambiguity.

Connect validation to your CRM

Sign up at uChecker, get your API key and 30 free checks to test the integration. API reference is in the developer guide.

Try for free
email validation CRMCRM email verificationBitrix24 email validationamoCRM email checkHubSpot email validationwebhook CRMbulk email validationemail validation API