A REST API for creating meetings, admitting participants and pulling back the transcript, the summary, the decisions and the action items — so the meeting ends where your work already lives, not in a tab someone has to remember to check.
Post a title, a start time and a host. You get back a meeting id, a join link you can put straight into your own interface, and a code for anyone joining by phone or from the home page.
Invitations, waiting rooms and AI notes are fields on the same request. There is nothing to install on anyone's machine — the join link opens in a browser, which is the whole reason this API is short.
curl -X POST https://meet.codexal.co/api/v1/meetings \
-H "Authorization: Bearer $CODEXAL_API_KEY" \
-H "Idempotency-Key: crm-deal-4821-kickoff" \
-H "Content-Type: application/json" \
-d '{
"title": "Quarterly review - Sales",
"starts_at": "2026-10-02T10:00:00Z",
"duration_minutes": 60,
"host_email": "lina@acme.com",
"waiting_room": true,
"ai_notes": { "enabled": true, "language": "auto" }
}'const res = await fetch("https://meet.codexal.co/api/v1/meetings", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.CODEXAL_API_KEY}`,
"Idempotency-Key": `crm-deal-${deal.id}-kickoff`,
"Content-Type": "application/json"
},
body: JSON.stringify({
title: "Quarterly review - Sales",
starts_at: "2026-10-02T10:00:00Z",
duration_minutes: 60,
host_email: "lina@acme.com",
ai_notes: { enabled: true, language: "auto" }
})
});
const meeting = await res.json();
crm.attachLink(deal.id, meeting.join_url);$ch = curl_init("https://meet.codexal.co/api/v1/meetings");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("CODEXAL_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"title" => "Quarterly review - Sales",
"starts_at" => "2026-10-02T10:00:00Z",
"host_email" => "lina@acme.com",
"ai_notes" => ["enabled" => true, "language" => "auto"],
]),
]);
$meeting = json_decode(curl_exec($ch), true);
echo $meeting["join_url"];import os, requests
res = requests.post(
"https://meet.codexal.co/api/v1/meetings",
headers={"Authorization": f"Bearer {os.environ['CODEXAL_API_KEY']}"},
json={
"title": "Quarterly review - Sales",
"starts_at": "2026-10-02T10:00:00Z",
"duration_minutes": 60,
"host_email": "lina@acme.com",
"ai_notes": {"enabled": True, "language": "auto"},
},
)
print(res.json()["join_url"]){
"id": "mtg_8f2c41a9",
"code": "K7M-2QX-9PD",
"join_url": "https://meet.codexal.co/lobby.php?id=mtg_8f2c41a9",
"status": "scheduled",
"starts_at": "2026-10-02T10:00:00Z",
"duration_minutes": 60,
"waiting_room": true,
"host": { "name": "Lina Haddad", "email": "lina@acme.com" },
"ai_notes": { "enabled": true, "language": "auto" },
"created_at": "2026-09-13T08:41:22Z"
}Keys are issued per organisation and per environment. Sandbox keys are obviously sandbox keys, and a key that only needs to read summaries cannot create meetings.
Send the key in an Authorization header. Never in a query string — a URL ends up in browser history, proxy logs and screenshots, and a key in any of those is a key you have to rotate.
meetings:read, meetings:write, participants:write, records:read, users:write. Ask for the ones your integration actually uses; we issue exactly those.
Two keys can be live at once so a rotation is a deploy, not an outage. Enterprise keys can be pinned to your egress IP ranges, which makes a leaked key useless off your network.
GET /api/v1/meetings?status=ended&limit=20 HTTP/1.1
Host: meet.codexal.co
Authorization: Bearer cxl_live_9f4a21c8e7b3d05a
Accept: application/json
# Sandbox keys carry their own prefix and never touch live data:
# Authorization: Bearer cxl_test_3b81d0fa54c9e762Keys are self-service, and a developer account is separate from a meeting account. Even if you already sign in to Codexal Meet to hold meetings, the thing that signs API calls is an organisation with its own wallet, its own allowed domains and its own revocation switch — so it gets its own account. Create one, make an app, and the public and secret keys are on screen in about a minute, with sandbox credit already in the wallet.
Five groups. Everything is a normal resource with normal verbs, paginated with limit and cursor, and everything returns JSON.
/v1/meetings/v1/meetings/v1/meetings/{id}/v1/meetings/{id}/v1/meetings/{id}/v1/meetings/{id}/invitations/v1/meetings/{id}/participants/v1/meetings/{id}/participants/{participant_id}/admit/v1/meetings/{id}/participants/{participant_id}/deny/v1/meetings/{id}/tokens/v1/meetings/{id}/transcript/v1/meetings/{id}/summary/v1/meetings/{id}/attendance/v1/meetings/{id}/records/v1/users/v1/users/v1/users/{id}/v1/webhooks/v1/webhooks/v1/webhooks/{id}/v1/webhooks/{id}/testA meeting ends, the notes are written, and your endpoint hears about it. Deliveries are signed, timestamped and retried with a backoff for 24 hours.
The first participant was admitted and the room is live.
The room closed, with the final duration and attendance.
Someone is in the waiting room — admit or deny them from your own screen.
Someone was admitted, with their name and join time.
The transcript has been written and can be fetched.
The AI recap is done: summary, decisions and action items.
POST /hooks/codexal HTTP/1.1
Codexal-Signature: t=1790592259,v1=8d61a0...c47f
{
"id": "evt_5c1d90ab",
"type": "summary.ready",
"created_at": "2026-10-02T11:04:19Z",
"data": {
"meeting_id": "mtg_8f2c41a9",
"language": "ar",
"decisions": 3,
"action_items": 5,
"summary_url": "https://meet.codexal.co/api/v1/meetings/mtg_8f2c41a9/summary"
}
}$payload = file_get_contents("php://input");
$header = $_SERVER["HTTP_CODEXAL_SIGNATURE"] ?? "";
parse_str(strtr($header, ",", "&"), $sig);
$expected = hash_hmac("sha256", $sig["t"] . "." . $payload, $secret);
// Constant-time, or the comparison itself leaks the signature.
if (!hash_equals($expected, $sig["v1"] ?? "")) {
http_response_code(400);
exit;
}
// A valid signature on an old event is still a replay.
if (abs(time() - (int) $sig["t"]) > 300) {
http_response_code(400);
exit;
}
$event = json_decode($payload, true);import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const sig = Object.fromEntries(
header.split(",").map((p) => p.split("="))
);
const expected = crypto
.createHmac("sha256", secret)
.update(`${sig.t}.${rawBody}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(sig.t)) < 300;
return fresh && crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(sig.v1)
);
}Every one of these is the same three moves: create a meeting from a record, put the join link in front of the right person, and write the recap back where it will actually be read.
A class on the timetable becomes a room with a waiting room and a lesson agenda. The recap and the attendance register go back to the course record, and captions are on for every student.
Codexal Meet for education →Create the call from the deal, and post the summary, the decisions and the next steps onto the deal timeline before the rep has opened their laptop again.
An appointment produces a single-use join link for the patient and a waiting room for the clinician. No patient identifiers in the URL, and records deleted on your schedule.
Safeguards for telehealth →Escalate a ticket into a video call in one click, then attach the transcript to the ticket so the next agent reads what happened instead of asking again.
Provision and deprovision accounts from your own joiner-mover-leaver process, mint join tokens for staff already signed in, and keep one meeting list in your intranet.
SSO & admin controls →Webhooks are plain signed JSON, so Zapier, Make, n8n or a twelve-line script can route a recap into Slack, Notion, a spreadsheet or a database with nothing else in between.
Every error carries a machine-readable type, the field that caused it, and a request id. Quote the id at us and we can find the exact call in the logs.
A field is missing or the wrong shape. The response names it.
The key is missing, revoked, or a sandbox key sent at live data.
A valid key without the scope for this call.
No such resource, or it belongs to another organisation.
An idempotency key was reused with a different body.
Over the limit. Retry-After says how long to wait.
Ours. Safe to retry with the same idempotency key.
HTTP/1.1 422 Unprocessable Entity
X-Request-Id: req_0a93f1c7
X-RateLimit-Remaining: 587
{
"error": {
"type": "invalid_request",
"message": "starts_at must be an ISO 8601 timestamp in UTC.",
"param": "starts_at",
"request_id": "req_0a93f1c7"
}
}The version is in the path. Inside v1 we only add — new fields and new endpoints, never a changed meaning for one you already read. If a breaking change ever becomes unavoidable it ships as v2, with six months of overlap and an email to every key holder, not a changelog entry you were supposed to notice.
There is no endpoint for audio or video, and there never will be. Media is encrypted between browsers with keys our servers do not hold, so there is nothing for an API to hand you — no live stream, no server-side recording, no back door with a scope name on it. Transcripts and summaries exist because they are produced with the meeting's consent and stored under your retention settings. That boundary is described in full on the security architecture page.
Create a developer account, add an app, and the console gives you the keys, the step-by-step guide, the embed generator and a running total of what every meeting, recap and question costs. Sandbox credit is included.