Skip to main content

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.

note

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).