Private Beta

The WriterMark SDK is in private beta. Full documentation is below — reach out at human@writermark.org to get started.

Integrate WriterMark

Add human-authorship certification to any writing application. The SDK observes how users type — not what they type — and issues cryptographic certificates proving a human wrote the text.

Ready to integrate? Reach out at human@writermark.org to request access to the private beta.


Quick Start: React + TipTap

The fastest path. One hook, one component, ~15 lines of code.

1. Install

npm install @writermark/sdk

2. Use the hook + indicator

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 } = useWriterMark('doc-1', editor, {
    writermarkUrl: 'https://writermark.org',
    onCheckpoint: (checkpoint, coverage, pass, cert) => {
      // Save checkpoint to restore certification across sessions
      localStorage.setItem('my-checkpoint', checkpoint)
    },
    previousCheckpoint: localStorage.getItem('my-checkpoint'),
  })

  return (
    <div>
      <EditorContent editor={editor} />
      <CertIndicator status={status} certificate={certificate} />
    </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.

useWriterMark options

PropTypeDescription
writermarkUrlstringServer URL (e.g. https://writermark.org)
onCheckpointfunctionCalled after each certification with (checkpoint, coverage, pass, certificate, authorshipMap)
previousCheckpointstring | nullRestore a saved checkpoint to continue certification across sessions
previousPassbooleanWhether the previous checkpoint was passing
previousAuthorshipMapAuthorshipMap | nullSaved authorship map from a previous session
debugbooleanLog certification details to console
telemetryConsentbooleanIf true and sourceApp is set, uploads anonymous telemetry for ML training
sourceAppstringApp identifier for telemetry (must be allowlisted server-side)

useWriterMark return values

FieldTypeDescription
status'idle' | 'certifying' | 'certified' | 'not-certified'Current certification state
coveragenumber | nullFraction of text covered by observed keystrokes (0–1)
certificatestring | nullSigned attestation JWT when passing
checkpointstring | nullLatest signed checkpoint JWT
certifyNow() => PromiseForce an immediate certification cycle
authorshipMapAuthorshipMap | nullRLE array tracking provenance of each character
isTrackingbooleanWhether the collector is attached and running

TipTap without React

If you use TipTap but not React, attach the collector directly and run the certification loop yourself.

import {
  Collector,
  attachToTipTap,
  createCertificationContext,
  computeMerkleRoot,
  compressEvents,
  normalizeText,
} from '@writermark/sdk'

// 1. Create collector and context
const collector = new Collector()
const ctx = createCertificationContext('https://writermark.org')
collector.start()

// 2. Attach to your TipTap editor instance
const cleanup = attachToTipTap(editor, collector, ctx)

// 3. Certify periodically (e.g. every 30s)
let lastIndex = 0
async function certify() {
  const events = collector.peekEvents()
  const newEvents = compressEvents(events.slice(lastIndex))
  if (newEvents.length < 10) return  // not enough data yet

  const text = editor.getText()
  const normalized = normalizeText(text)
  const data = new TextEncoder().encode(normalized)
  const hash = Array.from(
    new Uint8Array(await crypto.subtle.digest('SHA-256', data))
  ).map(b => b.toString(16).padStart(2, '0')).join('')

  const res = await fetch('https://writermark.org/certify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      documentId: 'my-doc',
      events: newEvents,
      textHash: hash,
      charCount: text.length,
      checkpoint: ctx.checkpoint,
      contentMerkleRoot: await computeMerkleRoot(text, ctx),
      authorshipMap: ctx.authorshipMap,
    }),
  })

  const result = await res.json()
  ctx.checkpoint = result.checkpoint
  ctx.isPassing = result.pass
  if (result.authorshipMap) ctx.authorshipMap = result.authorshipMap
  lastIndex = events.length
}

setInterval(certify, 30000)

// 4. Clean up when done
// cleanup()

Generic DOM (textarea / contenteditable)

For standard <textarea> or contenteditable elements, use attachToElement instead of attachToTipTap.

import { Collector, attachToElement } from '@writermark/sdk'

const collector = new Collector()
collector.start()

const textarea = document.querySelector('#my-editor')
const cleanup = attachToElement(textarea, collector)

// Then run the same certification loop as above,
// reading text from textarea.value instead of editor.getText()

Works with <textarea>, <input>, and any element with contenteditable.


Browser Script Tag

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>
  const collector = new WriterMark.Collector()
  collector.start()
  const cleanup = WriterMark.attachToElement(
    document.querySelector('#my-editor'),
    collector
  )
</script>

Custom Editor (Manual API)

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

// Record events from your editor's callbacks:
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', 'undo', 'redo', or 'unknown'.


API Reference

POST /certify

POST /certify

The main certification endpoint. Stateless — all state lives in the signed checkpoint. Rate limit: 30/min per IP.

Request body:

FieldTypeDescription
documentIdstringPersistent document identifier (required on first call)
eventsEditorEvent[]New events since last checkpoint
textHashstringSHA-256 of normalized text
charCountnumberCurrent text length
checkpointstring | nullPrevious checkpoint JWT (null for first call)
contentMerkleRootstring | nullMerkle root of document chunks
authorshipMapAuthorshipMap | nullRLE authorship intervals

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
}

POST /derive

POST /derive

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. Rate limit: 5/min per IP.

GET /public-key

GET /public-key

Returns the Ed25519 public key for verifying attestation JWTs.

{
  "publicKey": {
    "kty": "OKP",
    "crv": "Ed25519",
    "x": "..."
  }
}

GET /.well-known/jwks.json

GET /.well-known/jwks.json

Standard JWKS endpoint for public key discovery. Key ID: writermark-v1.


Components

CertIndicator

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}
  writermarkUrl="https://writermark.org"
/>
PropTypeDescription
statusCertificationStatusFrom useWriterMark
certificatestring | nullAttestation JWT from useWriterMark
writermarkUrlstring?Server URL for "View certificate" link (defaults to writermark.org)
classNamestring?CSS class override for the wrapper element

TelemetryConsentBanner

Opt-in consent banner for anonymous telemetry collection. Only renders when the user hasn't responded yet.

import { TelemetryConsentBanner } from '@writermark/sdk/react'

<TelemetryConsentBanner
  sourceApp="my-app"
  onConsent={(allowed) => setConsent(allowed)}
/>

Telemetry & Consent

WriterMark can optionally collect anonymous typing telemetry for improving the scoring model. This is entirely opt-in — users must explicitly consent, and no text content is ever transmitted.

To enable telemetry, set both telemetryConsent: true and sourceApp in the hook options. Your sourceApp identifier must be allowlisted on the server. Use the TelemetryConsentBanner component to ask users for consent.

Consent state is stored in localStorage under writermark_telemetry_consent. You can read/write it programmatically with getTelemetryConsent() and setTelemetryConsent(value).


Self-hosting

The WriterMark protocol is open. You can run your own certification server with your own signing keys. The server is a Node.js/Hono application — see the GitHub repository for setup instructions.

When self-hosting, point the SDK at your server URL instead of writermark.org.


Interested in integrating WriterMark? Reach out at human@writermark.org