Skip to main content

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:

WhatWhen it happens
Lookup: find the contact for a phone numberScreen pop and caller names
Search: find contacts by typed textContact search
Create contact: add a contact to the CRMOn request from a user or the API
Journal: record each finished call on the CRMAutomatically, for every finished external call, with retries
Click to call: turn the CRM's phone numbers into dialable numbersWhen 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 https and 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:write permission 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 |digits and |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_url is 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:

StateMeaning
disabledSwitched off.
needs_credentialsA credential the template names has no value.
unknownOn, but not called yet.
healthyThe last call worked.
degradedOne or two calls in a row failed.
failingThree 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, 429 or a 5xx answer) 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​

ActionPermission
View integrations, the log and the healthintegrations:read
Create, change, test, validate and retryintegrations:write
Look up and search contactscontacts: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:password as the credential and write Authorization: "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.