Connect a CRM with a template
MonoPbx connects to a CRM without code. You give the PBX a template: a short YAML or JSON description of the CRM's HTTP API, with the credentials the template names. The template engine then uses it for four things:
| What | When it happens |
|---|---|
| Lookup: find the contact for a phone number | Screen pop and caller names |
| Search: find contacts by typed text | Contact search |
| Create contact: add a contact to the CRM | On request from a user or the API |
| Journal: record each finished call on the CRM | Automatically, for every finished external call, with retries |
| Click to call: turn the CRM's phone numbers into dialable numbers | When a user dials a contact |
This tutorial walks through a complete integration with a sample CRM called sample-crm. Replace the values with your CRM's details.
Before you start
You need:
- The base address of your CRM's API. It must be
httpsand public, with no query string and no credentials in the address. - An API key or token for the CRM, with permission to read and create contacts and to write call entries.
- The
integrations:writepermission on MonoPbx.
1. Understand the template
A template has a fixed structure. Here is a template with lookup, journal and click-to-call:
schema: mpbx.crm-template/v1 # required, exactly this value
id: sample-crm # lowercase letters, digits, - and _
name: Sample CRM
base_url: https://crm.example.com/api/v1
secrets: [api_token] # credentials the admin enters, write-only
operations:
lookup:
request:
method: GET
path: "/contacts/lookup?phone={{phone|digits}}"
headers:
Authorization: "Bearer {{secret.api_token}}"
response:
contacts: "$.data[*]" # where the list of contacts is
fields:
id: "$.id"
name: "$.full_name" # required for lookup and search
company: "$.account.name"
email: "$.email"
phones: "$.phones[*]"
url: "$.web_url"
not_found_status: [404] # "no such contact", not an error
timeout_ms: 3000
journal:
request:
method: POST
path: "/calls"
headers:
Authorization: "Bearer {{secret.api_token}}"
body:
json:
direction: "{{direction}}"
number: "{{number|digits}}"
started_at: "{{started_at}}"
duration_secs: "{{duration_secs|num}}"
disposition: "{{disposition}}"
note: "Call {{call_id}}"
click_to_call:
default_country_code: "+52" # added to numbers written nationally
national_prefix: "01" # removed from numbers written nationally
Things to know:
- A template is data, not code. It can only use the
{{name}}placeholders listed for each operation,{{secret.<name>}}for a credential it declares, and the filters|digitsand|num. There are no conditions, loops or scripts. - Quote every value that contains
{{in YAML, otherwise YAML reads it as a mapping. - The address stays on the template's host. A rendered request that would leave the host named in
base_urlis refused. - JSON works too. A document that starts with
{is read as JSON. - Templates are at most 64 KiB. YAML anchors, aliases and tags are refused.
For the exact placeholders of each operation, read GET /api/v1/integrations/crm/reference. It lists the limits, the variables for each operation and a complete sample template.
2. Validate the template
Validation checks the whole template and reports every problem with its location in the document, for example operations.lookup.request.path. It makes no network call, so you can run it before you have credentials.
Send the template to POST /api/v1/integrations/crm/validate (permission integrations:write). The exact request body is in the OpenAPI document at /api/v1/openapi.json. Fix every reported issue and validate again. Creating or updating an integration runs the same checks.
3. Create the integration with its credentials
Create the integration with POST /api/v1/integrations/crm. Send the template, and send the credential values for each name listed in secrets. Permission integrations:write is needed.
- Credentials are write-only. They are sealed with your company's key when you save, they are never returned by any route, and they never appear in logs or the audit log. The reads list only the names of the credentials and whether each one is set.
- To change credentials later, send all of them again with
PUT /api/v1/integrations/crm/{id}/credentials. - If an edit moves the template to another address (scheme, host or port of
base_url), the stored credentials are deleted and the integration is switched off, so the values can only go to the address their owner approved.
Note the integration's id; you need it for the next steps.
4. Run the test console
The test console runs one operation with the variables you give and shows you what happened.
POST /api/v1/integrations/crm/{id}/test runs an operation. Start with dry_run: true: it builds the request and shows it, with credentials displayed as <name>, but sends nothing. This is the safest way to check the placeholders.
Then run the operation for real, for example a lookup for a known phone number. The answer shows the request, the CRM's status code, the latency and the contacts the template read.
:::caution The test console calls the real CRM
A create_contact run creates a contact, and a journal run writes an entry. Use a test contact or a test account in the CRM. The console allows at most 20 runs per company each minute.
:::
5. Switch the integration on
Turn the integration on (PATCH /api/v1/integrations/crm/{id}). A switched-on integration is used for lookups, search and journaling.
Check its health with GET /api/v1/integrations/crm. Each integration shows one of these states:
| State | Meaning |
|---|---|
disabled | Switched off. |
needs_credentials | A credential the template names has no value. |
unknown | On, but not called yet. |
healthy | The last call worked. |
degraded | One or two calls in a row failed. |
failing | Three or more calls in a row failed. |
A call that never left the PBX, such as a template or a missing credential problem, does not count against the CRM's health.
6. How journaling behaves
Every finished external call (inbound or outbound, with a phone number) becomes one journal job for each enabled integration whose template has a journal operation. The job is sent by the PBX in the background:
- A job that fails for a temporary reason (network trouble, a timeout,
408,425,429or a5xxanswer) is retried after 30 seconds, 2 minutes, 10 minutes, 1 hour and 4 hours. After six attempts it is dead. - A job the CRM refuses (any other
4xx, a blocked address, a template that cannot build the request, or missing credentials) is dead at once. - A dead job can be tried again with
POST /api/v1/integrations/crm/jobs/{id}/retry. Dead jobs are kept for 30 days. - A switched-off integration keeps its jobs waiting until it is switched on again.
If the journal operation uses contact_id, and the template also has a lookup, the PBX looks the number up first to get the CRM's contact id.
Use GET /api/v1/integrations/crm/{id}/log?filter=outcome:eq:error for the error log. The log keeps a machine error code, the HTTP status and the latency, and the number or text masked to its last four characters. It never keeps a response body, a URL or a credential.
Permissions
| Action | Permission |
|---|---|
| View integrations, the log and the health | integrations:read |
| Create, change, test, validate and retry | integrations:write |
| Look up and search contacts | contacts:read |
What a template cannot do yet
- OAuth refresh flows. A template authenticates with the credentials it names: API keys, bearer tokens in a header or in the query string, or HTTP Basic. For HTTP Basic, enter the already Base64-encoded
user:passwordas the credential and writeAuthorization: "Basic {{secret.basic}}"in the template. - Ready-made connectors for specific CRMs are planned on the same engine. Until then, write a template for your CRM.