Glyph API
Glyph API

Put a live voice tutor inside your software.

One API call opens the full Glyph player for a student. Glyph remembers earlier calls, and when a call ends you get the transcript by webhook or by asking for it. AI agents can do all of this through MCP.

REST APIhttps://glyph.myapps.ai/api/v1
MCP serverhttps://glyph.myapps.ai/api/mcp
OpenAPIhttps://glyph.myapps.ai/api/v1/openapi.json for Postman, Insomnia, and code generators
ConsoleTry every endpoint in the browser with your key

Web embed or your own Glyph

Every integration starts with the same call, POST /api/v1/embed-links. Choose how to show it with mode. In both, Glyph does the voice, tools, billing, and transcripts, and calls are charged to the student's credits the same way.

Web embedYour own Glyph
mode"embed" (default)"custom"
What students seeThe full Glyph player: orb, captions, settings, start buttonWhatever you design: your own orb, avatar, captions and buttons
What you add to your pageOne iframeThe SDK script and a few lines of JavaScript
What you get backWebhooks and transcriptsWebhooks and transcripts, plus live audio levels, call state, captions, and every control in the page
Settings (voice, language, push-to-talk, voice detection)Set when you create the linkSet when you create the link, and change live
Best forShipping in minutesA branded experience that matches your app
GuideEmbed the playerBuild your own player

The response always includes url, sdk_url, and a one-line how_to_use, so you can switch styles later without changing your server.

Quickstart

Five minutes from no key to a student talking to Glyph inside your app.

  1. Get an API keyOpen the console, sign in with your Glyph access code, and press Create key. The key works for your own account (you can also manage keys in Glyph under Settings → Developer API). Schools and businesses connecting many students: ask your Glyph administrator for an organization key.
  2. Open the player with one callSend the student's access code (or account id). You get back a url.
    export GLYPH_API_KEY=glk_your_key
    
    curl -X POST "https://glyph.myapps.ai/api/v1/embed-links" \
      -H "Authorization: Bearer $GLYPH_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"account_id": "GLYP-XXXX-XXXX"}'
  3. Show itUse the Glyph player as is, or draw your own. The student presses Start and talks.
    <iframe src="PLAYER_URL" allow="microphone; autoplay" style="width:100%;height:680px;border:0"></iframe>
  4. Read what was saidAbout 20 seconds after the student stops, the transcript is final. Ask for it, or register a webhook.
    curl
    curl "https://glyph.myapps.ai/api/v1/conversations/CONVERSATION_ID/transcript" \
      -H "Authorization: Bearer $GLYPH_API_KEY"
✦
Calling it again continues the same conversation. The next link for that student picks up their latest conversation with your key and seeds the tutor with the last call. Send "conversation": "new" to start fresh.

Keys and accounts

Send your key in the Authorization header on every request: Authorization: Bearer glk_...

Key typeWho creates itWhat it can do
PersonalAny student, in Settings → Developer API (up to 10)Acts only for that student's account. The account is implied, so you can leave account_id out.
OrganizationA Glyph administrator, in the admin panelActs for any student. Every request names the student.

Naming a student

Wherever the API wants an account you can send either the account id (a uuid, shown in Settings) or the student's access code such as GLYP-G5D6-7XRW. Put it in the JSON body as account_id, in the query string as ?account_id=, or in an X-Glyph-Account header.

Keeping keys safe

  • The API accepts calls from any website (CORS is open), but anyone who sees a key can spend its credits. Call it from your server when you can and hand the browser only the player URL.
  • Each app gets its own key, so you can revoke one without touching the others.
  • Revoking a key in Settings or the admin panel takes effect on the next request.
  • Conversations, links and webhooks belong to the key that created them. Other keys cannot see them.

Embed the player

The web embed: create the link with "mode": "embed" or leave mode out. Choose the voice, language, push-to-talk and voice detection with settings. Prefer your own design? See Build your own player.

The player URL opens the complete Glyph experience: the orb, live captions, web search, images, the student's course library and their credit balance. Usage is billed to the student's Glyph account, one credit per second of voice.

<!-- url comes from POST /api/v1/embed-links, made by your server -->
<iframe
  src="PLAYER_URL"
  allow="microphone; autoplay"
  style="width: 100%; height: 680px; border: 0; border-radius: 16px"
  title="Glyph tutor"
></iframe>

Good to know

  • Microphone: the iframe needs allow="microphone; autoplay" and your page must be served over https (localhost is fine while developing).
  • Any website: player links can be framed by any site, so there is nothing to register. Opening the URL in a new tab works too.
  • Lifetime: links expire after ttl_minutes (default 60, max 1440). Make a fresh link each time the student opens the player rather than storing one.
  • Revoking: DELETE /api/v1/embed-links/{id} cuts the player off within about 30 seconds, including a call in progress.
  • Engine: leave engine out to use the student's plan default. classic is the Realtime voice engine; live is GPT-Live on plans that include it.

Build your own player

Want your own orb, avatar, or layout instead of the Glyph player? Load the Glyph SDK. It runs the real player invisibly in a 1 pixel frame, so Glyph still does the voice, tools, billing and transcript saving, and hands your page everything it needs to draw: live audio levels for the student and for Glyph (each on its own, or combined), frequency bands, call state, captions and tool results, plus every control: start and stop, mute, hands-free or push-to-talk, voice detection tuning, voice, language, which tools Glyph may use, showing Glyph an image, interrupt, and typed messages. Both engines work, and calls are billed and saved exactly like the stock player.

  1. Your server creates a player link with "mode": "custom" (POST /api/v1/embed-links). The response includes sdk_url.
  2. Your page loads https://glyph.myapps.ai/sdk/glyph.js and calls GlyphVoice.connect(url).
  3. Call start() from a click handler, then draw from the levels event.
