EVERJUST Telephony — Agent Skill
Operate the voice / call-logging stack of an everjust.app tenant as a running agent:
find a contact's or lead's call history, log or place a call, read a recording or a
voicemail transcription, send an SMS tied to a call, and — most importantly — figure out
which provider this particular tenant is on before you touch anything. You reach every
model through the tenant's Odoo MCP / ORM (search, get, count, find, create,
update, call, describe_model; raw env[...] shown where clearer) — see
[[everjust-agent-mcp]] for opening the session against the right tenant DB, and
[[everjust-platform]] for the non-standard invariants (one-DB-per-tenant, sudo/ACL, secrets).
The three addons in scope:
voip_oca — "Phone Engine" (OCA/Dixmit, AGPL). The base: voip.pbx config, voip.call
session rows, a browser softphone widget, and hooks on res.users / mail.activity.
Installed on every tenant. By itself it is a generic SIP/WebRTC shell with a demo PBX.
everjust_ringover — EVERJUST's Ringover integration. Depends on voip_oca and crm.
Patches the voip_oca softphone to dial through the Ringover Web SDK (browser WebRTC),
syncs completed calls from Ringover's REST API into ringover.call, receives Ringover
webhooks, and can trigger a server-side callback. This is where call history actually lands.
everjust_phone — EVERJUST's Twilio stack. Independent of voip_oca/Ringover. Its own
models (everjust.phone.call, everjust.phone.sms), its own Twilio Voice SDK softphone,
its own webhooks. A different provider path.
Provider swap, not a stack. everjust_ringover and everjust_phone are alternative
voice backends. A tenant is on Ringover or Twilio (or neither — just the voip_oca
shell). They do not share tables. Read the provider first (Recipe 0); pick the right model.
When to use this skill
- Look up call history for a partner or a CRM lead (who called, when, how long, answered?).
- Get a recording / voicemail transcription for a call.
- Place / trigger an outbound call (Ringover callback API, or the browser softphone action).
- Log or reconcile a call that a webhook or REST sync should have created.
- Send an SMS tied to a call on the Twilio path, or read logged inbound/outbound SMS.
- Diagnose "no calls are logging" — usually unconfigured creds, wrong provider, or the
everjust.phone.call lead_id bug (see Pitfalls).
Do NOT use this skill for, and stop if the task is really:
- Texting as a product / mass-SMS / template SMS / gateway routing — that is the
everjust_sms_gateway (sms.sms, res.company.sms_provider) stack. Use [[everjust-sms]].
The everjust.phone.sms model here is only Twilio-call-adjacent SMS logging, not that.
- CRM pipeline work (create/qualify leads, stages, activities) — that is [[everjust-crm-sales]].
This skill only reads
crm.lead to attach call logs and creates a missed-call activity.
- Native mail / webmail — [[everjust-mail-ops]].
- Provisioning a Twilio/Ringover account, DNS, or SIP trunk — platform-ops, out of scope.
Install & config state (READ THIS — it is not uniform)
Ground-truthed by introspecting the live prod DBs (deployment-odoo-1, 2026-07). The prompt's
premise that "it isn't installed on any tenant" is stale — it is now installed on several,
but mostly installed-yet-unconfigured (dormant). Confirm per tenant; never assume.
| Tenant |
voip_oca |
everjust_ringover |
everjust_phone |
Provider in effect |
headsup |
installed |
installed |
uninstalled |
Ringover |
weldon |
installed |
uninstalled |
installed (unconfigured) |
Twilio (dormant) |
burekraft-llc |
installed |
uninstalled |
installed (unconfigured) |
Twilio (dormant) |
riftline-labs |
installed |
uninstalled |
installed (unconfigured) |
Twilio (dormant) |
trust-works-company |
installed |
uninstalled |
installed (unconfigured) |
Twilio (dormant) |
connectdomain |
installed |
uninstalled |
uninstalled |
voip_oca shell only |
control |
(module rows absent) |
— |
— |
none |
tcstartupweek |
installed |
uninstalled |
uninstalled |
voip_oca shell only |
- On the Twilio tenants, every
everjust_phone.* config param is empty (account_sid,
auth_token, api_key, api_secret, twiml_app_sid, phone_number all False) and
everjust.phone.call/.sms have 0 rows — the app is installed but has never been wired
or exercised. Any token/SMS/call call will return {"error": "Twilio not configured"}.
- On
headsup, Ringover REST creds (everjust.ringover_api_url/api_key) gate the sync — an
empty everjust.ringover_api_key means sync_recent/cron_sync_all no-op silently.
Key models (real _names and fields, verified live)
voip_oca (base engine — present on all tenants)
| Model |
Role |
Notable fields |
voip.pbx |
PBX/softphone config. voip_oca seeds a Default PBX (domain=pbx.everjust.app, ws_server=wss://pbx.everjust.app:7443, mode=test). |
name (req), domain, ws_server, mode (test|prod) |
voip.call |
A softphone session row (front-end lifecycle: calling→ongoing→terminated). Not the CDR of record on the Ringover path. |
phone_number (req), type_call (incoming|outgoing), state (aborted|calling|missed|ongoing|rejected|terminated), partner_id→res.partner, user_id→res.users, pbx_id→voip.pbx, start_date, end_date, activity_name |
res.users (ext) |
Per-user SIP creds for the engine. |
voip_pbx_id→voip.pbx, voip_username, voip_password (self-read/write-able) |
mail.activity (ext) |
Adds phone-call context to activities. |
main_partner_id→res.partner, main_partner; get_call_activities() returns overdue category='phonecall' activities |
everjust_ringover (Ringover path — installed on headsup)
| Model |
Role |
Notable fields |
ringover.call |
Ringover CDR of record — one row per completed call, synced from Ringover REST/webhook, deduped by cdr_id. This is the call-history table on Ringover tenants. |
cdr_id (dedupe key), call_id, direction (in|out), contact_number, from_number, to_number, start_time, end_time, duration (sec), is_answered (bool), state, hangup_by, recording_url, voicemail_url, ringover_user_id, ringover_user_name (agent), partner_id→res.partner, lead_id→crm.lead, user_id→res.users |
ringover.call also carries the operational methods: _sync_from_api_data, sync_recent,
cron_sync_all (10-min cron auto-created by _post_init_hook), initiate_call,
find_record_for_number (screen-pop resolver), _ringover_api_get.
everjust_phone (Twilio path — installed but unconfigured on 4 tenants)
| Model |
Role |
Notable fields |
everjust.phone.call |
Twilio call log, keyed by call_sid, upserted from webhooks. |
call_sid, direction (inbound|outbound), from_number, to_number, status (queued/ringing/in-progress/completed/busy/no-answer/canceled/failed), start_time, end_time, duration, recording_url, recording_sid, transcription, partner_id→res.partner, user_id→res.users. NO lead_id field — see Pitfalls. |
everjust.phone.sms |
Twilio SMS log (call-adjacent), keyed by message_sid. |
message_sid, direction (inbound|outbound), from_number, to_number, body, status, partner_id→res.partner, user_id→res.users |
Methods: everjust.phone.call.get_twilio_token(), log_call_from_webhook(data);
everjust.phone.sms.send_sms(to_number, body), log_sms_from_webhook(data). Config lives in
ir.config_parameter keys everjust_phone.{account_sid,auth_token,api_key,api_secret,twiml_app_sid,phone_number}.
Recipes
Route each through the tenant's Odoo MCP (open the session per [[everjust-agent-mcp]]).
0. FIRST: which provider is this tenant on, and is it configured?
Never operate before you know the path. Check module install state, then creds.
# Which of the two providers is installed?
env['ir.module.module'].search_read(
[('name', 'in', ['everjust_ringover', 'everjust_phone', 'voip_oca'])],
['name', 'state'])
# -> everjust_ringover installed => use ringover.call (Recipes 1–3)
# -> everjust_phone installed => use everjust.phone.* (Recipes 4–5); CHECK creds:
ICP = env['ir.config_parameter'].sudo()
[bool(ICP.get_param('everjust_phone.%s' % k)) for k in
('account_sid','auth_token','api_key','api_secret','twiml_app_sid','phone_number')]
# all False => Twilio not wired; get_twilio_token/send_sms return {"error":"Twilio not configured"}
bool(ICP.get_param('everjust.ringover_api_key')) # Ringover REST sync gate
If only voip_oca is installed, there is no call-history table beyond ad-hoc voip.call
session rows — say so; don't invent a provider. Never print the cred values (they are
secrets — [[everjust-platform]]); check presence with bool(...).
1. (Ringover) Read a partner's / lead's call history
p = env['res.partner'].search([('phone', 'like', '5551234')], limit=1)
env['ringover.call'].search_read(
[('partner_id', '=', p.id)],
['direction', 'contact_number', 'start_time', 'duration', 'is_answered',
'state', 'recording_url', 'ringover_user_name', 'lead_id'],
order='start_time desc', limit=50)
# Or resolve a raw number to the best Odoo record (lead preferred, else contact):
env['ringover.call'].find_record_for_number('+16125551234') # -> {'model','res_id'} or {}
duration is seconds; is_answered=False on an inbound call is a missed call (the sync
auto-creates a "call back" mail.activity on the lead/partner — see Pitfalls #4).
2. (Ringover) Force a REST sync (pull recent calls now)
The webhook is only a nudge; the authoritative load is the REST pull, deduped by cdr_id.
env['ringover.call'].sync_recent() # pulls the last ~10 calls, upserts by cdr_id
# broader catch-up (what the 10-min cron runs):
env['ringover.call'].cron_sync_all() # pulls last ~50
Both are safe to call repeatedly (idempotent on cdr_id). They no-op if
everjust.ringover_api_url/everjust.ringover_api_key are unset — verify creds first (Recipe 0).
3. (Ringover) Trigger an outbound call (server-side callback)
res = env['ringover.call'].initiate_call('', '+1 (612) 555-1234') # first arg unused
# {'success': True} -> Ringover rings the agent's device(s), then dials the recipient
# {'error': '...'} -> not configured, no from_number, or bad destination
Requires everjust.ringover_api_key and everjust.ringover_from_number in System
Parameters. Ringover's /callback API needs integer E.164 — the method strips non-digits,
prefixes 1 for bare 10-digit US/CA, and casts to int; passing a +/string yourself to the
raw API returns HTTP 400 (see Pitfalls #3). For the browser softphone instead, the UI fires
the ringover_dial client action (partner.action_ringover_call() /
lead.action_ringover_call()) which drives the Ringover Web SDK — not something you invoke
headless over MCP.
4. (Twilio) Read / place a call, get a recording or transcription
# History (keyed by call_sid; transcription is voicemail STT):
env['everjust.phone.call'].search_read(
[('partner_id', '=', p.id)],
['direction','from_number','to_number','status','duration',
'recording_url','transcription','start_time'],
order='start_time desc', limit=50)
# Mint a browser Voice-SDK token (fails if Twilio unconfigured):
env['everjust.phone.call'].get_twilio_token() # -> {'token','identity'} or {'error':...}
There is no headless "dial" method on the Twilio path — an outbound call is initiated by
the browser Voice SDK, which hits the /phone/twiml/outbound webhook that returns a <Dial>.
partner.action_phone_call() just opens the softphone client action pre-filled. Do not
hand-write everjust.phone.call rows to fake a call; let log_call_from_webhook own that
(and note the lead_id bug in Pitfalls #2 before relying on it).
5. (Twilio) Send an SMS / read logged SMS
res = env['everjust.phone.sms'].send_sms('+16125551234', 'Following up on our call.')
# {'success': True, 'sid': '...'} or {'error': 'Twilio not configured' / <exception>}
env['everjust.phone.sms'].search_read(
[('partner_id', '=', p.id)],
['direction','from_number','to_number','body','status','create_date'],
order='create_date desc', limit=50)
This sends via the Twilio REST Client using everjust_phone.account_sid + auth_token +
phone_number. This is not the tenant SMS product. If the task is "text a customer" as a
first-class action (templates, mass-SMS, delivery gating, gateway routing), use [[everjust-sms]]
(sms.sms / res.company.sms_provider), not this call-adjacent logger.
Pitfalls (everjust-specific)
Confirm the provider before every operation — they are alternatives, not layers.
Ringover tenants have ringover.call; Twilio tenants have everjust.phone.*; a bare
voip_oca tenant has neither history table. Reaching for the wrong model returns empty or
raises Invalid field on model. Run Recipe 0 first.
everjust.phone.call has NO lead_id field, but log_call_from_webhook writes one.
The webhook handler builds vals with "lead_id": lead.id if lead else False and, whenever
a call matches a partner (so a lead lookup runs), the subsequent create/write raises
ValueError: Invalid field 'lead_id' on model 'everjust.phone.call'. Result: on the Twilio
path, CRM-linked call logging is broken — status-callback/recording webhooks 500, and
completed calls for known contacts may not log. This is a live bug (verified on weldon).
Do not assume a Twilio call was logged just because it happened; check for the row. Fixing it
means adding a lead_id = fields.Many2one('crm.lead') field (like ringover.call has) or
dropping the key from vals — a code change to everjust_phone/models/phone_call.py, not an
ORM write.
Ringover wants integer E.164; strings 400. The REST /callback and Ringover's SMS
webhook use from_number/to_number as int64, not strings. initiate_call already
normalizes and casts, so call it; if you ever hit the Ringover API directly, send digits as
integers (US/CA bare 10-digit → prefix 1), or you get HTTP 400.
Missed inbound Ringover calls auto-create a mail.activity ("call back …"). In
_sync_from_api_data, a call with direction='in' and is_answered=False spawns a To-Do
activity on the linked lead (else partner), assigned to the mapped Odoo user (matched by
Ringover agent email → res.users.login) or the current user. Re-running sync_recent is
deduped by cdr_id so it won't double-file the call, but be aware activities are a
side effect of the sync. This is why this skill touches crm.lead at all — pipeline work
itself is [[everjust-crm-sales]].
SECURITY — telephony webhooks are auth="none" but now SIGNATURE-VERIFIED and FAIL CLOSED.
(Updated 2026-07 — ww.everjust.app#138 closed what used to be an unauthenticated, fail-open
surface. If you are reading an older copy of this skill, that guidance is obsolete.)
- Twilio (
everjust_phone/controllers/twilio_webhooks.py — /phone/twiml/outbound|inbound| voicemail, /phone/webhook/status|recording|transcription|sms): every route now validates
X-Twilio-Signature via RequestValidator and returns 403 on an unsigned/bad-signature
POST (_reject_unsigned). Forged call/SMS/recording events are no longer possible.
- Ringover (
/ringover/webhook) and the SMS gateway (/sms/incoming) verify a JWT or
HMAC signature and now fail CLOSED — with no key configured they REJECT rather than
accept (previously _verify returned True and accepted anything).
Operational consequence — check this first when inbound "stops working": because these
now fail closed, a tenant whose signing keys were never set will silently drop 100% of inbound
SMS/call events. Symptom is "no new records, no errors in the UI". Set the per-tenant keys
(everjust.sms_webhook_key; the Ringover per-event keys) in Settings → Technical → System
Parameters — note the Ringover settings page only exposes the legacy single
ringover_webhook_secret, not the per-event keys the verifier actually reads.
Residual caution: unverified Ringover call-sync events still trigger a debounced REST resync
and answer 200 (rejections are log-only), and the JWT branch does not bind a claim to the body,
so a captured signature authenticates an arbitrary body until key rotation. Still prefer
corroboration before treating webhook-sourced rows as authoritative.
voip.call is a session row, not a CDR. On Ringover tenants the call history of record
is ringover.call (populated by REST sync), not voip.call. voip.call reflects the
front-end softphone lifecycle and may be sparse or absent for server-synced calls. Query
ringover.call for history; use voip.call only for live-session/UX state.
Installed ≠ operational. Four tenants have everjust_phone installed with zero config
and zero rows — token minting and SMS/REST all short-circuit to
{"error": "Twilio not configured"}. Likewise Ringover sync no-ops without an API key.
"The app is installed" is not "the app works" — verify creds (Recipe 0) before promising a
send or a sync.
Everything is per-tenant DB / company_id. Provider choice, creds, and call rows are
scoped to one tenant. When multi-tenant, confirm you're on the right DB before searching
(see [[everjust-agent-mcp]] / [[everjust-platform]]).
See also
- [[everjust-agent-mcp]] — connect to the tenant MCP; the
search/get/create/update/
call/describe_model toolset every recipe uses.
- [[everjust-platform]] — platform invariants (one-DB-per-tenant, sudo/ACL, secrets, /odoo debrand).
- [[everjust-sms]] — the tenant SMS product (
sms.sms, provider-swapped gateway). Use this,
not everjust.phone.sms, for texting as a first-class action.
- [[everjust-crm-sales]] — CRM pipeline; this skill only reads
crm.lead to attach call logs
and files a missed-call activity.
- [[everjust-mail-ops]] — native mail / webmail stack (sibling; also provider-gated).
1---2name: everjust-telephony3description: EVERJUST Telephony — Agent Skill4---56# EVERJUST Telephony — Agent Skill78Operate the **voice / call-logging stack** of an everjust.app tenant as a running agent:9find a contact's or lead's call history, log or place a call, read a recording or a10voicemail transcription, send an SMS tied to a call, and — most importantly — figure out11**which provider this particular tenant is on** before you touch anything. You reach every12model through the tenant's Odoo MCP / ORM (`search`, `get`, `count`, `find`, `create`,13`update`, `call`, `describe_model`; raw `env[...]` shown where clearer) — see14[[everjust-agent-mcp]] for opening the session against the right tenant DB, and15[[everjust-platform]] for the non-standard invariants (one-DB-per-tenant, sudo/ACL, secrets).1617The three addons in scope:1819- **`voip_oca`** — "Phone Engine" (OCA/Dixmit, AGPL). The base: `voip.pbx` config, `voip.call`20 session rows, a browser softphone widget, and hooks on `res.users` / `mail.activity`.21 **Installed on every tenant.** By itself it is a generic SIP/WebRTC shell with a demo PBX.22- **`everjust_ringover`** — EVERJUST's Ringover integration. Depends on `voip_oca` and `crm`.23 Patches the voip_oca softphone to dial through the **Ringover Web SDK** (browser WebRTC),24 syncs completed calls from Ringover's **REST API** into `ringover.call`, receives Ringover25 webhooks, and can trigger a server-side callback. This is where call history actually lands.26- **`everjust_phone`** — EVERJUST's **Twilio** stack. Independent of voip_oca/Ringover. Its own27 models (`everjust.phone.call`, `everjust.phone.sms`), its own Twilio Voice SDK softphone,28 its own webhooks. A *different* provider path.2930> **Provider swap, not a stack.** `everjust_ringover` and `everjust_phone` are alternative31> voice backends. A tenant is on **Ringover** *or* **Twilio** (or neither — just the voip_oca32> shell). They do not share tables. Read the provider first (Recipe 0); pick the right model.3334## When to use this skill3536- **Look up call history** for a partner or a CRM lead (who called, when, how long, answered?).37- **Get a recording / voicemail transcription** for a call.38- **Place / trigger an outbound call** (Ringover callback API, or the browser softphone action).39- **Log or reconcile a call** that a webhook or REST sync should have created.40- **Send an SMS tied to a call** on the Twilio path, or read logged inbound/outbound SMS.41- **Diagnose "no calls are logging"** — usually unconfigured creds, wrong provider, or the42 `everjust.phone.call` `lead_id` bug (see Pitfalls).4344**Do NOT use this skill for**, and stop if the task is really:45- **Texting as a product / mass-SMS / template SMS / gateway routing** — that is the46 `everjust_sms_gateway` (`sms.sms`, `res.company.sms_provider`) stack. Use [[everjust-sms]].47 The `everjust.phone.sms` model here is only Twilio-call-adjacent SMS logging, not that.48- **CRM pipeline work** (create/qualify leads, stages, activities) — that is [[everjust-crm-sales]].49 This skill only *reads* `crm.lead` to attach call logs and *creates* a missed-call activity.50- **Native mail / webmail** — [[everjust-mail-ops]].51- **Provisioning a Twilio/Ringover account, DNS, or SIP trunk** — platform-ops, out of scope.5253## Install & config state (READ THIS — it is not uniform)5455Ground-truthed by introspecting the live prod DBs (`deployment-odoo-1`, 2026-07). The prompt's56premise that "it isn't installed on any tenant" is **stale** — it is now installed on several,57but mostly **installed-yet-unconfigured** (dormant). Confirm per tenant; never assume.5859| Tenant | voip_oca | everjust_ringover | everjust_phone | Provider in effect |60|---|---|---|---|---|61| `headsup` | installed | **installed** | uninstalled | **Ringover** |62| `weldon` | installed | uninstalled | **installed** (unconfigured) | Twilio (dormant) |63| `burekraft-llc` | installed | uninstalled | **installed** (unconfigured) | Twilio (dormant) |64| `riftline-labs` | installed | uninstalled | **installed** (unconfigured) | Twilio (dormant) |65| `trust-works-company` | installed | uninstalled | **installed** (unconfigured) | Twilio (dormant) |66| `connectdomain` | installed | uninstalled | uninstalled | voip_oca shell only |67| `control` | (module rows absent) | — | — | none |68| `tcstartupweek` | installed | uninstalled | uninstalled | voip_oca shell only |6970- On the Twilio tenants, **every `everjust_phone.*` config param is empty** (`account_sid`,71 `auth_token`, `api_key`, `api_secret`, `twiml_app_sid`, `phone_number` all `False`) and72 `everjust.phone.call`/`.sms` have **0 rows** — the app is installed but has never been wired73 or exercised. Any token/SMS/call call will return `{"error": "Twilio not configured"}`.74- On `headsup`, Ringover REST creds (`everjust.ringover_api_url/api_key`) gate the sync — an75 empty `everjust.ringover_api_key` means `sync_recent`/`cron_sync_all` no-op silently.7677## Key models (real `_name`s and fields, verified live)7879### voip_oca (base engine — present on all tenants)8081| Model | Role | Notable fields |82|---|---|---|83| `voip.pbx` | PBX/softphone config. `voip_oca` seeds a **Default PBX** (`domain=pbx.everjust.app`, `ws_server=wss://pbx.everjust.app:7443`, `mode=test`). | `name` (req), `domain`, `ws_server`, `mode` (`test`\|`prod`) |84| `voip.call` | A **softphone session** row (front-end lifecycle: calling→ongoing→terminated). Not the CDR of record on the Ringover path. | `phone_number` (req), `type_call` (`incoming`\|`outgoing`), `state` (`aborted`\|`calling`\|`missed`\|`ongoing`\|`rejected`\|`terminated`), `partner_id`→res.partner, `user_id`→res.users, `pbx_id`→voip.pbx, `start_date`, `end_date`, `activity_name` |85| `res.users` (ext) | Per-user SIP creds for the engine. | `voip_pbx_id`→voip.pbx, `voip_username`, `voip_password` (self-read/write-able) |86| `mail.activity` (ext) | Adds phone-call context to activities. | `main_partner_id`→res.partner, `main_partner`; `get_call_activities()` returns overdue `category='phonecall'` activities |8788### everjust_ringover (Ringover path — installed on `headsup`)8990| Model | Role | Notable fields |91|---|---|---|92| `ringover.call` | **Ringover CDR of record** — one row per completed call, synced from Ringover REST/webhook, deduped by `cdr_id`. This is the call-history table on Ringover tenants. | `cdr_id` (dedupe key), `call_id`, `direction` (`in`\|`out`), `contact_number`, `from_number`, `to_number`, `start_time`, `end_time`, `duration` (sec), `is_answered` (bool), `state`, `hangup_by`, `recording_url`, `voicemail_url`, `ringover_user_id`, `ringover_user_name` (agent), `partner_id`→res.partner, `lead_id`→crm.lead, `user_id`→res.users |9394`ringover.call` also carries the operational methods: `_sync_from_api_data`, `sync_recent`,95`cron_sync_all` (10-min cron auto-created by `_post_init_hook`), `initiate_call`,96`find_record_for_number` (screen-pop resolver), `_ringover_api_get`.9798### everjust_phone (Twilio path — installed but unconfigured on 4 tenants)99100| Model | Role | Notable fields |101|---|---|---|102| `everjust.phone.call` | Twilio call log, keyed by `call_sid`, upserted from webhooks. | `call_sid`, `direction` (`inbound`\|`outbound`), `from_number`, `to_number`, `status` (`queued`/`ringing`/`in-progress`/`completed`/`busy`/`no-answer`/`canceled`/`failed`), `start_time`, `end_time`, `duration`, `recording_url`, `recording_sid`, `transcription`, `partner_id`→res.partner, `user_id`→res.users. **NO `lead_id` field** — see Pitfalls. |103| `everjust.phone.sms` | Twilio SMS log (call-adjacent), keyed by `message_sid`. | `message_sid`, `direction` (`inbound`\|`outbound`), `from_number`, `to_number`, `body`, `status`, `partner_id`→res.partner, `user_id`→res.users |104105Methods: `everjust.phone.call.get_twilio_token()`, `log_call_from_webhook(data)`;106`everjust.phone.sms.send_sms(to_number, body)`, `log_sms_from_webhook(data)`. Config lives in107`ir.config_parameter` keys `everjust_phone.{account_sid,auth_token,api_key,api_secret,twiml_app_sid,phone_number}`.108109## Recipes110111Route each through the tenant's Odoo MCP (open the session per [[everjust-agent-mcp]]).112113### 0. FIRST: which provider is this tenant on, and is it configured?114115Never operate before you know the path. Check module install state, then creds.116117```python118# Which of the two providers is installed?119env['ir.module.module'].search_read(120 [('name', 'in', ['everjust_ringover', 'everjust_phone', 'voip_oca'])],121 ['name', 'state'])122# -> everjust_ringover installed => use ringover.call (Recipes 1–3)123# -> everjust_phone installed => use everjust.phone.* (Recipes 4–5); CHECK creds:124ICP = env['ir.config_parameter'].sudo()125[bool(ICP.get_param('everjust_phone.%s' % k)) for k in126 ('account_sid','auth_token','api_key','api_secret','twiml_app_sid','phone_number')]127# all False => Twilio not wired; get_twilio_token/send_sms return {"error":"Twilio not configured"}128bool(ICP.get_param('everjust.ringover_api_key')) # Ringover REST sync gate129```130If only `voip_oca` is installed, there is no call-history table beyond ad-hoc `voip.call`131session rows — say so; don't invent a provider. **Never print the cred values** (they are132secrets — [[everjust-platform]]); check presence with `bool(...)`.133134### 1. (Ringover) Read a partner's / lead's call history135136```python137p = env['res.partner'].search([('phone', 'like', '5551234')], limit=1)138env['ringover.call'].search_read(139 [('partner_id', '=', p.id)],140 ['direction', 'contact_number', 'start_time', 'duration', 'is_answered',141 'state', 'recording_url', 'ringover_user_name', 'lead_id'],142 order='start_time desc', limit=50)143# Or resolve a raw number to the best Odoo record (lead preferred, else contact):144env['ringover.call'].find_record_for_number('+16125551234') # -> {'model','res_id'} or {}145```146`duration` is seconds; `is_answered=False` on an inbound call is a missed call (the sync147auto-creates a "call back" `mail.activity` on the lead/partner — see Pitfalls #4).148149### 2. (Ringover) Force a REST sync (pull recent calls now)150151The webhook is only a nudge; the authoritative load is the REST pull, deduped by `cdr_id`.152153```python154env['ringover.call'].sync_recent() # pulls the last ~10 calls, upserts by cdr_id155# broader catch-up (what the 10-min cron runs):156env['ringover.call'].cron_sync_all() # pulls last ~50157```158Both are safe to call repeatedly (idempotent on `cdr_id`). They **no-op** if159`everjust.ringover_api_url`/`everjust.ringover_api_key` are unset — verify creds first (Recipe 0).160161### 3. (Ringover) Trigger an outbound call (server-side callback)162163```python164res = env['ringover.call'].initiate_call('', '+1 (612) 555-1234') # first arg unused165# {'success': True} -> Ringover rings the agent's device(s), then dials the recipient166# {'error': '...'} -> not configured, no from_number, or bad destination167```168Requires `everjust.ringover_api_key` **and** `everjust.ringover_from_number` in System169Parameters. Ringover's `/callback` API needs **integer E.164** — the method strips non-digits,170prefixes `1` for bare 10-digit US/CA, and casts to `int`; passing a `+`/string yourself to the171raw API returns HTTP 400 (see Pitfalls #3). For the *browser* softphone instead, the UI fires172the `ringover_dial` client action (`partner.action_ringover_call()` /173`lead.action_ringover_call()`) which drives the Ringover Web SDK — not something you invoke174headless over MCP.175176### 4. (Twilio) Read / place a call, get a recording or transcription177178```python179# History (keyed by call_sid; transcription is voicemail STT):180env['everjust.phone.call'].search_read(181 [('partner_id', '=', p.id)],182 ['direction','from_number','to_number','status','duration',183 'recording_url','transcription','start_time'],184 order='start_time desc', limit=50)185# Mint a browser Voice-SDK token (fails if Twilio unconfigured):186env['everjust.phone.call'].get_twilio_token() # -> {'token','identity'} or {'error':...}187```188There is **no headless "dial" method** on the Twilio path — an outbound call is initiated by189the browser Voice SDK, which hits the `/phone/twiml/outbound` webhook that returns a `<Dial>`.190`partner.action_phone_call()` just opens the softphone client action pre-filled. Do **not**191hand-write `everjust.phone.call` rows to fake a call; let `log_call_from_webhook` own that192(and note the `lead_id` bug in Pitfalls #2 before relying on it).193194### 5. (Twilio) Send an SMS / read logged SMS195196```python197res = env['everjust.phone.sms'].send_sms('+16125551234', 'Following up on our call.')198# {'success': True, 'sid': '...'} or {'error': 'Twilio not configured' / <exception>}199env['everjust.phone.sms'].search_read(200 [('partner_id', '=', p.id)],201 ['direction','from_number','to_number','body','status','create_date'],202 order='create_date desc', limit=50)203```204This sends via the Twilio REST `Client` using `everjust_phone.account_sid` + `auth_token` +205`phone_number`. **This is not the tenant SMS product.** If the task is "text a customer" as a206first-class action (templates, mass-SMS, delivery gating, gateway routing), use [[everjust-sms]]207(`sms.sms` / `res.company.sms_provider`), not this call-adjacent logger.208209## Pitfalls (everjust-specific)2102111. **Confirm the provider before every operation — they are alternatives, not layers.**212 Ringover tenants have `ringover.call`; Twilio tenants have `everjust.phone.*`; a bare213 `voip_oca` tenant has neither history table. Reaching for the wrong model returns empty or214 raises `Invalid field on model`. Run Recipe 0 first.2152162. **`everjust.phone.call` has NO `lead_id` field, but `log_call_from_webhook` writes one.**217 The webhook handler builds `vals` with `"lead_id": lead.id if lead else False` and, whenever218 a call matches a partner (so a lead lookup runs), the subsequent `create`/`write` raises219 `ValueError: Invalid field 'lead_id' on model 'everjust.phone.call'`. Result: on the Twilio220 path, **CRM-linked call logging is broken** — status-callback/recording webhooks 500, and221 completed calls for known contacts may not log. This is a live bug (verified on `weldon`).222 Do not assume a Twilio call was logged just because it happened; check for the row. Fixing it223 means adding a `lead_id = fields.Many2one('crm.lead')` field (like `ringover.call` has) or224 dropping the key from `vals` — a code change to `everjust_phone/models/phone_call.py`, not an225 ORM write.2262273. **Ringover wants integer E.164; strings 400.** The REST `/callback` and Ringover's SMS228 webhook use `from_number`/`to_number` as **int64**, not strings. `initiate_call` already229 normalizes and casts, so call *it*; if you ever hit the Ringover API directly, send digits as230 integers (US/CA bare 10-digit → prefix `1`), or you get HTTP 400.2312324. **Missed inbound Ringover calls auto-create a `mail.activity` ("call back …").** In233 `_sync_from_api_data`, a call with `direction='in'` and `is_answered=False` spawns a To-Do234 activity on the linked lead (else partner), assigned to the mapped Odoo user (matched by235 Ringover agent email → `res.users.login`) or the current user. Re-running `sync_recent` is236 deduped by `cdr_id` so it won't double-file the *call*, but be aware activities are a237 side effect of the sync. This is why this skill touches `crm.lead` at all — pipeline work238 itself is [[everjust-crm-sales]].2392405. **SECURITY — telephony webhooks are `auth="none"` but now SIGNATURE-VERIFIED and FAIL CLOSED.**241 *(Updated 2026-07 — ww.everjust.app#138 closed what used to be an unauthenticated, fail-open242 surface. If you are reading an older copy of this skill, that guidance is obsolete.)*243 - **Twilio** (`everjust_phone/controllers/twilio_webhooks.py` — `/phone/twiml/outbound|inbound|244 voicemail`, `/phone/webhook/status|recording|transcription|sms`): every route now validates245 `X-Twilio-Signature` via `RequestValidator` and returns **403 on an unsigned/bad-signature246 POST** (`_reject_unsigned`). Forged call/SMS/recording events are no longer possible.247 - **Ringover** (`/ringover/webhook`) and the **SMS gateway** (`/sms/incoming`) verify a JWT or248 HMAC signature and now **fail CLOSED** — with no key configured they REJECT rather than249 accept (previously `_verify` returned `True` and accepted anything).250251 **Operational consequence — check this first when inbound "stops working":** because these252 now fail closed, a tenant whose signing keys were never set will silently drop 100% of inbound253 SMS/call events. Symptom is "no new records, no errors in the UI". Set the per-tenant keys254 (`everjust.sms_webhook_key`; the Ringover per-event keys) in **Settings → Technical → System255 Parameters** — note the Ringover *settings page* only exposes the legacy single256 `ringover_webhook_secret`, not the per-event keys the verifier actually reads.257258 Residual caution: unverified Ringover call-sync events still trigger a debounced REST resync259 and answer 200 (rejections are log-only), and the JWT branch does not bind a claim to the body,260 so a captured signature authenticates an arbitrary body until key rotation. Still prefer261 corroboration before treating webhook-sourced rows as authoritative.2622636. **`voip.call` is a session row, not a CDR.** On Ringover tenants the call history of record264 is `ringover.call` (populated by REST sync), not `voip.call`. `voip.call` reflects the265 front-end softphone lifecycle and may be sparse or absent for server-synced calls. Query266 `ringover.call` for history; use `voip.call` only for live-session/UX state.2672687. **Installed ≠ operational.** Four tenants have `everjust_phone` installed with **zero config269 and zero rows** — token minting and SMS/REST all short-circuit to270 `{"error": "Twilio not configured"}`. Likewise Ringover sync no-ops without an API key.271 "The app is installed" is not "the app works" — verify creds (Recipe 0) before promising a272 send or a sync.2732748. **Everything is per-tenant DB / `company_id`.** Provider choice, creds, and call rows are275 scoped to one tenant. When multi-tenant, confirm you're on the right DB before searching276 (see [[everjust-agent-mcp]] / [[everjust-platform]]).277278## See also279280- [[everjust-agent-mcp]] — connect to the tenant MCP; the `search`/`get`/`create`/`update`/281 `call`/`describe_model` toolset every recipe uses.282- [[everjust-platform]] — platform invariants (one-DB-per-tenant, sudo/ACL, secrets, /odoo debrand).283- [[everjust-sms]] — the tenant SMS *product* (`sms.sms`, provider-swapped gateway). Use this,284 not `everjust.phone.sms`, for texting as a first-class action.285- [[everjust-crm-sales]] — CRM pipeline; this skill only reads `crm.lead` to attach call logs286 and files a missed-call activity.287- [[everjust-mail-ops]] — native mail / webmail stack (sibling; also provider-gated).