REST API reference
The hosted API behind viaid.ai (Vercel serverless functions + Supabase). Everything below is what the CLI-free, browser-only "Create a Badge" / "Read a Badge" flow actually calls — pulled from the live client code, not from a spec.
Keys are minted client-side in the browser via WebCrypto and never leave the browser in plaintext; only public keys and signatures are sent to these endpoints.
GET /api/witness
Returns the Witness Service's current public key, used to build the badge's inception
object before signing.
Response 200
{ "voucher_pub": "<base64 SPKI public key>" }
POST /api/mint
Submits an owner-and-agent-signed badge core for the Witness Service to co-sign, validate, and persist.
Request body
{
"core": {
"schema": "viaid.badge/0.1",
"agent_id": "via_...",
"inception": { "...": "..." },
"keys": { "owner_pub": "...", "agent_pub": "...", "voucher_pub": "..." },
"assurance_tier": "WITNESSED",
"revocation_state": "FRESH",
"evidence": null,
"log": [],
"created_note": "A badge is evidence, not a safety or compliance guarantee."
},
"owner_sig": "<base64 Ed25519 signature over the canonicalized core>",
"agent_sig": "<base64 Ed25519 signature over the canonicalized core>"
}
See Badge schema for what core actually contains and how the canonical
form used for signing is computed.
Response 200
{ "short_code": "M16XMGN", "verify_path": "/a/M16XMGN" }
Response (error) — a non-2xx status with a JSON body containing an error field. The
client surfaces error directly if present, otherwise a generic "mint failed" message.
The exact validation this endpoint performs server-side (signature re-verification,
agent_id recomputation, collision checks) is not itself part of the public contract above —
only the request/response shape a client needs to integrate against is. Treat server-side
validation as "whatever it takes to make a forged or malformed badge fail," not a fixed list.
GET /a/:shortCode
Not a JSON endpoint. Returns a fully server-rendered HTML verify page for the badge — the
same page a human sees after minting, with the verdict, QR code, coverage, verification steps,
and tamper-evident log. There is no separate JSON verification API today; if you need
machine-readable verification, verify the badge file directly with the CLI's
verify command instead, which returns a structured object.
POST /api/download-gate
Email-gated download requests. Deliberately not a security boundary — see
Enterprise and the inline comments in lib/email-gate.mjs for why. This
does a lightweight plausibility check (does the email look like a personal webmail address?)
and logs the request; it does not verify the email is real or send any confirmation mail.
Request body
{ "email": "you@company.com", "asset": "<asset identifier>", "referringPage": "/optional/path" }
Response 200
{ "ok": true }
Response 403 (email looks like a personal/consumer address)
{ "error": "personal_email", "message": "Please use your work email to download this resource." }
POST /api/contact
General questions and "schedule a demo" requests from the site's contact form. Every submission is written to Supabase (durable record) and, if configured, triggers an email notification via Resend — the destination address is never present in any client-side code.
Request body
{
"name": "Jane Doe",
"email": "jane@company.com",
"reason": "general",
"message": "How does the Witness co-signature work?",
"company": "Optional Co",
"hp": ""
}
reason is "general" or "demo". company is optional. hp is a honeypot field — leave
it empty; a filled honeypot field silently short-circuits the request (see
Enterprise for the anti-spam approach, or the inline comments in
api/contact.js).
Response 200
{ "ok": true, "emailed": true }
emailed reflects whether the notification email actually sent — the submission is recorded
in Supabase either way, so a false here means "check Supabase," not "this was lost."
Response 400 — validation failure, with a human-readable error message.
Response 429 — rate-limited (if a rate-limit store is configured; otherwise this route
does not rate-limit).