Dropcontact + Odoo CRM: add a one‑click enrichment button
Published
What we’ll build
- A small addon that:
- stores your Dropcontact API key in System Parameters,
- adds a button on the Lead form: “Enrich with Dropcontact”,
- queues a background job that calls the Dropcontact batch API and polls until the result is ready,
- writes useful fields (verified email, job/role, company/website/LinkedIn) back on the lead,
- keeps the raw JSON payload on the record for audit/debugging.
Why a queue?
- Dropcontact is asynchronous (you submit a batch, then poll a request id). Using OCA
queue_job
keeps the UI snappy, handles retries/backoff, and avoids hitting rate limits.
Model changes (fields + button)
# mt_crm_dropcontact/models/crm_lead.py
from odoo import api, fields, models, _
from odoo.exceptions import UserError
try:
# OCA queue_job
from odoo.addons.queue_job.job import job
except Exception: # pragma: no cover
job = None
class CrmLead(models.Model):
_inherit = "crm.lead"
dc_email = fields.Char(string="DC Verified Email")
dc_phone = fields.Char(string="DC Phone")
dc_company = fields.Char(string="DC Company")
dc_website = fields.Char(string="DC Website")
dc_linkedin = fields.Char(string="DC LinkedIn")
dc_job = fields.Char(string="DC Job Title")
dc_status = fields.Selection([
("empty", "Not enriched"),
("queued", "Queued"),
("running", "Running"),
("done", "Done"),
("error", "Error"),
], default="empty", string="Dropcontact Status", readonly=True)
dc_last_result = fields.Json(string="DC Raw Result", readonly=True)
def action_enrich_dropcontact(self):
self.ensure_one()
if not self.email_from and not (self.partner_name or self.contact_name or self.name):
raise UserError(_("Provide at least an email or a name/company before enriching."))
self.write({"dc_status": "queued"})
if job:
self.with_delay(priority=50).job_enrich_dropcontact()
else:
# fallback (synchronous) – not recommended in production
self.job_enrich_dropcontact()
def _dc_build_payload(self):
"""Build a single‑record payload for Dropcontact batch API.
The API accepts multiple hints: email, firstname/lastname, company, website, linkedin.
"""
return {
"email": (self.email_from or "").strip() or None,
"firstname": (self.contact_name or "").split(" ")[0] if self.contact_name else None,
"lastname": (self.contact_name or self.partner_name or self.name or "").split(" ")[-1],
"company": (self.company_name or (self.partner_id and self.partner_id.name) or "") or None,
"website": (self.website or (self.partner_id and self.partner_id.website) or "") or None,
# You can also pass a LinkedIn profile URL when available
# "linkedin": self.x_linkedin_url or None,
}
@job
def job_enrich_dropcontact(self): # type: ignore[misc]
self.ensure_one()
icp = self.env["ir.config_parameter"].sudo()
token = icp.get_param("dropcontact.api_key")
if not token:
self.write({"dc_status": "error"})
raise UserError(_("Missing System Parameter dropcontact.api_key"))
import json
import time
import requests
headers = {
"Content-Type": "application/json",
"X-Access-Token": token, # Dropcontact header name
}
payload = {"data": [self._dc_build_payload()]}
# Submit batch
r = requests.post("https://api.dropcontact.io/batch", data=json.dumps(payload), headers=headers, timeout=30)
r.raise_for_status()
data = r.json()
request_id = data.get("request_id") or data.get("requestId")
if not request_id:
self.write({"dc_status": "error", "dc_last_result": data})
return
# Poll until ready (Dropcontact replies with status: waiting/running/done)
self.write({"dc_status": "running"})
for _ in range(30): # ~ up to ~90s
time.sleep(3)
g = requests.get(f"https://api.dropcontact.io/batch/{request_id}", headers=headers, timeout=30)
g.raise_for_status()
out = g.json()
status = out.get("status")
if status == "done":
# Extract the first result
items = out.get("data") or out.get("result") or []
first = items[0] if items else {}
# Common keys in Dropcontact responses
email = first.get("email") or first.get("email_finder")
phone = first.get("phone") or first.get("phone_number")
company = (first.get("company") or {}).get("name") if isinstance(first.get("company"), dict) else first.get("company")
website = (first.get("company") or {}).get("website") or first.get("website")
linkedin = first.get("linkedin") or (first.get("company") or {}).get("linkedin")
job = first.get("job") or first.get("job_title")
vals = {
"dc_email": email,
"dc_phone": phone,
"dc_company": company,
"dc_website": website,
"dc_linkedin": linkedin,
"dc_job": job,
"dc_last_result": out,
"dc_status": "done",
}
self.write(vals)
return
# timeout
self.write({"dc_status": "error"})
Add a button and fields on the form view
<!-- mt_crm_dropcontact/views/crm_lead_views.xml -->
<odoo>
<record id="view_crm_lead_form_dropcontact" model="ir.ui.view">
<field name="name">crm.lead.form.dropcontact</field>
<field name="model">crm.lead</field>
<field name="inherit_id" ref="crm.crm_lead_view_form"/>
<field name="arch" type="xml">
<xpath expr="//header" position="inside">
<button name="action_enrich_dropcontact" type="object" string="Enrich with Dropcontact"
class="oe_highlight" attrs="{'invisible':[('type','!=','opportunity')]}"/>
</xpath>
<xpath expr="//sheet/notebook" position="inside">
<page string="Dropcontact">
<group>
<field name="dc_status"/>
<field name="dc_email"/>
<field name="dc_phone"/>
<field name="dc_company"/>
<field name="dc_website"/>
<field name="dc_linkedin"/>
<field name="dc_job"/>
</group>
<group string="Raw JSON">
<field name="dc_last_result" widget="json"/>
</group>
</page>
</xpath>
</field>
</record>
</odoo>
System parameter & security
- Store the API key under Settings → Technical → System Parameters as
dropcontact.api_key
. - If you keep the synchronous fallback, ensure the server has outbound HTTPS; but prefer
queue_job
to avoid timeouts. - Add an access group if only specific roles should trigger enrichment.
Batch enrichment from the list view
- Add a contextual action (server action) that loops over selected leads and calls
action_enrich_dropcontact()
; jobs will queue individually.
Data mapping hints (based on the public API)
- URL:
https://api.dropcontact.io/batch
- Auth header:
X-Access-Token: <YOUR_TOKEN>
- Submit JSON
{"data": [ { "email": "…", "firstname": "…", "lastname": "…", "company": "…", "website": "…", "linkedin": "…" } ] }
- Response gives a
request_id
(orrequestId
) → pollGET /batch/{request_id}
untilstatus == "done"
. - The returned record typically includes
email
(verified),phone
/phone_number
,job
/job_title
, and acompany
object withname
,website
,linkedin
.
GDPR and consent
- Only enrich business contacts you’re allowed to process. Be transparent in your privacy notice and honor opt‑out requests.
Going further
- Mirror the same fields on
res.partner
and allow enrichment for existing contacts. - Limit concurrency through queue channels if you enrich thousands at once.
- Log each enrichment in a custom model for auditability and cost tracking.