# EspoCRM Connector

> Sync EspoCRM accounts, contacts, leads and opportunities into Odoo over the REST API, with a sync control tower and field-mapping studio.

- **What it is:** Sync EspoCRM accounts, contacts, leads and opportunities into Odoo over the REST API, with a sync control tower and field-mapping studio.
- **For:** Sales
- **Odoo versions:** 16.0, 17.0, 18.0, 19.0, 20.0 - Community and Enterprise
- **Runs on:** Odoo.sh or Odoo you host yourself - not Odoo Online, which accepts no third-party apps
- **Price:** 89 EUR, one-time, on the Odoo Apps Store
- **Technical name:** bambooforge_espocrm_connector
- **Run for real:** Tested for real against EspoCRM 9.3: 3 accounts, 5 contacts, 4 leads and 3 opportunities came into Odoo with 0 errors and every binding accounted for.
- **Not for:** The connector reads Accounts, Contacts, Leads and Opportunities from your EspoCRM server and writes the corresponding Odoo res.partner and crm.lead records. It never writes, updates or deletes anything in EspoCRM.; Credentials (API key, webhook secret) are stored in administrator-only fields.; In scope today: read-only import of the four entities, per-field mapping with transforms, schema introspection, dry-run + validation + rollback, a resilient queue, and scheduled import/reconciliation.; Out of scope / roadmap: write-back to EspoCRM, inbound webhooks (the route is a no-op), and incremental modifiedAt filtering (v1 lists by maxSize/offset).
- **Manual:** https://docs.bambooforge.dev/bambooforge_espocrm_connector/
- **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_espocrm_connector.action_espocrm_control_tower
- **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_espocrm_connector
- **Manual last updated:** 2026-10-15

---
# EspoCRM Connector

A read-only, import-first bridge from EspoCRM into Odoo 20: pull your Accounts,
Contacts, Leads and Opportunities into native Odoo contacts and CRM records over the
EspoCRM REST API — with a resilient job queue, dry-run safety, pre-flight validation and
rollback so you stay in control. The connector never writes back to EspoCRM.

This page is the complete manual. If you follow it top to bottom you can install,
connect, run your first import, automate it, and fix the common issues **without
contacting support**.

## Overview

The connector reads from EspoCRM over its REST API and creates/updates the matching Odoo
records. It is **one-way only — Odoo ← EspoCRM** (pull). Nothing is ever written back to
EspoCRM.

It covers four EspoCRM source entities mapped onto two Odoo models:

| EspoCRM entity | Lands in Odoo as | Notes |
|---|---|---|
| Account | `res.partner` (company) | Imported as a company contact (`is_company = True`). |
| Contact | `res.partner` (individual) | Imported as an individual, linked to its account where available. |
| Lead | `crm.lead` (type *Lead*) | The raw EspoCRM status is kept on the record. |
| Opportunity | `crm.lead` (type *Opportunity*) | Amount, close date, probability and the raw EspoCRM stage are kept. |

Because four EspoCRM entities map onto only two Odoo models, the connector is keyed by the
**EspoCRM entity** end to end. Accounts and Contacts never collide on `res.partner`, and
Leads never collide with Opportunities on `crm.lead`.

Every remote call goes through a queue: nothing is written to Odoo until a job runs, jobs
retry with back-off on transient failures, and a circuit breaker pauses an instance that
keeps failing.

## Tested against the real thing

Automated tests run against a bundled mock on every release; the line below is the part no mock can give you - the connector pointed at the real platform, with real records.

Tested for real against EspoCRM 9.3: 3 accounts, 5 contacts, 4 leads and 3 opportunities came into Odoo with 0 errors and every binding accounted for.

## Requirements

- **Odoo**: 20.0, Community or Enterprise. The connector depends on *Contacts*
  (`contacts`) and *CRM* (`crm`), which install automatically.
- **EspoCRM**: any EspoCRM server exposing the standard REST API at `/api/v1`. The
  connector uses only generic list and single-record endpoints, so it is version-tolerant.
  EspoCRM must be reachable from the Odoo server.
