speed-to-lead — Developer API & MCP reference

Customer-facing reference for the two integration surfaces:

Surface Base URL Auth Status
MCP server https://api.composedchaos.net/stl-mcp/ Authorization: Bearer stl_… Live
REST API https://api.composedchaos.net/stl-api/v1 X-API-Key: stl_… header Live

Both are served over IPv4 and IPv6, TLS only. All timestamps are ISO-8601; send timezone-aware values (2026-07-14T09:00:00-05:00).

API keys & scopes

Keys are issued from the admin console (shown once — store them). Format stl_…. Two scopes:

A scoped key never sees another tenant's data; cross-tenant objects return 404 exactly as if they didn't exist.

Errors & rate limits

Errors are JSON: {"detail": "<reason>"}.

Code Meaning
401 missing/invalid key
403 key scope not authorized for this endpoint
404 not found (or belongs to another tenant)
409 booking conflict (slot no longer available)
422 validation error (bad input, malformed email)
429 rate limited — honor the Retry-After header
503 dependency temporarily unavailable — retry with backoff

Per-client per-minute limits (defaults): parse 10, drive-time 60, availability 20, writes (leads/appointments) 30, reads 120, webhook registration 10.

Pricing & metering


REST endpoints

Micro-APIs (api or client scope)

POST /parse-lead — marketplace notification email in, structured lead out (Angi, Thumbtack, Google Local Services, …). Send either raw_email (full RFC-822 source) or text (+ optional subject, from_addr).

curl -s https://api.composedchaos.net/stl-api/v1/parse-lead \
  -H "X-API-Key: $STL_KEY" -H 'Content-Type: application/json' \
  -d '{"text": "New lead! John D, (913) 555-0142, water heater leaking, Olathe KS", "from_addr": "leads@thumbtack.com"}'

Response: {"source": "thumbtack", "lead": {"name", "phone", "email", "address", "job_desc", "urgency": "normal|urgent|unknown", "lead_ok": bool}, "usage": {"input", "output", "cost_usd"}}. lead_ok: false means the email was a receipt/digest/marketing, not a new lead. 422 = unparseable input, 503 = parsing temporarily unavailable (safe to retry).

POST /drivetime/route — drive time between two points. Body {"origin": {"lat", "lon"}, "destination": {"lat", "lon"}}{"seconds", "meters", "source": "valhalla"|"haversine"} (haversine = estimate while the routing engine is unreachable).

POST /drivetime/feasibility — which appointment slots are actually reachable given a day's existing jobs. Body:

{"job": {"lat": 38.88, "lon": -94.81},
 "window_start": "2026-07-14T08:00:00-05:00",
 "window_end":   "2026-07-14T18:00:00-05:00",
 "duration_minutes": 90,
 "appointments": [{"start": "…", "end": "…", "lat": 39.0, "lon": -94.6}],
 "home_base": {"lat": 38.9, "lon": -94.7},
 "buffer_minutes": 15, "granularity_minutes": 30}

{"feasible_slots": [{"start", "end", "drive_in_seconds", "drive_out_seconds", "drive_source"}], "count"}.

Tenant endpoints (client scope — implicitly your tenant)

Endpoint Notes
GET /clients/{slug} your client record (billing/calendar credentials redacted)
GET /clients/{slug}/kb/{tier} knowledgebase docs; tier general, negative, or escalation
PUT /clients/{slug}/kb/{tier} {"path", "content_md"} — creates a new version, history kept
GET /leads?disposition=&limit= dispositions: qualified, escalated, info-only, spam, booked
POST /leads inject a lead: {"client", "job_desc", name?, phone?, email?, address?, source?, urgency?, notify_client?}201 {"id", "client"}. Fires lead.created and (by default) notifies the business owner
GET /availability?client=&days=&address= proposable slots: {"mode", "slots": [{"start", "end", …}]}; pass address for drive-time-aware results on Field-tier clients
POST /appointments {"client", "start", "job_desc", name?, phone?, address?}201 {"appointment_id", "lead_id", "start", "end", "status", "lead_created"}; 409 if the slot was taken
GET /appointments?client= upcoming appointments
GET /conversations/{id} channel, disposition, summary, full transcript
GET /metrics current-period usage vs. allowances
GET /calendar-url your read-only ICS feed URL (subscribe from Google/Outlook/Apple; token-guarded, rotatable)

Webhooks (client scope)

POST /webhooks with {"url": "https://…", "events": ["lead.created", "appointment.booked"]} → the hook row including the once-shown whsec_… secret. GET /webhooks lists, DELETE /webhooks/{id} removes.

Deliveries are POST with JSON body and headers:

import hmac, hashlib
def verify(secret: str, body: bytes, header: str) -> bool:
    digest = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(header, "sha256=" + digest)

Respond 2xx quickly; anything else is retried a bounded number of times.


MCP server

https://api.composedchaos.net/stl-mcp/ — streamable-HTTP transport, authenticated with Authorization: Bearer <client-scope key> (api-scope keys are rejected). Every tool is automatically scoped to your tenant and metered on the same billing meters as the REST API.

Claude Code:

claude mcp add --transport http speed-to-lead \
  https://api.composedchaos.net/stl-mcp/ \
  --header "Authorization: Bearer stl_…"

Any other MCP-capable client:

{"mcpServers": {"speed-to-lead": {
    "url": "https://api.composedchaos.net/stl-mcp/",
    "headers": {"Authorization": "Bearer stl_…"}}}}

Tools

Tool What it does
list_clients the client record(s) your key can see
list_leads recent leads, filterable by disposition
create_lead inject a lead (fires webhooks + owner notification)
get_conversation transcript + summary for one conversation
get_knowledgebase KB docs for a tier
drive_time point-to-point drive time
feasible_slots drive-time-aware open slots for a job
check_availability proposable appointment slots
book_appointment book a slot (409-safe on conflicts)
parse_lead_email marketplace email → structured lead