<script src="https://glyph.myapps.ai/sdk/glyph.js"></script>

<div id="blob"></div>
<p id="caption"></p>
<button id="talk">Start</button>

<script>
  // playerUrl comes from POST /api/v1/embed-links, made by your server
  const glyph = GlyphVoice.connect(playerUrl);

  // ~20 times a second while a call runs: 0..1 loudness of the student and of Glyph
  glyph.on("levels", ({ input, output, speaking }) => {
    const energy = Math.max(output, input * 0.6);
    blob.style.transform = `scale(${1 + energy * 0.5})`;
    blob.classList.toggle("glyph-speaking", speaking);
  });

  glyph.on("transcript", ({ turns }) => {
    caption.textContent = turns.at(-1)?.text ?? "";
  });

  glyph.on("state", ({ state }) => {
    talk.textContent = state === "active" || state === "connecting" ? "Stop" : "Start";
  });

  glyph.on("ended", ({ sessionId, conversationId }) => {
    // tell your server; the final transcript is ready about 20 seconds later
  });

  talk.onclick = () => (glyph.state === "active" ? glyph.stop() : glyph.start());
</script>

SDK reference

Method or eventWhat it does
GlyphVoice.connect(url)Loads the hidden player for a link. Returns a controller. Every method below returns a Promise that rejects with a readable Error when it cannot apply.
start() / stop()Starts or ends the call. Call start() from a click so the browser allows the microphone and audio.
mute() / unmute() / toggleMute() / setMuted(bool)The student's microphone. Both engines.
configure({ input_mode, voice, language, vad, instructions })Change any player setting live (see Player settings). Classic applies changes mid-call; Live applies voice and instructions on the next call.
setInputMode(mode) / setVoice(v) / setLanguage(code) / setVad(vad) / setInstructions(text)Shortcuts for configure().
talkStart() / talkEnd()Push-to-talk: hold the microphone open, then send. Needs input_mode "ptt" (classic engine).
bindPushToTalk(element)Turns any element into a hold-to-talk button (mouse, touch, Space and Enter). Returns an unbind function.
interrupt()Stops Glyph mid-sentence (classic engine; on Live the student can simply talk over Glyph).
sendText(text)Sends a typed message as the student; Glyph answers out loud (classic engine).
sendImage(image, { prompt })Shows Glyph a picture, like dropping one on the player: a File, a Blob, a data: URL or an https URL. prompt asks a question about it (classic engine). Works on both engines; the result arrives as a tool event and in the transcript.
bindImageDrop(element)Lets students drop an image onto any element of your page. Returns an unbind function.
setTools(names) / setTheme(theme)Which tools Glyph may use ([] for none), and the orb color of the stock player.
destroy()Ends any call and removes the hidden player.
readyThe player loaded. Payload: engine, conversationId, settings.
stateidle, connecting, active, or ended.
levelsAbout 20 times a second during a call: input (student) and output (Glyph) loudness 0 to 1, combined (both together), speaking, userSpeaking, and bands.input / bands.output with 16 frequency bands each.
settingsWhenever a setting changes: engine, input_mode, voice, language, vad, muted, talking, tools { available, enabled }, theme, plus the voices, languages, engines and themes you can choose from.
transcriptEvery change: spoken turns as { id, role, text, final }. Good for live captions.
toolEach tool call, result or error as it happens: { id, name, status, arguments, result, error, images }. Show generated images the moment they exist.
sessionThe call's session id, for GET /api/v1/sessions/{id}/transcript.
endedThe call finished: sessionId and conversationId. The final transcript lands about 20 seconds later.
creditsRemaining credits after each charge.
status / errorHuman-readable progress and problems (no microphone, out of credits, a control that does not apply).
glyph.state, glyph.levels, glyph.settings, glyph.transcript, glyph.toolsThe latest values, if you prefer reading over events, e.g. inside requestAnimationFrame.

Player settings

Set them when you create the link (settings in POST /api/v1/embed-links, for the web embed and your own Glyph), or change them live with configure(). All are optional.

SettingValuesEngines
input_mode"vad" hands-free (default), or "ptt" push-to-talkvad: both. ptt: classic
voiceClassic: marin, cedar, ash, ballad, coral, sage, verse, echo, alloy, shimmer. Live: meridian, quartz, ripple, vesper, willow, stone, gleam, bossa, tempo, beacon, delta, cinderBoth (Live: next call)
language"" auto-detect, or a code such as "en", "es", "fr". Improves captions and transcriptsClassic
vad.type"server_vad" (volume based, default) or "semantic_vad" (waits until the student finishes a thought)Classic
vad.threshold0 to 1, default 0.5. Higher needs louder speech; raise it in noisy roomsClassic, server_vad
vad.silence_ms100 to 3000, default 500. Silence before Glyph replies; raise it for students who pauseClassic, server_vad
vad.prefix_padding_ms0 to 2000, default 300. Audio kept from just before speech startedClassic, server_vad
vad.eagerness"low", "medium", "high" or "auto". How quickly Glyph jumps inClassic, semantic_vad
vad.interrupttrue (default) lets the student talk over Glyph to stop itClassic
instructionsReplaces Glyph's tutor instructions, up to 16,000 characters. Live only through configure(), never stored in linksBoth (Live: next call)
toolsWhich tools Glyph may use, by name (see the list below). Leave out for all; [] for noneBoth (Live: next call)
themeOrb color of the Glyph player: aurora, ember, lagoon, violet, solar, glacier, jade, rose, midnight, candy, copper, monoWeb embed
curl
curl -X POST "https://glyph.myapps.ai/api/v1/embed-links" \
  -H "Authorization: Bearer $GLYPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"account_id": "GLYP-XXXX-XXXX", "mode": "custom",
       "settings": {"input_mode": "ptt", "voice": "cedar", "language": "es",
                    "vad": {"type": "server_vad", "threshold": 0.6, "silence_ms": 800},
                    "tools": ["generate_image", "run_signal_orchestration"]}}'