- **Python**: no extra libraries beyond a standard Odoo 20 install.
- **Odoo access**: any internal user can open the connector screens. The EspoCRM API key
  and webhook secret are stored in system-only fields, so only a **Settings /
  Administration** user can read or change them.
- **Network**: outbound HTTPS from Odoo to your EspoCRM server. By default the connector
  refuses internal/loopback/private hosts as an SSRF safeguard (see *Safety features*).

## Installation

1. Copy `bambooforge_espocrm_connector` into your Odoo addons path.
2. Restart the Odoo service.
3. Open **Apps**, click **Update Apps List**, search for *EspoCRM*, and press
   **Activate / Install**. Dependencies (*Contacts*, *CRM*) install automatically.

No EspoCRM server is required to evaluate the connector: a mock EspoCRM REST API ships
inside the module, so you can install, explore and run the full import flow against sample
data before pointing it at a real server. To use it, set the instance **Base URL** to
`<your-odoo-url>/espocrm/mock_api` with **API Path** `api/v1` and any non-empty API
key.

## Step 1 — Create an API key in EspoCRM

The connector authenticates with a single **API key**, sent as the `X-Api-Key` header on
every request. There is no OAuth and no token exchange. Create a read-capable API user in
EspoCRM:

1. Sign in to EspoCRM as an administrator.
2. Go to **Administration ▸ API Users**.
3. Click **Create API User**.
4. Set **Authentication Method** to **API Key**. EspoCRM generates the key for you.
5. Give the API user **read access** to *Accounts*, *Contacts*, *Leads* and
   *Opportunities* via its **Roles** (read-only is enough — the connector never writes to
   EspoCRM).
6. Save, then copy the generated **API Key**. You will paste it into Odoo in Step 2.

Keep the key somewhere safe; EspoCRM shows it on the API user record, but treat it like a
password.

## Step 2 — Create the connection in Odoo

Open **EspoCRM Connector ▸ Configuration ▸ Instances** and create a record. The fields
that matter:

| Field | What to enter |
|---|---|
| Name | A label for this server, e.g. *Production EspoCRM*. Must be unique per company. |
| Company | The Odoo company that owns the imported records. |
| Base URL | Your EspoCRM server root, e.g. `https://crm.example.com`. For the bundled demo, use `<your-odoo-url>/espocrm/mock_api`. |
| Authentication | **API Key** — the only supported method. The key is sent as the `X-Api-Key` header. |
| API Key | The key from Step 1 (visible to administrators only). |
| API Path | The REST path. Leave the default `api/v1` unless your server differs. |
| Verify SSL | Keep on for production. Turn off only for self-signed test certificates. |
| Allow internal host | Off by default. Turn on only to reach an EspoCRM on localhost or a private network (this lowers the SSRF guard — see *Safety features*). |
| Timeout (s) | HTTP timeout per request. Default `30`. |

Then click **Test Connection**. The connector calls a lightweight EspoCRM endpoint
(`App/user`, falling back to a one-row `Account` list) and, on success, sets the state
to **Connected** and seeds default Lead/Opportunity status mappings. If it fails, the exact
error is stored on the form (**last connection error**) and recorded in **Logs** (see
*Troubleshooting*).

Tip: use **Quick Setup** (button on the instance) to pick a use case and apply a
recommended set of auto-import toggles and a safety profile in one step.

## Step 3 — First import (dry-run, then live)

New instances start with **Dry-run** ON. In dry-run, import jobs *simulate* writes: instead
of creating or changing Odoo records they produce **Validation Results** you can review
under **EspoCRM Connector ▸ Operations ▸ Validation Results**. This lets you confirm what
*would* happen before anything is written.

To run a first import:

1. On the instance, open the **Import** tab and click **Queue Account Import** (and/or
   **Queue Contact Import**, **Queue Lead Import**, **Queue Opportunity Import**). This
   enqueues jobs; it does not block the UI.
2. Jobs are processed by the **Espocrm Queue Processor** scheduled action (every minute),
   or immediately if you trigger it from **Operations ▸ Queue Jobs**.
3. Review **Validation Results** while still in dry-run.
4. When satisfied, open the instance, turn **Dry-run** OFF, and run the imports again to
   write the records for real.

