MathVoice API
AST-powered speech-to-LaTeX editing engine. Three endpoints. One integration. Embed voice-controlled math editing in any EdTech platform.
MathVoice stores formulas as Abstract Syntax Trees. A command like "change the denominator to y²" targets a specific node and modifies only that node. This is architecturally impossible with string-based editors like Equatio.
The API exposes three pure operations: normalize speech → tokens, intent → structured operation, mutate → new tree + diff log + MathML. Chain them or call them independently.
Quickstart
Three calls. One equation edited by voice. Copy any language tab below.
// npm install @mathvoice/react (React widget, optional) const BASE = 'https://mathvoice.app'; const KEY = 'mv_live_your_key'; // 1. Normalize — FREE, no auth needed const norm = await fetch(`${BASE}/api/normalize`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({text: 'change the denominator to y squared'}) }).then(r => r.json()); // { normalized: "REPLACE denominator TO y^{2}", confidence: 0.91 } // 2. Parse intent const intent = await fetch(`${BASE}/api/intent`, { method: 'POST', headers: {'Content-Type': 'application/json', 'X-Api-Key': KEY}, body: JSON.stringify({ rawText: 'change the denominator to y squared', normalizedText: norm.normalized, formulaContext: {latex: '\\frac{x}{2a}'}, editMode: 'CORRECT', }) }).then(r => r.json()); // { type:"REPLACE_VALUE", target:{role:"denominator"}, value:{raw:"y^{2}"}, // confidence:0.94, tier:"regex" } // 3. Mutate AST const result = await fetch(`${BASE}/api/mutate`, { method: 'POST', headers: {'Content-Type': 'application/json', 'X-Api-Key': KEY}, body: JSON.stringify({ast: currentAst, intent, editMode: 'CORRECT'}) }).then(r => r.json()); console.log(result.latexAfter); // "\\frac{x}{y^{2}}" console.log(result.diff[0]); // {op:"REPLACE_VALUE",role:"denominator",before:"2a",after:"y^{2}"}
# pip install requests import requests, json BASE = "https://mathvoice.app" HEADERS = {"X-Api-Key": "mv_live_your_key", "Content-Type": "application/json"} # 1. Normalize (free) norm = requests.post(f"{BASE}/api/normalize", json={"text": "change the denominator to y squared"}).json() # 2. Intent intent = requests.post(f"{BASE}/api/intent", headers=HEADERS, json={ "rawText": "change the denominator to y squared", "normalizedText": norm["normalized"], "formulaContext": {"latex": r"\frac{x}{2a}"}, "editMode": "CORRECT", }).json() # 3. Mutate result = requests.post(f"{BASE}/api/mutate", headers=HEADERS, json={"ast": current_ast, "intent": intent}).json() print(result["latexAfter"]) # \frac{x}{y^{2}} print(result["diff"][0]) # {op:REPLACE_VALUE, role:denominator, before:2a, after:y^{2}}
# gem install httparty require 'httparty' require 'json' BASE = "https://mathvoice.app" HEADERS = {"X-Api-Key" => "mv_live_your_key", "Content-Type" => "application/json"} # 1. Normalize (free) norm = HTTParty.post("#{BASE}/api/normalize", body: {text: "change the denominator to y squared"}.to_json, headers: {"Content-Type" => "application/json"}) # 2. Intent intent = HTTParty.post("#{BASE}/api/intent", body: {rawText: "change the denominator to y squared", normalizedText: norm["normalized"], formulaContext: {latex: '\\frac{x}{2a}'}, editMode: "CORRECT"}.to_json, headers: HEADERS) puts intent["type"] # REPLACE_VALUE puts intent["confidence"] # 0.94
# 1. Normalize (free) curl -sX POST https://mathvoice.app/api/normalize \ -H "Content-Type: application/json" \ -d '{"text":"change the denominator to y squared"}' # 2. Intent curl -sX POST https://mathvoice.app/api/intent \ -H "Content-Type: application/json" \ -H "X-Api-Key: mv_live_your_key" \ -d '{ "rawText":"change the denominator to y squared", "formulaContext":{"latex":"\\frac{x}{2a}"}, "editMode":"CORRECT" }' # 3. Mutate curl -sX POST https://mathvoice.app/api/mutate \ -H "Content-Type: application/json" \ -H "X-Api-Key: mv_live_your_key" \ -d '{"ast":{...},"intent":{...}}'
Authentication
Pass your API key in the X-Api-Key header. All keys start with mv_.
X-Api-Key: mv_live_abc123xyz789
/api/normalize requires no authentication and has no rate limit. Get a key on the pricing page.
X-Api-Key.Rate Limits & Quotas
Per-minute rate limit
All /api/ routes enforce a sliding-window (60-second) rate limit keyed to IP address and API key hash.
| Auth | Limit | Applies to |
|---|---|---|
| No API key | 60 req / min per IP | All endpoints |
| Valid API key (any tier) | 300 req / min | All endpoints |
Exceeded requests receive 429 Too Many Requests with a Retry-After header (seconds until the window resets).
Monthly quota
Billable endpoints — /api/intent, /api/mutate, /api/mathml, /api/tts, /api/transcribe — count against your plan's monthly quota. /api/normalize and /api/health are always free and unmetered. Only successful (2xx) responses are counted; 4xx/5xx responses are not. Quotas reset on the 1st of each month (UTC).
| Plan | Monthly requests |
|---|---|
| Free | 1,000 |
| Starter | 50,000 |
| Professional | 500,000 |
| Enterprise | Unlimited |
Billable responses include usage headers so you can track consumption client-side:
| Header | Meaning |
|---|---|
X-Usage-Limit | Your plan's monthly request quota |
X-Usage-Used | Requests used this month (before the current request) |
X-Usage-Remaining | Requests remaining this month |
When the monthly quota is exhausted, billable endpoints return 429 Too Many Requests with a JSON body containing error, quota, and used. Upgrade your plan on the pricing page or contact us for a custom limit.
POST /api/normalize
Convert raw speech text to normalised math tokens. Free tier — no authentication, no rate limit, ~0ms latency.
Request Body
| Param | Type | Description | |
|---|---|---|---|
| text | string | required | Raw ASR transcript or typed command |
Response
POST /api/intent
Parse a voice command into a structured IntentResult. Three-tier pipeline: regex (<1ms) → LLM (~500ms) → UNKNOWN. The regex tier is free within Pro; LLM counts toward the 1,000/month quota.
Request Body
| Param | Type | Description | |
|---|---|---|---|
| rawText | string | optional | Original ASR transcript |
| normalizedText | string | optional | Output from /api/normalize. Preferred — improves regex confidence. |
| formulaContext | object | optional | {latex:string, astFlat?:object} — current formula, used by LLM for disambiguation |
| editMode | string | optional | "CORRECT" | "ALGEBRA" | "ASK" — default "CORRECT" |
Response — IntentResult
POST /api/mutate
Apply an IntentResult to an AST. Returns the new tree, before/after LaTeX, a diff log, and a <math> MathML string. Pure computation — ~0ms, no external calls.
Request Body
| Param | Type | Description | |
|---|---|---|---|
| ast | object | required | Current AST from a previous /api/mutate response, or pass {type:"RAW",latex:"…"} |
| intent | object | required | IntentResult from /api/intent |
| editMode | string | optional | Must be "ALGEBRA" for APPLY_INVERSE or TRANSPOSE_TERM operations |
Response — MutationResult
GET /api/mathml
Convert a LaTeX string directly to a <math> element compatible with JAWS + MathPlayer and NVDA + MathCAT.
Query Parameters
| Param | Type | Description | |
|---|---|---|---|
| latex | string | required | URL-encoded LaTeX. e.g. %5Cfrac%7B-b%7D%7B2a%7D |
Response
POST /api/tts
Synthesise speech audio from SSML or plain text via Google Cloud Text-to-Speech. Returns base64-encoded MP3 audio. Falls back to a stub response (no audio, SSML echoed) when GOOGLE_TTS_API_KEY is not configured.
Request Body
| Param | Type | Description | |
|---|---|---|---|
| ssml | string | optional | SSML string. Takes precedence over text. |
| text | string | optional | Plain-text fallback if SSML is not provided. |
| voice | string | optional | Google TTS voice name. Default: en-US-Neural2-C. |
| speakingRate | number | optional | Speaking rate multiplier. Default: 0.9. |
Response
Decode audioContent from base64 to get the raw MP3 bytes. When TTS is unavailable the response includes "stub": true instead of audio.
GET /api/health
Returns the service status and lists any missing required environment variables. Useful for uptime monitors and deployment verification.
Response
Returns HTTP 200 when all required environment variables are present, 503 when any are missing. status is "ok" or "degraded".
Intent Types
AST Schema
Every node has a type field. Roles (numerator, denominator, exponent, etc.) are direct child properties — not string paths.
Edit Modes
| Mode | Behaviour | Example command | Effect |
|---|---|---|---|
| CORRECT | Structural edits only. No algebraic equivalence. | "change exponent to 3" | Replaces the exponent node value |
| ALGEBRA | APPLY_INVERSE & TRANSPOSE_TERM available. | "subtract 2x from both sides" | Subtracts 2x from lhs AND rhs |
| ASK | Returns both interpretations for the client to pick. | "move the root" | Shows REPARENT_NODE vs WRAP_NODE options |
editMode in both /api/intent (so the LLM receives the constraint) and /api/mutate (so the mutation engine enforces it). APPLY_INVERSE with editMode:"CORRECT" returns a 400 error.Error Codes
| Status | Meaning | Resolution |
|---|---|---|
| 400 | Bad request — missing required field, malformed JSON, or mode constraint violation | Check request body against the schema |
| 401 | Invalid or missing API key | Set X-Api-Key: mv_your_key |
| 429 | Rate limit exceeded | Check X-RateLimit-Reset header; consider Institutional tier |
| 502 | Upstream error — Anthropic or Google TTS unavailable | Retry with exponential backoff |
| 500 | Internal server error | Contact hello@mathvoice.app |
All error responses include: { "error": "description", "hint": "optional guidance" }
SDK & npm Package
initialLatex, onMutate, apiBase, editMode, asrProvider, theme.npm install @mathvoice/react
import { MathVoiceEditor } from '@mathvoice/react'; export default function MyLesson() { return ( <MathVoiceEditor initialLatex="\\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}" editMode="CORRECT" apiBase="/api" onMutate={(result) => console.log(result.latexAfter)} onError={(err) => console.error(err)} /> ); }
npm install @mathvoice/sdk
Accessibility
MathVoice is built for blind and low-vision STEM students. Compliance details for procurement conversations:
| Standard | Level | Status |
|---|---|---|
| WCAG 2.2 | AA | ✓ Conformant — automated (axe-core) and manual AT + WCAG 2.2 criteria testing complete. VPAT available. |
| Section 508 | — | ✓ Conformant via WCAG 2.2 AA mapping — see VPAT |
| EN 301 549 | — | ⚠️ Self-certified (EU institutional sales — contact for details) |
- KaTeX renders with
output: 'htmlAndMathml'. The formula container usesrole="img"with anaria-label(MathSpeak text); the embedded<math>element is also accessible for NVDA + MathCAT interactive navigation. - JAWS + MathPlayer and NVDA + MathCAT navigate the MathML interactively.
aria-live="polite"regions announce mutations immediately after they occur.- Full keyboard operability: Tab navigation, visible focus indicators (3:1 contrast ratio), skip links.
- All colour contrast ratios meet WCAG 2.2 AA (≥4.5:1 for normal text, ≥3:1 for large).
- Voice audio is processed entirely in the browser by default. Only the JSON
IntentResult— containing no personally identifiable information — is transmitted to the API. FERPA-compliant.
Download the full VPAT (PDF) or contact hello@mathvoice.app for a signed DPA.
Privacy & FERPA
Voice audio never leaves the user's device in the default Web Speech API configuration. The only data transmitted to the MathVoice API is the JSON IntentResult:
{ "type": "REPLACE_VALUE", "target": { "role": "denominator" }, "value": { "raw": "n" } }
This object contains no audio, no voice biometrics, no user identifiers, and no personally identifiable information. This is the FERPA compliance claim.
The optional Whisper ASR fallback (used on Safari iOS & Firefox, where on-device speech recognition is unavailable) transmits audio over HTTPS to Groq for transcription. Groq's API has a no-data-retention policy — audio is not stored or used for training — which gives a cleaner FERPA posture than consumer speech APIs. Schools should still obtain a signed DPA via the contact form before enabling this mode for students under 18. For fully on-device transcription (audio never leaves the browser), an in-browser Whisper (WASM) mode is on the roadmap.
OpenAPI Spec
A downloadable Postman collection covering all endpoints is available in the public repository. Import it directly into Postman or Insomnia.
Need an OpenAPI 3.1 JSON spec for your platform? Contact hello@mathvoice.app and we'll send it over.