{"openapi":"3.1.0","info":{"title":"Speech Rec Integration API","version":"0.1.0","description":"Submit call recordings for transcription, diarization and automated compliance QA. Submission is asynchronous: POST returns 202 with a job id, then either poll the job or supply a callbackUrl."},"servers":[{"url":"https://did.speechrec.app/api/v1"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Authorization: Bearer sk_live_…"},"apiKeyHeader":{"type":"apiKey","in":"header","name":"X-API-Key"}},"schemas":{"Call":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"},"language":{"type":"string","nullable":true},"durationSec":{"type":"number","nullable":true},"title":{"type":"string","nullable":true},"summary":{"type":"string","nullable":true},"transcript":{"type":"string"},"flagged":{"type":"boolean"},"escalated":{"type":"boolean"},"qa":{"type":"object","nullable":true},"sales":{"$ref":"#/components/schemas/Sales"},"contactId":{"type":"string","nullable":true}}},"Job":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["queued","processing","done","failed"]},"callId":{"type":"string","nullable":true},"error":{"type":"string","nullable":true}}},"Contact":{"type":"object","required":["id"],"properties":{"id":{"type":"string","description":"Your CRM's own id for this account."},"name":{"type":"string"},"email":{"type":"string"},"phones":{"type":"array","items":{"type":"string"},"description":"Any format. Matched on the last nine digits, so \"+44 7700 900123\" and \"07700900123\" are the same account."},"ownerAgent":{"type":"string"},"stage":{"type":"string"},"currency":{"type":"string"},"balance":{"type":"number"},"equity":{"type":"number"},"lifetimeDeposits":{"type":"number"},"depositCount":{"type":"integer"},"lastDepositAt":{"type":"string","format":"date-time"},"lastDepositAmount":{"type":"number"},"accountOpenedAt":{"type":"string","format":"date-time"},"openPositions":{"type":"array","items":{"type":"object","properties":{"symbol":{"type":"string"},"side":{"type":"string","enum":["buy","sell"]},"size":{"type":"number"},"openedAt":{"type":"string","format":"date-time"}}}},"asOf":{"type":"string","format":"date-time","description":"When this snapshot was taken. IMPORTANT for a nightly export: every funding signal compares the snapshot against the call date, and a snapshot older than the call can prove nothing either way. Defaults to now."}}},"Sales":{"type":"object","nullable":true,"description":"The commercial read of the call. NULL when this layer did not run — which is a different fact from a call where funding never came up, and integrators writing to a CRM field need to be able to tell them apart.","properties":{"intent":{"type":"object","nullable":true,"description":"Deposit intent 0-100 and its band. Arithmetic over the extracted observations, not a model opinion — see /api/v1/calls/{id}/signals for the working.","properties":{"score":{"type":"integer"},"band":{"type":"string","enum":["hot","warm","cool","cold"]}}},"deposit":{"type":"object","properties":{"discussed":{"type":"boolean"},"stance":{"type":"string","enum":["committed","interested","hesitant","declined","agent_only","none"],"description":"Describes the CUSTOMER. \"agent_only\" means the agent raised funding and the customer did not engage."},"amount":{"type":"number","nullable":true,"description":"Null when no figure was spoken. Never zero — a zero would be indistinguishable from a refusal in an aggregate."},"currency":{"type":"string","nullable":true},"timeframe":{"type":"string","nullable":true}}},"withdrawalDiscussed":{"type":"boolean"},"followUpAgreed":{"type":"boolean"},"instruments":{"type":"array","description":"Canonicalised: \"gold\", \"XAUUSD\" and \"bullion\" all resolve to one symbol.","items":{"type":"object","properties":{"symbol":{"type":"string"},"name":{"type":"string"},"class":{"type":"string","enum":["fx","metals","energy","indices","crypto","equities","bonds","softs"]}}}},"objections":{"type":"array","items":{"type":"string"}},"source":{"type":"string","enum":["llm","scan"],"description":"\"scan\" means only the deterministic instrument pass ran, so there is no intent score — a fact about us, not about the call."}}}}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"paths":{"/calls":{"post":{"summary":"Submit a recording (async)","description":"Accepts our own JSON shape or a raw Twilio/Telnyx/Vonage webhook. Idempotent via the Idempotency-Key header or the provider event id.","parameters":[{"name":"Idempotency-Key","in":"header","schema":{"type":"string"},"required":false}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"Recording URL (https). Provider webhook fields are also accepted."},"callbackUrl":{"type":"string","description":"POSTed {event,jobId,callId} when finished."},"language":{"type":"string","description":"ISO code. OMIT for auto-detect — recommended, and required for code-switched calls."},"diarize":{"type":"boolean","default":true},"numSpeakers":{"type":"integer","description":"Expected speakers. Defaults to 2, which is correct for a standard two-party call; auto-detect (0) is unreliable on 8 kHz telephony."},"enhance":{"type":"boolean","default":true},"accuracy":{"type":"boolean","default":true},"redact":{"type":"boolean","description":"PII redaction (non-destructive)."},"tags":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Duplicate (idempotent replay)","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["queued","processing","done","failed"]},"callId":{"type":"string","nullable":true},"error":{"type":"string","nullable":true}}}}}},"202":{"description":"Accepted","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["queued","processing","done","failed"]},"callId":{"type":"string","nullable":true},"error":{"type":"string","nullable":true}}}}}},"400":{"description":"Invalid payload or recording URL"},"401":{"description":"Missing/invalid API key, or bad provider signature"}}},"get":{"summary":"List calls","responses":{"200":{"description":"OK"}}}},"/calls/{id}":{"get":{"summary":"Fetch a call (or poll by job id)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"segments","in":"query","schema":{"type":"string","enum":["1"]},"description":"Include per-segment transcript with speakers/timings"}],"responses":{"200":{"description":"Call","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"},"language":{"type":"string","nullable":true},"durationSec":{"type":"number","nullable":true},"title":{"type":"string","nullable":true},"summary":{"type":"string","nullable":true},"transcript":{"type":"string"},"flagged":{"type":"boolean"},"escalated":{"type":"boolean"},"qa":{"type":"object","nullable":true},"sales":{"$ref":"#/components/schemas/Sales"},"contactId":{"type":"string","nullable":true}}}}}},"202":{"description":"Still processing","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["queued","processing","done","failed"]},"callId":{"type":"string","nullable":true},"error":{"type":"string","nullable":true}}}}}},"404":{"description":"Not found"}}}},"/jobs/{id}":{"get":{"summary":"Job status","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["queued","processing","done","failed"]},"callId":{"type":"string","nullable":true},"error":{"type":"string","nullable":true}}}}}},"404":{"description":"Not found"}}}},"/jobs":{"get":{"summary":"Recent jobs + queue stats","responses":{"200":{"description":"OK"}}}},"/webhooks/{provider}":{"post":{"summary":"Provider webhook endpoint","description":"Paste this URL into Twilio/Telnyx/Vonage. Signatures are verified when the provider secret is configured (TWILIO_AUTH_TOKEN / TELNYX_PUBLIC_KEY / VONAGE_SIGNATURE_SECRET).","parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","enum":["twilio","telnyx","vonage","generic"]}}],"responses":{"202":{"description":"Accepted"}}}},"/crm/contacts":{"post":{"summary":"Push account context (one contact or a batch)","description":"A push, not a pull: nothing here stores your CRM credentials or maps your schema. Send on whatever cadence suits — a nightly export, a webhook per change — and set asOf so the funding signals know how current the snapshot is. One bad row does not reject the batch; the response names it.","requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Contact"},{"type":"object","properties":{"contacts":{"type":"array","maxItems":1000,"items":{"$ref":"#/components/schemas/Contact"}}}}]}}}},"responses":{"200":{"description":"Written","content":{"application/json":{"schema":{"type":"object","properties":{"written":{"type":"integer"},"failed":{"type":"integer"},"errors":{"type":"array","items":{"type":"object"}}}}}}},"400":{"description":"Nothing could be written"},"403":{"description":"The key lacks the crm:write scope. Keys issued before this endpoint existed do not have it — issue a new one for the CRM integration."},"413":{"description":"More than 1000 contacts in one request"}}}},"/crm/contacts/{id}":{"get":{"summary":"Fetch a contact and its recent calls","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Contact","content":{"application/json":{"schema":{"type":"object","properties":{"contact":{"$ref":"#/components/schemas/Contact"},"calls":{"type":"array","items":{"type":"object"}}}}}}},"404":{"description":"Not found"}}},"delete":{"summary":"Remove a contact","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"404":{"description":"Not found"}}}},"/calls/{id}/signals":{"get":{"summary":"What this call means next to the account","description":"Computed live on every request, never cached onto the call: each signal compares what was said then against what the account looks like NOW, so a stored one would still be reporting \"never funded\" a year after the money landed. Returns matched:false when no contact could be attached, which is not the same as a clean account.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Signals","content":{"application/json":{"schema":{"type":"object","properties":{"matched":{"type":"boolean"},"matchedBy":{"type":"string","enum":["explicit","phone"],"nullable":true,"description":"\"phone\" is a match on the last nine digits and is a guess; \"explicit\" came from the contactId you submitted."},"asOf":{"type":"string","nullable":true},"fresh":{"type":"boolean","description":"False when the CRM snapshot predates the call, in which case the funding signals are withheld rather than asserted."},"signals":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string","enum":["committed_unfunded","interested_unfunded","funded","funding_unknown","withdrawal_risk","no_follow_up","concentration","new_asset_class","upgrade","reengaged","pressure"]},"severity":{"type":"string","enum":["high","medium","info"]},"headline":{"type":"string"},"detail":{"type":"string"}}}}}}}}},"404":{"description":"No such call"}}}},"/health":{"get":{"summary":"Health + queue depth","security":[],"responses":{"200":{"description":"OK"}}}}}}