Imported records land in the standard Odoo apps:

- Accounts and Contacts → **Contacts** (companies and individuals).
- Leads and Opportunities → **CRM** (the pipeline; Leads as type *Lead*, Opportunities as
  type *Opportunity*).

Each imported record is tracked by a **binder** keyed on the EspoCRM entity and remote id,
so re-imports update the same Odoo record instead of duplicating it.

## Field mapping & customization

- **Field Mappings** (**Configuration ▸ Field Mappings**) map EspoCRM source fields to
  Odoo target fields, per EspoCRM entity, in the **EspoCRM → Odoo** direction. Click
  **Suggest Mappings** on the instance to seed the business-critical defaults, then adjust.
  Out of the box the suggested mappings include, for example:
  
  | Entity | EspoCRM source path | Odoo target field |
  |---|---|---|
  | Account | `name` / `website` / `emailAddress` / `phoneNumber` | `name` / `website` / `email` / `phone` |
  | Contact | `name` / `emailAddress` / `phoneNumber` | `name` / `email` / `phone` |
  | Lead | `name` / `emailAddress` / `accountName` | `name` / `email_from` / `partner_name` |
  | Opportunity | `name` / `amount` / `probability` | `name` / `expected_revenue` / `probability` |
- **Per-field transforms**: each mapping can coerce values (String, Integer, Float,
  Boolean, Uppercase/Lowercase, JSON String, CSV Join, First Item) or run a safe Python
  **Expression** (variables `value` and `default_value`). A **Preview** shows the
  result on a sample input before you save.
- **Schema Fields** (**Configuration ▸ Schema Fields**) lists the discovered EspoCRM fields
  per entity alongside the Odoo target fields. Click **Schema Introspection** on the
  instance to refresh it; the connector samples a live record (or the bundled mock when the
  API is unreachable) and flattens its shape.
- **Suggest Mappings** vs **Schema Introspection**: run introspection first if you want the
  *intelligent* suggestions (name/type similarity scored) rather than only the core
  blueprint set.

## Automation (scheduled actions)

The module ships these scheduled actions (**Settings ▸ Technical ▸ Scheduled Actions**):

| Scheduled action | Default | Purpose |
|---|---|---|
| Espocrm Queue Processor | every 1 min | Processes queued import jobs. |
| Espocrm Reconciliation | every 15 min | Pulls recent remote records for entities with auto-reconcile enabled and re-queues any that are missing or previously failed. |
| Espocrm Maintenance | every 1 hr | Recovers stale/locked jobs and trims old jobs and logs (per the retention settings). |
| Espocrm Flow Scheduler | every 5 min | Runs scheduled import flows. |
| Espocrm Flow Metrics | every 1 hr | Aggregates flow-run metrics. |
| Espocrm Auto Recover | every 15 min | Reopens a tripped circuit breaker once the cooldown passes and no blocking validation issues remain. |

Turn on **Auto-import** / **Auto-reconcile** per entity on the instance (Accounts,
Contacts, Leads, Opportunities) to let the scheduled actions keep things in sync
hands-free.

**Webhooks (not used in this version).** EspoCRM can emit outbound webhooks, but wiring
them into the import queue is a roadmap item. A route at `/espocrm/webhook` exists only so
that existing subscriptions or health checks do not return 404 — it acknowledges with HTTP
200 and **imports nothing**. Do not rely on it to bring in data; use the scheduled import
and reconciliation actions instead.

## Safety features

- **Dry-run mode** (`dry_run_mode`, ON by default) — import jobs simulate writes and
  produce Validation Results to review before you go live.
- **Pre-flight validation** (`validation_enabled`) with **Minimal / Standard / Strict**
  business-validation profiles — gates risky writes; *Standard* is the default. Use
  **Run Validation Sweep** to validate pending/failed jobs on demand, and **Auto-fix Open
  Issues** to clear the ones the connector can resolve.
- **Resilient queue** — every import is a job with retry and configurable back-off
  (fixed or exponential), plus a dead-letter state for jobs that exhaust their retries.
