Human-authorship certification for text editors. The SDK observes how users type and issues cryptographic certificates proving a human wrote the text. No text content is stored on the server.
Status: Alpha — this SDK is in early development. Breaking changes may occur before the official 1.0.0 release. Reach out at human@writermark.org if you need help getting started.
npm install @writermark/sdkOptional for DOCX verification support:
npm install jszipThe fastest path. One hook, one component, ~15 lines of code.
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { useWritermark, CertIndicator } from '@writermark/sdk/react'
function MyEditor() {
const editor = useEditor({ extensions: [StarterKit] })
const { status, certificate, certifyNow } = useWritermark('doc-1', editor, {
onCheckpoint: (checkpoint, coverage, pass, cert, map, checkpoints) => {
localStorage.setItem('my-checkpoint', checkpoint)
localStorage.setItem('my-checkpoints', JSON.stringify(checkpoints))
},
previousCheckpoint: localStorage.getItem('my-checkpoint'),
previousCheckpoints: JSON.parse(localStorage.getItem('my-checkpoints') || 'null'),
})
return (
<div>
<EditorContent editor={editor} />
<CertIndicator status={status} certificate={certificate} certifyNow={certifyNow} />
</div>
)
}That's it. The hook handles the full certification lifecycle — collecting behavioral telemetry, sending it to the server every 30 seconds, and maintaining a signed checkpoint chain. The CertIndicator shows a colored dot with the current status and a hover panel with score details.
| Prop | Type | Description |
|---|---|---|
| writermarkUrl | string? | API URL (defaults to https://api.writermark.org) |
| onCheckpoint | function | Called after each certification with (checkpoint, coverage, pass, certificate, authorshipMap, checkpoints, vdfState) |
| previousCheckpoint | string | null | Restore a saved checkpoint to continue certification across sessions |
| previousCheckpoints | string[] | null | Rolling window of recent checkpoint JWTs (max 2). Falls back to previousCheckpoint if not provided. |
| previousPass | boolean | Whether the previous checkpoint was passing |
| previousAuthorshipMap | AuthorshipMap | null | Saved authorship map from a previous session |
| previousVdfState | VdfState | null | VDF temporal proof state (not yet integrated) |
| debug | boolean | Log certification details to console |
| Field | Type | Description |
|---|---|---|
| status | 'idle' | 'certifying' | 'certified' | 'not-certified' | Current certification state |
| coverage | number | null | Fraction of text covered by observed keystrokes (0–1) |
| certificate | string | null | Signed attestation JWT when passing |
| checkpoint | string | null | Latest signed checkpoint JWT |
| certifyNow | () => Promise | Force an immediate certification cycle |
| authorshipMap | AuthorshipMap | null | RLE array tracking provenance of each character |
| isTracking | boolean | Whether the collector is attached and running |
Use WritermarkSession — one constructor, full lifecycle handled for you. No manual fetch loops.
import { WritermarkSession } from '@writermark/sdk'
const session = new WritermarkSession(editor, {
documentId: 'my-doc',
onCheckpoint: (checkpoint, coverage, pass, cert, map, checkpoints) => {
myDatabase.save({ checkpoint, checkpoints, authorshipMap: map })
},
onStatusChange: (status) => {
console.log('certification status:', status)
},
previousCheckpoint: myDatabase.get('checkpoint'),
previousCheckpoints: myDatabase.get('checkpoints'),
})
// Force immediate certification (e.g. before publish or export)
const result = await session.certifyNow()
// Clean up when done
session.destroy()WritermarkSession handles event collection, compression, the 30-second certification loop, checkpoint chain management, clipboard enrichment on copy, and certified paste detection — all internally. Same class works for all editor types.
| Option | Type | Description |
|---|---|---|
| documentId | string | Persistent identifier for the document (required) |
| writermarkUrl | string? | API URL (defaults to https://api.writermark.org) |
| onCheckpoint | function | Called after each certification with (checkpoint, coverage, pass, certificate, authorshipMap, checkpoints, vdfState) |
| onStatusChange | function | Called when status changes: `'idle' |
| onCertifyResult | function | Called with the full server response object after each certification |
| previousCheckpoint | string | null | Restore a saved checkpoint to continue across sessions |
| previousCheckpoints | string[] | null | Rolling window of recent checkpoint JWTs (max 2) |
| previousPass | boolean | Whether the previous checkpoint was passing |
| previousAuthorshipMap | AuthorshipMap | null | Saved authorship map from a previous session |
| previousVdfState | VdfState | null | VDF temporal proof state (not yet integrated) |
| getText | () => string | Custom text getter (auto-detected for TipTap, textarea, contenteditable) |
| debug | boolean | Log certification details to console |
| Method | Returns | Description |
|---|---|---|
| certifyNow() | Promise<object | null> | Force an immediate certification cycle. Call before exporting or copying a certificate to ensure it matches the latest text. |
| getStatus() | CertificationStatus | Current certification state |
| getCertificate() | string | null | Latest attestation JWT |
| getCheckpoint() | string | null | Latest checkpoint JWT |
| getCheckpoints() | string[] | Full rolling checkpoint window |
| getVdfState() | VdfState | null | Current VDF state (not yet integrated) |
| getAuthorshipMap() | AuthorshipMap | null | Current authorship map |
| getCoverage() | number | null | Keystroke coverage fraction (0–1) |
| isActive() | boolean | Whether the session is running |
| destroy() | void | Stop the session and clean up all listeners |
Pass any <textarea>, <input>, or contenteditable element. The session auto-detects the element type.
import { WritermarkSession } from '@writermark/sdk'
const session = new WritermarkSession(
document.querySelector('#my-editor'),
{
documentId: 'my-doc',
onCheckpoint: (checkpoint, coverage, pass) => {
localStorage.setItem('checkpoint', checkpoint)
},
}
)
session.destroy()Note: clipboard enrichment on copy (certified paste provenance) requires TipTap.
For non-bundled environments, load the SDK as a script tag. Everything is available on window.Writermark.
<script src="https://writermark.org/sdk.js"></script>
<script>
var session = new Writermark.WritermarkSession(
document.querySelector('#my-editor'),
{
documentId: 'my-doc',
onCheckpoint: function(checkpoint) {
localStorage.setItem('wm-checkpoint', checkpoint)
},
}
)
</script>The SDK includes a complete verification toolkit.
Hand it a File object from an <input type="file"> or drag-and-drop. Supports .wtxt, .docx, .rtf, .md, .txt, and .html.
import { verifyFile } from '@writermark/sdk'
const input = document.querySelector('input[type="file"]')
input.addEventListener('change', async () => {
const result = await verifyFile(input.files[0])
if (result.valid) {
console.log('Verified!', result.score, result.confidence)
} else {
console.log('Failed:', result.detail)
}
})DOCX support requires JSZip as an optional dependency.
import { verify } from '@writermark/sdk'
const result = await verify({
token: 'eyJhbGciOi...', // raw JWT or full certificate text
text: 'The original document text...',
})
console.log(result.valid) // true/false
console.log(result.signatureOnly) // false (text was checked too)
console.log(result.score) // 0.82
console.log(result.confidence) // 0.88const result = await verify({ token: certificateText })
// result.signatureOnly === trueimport { extractFromFile } from '@writermark/sdk'
const { text, token, fileType } = await extractFromFile(file)
// fileType: 'wtxt' | 'docx' | 'rtf' | 'md' | 'txt' | 'html'| Field | Type | Description |
|---|---|---|
| valid | boolean | Whether the certificate is valid |
| pass | boolean? | Whether the document passed certification |
| score | number? | Human-authorship score (0–1) |
| confidence | number? | Confidence level (0–1) |
| issuedAt | string? | ISO timestamp of certification |
| signatureOnly | boolean | True if only the signature was verified (no text hash check) |
| detail | string? | Human-readable detail on failure |
verifyFile() also returns text, token, and fileType on the result object.
For editors that aren't TipTap or standard DOM elements, use the Collector directly. Call record* methods from your editor's event handlers.
import { Collector } from '@writermark/sdk'
const collector = new Collector()
collector.start()
collector.recordKey('KeyA', cursorPosition)
collector.recordKeyUp('KeyA')
collector.recordBackspace('Backspace', cursorPosition)
collector.recordEnter('Enter', cursorPosition)
collector.recordPaste(charCount, pastedText, 'external', cursorPosition)
collector.recordCopyOrCut(selectedText, copyStart, copyLength, isCut)
collector.recordCursorJump(distance)
collector.recordSelect(selectionLength)
collector.recordUndo()
collector.recordRedo()
collector.recordFocus()
collector.recordBlur()
collector.recordScroll(deltaY)
collector.recordMouse(xRatio, yRatio) // 0-1 normalized
collector.recordVisibility(isVisible)
collector.recordMutation(position, deleteLength, insertLength, 'typed')The recordMutation method is key — it tracks document changes for the authorship map. The insertSource parameter should be 'typed', 'paste-internal', 'paste-external', 'paste-certified', 'undo', or 'redo'.
All API endpoints are served from https://api.writermark.org.
The main certification endpoint. Stateless — all state lives in the signed checkpoint. Rate limit: 30/min per IP.
Request body:
| Field | Type | Description |
|---|---|---|
| documentId | string | Persistent document identifier (required on first call) |
| events | EditorEvent[] | New events since last checkpoint |
| checkpoints | string[] | Rolling window of recent checkpoint JWTs (max 2). Empty array on first call. |
| recentEvents | EditorEvent[] | Events from the previous window (for ML context). Empty on first call. |
| merkleRoot | string | null | Merkle root of document chunks |
| authorshipMap | AuthorshipMap | null | RLE authorship intervals |
| textHash | string | SHA-256 of normalized text |
| charCount | number | Current text length |
| vdfState | VdfState | null | VDF temporal proof state (not yet integrated) |
The daemon also accepts legacy fields checkpoint (singular) and contentMerkleRoot for backward compatibility.
Response:
{
"checkpoint": "eyJhbGciOi...",
"score": 0.72,
"behavioralScore": 0.85,
"coverage": 0.95,
"pass": true,
"certificate": "eyJhbGciOi...",
"authorshipMap": [[0, 150, "human_evidenced"]],
"confidence": 0.88,
"activeWritingTimeMs": 120000,
"revisionPercent": 8.5
}Verify a certificate's signature. Body: { "token": "eyJ..." }
Verify a certificate matches specific text. Body: { "text": "...", "token": "eyJ..." }
Create a standalone certificate for an excerpt (e.g. copy-paste certification). Requires a valid source JWT and Merkle proofs for the excerpt's chunks.
Returns the Ed25519 public key for verifying attestation JWTs.
{
"publicKey": {
"kty": "OKP",
"crv": "Ed25519",
"x": "..."
}
}Standard JWKS endpoint for public key discovery. Key ID: writermark-v1.
Drop-in React component showing a colored status dot, label, and hover popup with certificate details.
import { CertIndicator } from '@writermark/sdk/react'
<CertIndicator
status={status}
certificate={certificate}
certifyNow={certifyNow}
/>| Prop | Type | Description |
|---|---|---|
| status | CertificationStatus | From useWritermark |
| certificate | string | null | Attestation JWT from useWritermark |
| certifyNow | () => Promise? | If provided, the indicator runs an immediate certification cycle before copying or viewing. Pass certifyNow from useWritermark. |
| writermarkUrl | string? | Server URL for "View certificate" link (defaults to writermark.org) |
| className | string? | CSS class override for the wrapper element |
- Writermark — Verify certificates and try the demo
- Documentation — Full developer docs
- Wintertext — Free desktop writing app with Writermark built in
MIT