Glyph's tools

Everything Glyph can do on its own during a call. Choose any of them with the tools setting; they show up in transcripts and in the SDK's tool event under these names. Students can also show Glyph a picture: in the player by dropping it, in your own UI with sendImage().

ToolWhat it does
run_signal_orchestrationResearch. Answers source-grounded and latest-news questions by searching and reading several sources.
search_google_serpWeb search. A quick Google search.
get_latest_newsNews. Reads the latest headlines from a news site.
generate_imageImage generation. Draws a new image and returns a hosted link.
consult_course_libraryCourse materials. Answers from the student's uploaded notes and readings (when the course library is on).
compute_with_codeCompute. Runs code for exact math and statistics (when the course library is on).
web_searchLive web search. The Live engine's built-in web search (Live only).
✦
Works on any website. Your page needs https (localhost is fine while developing), and the key stays on your server: the browser only ever sees the player link. Try it live in the console: Player → Your own UI (SDK).

Conversations and transcripts

A conversation is one student's thread with your app. Each call inside it is a session. The player saves what both sides say as the call goes. When the call ends, Glyph waits for the last words to land, then marks the transcript complete, usually within 20 seconds.

// After the student closes the player, wait for the final transcript.
async function finalTranscript(sessionId) {
  for (let attempt = 0; attempt < 12; attempt++) {
    const response = await fetch(`https://glyph.myapps.ai/api/v1/sessions/${sessionId}/transcript`, {
      headers: { Authorization: `Bearer ${process.env.GLYPH_API_KEY}` },
    });
    const transcript = await response.json();
    if (transcript.complete) return transcript;
    await new Promise((resolve) => setTimeout(resolve, 5000));
  }
  throw new Error("Transcript not final yet");
}

Tools and images in transcripts

When Glyph uses a tool during a call (web search, news, research, image generation, a picture the student shows, course materials), the transcript gets tool turns between the spoken ones: one when the tool is called and one with its result or error. Each tool turn has structured fields, so you never parse the text:

FieldMeaning
tool.namegenerate_image, search_google_serp, get_latest_news, run_signal_orchestration, scan_image, consult_course_library, compute_with_code, or live_research (the Live engine's own web searches and research)
tool.statuscalled, done, error, or info (progress notes)
tool.argumentsWhat Glyph asked the tool for (called turns)
tool.resultWhat the tool returned (done turns). Very long results are shortened to keep the turn whole
tool.errorWhy it failed (error turns)
imagesHosted image URLs from the result: generated images (with their prompt) and pictures the student showed. Ready for an img tag
a tool turn
{
  "role": "tool",
  "sequence": 5,
  "text": "generate_image result:\n{\"kind\":\"generated_image\",...}",
  "tool": {
    "name": "generate_image",
    "status": "done",
    "result": {
      "kind": "generated_image",
      "prompt": "The storming of the Bastille, painted",
      "imageUrl": "https://cdn.example.com/glyph/bastille.png"
    }
  },
  "images": [
    {
      "url": "https://cdn.example.com/glyph/bastille.png",
      "source": "generated",
      "prompt": "The storming of the Bastille, painted"
    }
  ]
}

Webhooks carry the same turns. Showing only the conversation? Keep turns whose role is user or assistant.

Why a transcript can be empty

transcript_note is null when all went well. Otherwise it says why:

NoteMeaning
No speech was captured in this call.The student opened the player and closed it without talking.
Transcript unavailable: session history is turned off…The Glyph server does not store transcripts. Its operator must enable session history.
Transcript unavailable: the call was not linked to a saved session.The call could not be matched to saved turns, for example a raw session that never posted turns.

Your own data

Attach up to 8 KB of JSON as metadata when you create a conversation, for example your course or user ids. It comes back with the conversation and in every webhook.

Webhooks

Register a URL once and Glyph POSTs a session.ended event to it after every call, with the complete transcript. Answer with any 2xx status within 15 seconds.

curl
curl -X POST "https://glyph.myapps.ai/api/v1/webhooks" \
  -H "Authorization: Bearer $GLYPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-app.example.com/hooks/glyph"}'
# → { "id": "wh_...", "secret": "whsec_..." }  keep the secret, it is shown once

What you receive

X-Glyph-Eventsession.ended
X-Glyph-DeliveryUnique per delivery. Use it to ignore repeats.
X-Glyph-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body" with your secret>
session.ended
{
  "type": "session.ended",
  "session": {
    "id": "live_8f2c1a7b3d9e",
    "engine": "live",
    "status": "ended",
    "started_at": "2026-10-05T07:12:51.000Z",
    "ended_at": "2026-10-05T07:14:22.000Z",
    "close_reason": "user_stop",
    "usage_seconds": 91,
    "summary": null,
    "transcript_note": null
  },
  "conversation": {
    "id": "conv_fOl2E6E1-3caIeRZt8ZMuQ",
    "title": "Can you quiz me on the French Revolution?",
    "metadata": {
      "course": "HIST-201"
    }
  },
  "account_id": "3f6c2a8e-1b4d-4c7a-9e2f-5a8b7c6d4e3f",
  "transcript": [
    {
      "role": "user",
      "text": "Can you quiz me on the French Revolution?",
      "sequence": 0
    },
    {
      "role": "assistant",
      "text": "Of course. First question: what year was the storming of the Bastille?",
      "sequence": 1
    },
    {
      "role": "tool",
      "text": "generate_image result:\n{\"kind\":\"generated_image\",...}",
      "sequence": 2,
      "tool": {
        "name": "generate_image",
        "status": "done",
        "result": {
          "kind": "generated_image",
          "imageUrl": "https://cdn.example.com/glyph/bastille.png"
        }
      },
      "images": [
        {
          "url": "https://cdn.example.com/glyph/bastille.png",
          "source": "generated",
          "prompt": "The storming of the Bastille, painted"
        }
      ]
    }
  ]
}