- **Circuit breaker** (`circuit_state`: *Open / Protected / Tripped*) — after repeated
  failures (default threshold **5**), or immediately on an auth/configuration error, the
  instance auto-pauses (*Tripped*). The **Espocrm Auto Recover** action reopens it after a
  30-minute cooldown, or click **Resume Sync**.
- **Rollback snapshots** (`rollback_enabled`) — imports capture a snapshot so you can undo
  a batch from **Operations ▸ Rollback Snapshots**.
- **SSRF guard** — the connector refuses internal/loopback/private/reserved hosts (including
  cloud metadata addresses) for the Base URL unless **Allow internal host** is explicitly
  enabled.
- **Safety profiles** — *Conservative / Balanced / Aggressive* presets set dry-run,
  validation, batch size and delete strategy together; click **Apply Safety Profile**.
- **Credential isolation** — the API key and webhook secret live in system-only fields,
  readable and editable only by administrators.

## Troubleshooting

| Symptom | Cause and fix |
|---|---|
| Test Connection fails with 401/403 | Wrong or missing API key, or the API user lacks read access. Re-check Step 1 and the **API Key** field; confirm the API user's role grants read on Accounts/Contacts/ Leads/Opportunities. |
| "Base URL is not allowed … non-public address" | The Base URL points at localhost or a private/reserved IP. For a self-hosted server, enable **Allow internal host** on the instance (test/self-hosted only). |
| SSL errors on Test Connection | Self-signed or invalid certificate. Use a valid cert, or turn off **Verify SSL** for testing only. |
| "EspoCRM resource not found" / 404 | Wrong **API Path** or Base URL. The default path is `api/v1`; the connector calls `<base>/api/v1/<Entity>`. Confirm the server root has no trailing path. |
| "EspoCRM rate limit reached" (429) | The server is throttling. The client retries with back-off automatically; lower the per-entity **import limit** or raise **API retry** settings if it persists. |
| Jobs stay in *Pending* | The Queue Processor is off or the instance is paused. Confirm the **Espocrm Queue Processor** scheduled action is active and the instance is not **Paused**. |
| Instance shows *Tripped* | The circuit breaker tripped after repeated failures (or an auth error). Fix the server/credentials; **Auto Recover** reopens it after cooldown, or click **Resume Sync**. |
| Nothing is written after Import | You are in **Dry-run**. Review **Validation Results**, then turn dry-run off and re-run the import. |
| Records imported twice | Imports are keyed by EspoCRM entity + remote id via the binder, so this should not happen. If it does, check that two instances do not point at the same EspoCRM server. |
| Jobs end up *Dead* | They exhausted their retries. Open **Operations ▸ Queue Jobs** (Dead Letter), read the error on the job and in **Logs**, fix the cause, then re-queue. |

For anything else, **Operations ▸ Logs** records every API call, payload and error with a
timestamp.

## Frequently asked questions

**Does this connector write anything back to EspoCRM?**
No. It is read-only and import-first: it lists and fetches records from EspoCRM and creates
or updates the matching Odoo records. Write-back to EspoCRM is a roadmap item.

**Which versions are supported?**
Odoo 20.0 on the Odoo side. On the EspoCRM side the connector uses only the generic
`/api/v1` list and single-record endpoints, so it is version-tolerant. Validate your
exact build with the bundled mock first.

**Do I need an EspoCRM server to evaluate it?**
No. A mock EspoCRM REST API ships inside the module. Point the Base URL at
`<your-odoo-url>/espocrm/mock_api` (API Path `api/v1`, any API key) to run the full
import flow against sample data.

**How does authentication work?**
A single API key sent as the `X-Api-Key` header. Create an API user with API-Key
authentication under **Administration ▸ API Users** in EspoCRM. No OAuth, no tokens.

**Where do Leads and Opportunities end up?**
Both land in the **CRM** pipeline as `crm.lead` records — Leads as type *Lead*,
Opportunities as type *Opportunity* (with amount, close date, probability and the raw
EspoCRM stage).

**Is it safe to run against production data?**
Yes. Dry-run is ON by default, validation gates risky writes, and rollback snapshots let
you undo a batch. You decide when to go live.

