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
| Tier | Endpoint | Monthly limit |
|---|---|---|
| Free | /api/normalize | Unlimited |
| Pro | /api/intent β regex tier | Unlimited |
| Pro | /api/intent β LLM tier | 1,000 calls |
| Pro | /api/mutate, /api/mathml, /api/speak | 10,000 calls |
| Institutional | All | Custom SLA |
Rate limit headers: X-RateLimit-Remaining, X-RateLimit-Limit, X-RateLimit-Reset (Unix timestamp). Exceeded requests receive 429 Too Many Requests.
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/speak
Convert a formula to MathSpeak Initiative-style SSML and plain text. Used by the cloud TTS path and screen reader announcement strings.
Request Body
| Param | Type | Description | |
|---|---|---|---|
| latex | string | optional | LaTeX string. Parsed to AST internally. |
| ast | object | optional | Pre-parsed AST. Faster if you already have one. |
Response
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 [email protected] |
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.1 | AA | β οΈ Self-assessed β axe-core automated tests pass; manual AT testing in progress. VPAT available. |
| Section 508 | β | β οΈ Self-assessed via WCAG 2.1 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.1 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 [email protected] 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 [email protected] and we'll send it over.