Developer guide

Executive Coach API

Everything the coaching web app does is available over HTTPS JSON APIs: email sign-in, conversations with an evidence-grounded coach, voice, coaching plans, and admin controls. Other systems can hold conversations through inbound webhooks and receive events through signed outbound webhooks.

Base URL
FormatJSON request and response bodies (Content-Type: application/json), UTF-8
Machine-readableOpenAPI 3.1 specification, which you can import into Postman, Insomnia or a code generator

Authentication

There are three kinds of caller:

CallerCredentialHeader
People (web app, scripts acting as a person)Session token from email sign-in, valid for 14 daysX-Session-Token: …
Integrations (bots, workflows, CRMs)Integration key cik_…, created by an adminX-Api-Key: cik_…
AdminsA session token for a user with the admin roleX-Session-Token: …

Signing in by email code

curl -X POST $BASE/api/auth/request -H 'Content-Type: application/json' \
  -H "x-amz-content-sha256: $(printf '%s' '{"email":"you@veon.com"}' | shasum -a 256 | cut -d' ' -f1)" \
  -d '{"email":"you@veon.com"}'
# a 6-digit code arrives by email (valid for 10 minutes, 5 attempts)

curl -X POST $BASE/api/auth/verify ... -d '{"email":"you@veon.com","code":"123456"}'
# → {"token":"…"}   then send:  X-Session-Token: <token>
Request-body hash. Session endpoints sit behind CloudFront with origin access control. For any POST, PUT, PATCH or DELETE, send x-amz-content-sha256 set to the hex SHA-256 of the exact request body (the hash of an empty string when there is no body). The web app does this automatically. Inbound webhooks don't need it.

Who may sign up depends on the server's SIGNUP_MODE: open, domain (an allowed email domain) or invite (an admin adds the person first). /api/auth/request always returns {"ok":true}, so it never reveals which addresses exist.

Conversation flow

  1. POST /api/conversations with {"topic":"conflict_resolution"}, or no topic. The response holds the conversation and the coach's opening message.
  2. POST /api/conversations/{id}/messages with {"text":"…","voice":false}. The response returns the coach's message: text, citations, follow_ups, commitment, escalate, plus a sig used for text-to-speech.
  3. POST /api/conversations/{id}/end returns the coaching plan: goal, actions[] (with by_when and success_measure), key_insights, reflection_question and sources, together with the leader's email_pref.
  4. POST /api/conversations/{id}/email-plan emails the plan to the leader. With email_plan = "always" the server sends it when the session ends.
// a coach message
{
  "id": "1762151234567890000", "role": "assistant", "channel": "web",
  "text": "Start with decision rights, not personalities… [1]",
  "citations": [{"n": 1, "title": "Principles of Management (2019 edition)", "author": "OpenStax", "year": 2019,
                 "license": "CC-BY-4.0", "url": "https://openstax.org/…"}],
  "follow_ups": ["Help me script the opening", "What if they escalate?"],
  "commitment": "", "escalate": false, "sig": "…"
}

Each turn uses the leader's profile, the coach's memory of them, open commitments, a rolling session summary and passages from the vetted library, which is an Amazon Bedrock Managed Knowledge Base. escalate:true means the message touched HR, legal, medical or safety matters, and the coach pointed the leader to the right human support.

Commitments & reminders

Commitments are the actions a leader agrees to. Aspasia captures them from the conversation, adds the actions from each coaching plan, and you can add your own. A reminder is only created when the leader explicitly asks (in chat: “remind me tomorrow morning to…”), or taps the bell on a commitment or plan action.

  • POST /api/commitments add · PATCH /api/commitments/{id} mark done / rename / set a due date · DELETE remove. Marking a commitment done cancels its reminders.
  • POST /api/reminders with {"text","when":"2026-11-05T09:00:00","tz":"Europe/London","tz_offset":0}. A time without a zone is read in tz_offset (minutes east of UTC). Send tz and tz_offset on messages too, so “tomorrow at 9” means the leader’s tomorrow.
  • At the set time the leader gets an email with a calendar file (.ics) attached, checked every 5 minutes. The app also offers Google Calendar, Outlook and .ics download for any reminder time.
  • Events: commitment.created, reminder.created, reminder.sent.