**Does it support real-time webhooks?**
Not in this version. Real-time inbound webhooks are a roadmap item; the
`/espocrm/webhook` route exists only to avoid 404s and imports nothing. Use the scheduled
import and reconciliation actions instead.

**What support and refund policy do I get?**
Every request is answered within 24 hours, setup help included. If you report a bug within
2 months of purchase and it is not resolved within 15 days, you are entitled to a full
money-back refund.

## Data, privacy & limits

- The connector **reads** Accounts, Contacts, Leads and Opportunities from your EspoCRM
  server and **writes** the corresponding Odoo `res.partner` and `crm.lead` records. It
  never writes, updates or deletes anything in EspoCRM.
- Credentials (API key, webhook secret) are stored in administrator-only fields.
- **In scope today**: read-only import of the four entities, per-field mapping with
  transforms, schema introspection, dry-run + validation + rollback, a resilient queue, and
  scheduled import/reconciliation.
- **Out of scope / roadmap**: write-back to EspoCRM, inbound webhooks (the route is a
  no-op), and incremental `modifiedAt` filtering (v1 lists by `maxSize`/`offset`).

## Support & updates

- Support: [support@bambooforge.dev](mailto:support@bambooforge.dev) — answered within 24 hours, setup help included.
- Refund: report a bug within 2 months of purchase; if unresolved within 15 days, full
  refund.
- Full source is included. Updates track the supported Odoo 20 / EspoCRM REST `api/v1`
  line.

## Upgrading & version compatibility

This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its
own dedicated build of this module — always install the build that matches your Odoo
version. Mixing a build with a different Odoo series is not supported.

### Patch upgrades (same series, e.g. 20.0.1.0.0 → later)

1. **Back up** your database and filestore first.
2. Replace the module folder with the newer build.
3. Restart Odoo with the module updated:
   
   ```
   ./odoo-bin -c your.conf -u bambooforge_espocrm_connector -d your_db
   ```
4. Odoo applies any schema/data changes automatically. Your existing records and
   configuration are preserved.

### Cross-version migration (moving to a newer Odoo)

Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade
tooling. When you migrate the database to the next Odoo series, install the matching
build of this module for that series. Data created by this module carries over with
the database migration.

After any upgrade the module's scheduled actions resume on their normal cadence —
no manual re-activation is required.

## Uninstallation

You can remove this module at any time from **Apps → (this module) → Uninstall**,
or from the command line. Uninstalling is clean and reversible by reinstalling —
but note what is and is not deleted.

### What is removed

- The module's own tables and every record in them (20 models, prefixed `espocrm.*`) — this is the data this module created.
- The menus, actions, views and reports this module installed.
- Its scheduled actions (cron jobs) — they stop immediately on uninstall.
- Connection records, credentials, field mappings, queue jobs and sync logs stored in Odoo.

### What is preserved

- **Your remote platform is never touched.** Uninstalling only removes the Odoo-side
  connector; products, customers and orders on the external store/service are untouched.
- Records already imported into standard Odoo models (e.g. contacts, products, sales
  orders) remain — they are ordinary Odoo records once created.
- Attachments and chatter messages on standard records are kept.

As always, take a database backup before uninstalling in production.

## Changelog

### 20.0.1.0.1

- Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships.

### 20.0.1.0.0

Current release for Odoo 20.0. This build includes:

- Sync EspoCRM accounts, contacts, leads & opportunities into Odoo 20 over the REST API.
- Now with a live Sync Control Tower, bulk dead-letter & conflict consoles and a field-mapping studio. Read-only against EspoCRM, resilient queue, dry-run safety, auditable source.

Feature additions and fixes ship as new builds on the Odoo Apps store; this page and
the module's `version` reflect the current published release. Always keep the build
matched to your Odoo series (see *Upgrading & version compatibility*).

## How we make sure it works

Before every release, **61 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20.
Among them:

- Account and contact bindings do not collide.
- Lock claims pending then refuses second claim.
- Action domain selects only conflicts.
- Action domain selects only dead.
- Custom admin path is honoured.

Tested for real against EspoCRM 9.3: 3 accounts, 5 contacts, 4 leads and 3 opportunities came into Odoo with 0 errors and every binding accounted for.

The complete list is in the source you receive.
