Wire JustCall in through the phone provider abstraction

FeatureVOIPService
Shipped
September 3, 2026 at 4:34 AM UTC
Author
Kamo
Commit
41bc056

JustCall (api.justcall.io/v2.1) as a full PhoneProvider: call history, texts, voicemail, recordings, the agent directory and availability, plus signed account webhooks and an optional contacts import. Auth is one account-wide key/secret pair sent as the raw "key:secret" Authorization value — not a Bearer token, which authenticates nothing and returns a 401 that reads like a bad credential. Three shapes of JustCall's API drive the design: * Voicemail is not a resource. It is a call whose call_info.type is "Voicemail", carrying its transcription inline and its audio behind the same recording endpoint as any other call — so getVoicemails is getCallHistory with a filter, and there is no read state to mark. * There is no softphone credential. JustCall publishes no SIP registrar and no WebRTC token to third parties, and its only outbound-call endpoint drives an AI Voice Agent rather than a person. voiceCalls is therefore false, which is what useSoftphone already gates registration on, and getSipCredentials refuses with an explanation instead of half-registering. * The directory is read-only, and there is no device concept, so those keep the interface's throwing defaults rather than pretending to write. Webhooks are registered per instance at **************** one subscription per event type. JustCall's HMAC-SHA256 covers the registered URL, so that query string is inside the signed material and cannot be rewritten to land one org's events on another's phone server. The URL is recorded in config_json at provisioning because verification has to sign the same string JustCall did — rebuilding it from configuration would start failing silently the moment the base URL changed. Inbound texts and call events reach the paths RingCentral already uses. VoipMessageService's handler was RingCentral-shaped throughout, so its parsing is split from its ingest: both rails now share the dedupe, keyword interception, conversation upsert and lead linking. That sharing is deliberate — the keyword step is what records an opt-out, and a second rail growing its own copy of the sequence is how one ends up not honouring STOP. SmsKeywordService's reply switch gained the new rail for the same reason: it is a statement switch, so an unlisted constant would have answered nobody. MMS is by public URL only — JustCall accepts no uploaded bytes — so an attachment is parked behind a short-lived unguessable URL on this service and that link is handed over. Deliberately not a presigned object-store URL: MinIO here is **************** a presigned URL is signed for the host it names, and JustCall's fetcher cannot resolve an in-cluster address — it would 404 for them while working perfectly from inside the cluster. This service is already publicly reachable because it receives their webhooks. Contact import is off unless switched on: a customer's address book is per-member access controlled, and importing it the moment a key is pasted would be a surprise. Contacts are owned via the JustCall agent_id the extension sync already recorded, and any whose agent maps to no Kamo member is skipped rather than parked somewhere shared. Persistence goes to EmailService, which owns the contact tables. JustCall's AI call summary, sentiment and score ride on the same call read and are surfaced on CallLogEntry. Not persisted: PHONE_CALL_LOG has no such columns, the provider returns them on every read, and adding columns would cost a DDL window for a duplicate of their store. No DDL in this change.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing