# CRM Connector Integration HUM can resolve inbound/outbound callers against an external CRM and push completed‑call activity back into it — **without** any CRM‑specific code in the call path. An org registers a **CRM Connector** (a base URL + a shared secret), and HUM speaks a small, stable HTTP contract that any partner can implement. This is the same pattern as HUM's outbound webhooks, but **request/response**: the CRM answers lookups and returns the contact it created, so HUM can attribute the call correctly. ``` inbound call ─► caller lookup ─► POST /contacts/lookup ─┐ ├─► (not found & create enabled) │ POST /contacts call ends ─► transcript + AI summary ─► POST /calls ◄───┘ ``` - `POST {base_url}/contacts/lookup` — resolve a phone number to a contact - `POST {base_url}/contacts` — create a contact HUM couldn't find - `POST {base_url}/calls` — attribute a finished call as activity - `POST {base_url}/ping` — connectivity / auth check You implement those four endpoints. HUM handles orchestration, retries, timeouts, logging, and signing. --- ## Authentication & integrity Every request carries **both**: | Header | Value | | --- | --- | | `Authorization` | `Bearer ` | | `X-HUM-Signature` | `sha256=` | | `X-HUM-Event` | `contact.lookup` \| `contact.create` \| `call.sync` \| `ping` | | `Content-Type` | `application/json` | | `User-Agent` | `bluehive-hum-crm/1.0` | `secret` is the per‑connector value HUM shows you **once** at creation (and on rotate). Verify it two ways: 1. Constant‑time compare the bearer token to your stored secret. 2. Recompute `HMAC-SHA256(secret, rawBody)` over the **raw** request body (before JSON parsing) and constant‑time compare to the hex in `X-HUM-Signature` (strip the `sha256=` prefix). > Always verify the signature against the exact bytes received. Re‑serializing > the parsed JSON will change whitespace/key order and break the HMAC. ### Node/TypeScript verification example ```ts import { createHmac, timingSafeEqual } from 'crypto'; function verify(rawBody: string, secret: string, headers: Record): boolean { const bearer = (headers['authorization'] || '').replace(/^Bearer\s+/i, ''); const eq = (a: string, b: string) => { const ab = Buffer.from(a), bb = Buffer.from(b); return ab.length === bb.length && timingSafeEqual(ab, bb); }; if (!eq(bearer, secret)) return false; const sig = headers['x-hum-signature'] || ''; const expected = `sha256=${createHmac('sha256', secret).update(rawBody).digest('hex')}`; return eq(sig, expected); } ``` --- ## The contract All request/response bodies are JSON. HUM tolerates extra fields it doesn't recognize, so you may return more than the documented keys. Respond within the connector's `timeout_ms` (default **4000 ms**, max 30000). ### 1. `POST /contacts/lookup` Resolve a number to a CRM contact. **Request** ```json { "phone": "+15555550123", "org_id": "org_abc123", "direction": "inbound", "call_id": "c_9f2b…", "hum_contact_url": "https://hum.bluehive.com/contacts/oc_7d1a…" } ``` `hum_contact_url` is a deep link back to the contact inside HUM. It is present when HUM already has the caller in its address book, and `null` otherwise. Store it so your CRM can offer an "Open in BlueHive HUM" link on the record. **Response — found** ```json { "found": true, "contact": { "id": "WGL-CON-AB12CD34", "name": "Jane Roe", "first_name": "Jane", "last_name": "Roe", "email": "jane@acme.com", "company": "Acme Co", "url": "https://crm.example.com/contacts/WGL-CON-AB12CD34" } } ``` **Response — not found** ```json { "found": false } ``` `contact.id` is whatever stable identifier your CRM uses; HUM stores it and sends it back on `POST /calls` as `crm_contact_id`. ### 2. `POST /contacts` Called only when lookup returned `found: false` **and** the connector has contact creation enabled. The body includes caller enrichment when available. **Request** ```json { "phone": "+15555550123", "org_id": "org_abc123", "source": "bluehive-hum", "name": "Jane Roe", "hum_contact_url": "https://hum.bluehive.com/contacts/oc_7d1a…", "lookup": { "caller_name": "Jane Roe", "caller_type": "consumer", "carrier_name": "Verizon", "line_type": "mobile", "country_code": "US", "national_format": "(555) 555-0123" } } ``` `name` and the entire `lookup` object may be `null` if caller lookup returned nothing. **Response** ```json { "id": "WGL-CON-AB12CD34", "name": "Jane Roe", "url": "https://crm.example.com/contacts/WGL-CON-AB12CD34", "created": true } ``` Make this **idempotent**: if you already have the number, return the existing contact with `created: false`. ### 3. `POST /calls` Sent once per call, after the transcript and AI summary are ready. Body is HUM's enriched `call.completed` envelope plus `crm_contact_id` (the id you returned from lookup/create, or `null`). **Request** ```json { "id": "c_9f2b…", "call_id": "c_9f2b…", "call_url": "https://hum.bluehive.com/calls/c_9f2b…", "from": "+15555550123", "to": "+15555550199", "direction": "inbound", "status": "completed", "duration_sec": 184, "started_at": "2026-06-01T17:02:11Z", "ended_at": "2026-06-01T17:05:15Z", "flow_id": "flow_intake", "contact_id": null, "hum_contact_url": "https://hum.bluehive.com/contacts/oc_7d1a…", "summary": "Caller asked about appointment availability…", "sentiment": "positive", "key_points": ["Wants Tuesday AM", "New patient"], "next_actions": ["Send intake form"], "call_transcript": "Agent: Thanks for calling…", "recording_url": "https://api.hum.bluehive.com/api/calls/c_9f2b…/recording?t=…", "outcome_data": { "…": "…" }, "caller": { "…": "caller context object" }, "previous_call": null, "crm_contact_id": "WGL-CON-AB12CD34" } ``` `recording_url` is a short‑lived signed URL — fetch/persist it promptly if you want the audio; the token expires. It is `null` when the call has no stored recording. **Response** ```json { "ok": true, "id": "your-activity-id", "attributed": true } ``` ### Linking back to HUM Every payload HUM sends carries deep links so your CRM can route a user back into HUM in one click: | Field | Sent on | Points to | | --- | --- | --- | | `hum_contact_url` | `contact.lookup`, `contact.create`, `call.sync` | The contact in HUM (when HUM knows the caller) | | `call_url` | `call.sync` | The call detail page in HUM | | `recording_url` | `call.sync` | Short‑lived signed audio URL | `hum_contact_url` is `null` when HUM has no contact record for the number yet (e.g. a first‑time caller looked up before HUM creates its own row). Persist it whenever it is non‑null and render an "Open in BlueHive HUM" affordance on the matched contact. ### 4. `POST /ping` A signed connectivity probe (sent by the admin "Test" button). Return `200` with any JSON, e.g.: ```json { "ok": true, "service": "my-crm-bridge" } ``` --- ## Configuring a connector (HUM admin API) All routes are org‑scoped and **admin‑only** (org role `admin`). Authenticate with your normal HUM session/API auth. ### Create — returns the secret once ```bash curl -X POST https://hum.bluehive.com/api/crm-connectors \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "label": "My CRM (prod)", "provider": "generic", "base_url": "https://crm.example.com/api/crm/v1", "timeout_ms": 4000, "resolve_enabled": true, "create_enabled": true, "sync_calls_enabled": true }' # → { "ok": true, "id": "…", "secret": "…store this in your CRM…" } ``` Take the returned `secret` and configure it on the CRM side as the shared key. | Field | Default | Meaning | | --- | --- | --- | | `label` | – | Human label | | `provider` | `generic` | Free‑form provider tag | | `base_url` | – | Public HTTPS base (internal hosts rejected) | | `custom_headers` | `{}` | Up to 10 extra request headers | | `timeout_ms` | `4000` | Per‑request timeout (1000–30000) | | `active` | `true` | Master on/off | | `resolve_enabled` | `true` | Allow `/contacts/lookup` | | `create_enabled` | `true` | Allow `/contacts` create on miss | | `sync_calls_enabled` | `true` | Allow `/calls` sync | ### Other admin routes | Method & path | Purpose | | --- | --- | | `GET /api/crm-connectors` | List connectors (secret shown as `secret_hint` = last 4) | | `PATCH /api/crm-connectors/:id` | Update config / toggles | | `POST /api/crm-connectors/:id/rotate-secret` | New secret (returned once) | | `DELETE /api/crm-connectors/:id` | Remove connector | | `GET /api/crm-connectors/:id/logs` | Last 100 operation logs | | `POST /api/crm-connectors/:id/test` | Synthetic signed `/ping` | --- ## Testing your endpoint manually You can replay a signed request locally: ```bash SECRET='your-shared-secret' BODY='{"phone":"+15555550123","org_id":"org_abc123","direction":"inbound","call_id":"test"}' SIG="sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')" curl -X POST https://crm.example.com/api/crm/v1/contacts/lookup \ -H "Authorization: Bearer $SECRET" \ -H "X-HUM-Signature: $SIG" \ -H 'X-HUM-Event: contact.lookup' \ -H 'Content-Type: application/json' \ --data "$BODY" ``` A correct implementation returns `{"found": …}` for a valid signature and `401` when the bearer or signature is wrong.