Verify the signature

Always check the signature before trusting a delivery, and reject timestamps older than five minutes.

import crypto from "node:crypto";
import express from "express";

const app = express();

// Use the raw body: the signature covers the exact bytes Glyph sent.
app.post("/hooks/glyph", express.raw({ type: "application/json" }), (req, res) => {
  const body = req.body.toString("utf8");
  const parts = Object.fromEntries(
    (req.get("X-Glyph-Signature") ?? "").split(",").map((part) => part.split("=", 2)),
  );
  const expected = crypto
    .createHmac("sha256", process.env.GLYPH_WEBHOOK_SECRET)
    .update(`${parts.t}.${body}`)
    .digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  const valid =
    fresh &&
    typeof parts.v1 === "string" &&
    parts.v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
  if (!valid) return res.status(400).send("bad signature");

  const event = JSON.parse(body);
  // Deliveries can repeat: X-Glyph-Delivery is unique per delivery.
  saveTranscript(event.conversation?.id, event.session.id, event.transcript);
  res.sendStatus(200); // answer fast; do slow work afterwards
});

Retries

A delivery that fails or times out is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, then every 6 hours, for up to 24 hours. GET /api/v1/webhooks/{id}/deliveries shows each attempt's status code and error, and POST /api/v1/webhooks/{id}/test sends a sample event so you can check your receiver.

✦
No server? Point the webhook at a Zapier "Catch Hook", a Make custom webhook, or an n8n webhook node to drop transcripts into Google Sheets, Notion, or email without writing code.

AI agents (MCP)

Glyph is an MCP server at https://glyph.myapps.ai/api/mcp (Streamable HTTP). Agents authenticate with the same API key and get these tools:

ToolWhat it does
open_playerA player URL for a student, continuing their latest conversation.
revoke_player_linkCut a player link off.
create_conversation / list_conversationsManage conversation threads.
get_transcript / get_conversation_transcriptWhat was said in one call, or a whole conversation.
get_credits / get_usage / get_ledgerBalance and plan, usage over time, and individual charges.
claude mcp add --transport http glyph https://glyph.myapps.ai/api/mcp \
  --header "Authorization: Bearer glk_your_key"

Conversation transcripts are also exposed as the resource glyph://conversations/{conversation_id}/transcript.

Recipes

A "Talk to your tutor" button on your site

  1. Store each user's Glyph access code (or account id) with their profile.
  2. When they click the button, your server calls POST /api/v1/embed-links and returns the url.
  3. Your page shows the url in an iframe. Make a new link every time.
  4. Register a webhook to save each transcript next to the user, keyed by conversation.id.

A learning platform or LMS

Create one conversation per student per course with metadata such as {"course": "HIST-201"}, and pass its conversation_id when making links. Each course keeps its own memory, and webhooks tell you which course a transcript belongs to.

Study notes after each call

When the operator enables summaries, every transcript also carries a three to five sentence summary of what was covered and what to revisit. Store it, or email it to the student from your webhook handler.

Your own voice client

Most apps should embed the player, or build their own UI with the SDK. Only if you need to run the WebRTC connection yourself, POST /api/v1/conversations/{id}/sessions takes your SDP offer and returns the answer for a Live session in that conversation. Two rules apply:

  • It is Live-only and needs the Glyph server to meter Live usage itself, so billing is exact without trusting the client. Classic voice is available through the player.
  • Your client saves what was said with POST /api/v1/sessions/{id}/turns, otherwise the transcript is empty.

Errors and limits

Every error has the same shape:

json
{
  "error": {
    "code": "not_found",
    "message": "Conversation not found."
  }
}
StatuscodeWhat to do
400invalid_requestFix the request; the message names the field.
401unauthorizedMissing, unknown, or revoked key.
402insufficient_creditsThe student is out of credits. credits_remaining is included.
403forbiddenA personal key acting for another account, or a feature the plan lacks.
404not_foundIt does not exist, or belongs to another key.
409conflictFor example, the personal key limit is reached.
503unavailableA server feature is not set up yet. The message says which.
500internal_errorRetry with backoff. Tell the operator if it persists.

Limits

  • Player links last 1 to 1440 minutes (default 60).
  • Revoking a player link takes effect within about 30 seconds.
  • Up to 10 active personal keys per student.
  • Lists return at most 100 items; use limit.
  • Conversation metadata is limited to 8 KB.
  • Webhook receivers must answer within 15 seconds; failed deliveries retry for 24 hours.

Server setup

For whoever runs the Glyph server. The API is part of the Glyph app; there is nothing extra to deploy.

SettingPurpose
GLYPH_API_SECRETRequired for player links. 32+ random characters; signs the links.
SESSION_HISTORY_ENABLED=trueRequired for transcripts (plus scripts/sql/001_voice_sessions.sql).
EMBED_ALLOWED_ORIGINSOptional. Player links work on any site regardless. Leave empty or * to let any site frame the signed-in portal too, or list sites to restrict it
GLYPH_PUBLIC_URLOptional. The public https URL used in player links when behind a proxy.
GLYPH_SUMMARIES_ENABLED=trueOptional. Adds a summary to each transcript (GLYPH_SUMMARY_MODEL to choose the model).
GLYPH_EMBED_TTL_MINUTESOptional. Default player link lifetime (60).
GLYPH_TRANSCRIPT_SETTLE_DELAYSOptional. Seconds between transcript re-reads after a call, default 2,5,10,20.
GLYPH_WEBHOOK_WORKER=falseOptional. Turns off webhook retries on this instance (keep one instance on).
LIVE_DIRECTOR_ENABLED=trueNeeded only for raw Live sessions from your own client.

