Kennu API
The Kennu API lets your systems create meetings, add attendees and content, send briefings and see who has read them. Webhooks tell your systems when something happens; the MCP server lets AI assistants do the same on your behalf.
- Base URL:
https://dev.kennu.io/api/v1 - JSON in, JSON out, UTF-8
- Every request is authenticated with an API key of your organization
Quickstart
- In Kennu, open Organization → Developers → API keys (owners and admins) and create a key with Full access. Copy it — it's shown once.
- Create a meeting:
curl https://dev.kennu.io/api/v1/meetings \
-H "Authorization: Bearer kn_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: crm-event-4711" \
-d '{
"title": "Partnership kickoff",
"starts_at": "2026-11-03T15:00:00Z",
"duration_minutes": 45,
"purpose": "Agree on the pilot scope.",
"attendees": [{ "email": "priya@brightwater.example", "name": "Priya Nair" }],
"template_id": "4f1c..."
}'- Send the briefing:
POST /api/v1/meetings/{id}/send. Put thebriefing_urlfrom the response into your calendar or video-call invite.
Authentication
Send your key in the Authorization header: Authorization: Bearer kn_live_.... Keys start with kn_live_ in production and kn_test_ elsewhere.
| Permission | Allows |
|---|---|
| Full access | Everything below |
| Read only | GET requests only |
A key belongs to your organization, not to a person. Meetings created through the API are organized by organizer_email (a member who may organize meetings) or, if you leave it out, by the member who created the key. Revoke keys you no longer need under Developers → API keys.
Errors
Errors use HTTP status codes and a consistent body:
{ "error": { "type": "validation_error", "message": "starts_at: Invalid ISO datetime" } }| Status | Type | Meaning |
|---|---|---|
| 401 | missing_api_key, invalid_api_key | No key, or a revoked or unknown key |
| 403 | insufficient_permission, not_an_organizer | Read-only key, or the organizer may not organize |
| 404 | not_found | Not in your organization |
| 409 | meeting_cancelled | The meeting was cancelled |
| 422 | validation_error, invalid_request, invalid_organizer | The request body isn't valid |
| 429 | rate_limit_exceeded | Slow down (see below) |
| 500 | internal_error | Our fault — try again |
Rate limits
About 10 requests per second per key. Above that you get 429 with a Retry-After header.
Idempotency
Send an Idempotency-Key header with POST, PATCH and DELETE requests. Repeating a request with the same key within 24 hours returns the first response (with Idempotent-Replayed: true) instead of doing it twice — useful for retries.
Pagination
Lists return { "object": "list", "data": [...], "has_more": true }. Use ?limit=1..100 (default 20) and ?after=<id of the last item> for the next page.
Meetings
The meeting object
{
"object": "meeting",
"id": "8f0e...",
"title": "Partnership kickoff",
"starts_at": "2026-11-03T15:00:00+00:00",
"duration_minutes": 45,
"purpose": "Agree on the pilot scope.",
"organizer": { "id": "5777...", "name": "Mara Lindqvist" },
"briefing_url": "https://dev.kennu.io/m/kIR3P-...",
"briefing_sent_at": null,
"cancelled_at": null,
"created_at": "2026-10-20T19:20:00+00:00",
"attendees": [
{ "email": "priya@brightwater.example", "name": "Priya Nair", "role": null, "organization": null, "status": "invited", "briefing_viewed_at": null }
],
"modules": [{ "id": "38e3...", "title": "About Halden Logistics", "type": "about", "organization_id": "8719..." }]
}status is invited until the person opens the briefing (joined); a guest's profile is shared only from then on.
List meetings
GET /api/v1/meetings — newest first, paginated.
Create a meeting
POST /api/v1/meetings
| Field | Type | |
|---|---|---|
| title | string, required | Up to 200 characters |
| starts_at | ISO 8601 date-time with offset, required | |
| duration_minutes | integer 5–600 | Default 30, or the template's |
| purpose | string | Up to 5,000 characters, or the template's |
| organizer_email | string | A member who may organize; default: the key's creator |
| attendees | array of { email, name? } | Members of your organization join as colleagues, everyone else as guests |
| module_ids | array of ids | Your organization's content; default: the template's |
| template_id | id | Preselects content, duration and purpose |
Returns the meeting (201). Nobody is emailed until you send the briefing.
Retrieve a meeting
GET /api/v1/meetings/{id} — includes attendees and who opened the briefing.
Update a meeting
PATCH /api/v1/meetings/{id} with any of title, starts_at, duration_minutes, purpose.
Cancel a meeting
DELETE /api/v1/meetings/{id} — the meeting stays, marked as cancelled; attendees see that on the briefing.
Send the briefing
POST /api/v1/meetings/{id}/send — emails everyone not yet invited: guests get a personal, one-time link, colleagues a notice. The response lists recipients, emailed, and links for any guest who couldn't be emailed.
Content, templates, members
| Endpoint | Returns |
|---|---|
GET /api/v1/modules | Your organization's content: title, type, department, Markdown, freshness |
GET /api/v1/templates | Briefing templates with their modules and defaults |
GET /api/v1/members | Members with email, role, department and whether they may organize |
Webhooks
Add an endpoint under Organization → Developers → Webhooks and choose events (none ticked means all).
| Event | When |
|---|---|
| meeting.created | A meeting was created (app or API) |
| meeting.updated | Title, time, duration or purpose changed |
| meeting.cancelled | The meeting was cancelled |
| briefing.sent | The briefing was sent |
| briefing.viewed | An attendee opened the briefing for the first time |
| attendee.joined | An invited attendee opened their invitation |
| access.requested | Someone asked for access via a forwarded link |
| question.asked | An attendee added a question for the meeting |
| content.review_requested | An organizer asked for a content review |
Each delivery is a POST with a JSON body { "type", "created_at", "data" } and the Standard Webhooks headers webhook-id, webhook-timestamp and webhook-signature. Verify the signature with your endpoint's signing secret (whsec_...):
import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.KENNU_WEBHOOK_SECRET);
const event = wh.verify(rawBody, {
"webhook-id": req.headers["webhook-id"],
"webhook-timestamp": req.headers["webhook-timestamp"],
"webhook-signature": req.headers["webhook-signature"],
});The signature is v1, + base64(HMAC-SHA256(secret, "{webhook-id}.{webhook-timestamp}.{body}")), with the secret base64-decoded after whsec_. Answer with any 2xx within 10 seconds. Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours and 24 hours; every attempt is listed on the Webhooks page.
MCP server
AI assistants that speak the Model Context Protocol (Claude, ChatGPT, Cursor and others) can use Kennu on your behalf through the MCP server at https://dev.kennu.io/api/mcp (Streamable HTTP). Authenticate with an API key:
{
"mcpServers": {
"kennu": {
"url": "https://dev.kennu.io/api/mcp",
"headers": { "Authorization": "Bearer kn_live_..." }
}
}
}| Tool | Does |
|---|---|
| list_meetings | Upcoming (or past) meetings with attendees and read status |
| get_briefing | A meeting's briefing: purpose, people, shared content |
| create_meeting_draft | Creates a meeting (nobody is emailed) |
| send_briefing | Sends a meeting's briefing |
| list_content | Your organization's content |
The assistant can do what the key can do — use a read-only key to let it read but not change anything.