Voice

  • Speech to text, streaming: GET /api/voice/stream-url returns a presigned Amazon Transcribe WebSocket URL, valid for 5 minutes. Stream 16 kHz mono PCM as AWS event-stream AudioEvent messages.
  • Speech to text, upload: POST /api/voice/stt takes a raw audio/wav, webm, ogg, mp4 or mpeg body (up to about 3 minutes) and returns {"text":…}.
  • Text to speech: POST /api/voice/tts with {"text","sig","voice"} returns audio/mpeg. Only text the coach produced can be spoken: pass the message's text and sig unchanged. Voices are listed in GET /api/config.
  • Set "voice": true on a message for a short, speakable reply without markdown, generated by a lower-latency model.

Inbound webhooks

Let a Slack or Teams bot, a WhatsApp bridge, n8n, Zapier or a CRM coach a leader by email address. Admins create keys in Admin → Webhooks & integrations.

curl -X POST $BASE/api/hooks/inbound \
  -H 'X-Api-Key: cik_…' -H 'Content-Type: application/json' \
  -d '{"email":"leader@veon.com","text":"Help me prepare for a tough board meeting","topic":"decision_making"}'

→ 200 {"conversation_id":"…","message_id":"…","reply":"…","citations":[…],"follow_ups":[…],"escalate":false,"commitment":""}
  • Pass the returned conversation_id to continue the same conversation.
  • Add "async": true to get 202 straight away. The reply then arrives as a conversation.turn outbound webhook, which suits platforms such as Slack that time out after 3 seconds.
  • Each key is limited to 1,200 requests per hour.

Outbound webhooks

Register HTTPS endpoints in the admin console and pick which events each one receives. Every delivery is a POST with this body:

{"id": "evt_…", "type": "conversation.turn", "created": 1762151234, "data": { … }}
EventWhenKey data
user.signed_upA new person is createdemail, uid
conversation.startedA session beginsconversation_id, topic, channel
conversation.turnThe leader spoke and the coach replied. Covers web, voice and inbound channels; the full exchange is stored in the threaduser_message, coach_message (text, citations), channel (web, voice or webhook:…), provider
conversation.endedA session endssummary, goal, actions, key_insights, reflection_question, commitments
commitment.createdThe leader commits to an actioncommitment
reminder.created / reminder.sentA reminder is set (in chat or from a commitment) / emailed at its timereminder (text, due, tz)
user.addedAn admin adds a person (with the welcome-email result)email, by, welcome
coaching_plan.emailedA coaching plan email is sentconversation_id, title
coach.escalationA message needs human support (HR, legal, medical or safety)message
knowledge.sync_startedAn admin starts a knowledge syncstatus
webhook.testAn admin clicks Send test(none)

Verifying signatures

Signatures follow the Standard Webhooks specification. Each delivery carries three headers: webhook-id, webhook-timestamp and webhook-signature: v1,<base64>. The signature is an HMAC-SHA256 over {id}.{timestamp}.{raw body}, keyed with the base64-decoded part of your whsec_… secret.

# Python: pip install standardwebhooks
from standardwebhooks import Webhook
event = Webhook("whsec_…").verify(raw_body, headers)      # raises on a bad signature or a timestamp more than 5 minutes off

// Node.js: npm i standardwebhooks
import { Webhook } from "standardwebhooks";
const event = new Webhook("whsec_…").verify(rawBody, headers);

A non-2xx response or a timeout of more than 10 seconds counts as a failure. Failed deliveries are retried up to 8 times, then moved to a dead-letter queue. The admin console shows each attempt. An endpoint is paused after 100 consecutive failures. Deduplicate on webhook-id.

Errors & limits

Errors return JSON in the form {"detail": "Human-readable message"}. Unexpected errors also include a request_id.

StatusMeaning
400 / 422Invalid input. The message names the field.
401Not signed in, the session expired, or an invalid integration key
403Not allowed (for example, the admin role is required)
404Not found, or not yours
413 / 415The audio is too long or not a supported type
429Rate limited. Respect Retry-After.
502 / 503An upstream service (models, email or voice) is briefly unavailable. Retry with backoff.

Main limits: 150 messages per user per hour; 200 text-to-speech requests per user per hour; sign-in at 20 code requests and 40 verifications per IP per hour (with a WAF rule on top); 1,200 inbound webhook calls per key per hour.

Endpoint reference

Generated from the live OpenAPI specification.