Integrate with Connex
Push every verified speed meeting into your CRM. HMAC-signed, replay-protected, idempotent — the standard webhook security model.
Overview
Every speed meeting that gets verified at a clinic auto-pushes from Connex to your CRM as a signed webhook. FieldVoice and a generic HTTPS webhook are available today. Veeva, Salesforce Health Cloud and Zoho adapters are available on request.
Authentication
Outbound webhooks: every payload is signed with HMAC-SHA256 using the shared secret shown in your Connex panel at integration creation time. You never send your Connex secret to us — Connex uses it to sign outbound payloads, and you use it to verify them.
Admin API (managing integrations programmatically) uses the standard Connex bearer
token from POST /v1/auth/login.
Adapters
Pick the one that matches your CRM. The adapter handles the schema mapping; you don't have to.
| Adapter | Status | Notes |
|---|---|---|
fieldvoice · FieldVoice | GA | Field schema mapping handled automatically. |
webhook · Generic Webhook | GA | Any HTTPS endpoint. You own the receiver. |
veeva · Veeva CRM | Preview | Available on request — contact integrations@medismo.in. |
salesforce-hc · Salesforce Health Cloud | Preview | Available on request — contact integrations@medismo.in. |
zoho · Zoho CRM | Preview | Available on request — contact integrations@medismo.in. |
Verifying signatures
Every webhook carries an X-Connex-Signature header in this format:
t=<unix_seconds>,v1=<hex_hmac_sha256> Compute HMAC-SHA256 over <t>.<rawBody> with your shared secret and
compare the hex digest to v1 in constant time. Reject
signatures more than 300 seconds old.
import crypto from 'node:crypto';
export function verifyConnex(rawBody, signatureHeader, secret) {
// Header looks like: t=1713456000,v1=abc123...
const parts = Object.fromEntries(
signatureHeader.split(',').map((p) => p.split('='))
);
const ts = parts.t;
const sig = parts.v1;
// Reject if older than 5 minutes (replay defence).
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
throw new Error('Signature too old');
}
const expected = crypto
.createHmac('sha256', secret)
.update(`${ts}.${rawBody}`)
.digest('hex');
// Constant-time compare.
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
throw new Error('Bad signature');
}
return JSON.parse(rawBody);
}Event envelope
Every event lands as JSON in the same envelope. data varies per event type — see below.
{
"id": "evt_8f3a2c1e9b",
"type": "meeting.completed",
"api_version": "2026-04-18",
"created_at": "2026-04-19T10:30:00.000Z",
"account_id": "acct_apex_pharma",
"data": { /* event-specific, see below */ }
}Events
Connex publishes 10 event types across the meeting lifecycle and the MR roster. Subscribe to all of them or pick a subset in your Connex panel.
meeting.scheduledA meeting was just booked.
Fires: When an MR creates a booking via /api/v1/scheduling/bookings.
Payload fields (data object)
| Field | Type | Description |
|---|---|---|
booking_id | string | Connex booking UUID |
mr_id | string | Connex MR profile UUID |
doctor_id | string | Connex doctor profile UUID |
slot_start | ISO 8601 | Meeting window start (timezone-aware) |
slot_end | ISO 8601 | Meeting window end |
location_id | string|null | Clinic location UUID, if any |
purpose | string | product_detailing | sample_delivery | follow_up | introduction |
Example envelope
{
"id": "evt_56dhurknj9",
"type": "meeting.scheduled",
"api_version": "2026-04-18",
"created_at": "2026-04-19T10:30:00.000Z",
"account_id": "acct_apex_pharma",
"data": {
"booking_id": "bk_8f3a2c1e9b",
"mr_id": "mr_a1b2c3d4e5",
"doctor_id": "dr_priya_sharma",
"slot_start": "2026-04-19T10:30:00.000Z",
"slot_end": "2026-04-19T10:30:00.000Z",
"location_id": "loc_apollo_jp_nagar",
"purpose": "product_detailing"
}
}meeting.confirmedDoctor confirmed a pending booking.
Fires: When a doctor flips status pending → confirmed.
Payload fields (data object)
| Field | Type | Description |
|---|---|---|
booking_id | string | Connex booking UUID |
confirmed_at | ISO 8601 | When the doctor confirmed |
Example envelope
{
"id": "evt_wd3vuvgwya",
"type": "meeting.confirmed",
"api_version": "2026-04-18",
"created_at": "2026-04-19T10:30:00.000Z",
"account_id": "acct_apex_pharma",
"data": {
"booking_id": "bk_8f3a2c1e9b",
"confirmed_at": "2026-04-19T10:30:00.000Z"
}
}meeting.arrival_verifiedMR arrived at the clinic and scanned the QR.
Fires: On successful QR + GPS verification at the clinic.
Payload fields (data object)
| Field | Type | Description |
|---|---|---|
booking_id | string | Connex booking UUID |
arrived_at | ISO 8601 | When the MR scanned in |
gps_distance_meters | number | Distance from clinic centroid (already validated) |
location_name | string | Clinic location name (may be empty) |
Example envelope
{
"id": "evt_o2tsqz6soc",
"type": "meeting.arrival_verified",
"api_version": "2026-04-18",
"created_at": "2026-04-19T10:30:00.000Z",
"account_id": "acct_apex_pharma",
"data": {
"booking_id": "bk_8f3a2c1e9b",
"arrived_at": "2026-04-19T10:30:00.000Z",
"gps_distance_meters": 12,
"location_name": "Apollo Hospital — JP Nagar"
}
}meeting.completedMeeting was marked complete with outcome data.
Fires: When the doctor marks complete OR the MR picks "Met the doctor" after the QR scan.
Payload fields (data object)
| Field | Type | Description |
|---|---|---|
booking_id | string | Connex booking UUID |
met_at | ISO 8601 | When the meeting was marked complete |
completion_notes | string|null | Free-text notes captured at completion |
samples_provided | boolean | Whether physical samples were given |
iei_provided | boolean | Whether an Item of Educational Interest was given (UCPMP) |
iei_value | number|null | INR value of the IEI (capped at ₹1000 by UCPMP) |
ai_extracted | object|null | AI-extracted structured fields (products, tone, topics, key_takeaway). Populated by a follow-up job. |
Example envelope
{
"id": "evt_5ky77u45rl",
"type": "meeting.completed",
"api_version": "2026-04-18",
"created_at": "2026-04-19T10:30:00.000Z",
"account_id": "acct_apex_pharma",
"data": {
"booking_id": "bk_8f3a2c1e9b",
"met_at": "2026-04-19T10:30:00.000Z",
"completion_notes": "Discussed Amlodipine 5mg efficacy data; handed 2 sample packs.",
"samples_provided": true,
"iei_provided": false,
"iei_value": null,
"ai_extracted": null
}
}meeting.no_showMR was at the clinic but the meeting did not happen.
Fires: When the MR picks "No meeting" after the QR scan.
Payload fields (data object)
| Field | Type | Description |
|---|---|---|
booking_id | string | Connex booking UUID |
scheduled_start | ISO 8601 | Original slot start |
noted_at | ISO 8601 | When no-show was recorded |
reason | string|null | Optional reason chosen by the MR |
Example envelope
{
"id": "evt_mhqnfok1v6",
"type": "meeting.no_show",
"api_version": "2026-04-18",
"created_at": "2026-04-19T10:30:00.000Z",
"account_id": "acct_apex_pharma",
"data": {
"booking_id": "bk_8f3a2c1e9b",
"scheduled_start": "2026-04-19T10:30:00.000Z",
"noted_at": "2026-04-19T10:30:00.000Z",
"reason": null
}
}meeting.cancelledBooking was cancelled.
Fires: When either party cancels a booking before completion.
Payload fields (data object)
| Field | Type | Description |
|---|---|---|
booking_id | string | Connex booking UUID |
cancelled_by | string | doctor | mr | system |
cancelled_at | ISO 8601 | When cancelled |
reason | string|null | Optional reason |
Example envelope
{
"id": "evt_2nbggnmprh",
"type": "meeting.cancelled",
"api_version": "2026-04-18",
"created_at": "2026-04-19T10:30:00.000Z",
"account_id": "acct_apex_pharma",
"data": {
"booking_id": "bk_8f3a2c1e9b",
"cancelled_by": "doctor",
"cancelled_at": "2026-04-19T10:30:00.000Z",
"reason": null
}
}meeting.compliance_flagConnex AI flagged a UCPMP risk on the completion notes.
Fires: For every medium- or high-severity flag returned by the post-meeting AI extraction.
Payload fields (data object)
| Field | Type | Description |
|---|---|---|
booking_id | string | Connex booking UUID |
flag_type | string | possible_gift | cap_risk | comparative_claim | off_label_suggestion | volume_anomaly |
severity | string | low | medium | high |
evidence | string | Short quote from completion notes |
ai_model_version | string | LLM model that produced the flag |
Example envelope
{
"id": "evt_nn4njreecv",
"type": "meeting.compliance_flag",
"api_version": "2026-04-18",
"created_at": "2026-04-19T10:30:00.000Z",
"account_id": "acct_apex_pharma",
"data": {
"booking_id": "bk_8f3a2c1e9b",
"flag_type": "cap_risk",
"severity": "medium",
"evidence": "Mentioned ₹2,000 honorarium during meeting.",
"ai_model_version": "gemini-1.5-flash"
}
}doctor.rated_mrDoctor rated an MR after a meeting.
Fires: When the doctor submits a rating (or re-rates).
Payload fields (data object)
| Field | Type | Description |
|---|---|---|
doctor_id | string | Connex doctor profile UUID |
mr_id | string | Connex MR profile UUID |
booking_id | string | Connex booking UUID |
rating | string | worth | okay | not_worth |
comment | string|null | Optional doctor comment |
Example envelope
{
"id": "evt_mtz1x6a7k4",
"type": "doctor.rated_mr",
"api_version": "2026-04-18",
"created_at": "2026-04-19T10:30:00.000Z",
"account_id": "acct_apex_pharma",
"data": {
"doctor_id": "dr_priya_sharma",
"mr_id": "mr_a1b2c3d4e5",
"booking_id": "bk_8f3a2c1e9b",
"rating": "worth",
"comment": "Useful clinical insights on Amlodipine."
}
}mr.onboardedA new MR was added to your team.
Fires: When a pharma admin adds a team member via /api/v1/company/team.
Payload fields (data object)
| Field | Type | Description |
|---|---|---|
mr_id | string | Connex MR profile UUID |
name | string | MR full name |
territory | string|null | Optional territory label |
company_id | string | Pharma company UUID |
email | string|null | MR login email |
phone | string|null | MR phone number |
Example envelope
{
"id": "evt_rm34g1uyqx",
"type": "mr.onboarded",
"api_version": "2026-04-18",
"created_at": "2026-04-19T10:30:00.000Z",
"account_id": "acct_apex_pharma",
"data": {
"mr_id": "mr_a1b2c3d4e5",
"name": "Rahul Singh",
"territory": "Bangalore South",
"company_id": "co_apex_pharma",
"email": "rahul@apexpharma.test",
"phone": "+91 98765 43210"
}
}mr.deactivatedAn MR was deactivated and can no longer book meetings.
Fires: When a pharma admin deactivates a team member.
Payload fields (data object)
| Field | Type | Description |
|---|---|---|
mr_id | string | Connex MR profile UUID |
deactivated_at | ISO 8601 | When deactivated |
reason | string|null | Optional reason |
Example envelope
{
"id": "evt_x6spks5dej",
"type": "mr.deactivated",
"api_version": "2026-04-18",
"created_at": "2026-04-19T10:30:00.000Z",
"account_id": "acct_apex_pharma",
"data": {
"mr_id": "mr_a1b2c3d4e5",
"deactivated_at": "2026-04-19T10:30:00.000Z",
"reason": null
}
}Retries & idempotency
Connex retries failed deliveries up to 8 times with exponential back-off:
- Attempt 1 · 0s
- Attempt 2 · 30s
- Attempt 3 · 2 min
- Attempt 4 · 10 min
- Attempt 5 · 1 hour
- Attempt 6 · 4 hours
- Attempt 7 · 12 hours
- Attempt 8 · 24 hours
4xx responses (except 408 and 429) are treated as permanent failures and are not retried — assume your code or config is
wrong, fix it, and replay the event from your panel.
Every delivery carries a unique Idempotency-Key header. Connex
guarantees at-least-once delivery; you guarantee dedup. The simplest
pattern: store the key in a unique index and treat conflicts as no-ops.
Errors
What we do when your endpoint returns various status codes:
| Status | Treated as | Action |
|---|---|---|
2xx | Success | Mark delivered. Stop. |
408 / 429 | Transient | Retry per schedule above. |
4xx (other) | Permanent | Mark failed. No retry. Visible in panel for manual replay. |
5xx | Transient | Retry per schedule above. |
| Network / timeout | Transient | Retry per schedule above. |