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 API | https://glyph.myapps.ai/api/v1 |
| MCP server | https://glyph.myapps.ai/api/mcp |
| OpenAPI | https://glyph.myapps.ai/api/v1/openapi.json for Postman, Insomnia, and code generators |
| Console | Try 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 embed | Your own Glyph | |
|---|---|---|
| mode | "embed" (default) | "custom" |
| What students see | The full Glyph player: orb, captions, settings, start button | Whatever you design: your own orb, avatar, captions and buttons |
| What you add to your page | One iframe | The SDK script and a few lines of JavaScript |
| What you get back | Webhooks and transcripts | Webhooks 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 link | Set when you create the link, and change live |
| Best for | Shipping in minutes | A branded experience that matches your app |
| Guide | Embed the player | Build 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.
- 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.
- 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"}' - 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>
- 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"
"conversation": "new" to start fresh.Keys and accounts
Send your key in the Authorization header on every request: Authorization: Bearer glk_...
| Key type | Who creates it | What it can do |
|---|---|---|
| Personal | Any 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. |
| Organization | A Glyph administrator, in the admin panel | Acts 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
engineout to use the student's plan default.classicis the Realtime voice engine;liveis 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.
- Your server creates a player link with
"mode": "custom"(POST /api/v1/embed-links). The response includessdk_url. - Your page loads
https://glyph.myapps.ai/sdk/glyph.jsand callsGlyphVoice.connect(url). - Call
start()from a click handler, then draw from thelevelsevent.
<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 event | What 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. |
ready | The player loaded. Payload: engine, conversationId, settings. |
state | idle, connecting, active, or ended. |
levels | About 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. |
settings | Whenever 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. |
transcript | Every change: spoken turns as { id, role, text, final }. Good for live captions. |
tool | Each tool call, result or error as it happens: { id, name, status, arguments, result, error, images }. Show generated images the moment they exist. |
session | The call's session id, for GET /api/v1/sessions/{id}/transcript. |
ended | The call finished: sessionId and conversationId. The final transcript lands about 20 seconds later. |
credits | Remaining credits after each charge. |
status / error | Human-readable progress and problems (no microphone, out of credits, a control that does not apply). |
glyph.state, glyph.levels, glyph.settings, glyph.transcript, glyph.tools | The 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.
| Setting | Values | Engines |
|---|---|---|
input_mode | "vad" hands-free (default), or "ptt" push-to-talk | vad: both. ptt: classic |
voice | Classic: marin, cedar, ash, ballad, coral, sage, verse, echo, alloy, shimmer. Live: meridian, quartz, ripple, vesper, willow, stone, gleam, bossa, tempo, beacon, delta, cinder | Both (Live: next call) |
language | "" auto-detect, or a code such as "en", "es", "fr". Improves captions and transcripts | Classic |
vad.type | "server_vad" (volume based, default) or "semantic_vad" (waits until the student finishes a thought) | Classic |
vad.threshold | 0 to 1, default 0.5. Higher needs louder speech; raise it in noisy rooms | Classic, server_vad |
vad.silence_ms | 100 to 3000, default 500. Silence before Glyph replies; raise it for students who pause | Classic, server_vad |
vad.prefix_padding_ms | 0 to 2000, default 300. Audio kept from just before speech started | Classic, server_vad |
vad.eagerness | "low", "medium", "high" or "auto". How quickly Glyph jumps in | Classic, semantic_vad |
vad.interrupt | true (default) lets the student talk over Glyph to stop it | Classic |
instructions | Replaces Glyph's tutor instructions, up to 16,000 characters. Live only through configure(), never stored in links | Both (Live: next call) |
tools | Which tools Glyph may use, by name (see the list below). Leave out for all; [] for none | Both (Live: next call) |
theme | Orb color of the Glyph player: aurora, ember, lagoon, violet, solar, glacier, jade, rose, midnight, candy, copper, mono | Web embed |
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().
| Tool | What it does |
|---|---|
run_signal_orchestration | Research. Answers source-grounded and latest-news questions by searching and reading several sources. |
search_google_serp | Web search. A quick Google search. |
get_latest_news | News. Reads the latest headlines from a news site. |
generate_image | Image generation. Draws a new image and returns a hosted link. |
consult_course_library | Course materials. Answers from the student's uploaded notes and readings (when the course library is on). |
compute_with_code | Compute. Runs code for exact math and statistics (when the course library is on). |
web_search | Live web search. The Live engine's built-in web search (Live only). |
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:
| Field | Meaning |
|---|---|
tool.name | generate_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.status | called, done, error, or info (progress notes) |
tool.arguments | What Glyph asked the tool for (called turns) |
tool.result | What the tool returned (done turns). Very long results are shortened to keep the turn whole |
tool.error | Why it failed (error turns) |
images | Hosted image URLs from the result: generated images (with their prompt) and pictures the student showed. Ready for an img tag |
{
"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:
| Note | Meaning |
|---|---|
| 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 -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 onceWhat you receive
X-Glyph-Event | session.ended |
X-Glyph-Delivery | Unique per delivery. Use it to ignore repeats. |
X-Glyph-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body" with your secret> |
{
"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.
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:
| Tool | What it does |
|---|---|
open_player | A player URL for a student, continuing their latest conversation. |
revoke_player_link | Cut a player link off. |
create_conversation / list_conversations | Manage conversation threads. |
get_transcript / get_conversation_transcript | What was said in one call, or a whole conversation. |
get_credits / get_usage / get_ledger | Balance 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
- Store each user's Glyph access code (or account id) with their profile.
- When they click the button, your server calls
POST /api/v1/embed-linksand returns the url. - Your page shows the url in an iframe. Make a new link every time.
- 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:
{
"error": {
"code": "not_found",
"message": "Conversation not found."
}
}| Status | code | What to do |
|---|---|---|
| 400 | invalid_request | Fix the request; the message names the field. |
| 401 | unauthorized | Missing, unknown, or revoked key. |
| 402 | insufficient_credits | The student is out of credits. credits_remaining is included. |
| 403 | forbidden | A personal key acting for another account, or a feature the plan lacks. |
| 404 | not_found | It does not exist, or belongs to another key. |
| 409 | conflict | For example, the personal key limit is reached. |
| 503 | unavailable | A server feature is not set up yet. The message says which. |
| 500 | internal_error | Retry 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.
| Setting | Purpose |
|---|---|
GLYPH_API_SECRET | Required for player links. 32+ random characters; signs the links. |
SESSION_HISTORY_ENABLED=true | Required for transcripts (plus scripts/sql/001_voice_sessions.sql). |
EMBED_ALLOWED_ORIGINS | Optional. 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_URL | Optional. The public https URL used in player links when behind a proxy. |
GLYPH_SUMMARIES_ENABLED=true | Optional. Adds a summary to each transcript (GLYPH_SUMMARY_MODEL to choose the model). |
GLYPH_EMBED_TTL_MINUTES | Optional. Default player link lifetime (60). |
GLYPH_TRANSCRIPT_SETTLE_DELAYS | Optional. Seconds between transcript re-reads after a call, default 2,5,10,20. |
GLYPH_WEBHOOK_WORKER=false | Optional. Turns off webhook retries on this instance (keep one instance on). |
LIVE_DIRECTOR_ENABLED=true | Needed 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.
Open the player
One call returns a player URL for a student. Show it two ways: mode "embed" (default) puts the full Glyph player in an iframe or a new tab; mode "custom" lets your page draw its own Glyph with the SDK at sdk_url while Glyph handles voice, tools, billing and transcripts. Give account_id and the student's latest conversation with your key continues, so Glyph remembers earlier calls. Pass conversation: "new" to start fresh, or conversation_id to pick one. Links are revocable and expire.
JSON body
| Field | Type | Description |
|---|---|---|
account_id | string | The student: account id (uuid) or access code like GLYP-G5D6-7XRW. Optional with a personal key. |
mode | "embed" | "custom" | embed (default): the Glyph player in an iframe or tab. custom: your own UI, using the SDK at sdk_url. |
settings | object | Player settings: input_mode "vad" (hands-free) or "ptt" (push-to-talk); voice; language ("" auto, or "en", "es", ...); vad { type "server_vad" | "semantic_vad", threshold 0..1, silence_ms, prefix_padding_ms, eagerness, interrupt }; tools (which tools Glyph may use, [] for none); theme (orb color of the Glyph player). All optional; the SDK can change them live. |
conversation_id | string | Play this conversation instead of the latest one. |
conversation | "latest" | "new" | With account_id: continue the latest conversation (default) or start a new one. |
title | string | Title for a newly created conversation. |
engine | "classic" | "live" | Force an engine. Omit to let the student's plan decide. |
ttl_minutes | integer | Link lifetime, 1 to 1440. Default 60. |
resume | boolean | Seed the call with the conversation's last transcript. Default true. |
Request
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-G5D6-7XRW","mode":"custom","settings":{"input_mode":"vad","voice":"marin","language":"en","vad":{"type":"server_vad","threshold":0.6,"silence_ms":700}},"ttl_minutes":60}'Response · 201
{
"id": "emb_cCRsU8h1NK211Qe97JdHrQ",
"url": "https://glyph.example.com/portal?embed=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"conversation_id": "conv_fOl2E6E1-3caIeRZt8ZMuQ",
"account_id": "3f6c2a8e-1b4d-4c7a-9e2f-5a8b7c6d4e3f",
"engine": "live",
"expires_at": "2026-10-05T08:11:41.000Z",
"revoked_at": null,
"created_at": "2026-10-05T07:11:41.000Z",
"status": "active",
"mode": "custom",
"settings": {
"input_mode": "vad",
"voice": "marin",
"language": "en",
"vad": {
"type": "server_vad",
"threshold": 0.6,
"silence_ms": 700
}
},
"sdk_url": "https://glyph.example.com/sdk/glyph.js",
"how_to_use": "Load sdk_url in your page, call GlyphVoice.connect(url), then start() from a click and draw from the levels event."
}List player links
Links created with this key, newest first, with their status (active, expired, revoked).
Query
| Parameter | Type | Description |
|---|---|---|
limit | integer | 1 to 100. Default 30. |
Request
curl -X GET "https://glyph.myapps.ai/api/v1/embed-links" \ -H "Authorization: Bearer $GLYPH_API_KEY"
Response · 200
{
"data": [
{
"id": "emb_cCRsU8h1NK211Qe97JdHrQ",
"url": "https://glyph.example.com/portal?embed=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"conversation_id": "conv_fOl2E6E1-3caIeRZt8ZMuQ",
"account_id": "3f6c2a8e-1b4d-4c7a-9e2f-5a8b7c6d4e3f",
"engine": "live",
"expires_at": "2026-10-05T08:11:41.000Z",
"revoked_at": null,
"created_at": "2026-10-05T07:11:41.000Z",
"status": "active"
}
]
}Revoke a player link
The player loses access within about 30 seconds. A call in progress is stopped at its next check.
Path
| Parameter | Type | Description |
|---|---|---|
idrequired | string | The link id (emb_...). |
Request
curl -X DELETE "https://glyph.myapps.ai/api/v1/embed-links/emb_cCRsU8h1NK211Qe97JdHrQ" \ -H "Authorization: Bearer $GLYPH_API_KEY"
Response · 200
{
"id": "emb_cCRsU8h1NK211Qe97JdHrQ",
"revoked": true
}Conversations
Threads that keep context across calls for one student.
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
| Field | Type | Description |
|---|---|---|
account_idrequired | string | The student: account id (uuid) or access code like GLYP-G5D6-7XRW. Optional with a personal key. |
title | string | Shown in lists. Filled from the student's first sentence when empty. |
metadata | object | Your own data, up to 8 KB, returned with the conversation and in webhooks. |
Request
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
{
"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"
}List conversations
Conversations created with this key, most recently active first.
Query
| Parameter | Type | Description |
|---|---|---|
account_id | string | Only this student's conversations. |
limit | integer | 1 to 100. Default 30. |
Request
curl -X GET "https://glyph.myapps.ai/api/v1/conversations" \ -H "Authorization: Bearer $GLYPH_API_KEY"
Response · 200
{
"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 a conversation
One conversation.
Path
| Parameter | Type | Description |
|---|---|---|
idrequired | string | conv_... |
Request
curl -X GET "https://glyph.myapps.ai/api/v1/conversations/conv_fOl2E6E1-3caIeRZt8ZMuQ" \ -H "Authorization: Bearer $GLYPH_API_KEY"
Response · 200
{
"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"
}Conversation transcript
Every call in the conversation with its turns, oldest first.
Path
| Parameter | Type | Description |
|---|---|---|
idrequired | string | conv_... |
Request
curl -X GET "https://glyph.myapps.ai/api/v1/conversations/conv_fOl2E6E1-3caIeRZt8ZMuQ/transcript" \ -H "Authorization: Bearer $GLYPH_API_KEY"
Response · 200
{
"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"
}
]
}
]
}
]
}List calls in a conversation
Sessions without their turns, oldest first.
Path
| Parameter | Type | Description |
|---|---|---|
idrequired | string | conv_... |
Request
curl -X GET "https://glyph.myapps.ai/api/v1/conversations/conv_fOl2E6E1-3caIeRZt8ZMuQ/sessions" \ -H "Authorization: Bearer $GLYPH_API_KEY"
Response · 200
{
"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.
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
| Parameter | Type | Description |
|---|---|---|
idrequired | string | The session id from a conversation or webhook. |
Request
curl -X GET "https://glyph.myapps.ai/api/v1/sessions/live_8f2c1a7b3d9e/transcript" \ -H "Authorization: Bearer $GLYPH_API_KEY"
Response · 200
{
"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 a session
Status, engine, duration and billing of one call.
Path
| Parameter | Type | Description |
|---|---|---|
idrequired | string | Session id. |
Request
curl -X GET "https://glyph.myapps.ai/api/v1/sessions/live_8f2c1a7b3d9e" \ -H "Authorization: Bearer $GLYPH_API_KEY"
Response · 200
{
"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
}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
| Parameter | Type | Description |
|---|---|---|
idrequired | string | conv_... |
JSON body
| Field | Type | Description |
|---|---|---|
sdprequired | string | Your WebRTC SDP offer. |
engine | "live" | Only live is supported here. |
voice | string | Voice name. |
instructions | string | Replaces the default tutor instructions. |
tools | array | Function tools your client will execute. |
resume | boolean | Seed with the conversation's last transcript. Default true. |
Request
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
{
"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
}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
| Parameter | Type | Description |
|---|---|---|
idrequired | string | Session id. |
JSON body
| Field | Type | Description |
|---|---|---|
turnsrequired | array | [{ id, role: user|assistant, text, sequence }] |
Request
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
{
"stored": 2,
"history_enabled": true
}Webhooks
Get the transcript pushed to your server when a call ends.
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
| Field | Type | Description |
|---|---|---|
urlrequired | string | https URL (http://localhost allowed for testing). |
events | string[] | Default ["session.ended"]. |
Request
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
{
"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"
}List webhooks
Webhooks registered with this key. Secrets are never shown again.
No parameters.
Request
curl -X GET "https://glyph.myapps.ai/api/v1/webhooks" \ -H "Authorization: Bearer $GLYPH_API_KEY"
Response · 200
{
"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"
}
]
}Send a test delivery
Queues a sample session.ended (with test: true) so you can check your receiver and signature code.
Path
| Parameter | Type | Description |
|---|---|---|
idrequired | string | wh_... |
Request
curl -X POST "https://glyph.myapps.ai/api/v1/webhooks/wh_PcjXNE-oHoQrPdZknqY4RA/test" \ -H "Authorization: Bearer $GLYPH_API_KEY"
Response · 202
{
"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"
}Recent deliveries
The last 50 deliveries with status codes and errors, for debugging your receiver.
Path
| Parameter | Type | Description |
|---|---|---|
idrequired | string | wh_... |
Query
| Parameter | Type | Description |
|---|---|---|
limit | integer | 1 to 100. Default 50. |
Request
curl -X GET "https://glyph.myapps.ai/api/v1/webhooks/wh_PcjXNE-oHoQrPdZknqY4RA/deliveries" \ -H "Authorization: Bearer $GLYPH_API_KEY"
Response · 200
{
"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 a webhook
Stops deliveries and discards any still queued.
Path
| Parameter | Type | Description |
|---|---|---|
idrequired | string | wh_... |
Request
curl -X DELETE "https://glyph.myapps.ai/api/v1/webhooks/wh_PcjXNE-oHoQrPdZknqY4RA" \ -H "Authorization: Bearer $GLYPH_API_KEY"
Response · 200
{
"id": "wh_PcjXNE-oHoQrPdZknqY4RA",
"deleted": true
}Accounts and credits
Look up students, balances, and usage.
Check your key
Which key is calling: its name and scope. A personal key also returns its account.
No parameters.
Request
curl -X GET "https://glyph.myapps.ai/api/v1/me" \ -H "Authorization: Bearer $GLYPH_API_KEY"
Response · 200
{
"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
}
}Look up an access code
Access code to account id. Every endpoint also accepts access codes directly, so this is optional.
Query
| Parameter | Type | Description |
|---|---|---|
access_coderequired | string | For example GLYP-G5D6-7XRW. |
Request
curl -X GET "https://glyph.myapps.ai/api/v1/accounts/lookup?access_code=GLYP-G5D6-7XRW" \ -H "Authorization: Bearer $GLYPH_API_KEY"
Response · 200
{
"account_id": "3f6c2a8e-1b4d-4c7a-9e2f-5a8b7c6d4e3f",
"access_code": "GLYP-G5D6-7XRW",
"active": true
}Credits and plan
Balance, plan, available engines and enabled features. One credit is one second of voice.
Path
| Parameter | Type | Description |
|---|---|---|
accountrequired | string | Account id, access code, or me (personal keys). |
Request
curl -X GET "https://glyph.myapps.ai/api/v1/accounts/GLYP-G5D6-7XRW/credits" \ -H "Authorization: Bearer $GLYPH_API_KEY"
Response · 200
{
"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
}
}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
| Parameter | Type | Description |
|---|---|---|
accountrequired | string | Account id, access code, or me (personal keys). |
Query
| Parameter | Type | Description |
|---|---|---|
days | integer | 7, 30 (default), 90 or 365. |
Request
curl -X GET "https://glyph.myapps.ai/api/v1/accounts/GLYP-G5D6-7XRW/usage" \ -H "Authorization: Bearer $GLYPH_API_KEY"
Response · 200
{
"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
}
]
}Credit transactions
Every charge with the balance before and after, newest first. Voice charges include seconds_spent.
Path
| Parameter | Type | Description |
|---|---|---|
accountrequired | string | Account id, access code, or me (personal keys). |
Query
| Parameter | Type | Description |
|---|---|---|
limit | integer | 1 to 100. Default 60. |
Request
curl -X GET "https://glyph.myapps.ai/api/v1/accounts/GLYP-G5D6-7XRW/ledger" \ -H "Authorization: Bearer $GLYPH_API_KEY"
Response · 200
{
"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"
}
]
}