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 | |
| Format | JSON request and response bodies (Content-Type: application/json), UTF-8 |
| Machine-readable | OpenAPI 3.1 specification, which you can import into Postman, Insomnia or a code generator |
Authentication
There are three kinds of caller:
| Caller | Credential | Header |
|---|---|---|
| People (web app, scripts acting as a person) | Session token from email sign-in, valid for 14 days | X-Session-Token: … |
| Integrations (bots, workflows, CRMs) | Integration key cik_…, created by an admin | X-Api-Key: cik_… |
| Admins | A session token for a user with the admin role | X-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>
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
POST /api/conversationswith{"topic":"conflict_resolution"}, or no topic. The response holds the conversation and the coach's opening message.POST /api/conversations/{id}/messageswith{"text":"…","voice":false}. The response returns the coach'smessage:text,citations,follow_ups,commitment,escalate, plus asigused for text-to-speech.POST /api/conversations/{id}/endreturns the coaching plan:goal,actions[](withby_whenandsuccess_measure),key_insights,reflection_questionandsources, together with the leader'semail_pref.POST /api/conversations/{id}/email-planemails the plan to the leader. Withemail_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/commitmentsadd ·PATCH /api/commitments/{id}mark done / rename / set a due date ·DELETEremove. Marking a commitment done cancels its reminders.POST /api/reminderswith{"text","when":"2026-11-05T09:00:00","tz":"Europe/London","tz_offset":0}. A time without a zone is read intz_offset(minutes east of UTC). Sendtzandtz_offseton 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-urlreturns a presigned Amazon Transcribe WebSocket URL, valid for 5 minutes. Stream 16 kHz mono PCM as AWS event-streamAudioEventmessages. - Speech to text, upload:
POST /api/voice/stttakes a rawaudio/wav,webm,ogg,mp4ormpegbody (up to about 3 minutes) and returns{"text":…}. - Text to speech:
POST /api/voice/ttswith{"text","sig","voice"}returnsaudio/mpeg. Only text the coach produced can be spoken: pass the message'stextandsigunchanged. Voices are listed inGET /api/config. - Set
"voice": trueon 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_idto continue the same conversation. - Add
"async": trueto get202straight away. The reply then arrives as aconversation.turnoutbound 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": { … }}
| Event | When | Key data |
|---|---|---|
user.signed_up | A new person is created | email, uid |
conversation.started | A session begins | conversation_id, topic, channel |
conversation.turn | The leader spoke and the coach replied. Covers web, voice and inbound channels; the full exchange is stored in the thread | user_message, coach_message (text, citations), channel (web, voice or webhook:…), provider |
conversation.ended | A session ends | summary, goal, actions, key_insights, reflection_question, commitments |
commitment.created | The leader commits to an action | commitment |
reminder.created / reminder.sent | A reminder is set (in chat or from a commitment) / emailed at its time | reminder (text, due, tz) |
user.added | An admin adds a person (with the welcome-email result) | email, by, welcome |
coaching_plan.emailed | A coaching plan email is sent | conversation_id, title |
coach.escalation | A message needs human support (HR, legal, medical or safety) | message |
knowledge.sync_started | An admin starts a knowledge sync | status |
webhook.test | An 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.
| Status | Meaning |
|---|---|
| 400 / 422 | Invalid input. The message names the field. |
| 401 | Not signed in, the session expired, or an invalid integration key |
| 403 | Not allowed (for example, the admin role is required) |
| 404 | Not found, or not yours |
| 413 / 415 | The audio is too long or not a supported type |
| 429 | Rate limited. Respect Retry-After. |
| 502 / 503 | An 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.