Database: run scripts/sql/003_glyph_api.sql once in the Neon SQL editor. It only creates the API's own tables and is safe to run again. Until it runs, the API answers 503 unavailable with that instruction.

API reference

Base URL https://glyph.myapps.ai/api/v1. All requests need Authorization: Bearer glk_.... Bodies are JSON unless noted. Every endpoint is also in the OpenAPI document and the console. There are 22 endpoints.

Embed player

One call returns a URL that opens the full Glyph player.

Conversations

Threads that keep context across calls for one student.

POST/api/v1/conversationsMCP: create_conversation

Create a conversation

A thread for one student. Every call in it is seeded with the previous call's transcript, so the tutor picks up where it left off.

JSON body

FieldTypeDescription
account_idrequiredstringThe student: account id (uuid) or access code like GLYP-G5D6-7XRW. Optional with a personal key.
titlestringShown in lists. Filled from the student's first sentence when empty.
metadataobjectYour own data, up to 8 KB, returned with the conversation and in webhooks.

Request

curl
curl -X POST "https://glyph.myapps.ai/api/v1/conversations" \
  -H "Authorization: Bearer $GLYPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"account_id":"GLYP-G5D6-7XRW","title":"French Revolution review","metadata":{"course":"HIST-201"}}'

Response · 201

json
{
  "id": "conv_fOl2E6E1-3caIeRZt8ZMuQ",
  "account_id": "3f6c2a8e-1b4d-4c7a-9e2f-5a8b7c6d4e3f",
  "title": "French Revolution review",
  "metadata": {
    "course": "HIST-201"
  },
  "last_session_id": null,
  "created_at": "2026-10-05T07:11:41.000Z",
  "updated_at": "2026-10-05T07:14:22.000Z"
}
GET/api/v1/conversationsMCP: list_conversations

List conversations

Conversations created with this key, most recently active first.

Query

ParameterTypeDescription
account_idstringOnly this student's conversations.
limitinteger1 to 100. Default 30.

Request

curl
curl -X GET "https://glyph.myapps.ai/api/v1/conversations" \
  -H "Authorization: Bearer $GLYPH_API_KEY"

Response · 200

json
{
  "data": [
    {
      "id": "conv_fOl2E6E1-3caIeRZt8ZMuQ",
      "account_id": "3f6c2a8e-1b4d-4c7a-9e2f-5a8b7c6d4e3f",
      "title": "French Revolution review",
      "metadata": {
        "course": "HIST-201"
      },
      "last_session_id": "live_8f2c1a7b3d9e",
      "created_at": "2026-10-05T07:11:41.000Z",
      "updated_at": "2026-10-05T07:14:22.000Z"
    }
  ]
}
GET/api/v1/conversations/{id}

Get a conversation

One conversation.

Path

ParameterTypeDescription
idrequiredstringconv_...

Request

curl
curl -X GET "https://glyph.myapps.ai/api/v1/conversations/conv_fOl2E6E1-3caIeRZt8ZMuQ" \
  -H "Authorization: Bearer $GLYPH_API_KEY"

Response · 200

json
{
  "id": "conv_fOl2E6E1-3caIeRZt8ZMuQ",
  "account_id": "3f6c2a8e-1b4d-4c7a-9e2f-5a8b7c6d4e3f",
  "title": "French Revolution review",
  "metadata": {
    "course": "HIST-201"
  },
  "last_session_id": "live_8f2c1a7b3d9e",
  "created_at": "2026-10-05T07:11:41.000Z",
  "updated_at": "2026-10-05T07:14:22.000Z"
}
GET/api/v1/conversations/{id}/transcriptMCP: get_conversation_transcript

Conversation transcript

Every call in the conversation with its turns, oldest first.

Path

ParameterTypeDescription
idrequiredstringconv_...

Request

curl
curl -X GET "https://glyph.myapps.ai/api/v1/conversations/conv_fOl2E6E1-3caIeRZt8ZMuQ/transcript" \
  -H "Authorization: Bearer $GLYPH_API_KEY"

Response · 200

json
{
  "conversation_id": "conv_fOl2E6E1-3caIeRZt8ZMuQ",
  "title": "French Revolution review",
  "sessions": [
    {
      "session_id": "live_8f2c1a7b3d9e",
      "conversation_id": "conv_fOl2E6E1-3caIeRZt8ZMuQ",
      "engine": "live",
      "status": "ended",
      "complete": true,
      "transcript_note": null,
      "usage_seconds": 91,
      "started_at": "2026-10-05T07:12:51.000Z",
      "ended_at": "2026-10-05T07:14:22.000Z",
      "close_reason": "user_stop",
      "summary": null,
      "turns": [
        {
          "role": "user",
          "text": "Can you quiz me on the French Revolution?",
          "sequence": 0
        },
        {
          "role": "assistant",
          "text": "Of course. First question: what year was the storming of the Bastille?",
          "sequence": 1
        },
        {
          "role": "user",
          "text": "1789.",
          "sequence": 2
        },
        {
          "role": "assistant",
          "text": "Correct, July 14th, 1789. Next: who was king at the time?",
          "sequence": 3
        },
        {
          "role": "tool",
          "text": "generate_image({\"prompt\":\"The storming of the Bastille, painted\"})",
          "sequence": 4,
          "tool": {
            "name": "generate_image",
            "status": "called",
            "arguments": {
              "prompt": "The storming of the Bastille, painted"
            }
          }
        },
        {
          "role": "tool",
          "text": "generate_image result:\n{\"kind\":\"generated_image\",\"prompt\":\"The storming of the Bastille, painted\",\"imageUrl\":\"https://cdn.example.com/glyph/bastille.png\"}",
          "sequence": 5,
          "tool": {
            "name": "generate_image",
            "status": "done",
            "result": {
              "kind": "generated_image",
              "prompt": "The storming of the Bastille, painted",
              "imageUrl": "https://cdn.example.com/glyph/bastille.png"
            }
          },
          "images": [
            {
              "url": "https://cdn.example.com/glyph/bastille.png",
              "source": "generated",
              "prompt": "The storming of the Bastille, painted"
            }
          ]
        }
      ]
    }
  ]
}
GET/api/v1/conversations/{id}/sessions

