v2.0.0 REST · JSON WCAG 2.1 AA ⚠️ self-assessed FERPA Safe MathML Output

MathVoice API

AST-powered speech-to-LaTeX editing engine. Three endpoints. One integration. Embed voice-controlled math editing in any EdTech platform.

/api/normalize is completely free β€” no API key, no rate limit, no account. Returns normalised tokens and a confidence score. Build on it today.

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.

Never expose your API key in client-side code. All AI processing happens server-side on MathVoice infrastructure β€” your integration only needs to pass your MathVoice API key in X-Api-Key.

Rate Limits

TierEndpointMonthly limit
Free/api/normalizeUnlimited
Pro/api/intent β€” regex tierUnlimited
Pro/api/intent β€” LLM tier1,000 calls
Pro/api/mutate, /api/mathml, /api/speak10,000 calls
InstitutionalAllCustom 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.

POST /api/normalize Free Β· No auth Β· <1ms Β· No LLM

Request Body

ParamTypeDescription
textstringrequiredRaw ASR transcript or typed command

Response

{ "normalized": "REPLACE denominator TO y^{2}", "tokens": ["REPLACE", "denominator", "TO", "y^{2}"], "confidence": 0.91, "raw": "change the denominator to y squared", "tier": "free" }
β–Ά Live demo β€” calls the real API

    

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.

POST/api/intentPro Β· API key Β· <1ms–500ms

Request Body

ParamTypeDescription
rawTextstringoptionalOriginal ASR transcript
normalizedTextstringoptionalOutput from /api/normalize. Preferred β€” improves regex confidence.
formulaContextobjectoptional{latex:string, astFlat?:object} β€” current formula, used by LLM for disambiguation
editModestringoptional"CORRECT" | "ALGEBRA" | "ASK" β€” default "CORRECT"

Response β€” IntentResult

{ "type": "REPLACE_VALUE", "target": { "role": "denominator" }, "value": { "raw": "y^{2}" }, "confidence": 0.94, "tier": "regex", "source": "regex" } // UNKNOWN response when command cannot be parsed: { "type": "UNKNOWN", "reason": "Could not identify a target role or operation", "confidence": 0 }
β–Ά Live demo

    

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.

POST/api/mutatePro Β· API key Β· ~0ms Β· Deterministic

Request Body

ParamTypeDescription
astobjectrequiredCurrent AST from a previous /api/mutate response, or pass {type:"RAW",latex:"…"}
intentobjectrequiredIntentResult from /api/intent
editModestringoptionalMust be "ALGEBRA" for APPLY_INVERSE or TRANSPOSE_TERM operations

Response β€” MutationResult

{ "success": true, "latexBefore": "\\frac{x}{2a}", "latexAfter": "\\frac{x}{y^{2}}", "astAfter": { "type": "FRACTION", /* … */ }, "diff": [ { "op": "REPLACE_VALUE", "role": "denominator", "before": "2a", "after": "y^{2}" } ], "mathml": "<math xmlns=\"...\"><mfrac>...</mfrac></math>" } // Error response (success: false): { "success": false, "error": "Cannot find role 'denominator' in formula", "latexAfter": "\\frac{x}{2a}" }

GET /api/mathml

Convert a LaTeX string directly to a <math> element compatible with JAWS + MathPlayer and NVDA + MathCAT.

GET/api/mathml?latex=…Pro Β· API key

Query Parameters

ParamTypeDescription
latexstringrequiredURL-encoded LaTeX. e.g. %5Cfrac%7B-b%7D%7B2a%7D

Response

{ "mathml": "<math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\">…</math>", "ast": { "type": "FRACTION", /* … */ }, "latex": "\\frac{-b}{2a}" }

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.

POST/api/speakPro Β· API key

Request Body

ParamTypeDescription
latexstringoptionalLaTeX string. Parsed to AST internally.
astobjectoptionalPre-parsed AST. Faster if you already have one.

Response

{ "ssml": "<speak>start fraction <break time=\"150ms\"/> negative b <break time=\"200ms\"/> over <break time=\"100ms\"/> 2a end fraction</speak>", "plainText": "start fraction negative b over 2a end fraction" }

Intent Types

REPLACE_VALUE
Replace the value of a specific node role. Most common. Works in all modes.
DELETE_NODE
Remove a node by role. Sets it to an empty placeholder.
INSERT_NODE
Add a new node at a specific role position.
REPARENT_NODE
Move a node from one position to another. Swaps values.
NEGATE_NODE
Negate the value at a role β€” add or remove leading minus.
WRAP_NODE
Wrap a node in SQRT or FRACTION.
APPLY_INVERSE ALGEBRA
ADD/SUBTRACT/MULTIPLY/DIVIDE the same value on both sides of an equation.
TRANSPOSE_TERM ALGEBRA
Move a term across the = sign, negating it on arrival.
UNKNOWN
Command not understood. Includes a reason string. Always handle this case.

AST Schema

Every node has a type field. Roles (numerator, denominator, exponent, etc.) are direct child properties β€” not string paths.

// Structural nodes { type: "FRACTION", numerator: MathNode, denominator: MathNode } { type: "POWER", base: MathNode, exponent: MathNode } { type: "SQRT", radicand: MathNode, index: MathNode | null } { type: "SUBSCRIPT", base: MathNode, subscript: MathNode } { type: "EQUALITY", lhs: MathNode, rhs: MathNode } { type: "FUNCTION", name: string, args: MathNode[] } { type: "GROUP", content: MathNode, delim: string } // Compound nodes { type: "SUM", terms: MathNode[] } { type: "PRODUCT", factors: MathNode[] } { type: "NEG", operand: MathNode } // Leaf nodes { type: "NUMBER", value: string } { type: "VARIABLE", name: string } { type: "SYMBOL", name: string, latex: string } { type: "OP", op: string } // Opaque pass-through (safe fallback) { type: "RAW", latex: string }

Edit Modes

ModeBehaviourExample commandEffect
CORRECTStructural edits only. No algebraic equivalence."change exponent to 3"Replaces the exponent node value
ALGEBRAAPPLY_INVERSE & TRANSPOSE_TERM available."subtract 2x from both sides"Subtracts 2x from lhs AND rhs
ASKReturns both interpretations for the client to pick."move the root"Shows REPARENT_NODE vs WRAP_NODE options
Pass 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

StatusMeaningResolution
400Bad request β€” missing required field, malformed JSON, or mode constraint violationCheck request body against the schema
401Invalid or missing API keySet X-Api-Key: mv_your_key
429Rate limit exceededCheck X-RateLimit-Reset header; consider Institutional tier
502Upstream error β€” Anthropic or Google TTS unavailableRetry with exponential backoff
500Internal server errorContact [email protected]

All error responses include: { "error": "description", "hint": "optional guidance" }

SDK & npm Package

βš›οΈ
@mathvoice/react v0.1.1
Drop-in React editor component. Props: 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)}
    />
  );
}
🟦
@mathvoice/sdk v0.1.1
Framework-free TypeScript. Typed request/response helpers for all MathVoice API endpoints. Zero runtime dependencies.
npm install @mathvoice/sdk

Accessibility

MathVoice is built for blind and low-vision STEM students. Compliance details for procurement conversations:

StandardLevelStatus
WCAG 2.1AA⚠️ 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)

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.