API reference
Embed the input in the browser; verify proofs on your server. Machine-readable: openapi.json.
1. The SDK
<script src="https://www.proofoftype.com/sdk/v1.js" async></script>It mounts every [data-proofoftype] element, and exposes window.ProofOfType. Every event is also dispatched on the element as a DOM event, e.g. el.addEventListener('proofoftype:verified', e => e.detail).
Attributes / options
| Attribute | Option | Default | What |
|---|---|---|---|
| data-name | name | proofoftype | Form field name. The proof goes in <name>_proof. |
| data-placeholder | placeholder | "Type here…" | Placeholder text. |
| data-max-words | maxWords | 500 | Word limit, 10–3000. |
| data-theme | theme | auto | auto, light or dark. |
| data-button | button | true (false in a form) | Show the built-in Verify button. In a form, the form's submit button verifies. |
| data-button-label | buttonLabel | Verify I typed this | Label of the built-in button. |
| data-form | form | true | Handle the surrounding <form> (fill hidden fields, verify on submit). |
Methods
| Method | Returns | What |
|---|---|---|
| ProofOfType.mount(el, options) | Input | Put an input in an element (or a selector). |
| ProofOfType.scan(root?) | — | Mount any new [data-proofoftype] elements (after you add them). |
| input.verify() | Promise<Result> | Ask the person to confirm on their phone; resolves with the text and proof. |
| input.reset() | — | Clear the text and the proof. |
| input.focus() | — | Focus the input. |
| input.on(event, fn) / off | Input | Listen to events. |
| input.destroy() | — | Remove the input. |
| input.result | Result | null | The last verified result. |
Events
| Event | Data | When |
|---|---|---|
| ready | { signedIn, username } | The input loaded. |
| signin | { username } | The person signed in with Proof of Type. |
| change | { words, chars } | While typing (counts only; the text comes with the proof). |
| verified | Result | A proof is ready. |
| error | { code, message } | Not verified: not_signed_in, empty, too_long, needs_app, cancelled, session, human_score, rate_limit, … |
| resize | { height } | The input changed height (the SDK resizes the frame for you). |
Result
{
text: "The exact text, as typed", // plain text; this is what's hashed
html: "<p>The exact text, as typed</p>", // with formatting (bold, lists…)
token: "eyJhbGciOiJFZERTQSIs…", // the proof (JWT)
author: "yogev", // Proof of Type username
score: 87, wpm: 52, words: 6, chars: 22,
proofId: "pot_…",
url: "https://www.proofoftype.com/v/eyJhbGci…" // public proof page
}2. Verify a proof: POST /api/v1/verify
Call it from your server before you save the text. CORS-enabled; no API key needed.
| Field | Type | What |
|---|---|---|
| token | string, required | The proof token. |
| text | string, optional | The text you received. Returns text_matches. |
| origin | string, optional | Your site, e.g. https://example.com. Returns origin_matches. |
curl -s https://www.proofoftype.com/api/v1/verify \
-H 'Content-Type: application/json' \
-d '{"token":"eyJhbGciOiJFZERTQSIs…","text":"Hello, world","origin":"https://your-site.com"}'{
"valid": true,
"text_matches": true,
"origin_matches": true,
"proof": {
"id": "pot_3f9…", "origin": "https://example.com", "author": "yogev",
"typed_at": "2026-10-11T18:20:31.000Z", "signed_on": "ios",
"score": 87, "wpm": 52, "words": 6, "chars": 22,
"text_sha256": "a59…", "version": 1
}
}Invalid tokens return { "valid": false, "reason": "bad_signature" | "malformed" | "unknown_key" | "bad_claims" }.
Accept a proof only if valid, text_matches and origin_matches are all true. Keep the proof id: one proof backs one piece of text.
3. Verify offline: the proof token
Tokens are JWTs signed with EdDSA (Ed25519). Public keys: https://www.proofoftype.com/.well-known/jwks.json. Issuer https://www.proofoftype.com, audience = your origin.
| Claim | What |
|---|---|
| iss | Always https://www.proofoftype.com |
| sub | The proof id (pot_…) |
| aud | The site the text was typed on (its origin, read by the browser) |
| iat | When it was verified (Unix seconds) |
| typed_by | Always "human" |
| author | The Proof of Type username who typed and signed it |
| signed_on | "ios" or "android": the phone that signed it |
| text_sha256 | SHA-256 (hex) of the exact UTF-8 text |
| score | Human score, 0–100 (typing rhythm and touch) |
| wpm | Typing speed, words per minute |
| words | Word count |
| chars | Visible characters (no spaces) |
| v | Token format version (1) |
// Or verify offline with our public key (EdDSA / Ed25519 JWT).
import { createRemoteJWKSet, jwtVerify } from 'jose';
import { createHash } from 'node:crypto';
const JWKS = createRemoteJWKSet(new URL('https://www.proofoftype.com/.well-known/jwks.json'));
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://www.proofoftype.com',
audience: 'https://your-site.com',
});
const hash = createHash('sha256').update(text, 'utf8').digest('hex');
if (payload.text_sha256 !== hash) throw new Error('Not the signed text');4. Public proof pages
https://www.proofoftype.com/v/<token> shows who typed it, where and when, and lets anyone paste the text to check it's an exact match. Link it next to the text if you like.
Limits
- Up to 3,000 words per input; 60 verifications per account per hour.
- One typing session backs one proof; typing must take real time (it's timed on our server).
- Your page must send a referrer origin to the frame (the SDK sets
referrerpolicy="origin").