List calls in a conversation

Sessions without their turns, oldest first.

Path

ParameterTypeDescription
idrequiredstringconv_...

Request

curl
curl -X GET "https://glyph.myapps.ai/api/v1/conversations/conv_fOl2E6E1-3caIeRZt8ZMuQ/sessions" \
  -H "Authorization: Bearer $GLYPH_API_KEY"

Response · 200

json
{
  "data": [
    {
      "id": "live_8f2c1a7b3d9e",
      "conversation_id": "conv_fOl2E6E1-3caIeRZt8ZMuQ",
      "account_id": "3f6c2a8e-1b4d-4c7a-9e2f-5a8b7c6d4e3f",
      "engine": "live",
      "status": "ended",
      "started_at": "2026-10-05T07:12:51.000Z",
      "ended_at": "2026-10-05T07:14:22.000Z",
      "close_reason": "user_stop",
      "usage_seconds": 91,
      "summary": null
    }
  ]
}

Sessions and transcripts

Each call is a session. Transcripts land when it ends.

GET/api/v1/sessions/{id}/transcriptMCP: get_transcript

Session transcript

One call's turns. While the call runs you get the turns saved so far. About 20 seconds after it ends, complete becomes true and the transcript is final. transcript_note explains an empty transcript.

Path

ParameterTypeDescription
idrequiredstringThe session id from a conversation or webhook.

Request

curl
curl -X GET "https://glyph.myapps.ai/api/v1/sessions/live_8f2c1a7b3d9e/transcript" \
  -H "Authorization: Bearer $GLYPH_API_KEY"

Response · 200

json
{
  "session_id": "live_8f2c1a7b3d9e",
  "conversation_id": "conv_fOl2E6E1-3caIeRZt8ZMuQ",
  "engine": "live",
  "status": "ended",
  "complete": true,
  "transcript_note": null,
  "usage_seconds": 91,
  "started_at": "2026-10-05T07:12:51.000Z",
  "ended_at": "2026-10-05T07:14:22.000Z",
  "close_reason": "user_stop",
  "summary": null,
  "turns": [
    {
      "role": "user",
      "text": "Can you quiz me on the French Revolution?",
      "sequence": 0
    },
    {
      "role": "assistant",
      "text": "Of course. First question: what year was the storming of the Bastille?",
      "sequence": 1
    },
    {
      "role": "user",
      "text": "1789.",
      "sequence": 2
    },
    {
      "role": "assistant",
      "text": "Correct, July 14th, 1789. Next: who was king at the time?",
      "sequence": 3
    },
    {
      "role": "tool",
      "text": "generate_image({\"prompt\":\"The storming of the Bastille, painted\"})",
      "sequence": 4,
      "tool": {
        "name": "generate_image",
        "status": "called",
        "arguments": {
          "prompt": "The storming of the Bastille, painted"
        }
      }
    },
    {
      "role": "tool",
      "text": "generate_image result:\n{\"kind\":\"generated_image\",\"prompt\":\"The storming of the Bastille, painted\",\"imageUrl\":\"https://cdn.example.com/glyph/bastille.png\"}",
      "sequence": 5,
      "tool": {
        "name": "generate_image",
        "status": "done",
        "result": {
          "kind": "generated_image",
          "prompt": "The storming of the Bastille, painted",
          "imageUrl": "https://cdn.example.com/glyph/bastille.png"
        }
      },
      "images": [
        {
          "url": "https://cdn.example.com/glyph/bastille.png",
          "source": "generated",
          "prompt": "The storming of the Bastille, painted"
        }
      ]
    }
  ]
}
GET/api/v1/sessions/{id}

Get a session

Status, engine, duration and billing of one call.

Path

ParameterTypeDescription
idrequiredstringSession id.

Request

curl
curl -X GET "https://glyph.myapps.ai/api/v1/sessions/live_8f2c1a7b3d9e" \
  -H "Authorization: Bearer $GLYPH_API_KEY"

Response · 200

json
{
  "id": "live_8f2c1a7b3d9e",
  "conversation_id": "conv_fOl2E6E1-3caIeRZt8ZMuQ",
  "account_id": "3f6c2a8e-1b4d-4c7a-9e2f-5a8b7c6d4e3f",
  "engine": "live",
  "status": "ended",
  "started_at": "2026-10-05T07:12:51.000Z",
  "ended_at": "2026-10-05T07:14:22.000Z",
  "close_reason": "user_stop",
  "usage_seconds": 91,
  "summary": null
}
POST/api/v1/conversations/{id}/sessions

Start a raw Live session

Advanced: for apps that run their own WebRTC client instead of the player. Send your SDP offer and get the answer. Live only, and only when the Glyph server meters Live usage itself (LIVE_DIRECTOR_ENABLED). Save turns with POST /sessions/{id}/turns so the transcript is not empty.

Path

ParameterTypeDescription
idrequiredstringconv_...

JSON body

