{"openapi":"3.0.3","info":{"title":"Tellco AI API","version":"1.0.0","description":"Public API for external CRM (Hubspot, Salesforce, Zoho), Zapier, Make.com, and custom integrations.\n\n**Authentication.** Mint a tenant API key under Settings → API Keys, then send `Authorization: Bearer rwk_...` on every request. Keys behave as a tenant admin user.\n\n**Webhooks.** Register URLs at POST /webhooks with an `events` filter. We POST HMAC-signed JSON (`X-Rightward-Signature: sha256=<hex>`) on every event. Empty `events` array = subscribe to all.\n\n**Rate limit.** 100 req/min per IP globally; 20 req/min on auth endpoints. Bursts allowed up to 10×.\n\n**Errors.** Failures return JSON with `{ error, message?, code? }`. The `error` field is human-readable."},"servers":[{"url":"https://tellco.ai/api/v1","description":"Production"}],"tags":[{"name":"Contacts","description":"Lead/contact CRUD"},{"name":"Messages","description":"Email, SMS, WhatsApp send + inbound webhooks"},{"name":"Contact Lists","description":"Bulk lists for campaigns"},{"name":"Campaigns","description":"Outbound voice/AI campaigns"},{"name":"CDRs","description":"Call Detail Records"},{"name":"Webhooks","description":"Event subscriptions for real-time push"}],"paths":{"/contacts":{"get":{"summary":"List contacts (paginated, cursor-based)","tags":["Contacts"],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"cursor","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"search","in":"query","schema":{"type":"string"}},{"name":"status","in":"query","schema":{"enum":["active","dnc","invalid","blacklisted"]}}],"responses":{"200":{"description":"Paginated contacts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactsPage"}}}}}},"post":{"summary":"Create a contact","tags":["Contacts"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactCreate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"400":{"description":"Validation error"}}}},"/contacts/{id}":{"get":{"summary":"Get a contact","tags":["Contacts"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Contact","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}}}},"put":{"summary":"Update a contact","tags":["Contacts"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactUpdate"}}}},"responses":{"200":{"description":"Updated"}}},"delete":{"summary":"Delete a contact","tags":["Contacts"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Deleted"}}}},"/contacts/{id}/messages":{"get":{"summary":"Get a contact's message thread","tags":["Messages"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"channel","in":"query","schema":{"enum":["email","sms","whatsapp","chat"]}}],"responses":{"200":{"description":"Messages"}}},"post":{"summary":"Send an email / SMS / WhatsApp message to a contact","description":"Outbound email + SMS + WhatsApp dispatch synchronously via Resend/SMTP/Twilio. The response carries the persisted contact_messages row with status='sent' on success, 'failed' on provider rejection. WhatsApp requires TWILIO_WHATSAPP_FROM (or per-tenant Sender) configured.","tags":["Messages"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["channel","body"],"properties":{"channel":{"enum":["email","sms","whatsapp","chat"]},"direction":{"enum":["outbound","inbound"],"default":"outbound"},"subject":{"type":"string","description":"Email subject (ignored for non-email channels)"},"body":{"type":"string","minLength":1,"maxLength":20000},"metadata":{"type":"object"}}}}}},"responses":{"201":{"description":"Sent and logged"},"400":{"description":"Contact has no destination (no email or no phone) for the chosen channel"},"502":{"description":"Provider rejected the send. Body contains upstream error verbatim."}}}},"/contact-lists":{"get":{"summary":"List contact lists","tags":["Contact Lists"],"responses":{"200":{"description":"Lists"}}},"post":{"summary":"Create a contact list","tags":["Contact Lists"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":255},"description":{"type":"string","maxLength":1000},"tags":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"201":{"description":"Created"}}}},"/contact-lists/{id}/import":{"post":{"summary":"Bulk-upload contacts (csv/tsv/xlsx/json/txt/docx)","description":"Multipart file upload. Detects format by extension. Accepts CSV/TSV with auto header detection, XLSX/XLS first sheet, JSON array of objects or strings, TXT one phone per line, DOCX first table.","tags":["Contact Lists"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"Import summary { imported, duplicates, errors, rejected_phones }"},"400":{"description":"Unsupported format / parse error / no contacts found"}}}},"/campaigns":{"get":{"summary":"List campaigns","tags":["Campaigns"],"responses":{"200":{"description":"Campaigns"}}},"post":{"summary":"Create a campaign (always starts as draft)","description":"Status is hard-coded to 'draft' on create as a safety guard; call POST /campaigns/{id}/start to actually dial.","tags":["Campaigns"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignCreate"}}}},"responses":{"201":{"description":"Created"}}}},"/campaigns/{id}/start":{"post":{"summary":"Start a campaign — DND scrub, balance check, transition to running","tags":["Campaigns"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Started"},"422":{"description":"INSUFFICIENT_BALANCE — body includes minimum_required + current_balance"}}}},"/campaigns/{id}/pause":{"post":{"summary":"Pause a running campaign","tags":["Campaigns"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Paused"}}}},"/campaigns/{id}/resume":{"post":{"summary":"Resume a paused campaign","tags":["Campaigns"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Resumed"}}}},"/campaigns/{id}/cancel":{"post":{"summary":"Cancel a campaign","tags":["Campaigns"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Cancelled"}}}},"/cdr":{"get":{"summary":"List call detail records","tags":["CDRs"],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"campaign_id","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"status","in":"query","schema":{"type":"string"}},{"name":"from","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Paginated CDRs"}}}},"/webhooks":{"get":{"summary":"List subscribed webhook URLs","tags":["Webhooks"],"responses":{"200":{"description":"Webhooks"}}},"post":{"summary":"Register a webhook URL","description":"Configure events array to filter; empty events = subscribe to all.","tags":["Webhooks"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"},"secret":{"type":"string","description":"HMAC secret for X-Rightward-Signature"},"events":{"type":"array","items":{"enum":["contact.created","contact.updated","contact.deleted","message.sent","message.failed","message.received","campaign.started","campaign.completed","campaign.call_completed","call.recorded","call.transcribed","automation.executed"]}}}}}}},"responses":{"201":{"description":"Created"}}}}},"components":{"securitySchemes":{"bearerApiKey":{"type":"http","scheme":"bearer","bearerFormat":"rwk_<base64url>","description":"Tenant API key. Mint via /settings/api-keys → Generate key. Behaves like a tenant admin user for RBAC. Send as `Authorization: Bearer rwk_...`."},"bearerJwt":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"User access token from POST /auth/login. Use API keys for machine-to-machine integrations instead."}},"schemas":{"Contact":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string","format":"uuid"},"phone":{"type":"string","example":"+919876543210"},"name":{"type":"string","nullable":true},"email":{"type":"string","format":"email","nullable":true},"company":{"type":"string","nullable":true},"status":{"enum":["active","dnc","invalid","blacklisted"]},"tags":{"type":"array","items":{"type":"string"}},"created_at":{"type":"string","format":"date-time"}}},"ContactCreate":{"type":"object","required":["phone"],"properties":{"phone":{"type":"string","example":"+919876543210","description":"6–20 digits, optional + prefix"},"name":{"type":"string","maxLength":255},"email":{"type":"string","format":"email"},"company":{"type":"string","maxLength":255},"tags":{"type":"array","items":{"type":"string"}}}},"ContactUpdate":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string","format":"email","nullable":true},"company":{"type":"string","nullable":true},"status":{"enum":["active","dnc","invalid","blacklisted"]},"tags":{"type":"array","items":{"type":"string"}}}},"ContactsPage":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Contact"}},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"cursor":{"type":"string","nullable":true},"next_cursor":{"type":"string","nullable":true},"total":{"type":"integer"}}}}},"CampaignCreate":{"type":"object","required":["name","type"],"properties":{"name":{"type":"string"},"type":{"enum":["simple_ivr","ai_agent","custom_ivr","call_patch"]},"priority":{"type":"integer","minimum":1,"maximum":10,"default":5},"cli":{"type":"string","description":"Optional outbound caller ID; round-robin if omitted"},"voice_file_id":{"type":"string","format":"uuid","nullable":true},"ivr_flow_id":{"type":"string","format":"uuid","nullable":true},"ai_config":{"type":"object","nullable":true,"properties":{"agent_id":{"type":"string"},"provider":{"type":"string"},"system_prompt":{"type":"string"},"first_message":{"type":"string"}}},"contacts":{"type":"object","properties":{"method":{"enum":["list","upload","manual"]},"list_id":{"type":"string","format":"uuid"}}},"settings":{"type":"object"}}}}},"security":[{"bearerApiKey":[]}],"x-webhook-events":[{"event":"contact.created","when":"New contact added (dashboard, API, or inbound reply)"},{"event":"contact.updated","when":"Contact PUT"},{"event":"contact.deleted","when":"Contact DELETE"},{"event":"message.sent","when":"Outbound email/sms/whatsapp delivered to provider"},{"event":"message.failed","when":"Outbound provider rejected (auth/balance/geo/template)"},{"event":"message.received","when":"Inbound SMS or WhatsApp from a lead (Twilio webhook → us → you)"},{"event":"campaign.started","when":"POST /campaigns/{id}/start succeeded"},{"event":"campaign.completed","when":"Campaign reached terminal state"},{"event":"campaign.call_completed","when":"Single dial finished + CDR persisted"},{"event":"call.recorded","when":"Recording uploaded to CDN, recording_url set"},{"event":"call.transcribed","when":"Transcript ready, transcript + sentiment set"},{"event":"automation.executed","when":"Automation rule fired and finished"}]}