FieldTypeDescription
sdprequiredstringYour WebRTC SDP offer.
engine"live"Only live is supported here.
voicestringVoice name.
instructionsstringReplaces the default tutor instructions.
toolsarrayFunction tools your client will execute.
resumebooleanSeed with the conversation's last transcript. Default true.

Request

curl
curl -X POST "https://glyph.myapps.ai/api/v1/conversations/conv_fOl2E6E1-3caIeRZt8ZMuQ/sessions" \
  -H "Authorization: Bearer $GLYPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"engine":"live","sdp":"v=0\r\no=- 4611731400430051336 2 IN IP4 127.0.0.1\r\n..."}'

Response · 201

json
{
  "session": {
    "id": "live_8f2c1a7b3d9e",
    "conversation_id": "conv_fOl2E6E1-3caIeRZt8ZMuQ",
    "account_id": "3f6c2a8e-1b4d-4c7a-9e2f-5a8b7c6d4e3f",
    "engine": "live",
    "status": "active",
    "started_at": "2026-10-05T07:12:51.000Z",
    "ended_at": null,
    "close_reason": "user_stop",
    "usage_seconds": null,
    "summary": null
  },
  "sdp": "v=0\r\n...",
  "model": "gpt-live-1",
  "voice": "marin",
  "resumed_turns": 4,
  "credits_remaining": 4213
}
POST/api/v1/sessions/{id}/turns

Save turns for a raw session

Raw sessions only (the player saves its own). Re-sending a turn with the same id updates its text.

Path

ParameterTypeDescription
idrequiredstringSession id.

JSON body

FieldTypeDescription
turnsrequiredarray[{ id, role: user|assistant, text, sequence }]

Request

curl
curl -X POST "https://glyph.myapps.ai/api/v1/sessions/live_8f2c1a7b3d9e/turns" \
  -H "Authorization: Bearer $GLYPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"turns":[{"id":"t1","role":"user","text":"Hi Glyph","sequence":0},{"id":"t2","role":"assistant","text":"Hi! What are we studying?","sequence":1}]}'

Response · 200

json
{
  "stored": 2,
  "history_enabled": true
}

Webhooks

Get the transcript pushed to your server when a call ends.

POST/api/v1/webhooks

Register a webhook

Glyph POSTs session.ended to this URL about 20 seconds after each call ends, with the full transcript. The secret signs every delivery and is only shown now. Failed deliveries retry for 24 hours.

JSON body

FieldTypeDescription
urlrequiredstringhttps URL (http://localhost allowed for testing).
eventsstring[]Default ["session.ended"].

Request

curl
curl -X POST "https://glyph.myapps.ai/api/v1/webhooks" \
  -H "Authorization: Bearer $GLYPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://your-app.example.com/hooks/glyph","events":["session.ended"]}'

Response · 201

json
{
  "id": "wh_PcjXNE-oHoQrPdZknqY4RA",
  "url": "https://your-app.example.com/hooks/glyph",
  "events": [
    "session.ended"
  ],
  "active": true,
  "created_at": "2026-10-05T07:11:41.000Z",
  "secret": "whsec_DR7T7dbtE5416I8eYgGllX-T_MUCw-8ABTgFZlgBeos"
}
GET/api/v1/webhooks

List webhooks

Webhooks registered with this key. Secrets are never shown again.

No parameters.

Request

curl
curl -X GET "https://glyph.myapps.ai/api/v1/webhooks" \
  -H "Authorization: Bearer $GLYPH_API_KEY"

Response · 200

json
{
  "data": [
    {
      "id": "wh_PcjXNE-oHoQrPdZknqY4RA",
      "url": "https://your-app.example.com/hooks/glyph",
      "events": [
        "session.ended"
      ],
      "active": true,
      "created_at": "2026-10-05T07:11:41.000Z"
    }
  ]
}
POST/api/v1/webhooks/{id}/test

Send a test delivery

Queues a sample session.ended (with test: true) so you can check your receiver and signature code.

Path

ParameterTypeDescription
idrequiredstringwh_...

Request

curl
curl -X POST "https://glyph.myapps.ai/api/v1/webhooks/wh_PcjXNE-oHoQrPdZknqY4RA/test" \
  -H "Authorization: Bearer $GLYPH_API_KEY"

Response · 202

json
{
  "id": "dlv_Qm3aX0cGk2L9sVwUe1HbOg",
  "event": "session.ended",
  "attempts": 0,
  "next_attempt_at": "2026-10-05T07:14:42.000Z",
  "delivered_at": null,
  "last_status": null,
  "last_error": null,
  "created_at": "2026-10-05T07:14:42.000Z",
  "status": "pending"
}
GET/api/v1/webhooks/{id}/deliveries

Recent deliveries

The last 50 deliveries with status codes and errors, for debugging your receiver.

Path

ParameterTypeDescription
idrequiredstringwh_...

Query

ParameterTypeDescription
limitinteger1 to 100. Default 50.

Request

curl
curl -X GET "https://glyph.myapps.ai/api/v1/webhooks/wh_PcjXNE-oHoQrPdZknqY4RA/deliveries" \
  -H "Authorization: Bearer $GLYPH_API_KEY"

Response · 200

json
{
  "data": [
    {
      "id": "dlv_Qm3aX0cGk2L9sVwUe1HbOg",
      "event": "session.ended",
      "attempts": 1,
      "next_attempt_at": null,
      "delivered_at": "2026-10-05T07:14:43.000Z",
      "last_status": 200,
      "last_error": null,
      "created_at": "2026-10-05T07:14:42.000Z",
      "status": "delivered"
    }
  ]
}
DELETE/api/v1/webhooks/{id}

Delete a webhook

Stops deliveries and discards any still queued.

Path

ParameterTypeDescription
idrequiredstringwh_...

Request

curl
curl -X DELETE "https://glyph.myapps.ai/api/v1/webhooks/wh_PcjXNE-oHoQrPdZknqY4RA" \
  -H "Authorization: Bearer $GLYPH_API_KEY"

Response · 200

json
{
  "id": "wh_PcjXNE-oHoQrPdZknqY4RA",
  "deleted": true
}

Accounts and credits

Look up students, balances, and usage.

GET/api/v1/me

Check your key

Which key is calling: its name and scope. A personal key also returns its account.

No parameters.

Request

curl
curl -X GET "https://glyph.myapps.ai/api/v1/me" \
  -H "Authorization: Bearer $GLYPH_API_KEY"

Response · 200

json
{
  "client": {
    "id": "glc_9aV2kQ0pLx7Rm4TzYb1NcA",
    "name": "Tutor app",
    "key_prefix": "glk_Xy3k9Q",
    "scope": "organization",
    "account_id": null,
    "created_at": "2026-10-01T12:00:00.000Z",
    "last_used_at": "2026-10-05T07:11:41.000Z",
    "revoked_at": null
  }
}
GET/api/v1/accounts/lookup

Look up an access code

Access code to account id. Every endpoint also accepts access codes directly, so this is optional.

Query

ParameterTypeDescription
access_coderequiredstringFor example GLYP-G5D6-7XRW.

Request

curl
curl -X GET "https://glyph.myapps.ai/api/v1/accounts/lookup?access_code=GLYP-G5D6-7XRW" \
  -H "Authorization: Bearer $GLYPH_API_KEY"

Response · 200

json
{
  "account_id": "3f6c2a8e-1b4d-4c7a-9e2f-5a8b7c6d4e3f",
  "access_code": "GLYP-G5D6-7XRW",
  "active": true
}
GET/api/v1/accounts/{account}/creditsMCP: get_credits

Credits and plan

Balance, plan, available engines and enabled features. One credit is one second of voice.

Path

ParameterTypeDescription
accountrequiredstringAccount id, access code, or me (personal keys).

Request

curl
curl -X GET "https://glyph.myapps.ai/api/v1/accounts/GLYP-G5D6-7XRW/credits" \
  -H "Authorization: Bearer $GLYPH_API_KEY"

Response · 200

json
{
  "account_id": "3f6c2a8e-1b4d-4c7a-9e2f-5a8b7c6d4e3f",
  "access_code": "GLYP-G5D6-7XRW",
  "plan": "ultra",
  "plan_name": "Ultra",
  "credits_remaining": 19808,
  "credits_limit": 20000,
  "next_credit_reset_at": "2026-11-01",
  "credits_per_second": 1,
  "engines": [
    "classic",
    "live"
  ],
  "default_engine": "classic",
  "features": {
    "direct_image_input": true,
    "session_history": true,
    "course_library": true
  }
}
GET/api/v1/accounts/{account}/usageMCP: get_usage

Usage summary

Credits used, voice minutes (Classic and Live), number of calls, a day-by-day series and a breakdown by feature for the last 7, 30, 90 or 365 days. Days are UTC. credits_added counts monthly refills and grants.

Path

ParameterTypeDescription
accountrequiredstringAccount id, access code, or me (personal keys).

Query

ParameterTypeDescription
daysinteger7, 30 (default), 90 or 365.

Request

curl
curl -X GET "https://glyph.myapps.ai/api/v1/accounts/GLYP-G5D6-7XRW/usage" \
  -H "Authorization: Bearer $GLYPH_API_KEY"

Response · 200

json
{
  "account_id": "3f6c2a8e-1b4d-4c7a-9e2f-5a8b7c6d4e3f",
  "days": 30,
  "from": "2026-09-10",
  "to": "2026-10-09",
  "totals": {
    "credits_used": 1847,
    "credits_added": 0,
    "voice_seconds": 1722,
    "classic_seconds": 640,
    "live_seconds": 1082,
    "calls": 14
  },
  "daily": [
    {
      "date": "2026-10-08",
      "credits": 412,
      "voice_seconds": 377
    },
    {
      "date": "2026-10-09",
      "credits": 96,
      "voice_seconds": 91
    }
  ],
  "by_feature": [
    {
      "feature": "live_voice",
      "label": "Live voice",
      "credits": 1082,
      "voice_seconds": 1082,
      "charges": 41
    },
    {
      "feature": "classic_voice",
      "label": "Classic voice",
      "credits": 640,
      "voice_seconds": 640,
      "charges": 9
    },
    {
      "feature": "library_search",
      "label": "Course library search",
      "credits": 75,
      "voice_seconds": 0,
      "charges": 5
    }
  ]
}
GET/api/v1/accounts/{account}/ledgerMCP: get_ledger

Credit transactions

Every charge with the balance before and after, newest first. Voice charges include seconds_spent.

Path

ParameterTypeDescription
accountrequiredstringAccount id, access code, or me (personal keys).

Query

ParameterTypeDescription
limitinteger1 to 100. Default 60.

Request

curl
curl -X GET "https://glyph.myapps.ai/api/v1/accounts/GLYP-G5D6-7XRW/ledger" \
  -H "Authorization: Bearer $GLYPH_API_KEY"

Response · 200

json
{
  "account_id": "3f6c2a8e-1b4d-4c7a-9e2f-5a8b7c6d4e3f",
  "credits_remaining": 19808,
  "credits_limit": 20000,
  "transactions": [
    {
      "id": "298",
      "route": "/api/live/bill",
      "credits_charged": 7,
      "credits_before": 19815,
      "credits_after": 19808,
      "tokens_input": 7,
      "tokens_output": 0,
      "tokens_total": 7,
      "seconds_spent": 7,
      "created_at": "2026-10-05T16:32:57.814Z"
    }
  ]
}