1. Handwrite Regular Accounts
Handwrite
  • Handwrite Regular Accounts
    • Start here — Handwrite for outside developers
    • API specification notes
    • Socket specification notes
    • Account
      • README — GET /api/v1/me
      • README — DELETE /api/v1/me
      • README — GET /api/v1/me/policy
      • README — GET /api/v1/me/usage
      • Account API — README
      • Who this credential acts for
      • Delete the account (after a 30-day grace period)
      • What your teams' and classes' policies allow
      • Your own usage, and what your teams and classes counted
    • Documents
      • README — GET /api/v1/documents
      • README — POST /api/v1/documents
      • README — GET /api/v1/documents/search
      • README — GET /api/v1/documents/{id}
      • README — PATCH /api/v1/documents/{id}
      • README — DELETE /api/v1/documents/{id}
      • README — GET /api/v1/documents/{id}/content
      • README — PUT /api/v1/documents/{id}/content
      • README — POST /api/v1/documents/{id}/live
      • README — GET /api/v1/export
      • Documents API — README
      • List your documents (or shared, or changes since)
      • Create a document
      • Search your own and shared documents
      • A document's metadata
      • Change title, text, content type or language
      • Delete a document (owner)
      • The document's text
      • Replace the text
      • Join the document's live co-editing room
      • Every document you own, with its text
    • Files
      • README — GET /api/v1/documents/{id}/file
      • README — POST /api/v1/documents/{id}/file
      • README — POST /api/v1/documents/{id}/file/commit
      • README — POST /api/v1/documents/{id}/extract
      • Files API — README
      • Download the original file
      • Start uploading the original file
      • Make an uploaded file the document's file
      • Replace the text with the file's text
    • Audio
      • README — GET /api/v1/documents/{id}/audio
      • README — POST /api/v1/audio/stream
      • README — POST /api/v1/audio/speech
      • README — GET /api/v1/voices
      • README — GET /api/v1/quota
      • Audio API — README
      • The whole document as one audio file
      • Stream speech as it's made
      • Speech with word timings (read-along)
      • Voices you can use
      • Characters left, limit, plan and reset time
    • Sharing
      • README — GET /api/v1/documents/{id}/permissions
      • README — POST /api/v1/documents/{id}/permissions
      • README — PATCH /api/v1/documents/{id}/permissions/{principal}
      • README — DELETE /api/v1/documents/{id}/permissions/{principal}
      • README — DELETE /api/v1/documents/{id}/invitations/{inviteId}
      • README — GET /api/v1/invitations
      • README — POST /api/v1/invitations/{id}/accept
      • README — POST /api/v1/invitations/{id}/decline
      • Sharing API — README
      • Who a document is shared with
      • Share by principal, or invite by email (owner)
      • Change someone's role (owner)
      • Remove access (owner, or leave yourself)
      • Cancel a pending invitation (owner)
      • Document invitations to your verified email
      • Accept a document invitation
      • Decline a document invitation
    • Settings
      • README — GET /api/v1/settings
      • README — PATCH /api/v1/settings
      • Settings API — README
      • Synced app and reader settings
      • Change settings (per key, the newer updatedAt wins)
    • Webhooks
      • Incoming webhook delivery — README
      • README — GET /api/v1/webhooks
      • README — POST /api/v1/webhooks
      • README — GET /api/v1/webhooks/{id}
      • README — DELETE /api/v1/webhooks/{id}
      • Webhooks API — README
      • Your webhook endpoints
      • Register an endpoint (up to 10)
      • One endpoint
      • Delete an endpoint (queued retries are dropped)
    • Meta
      • README — GET /api/v1/openapi.json
      • Meta API — README
      • This OpenAPI description
    • OAuth
      • README — POST /api/oauth/token
      • README — POST /api/oauth/revoke
      • OAuth API — README
      • Exchange a code, or refresh
      • Revoke a token (RFC 7009)
    • Live socket
      • Live document socket — README
    • A document or sharing event
  1. Handwrite Regular Accounts

API specification notes

HTTP contract#

Base URL: https://handwrites.io/api/v1. OAuth token and revocation endpoints use https://handwrites.io/api/oauth. The downloadable specification is OpenAPI 3.1, API version 1.0.0.
Send Authorization: Bearer <token> on authenticated operations. JSON bodies need Content-Type: application/json; text replacement uses text/plain or text/markdown; OAuth uses application/x-www-form-urlencoded. A 204 response has no JSON body. File and whole-document audio requests redirect by default; ?redirect=false returns a signed link as JSON.
Personal keys (hw_…) and fine-grained tokens (hwf_…) act for their creator. OAuth access tokens (hwo_…) act for the person who granted consent. Scope checks and document access both apply. Fine-grained tokens expire within 30 days and can be limited to 1–50 chosen documents. A document-limited token cannot register webhooks. Organisation policy can further restrict credentials and features.
GET /me needs authentication but no named scope. DELETE /me is a session-only app operation: personal keys and OAuth tokens cannot delete an account. It returns 202 with a scheduled purge time, disables the account and revokes sign-in access immediately. Data is purged after 30 days unless support restores the account. An outside integration should direct the user to Handwrite account controls.

Errors and retries#

REST errors have the shape:
{"error":{"code":"not_found","message":"Document not found."}}
OAuth token endpoints instead return OAuth fields such as error and error_description.
StatusClient action
400Correct the body, content type or parameter.
401Acquire a valid credential; do not retry a revoked token forever.
402A plan or entitlement is required; show the relevant account action.
403Check scopes, document role and effective organisation policy. account_deleted stops account activity.
404Missing or inaccessible resource; do not infer whether another user's document exists.
409Reconcile conflict; live_session_active means use the socket workflow for text. For idempotency_in_progress, honor Retry-After.
412If-Match is stale. Fetch the latest document and reconcile the local edit.
413Reduce text, file or audio size as the operation requires.
422Fix a semantic error such as unreadable file, empty text or reused idempotency key.
429Distinguish rate_limited from quota_exceeded; honor Retry-After where present.
500/502/503Preserve the user's work. Retry with bounded backoff only when the operation is safe to repeat.
Authenticated responses normally include Request-Id, Handwrite-Requests-Limit and Handwrite-Requests-Remaining. An early authentication or edge rejection may not include every header. Default daily limit: 20,000 requests per key, or account for app sessions, resetting at midnight UTC. Bursts are approximately 120 requests with 10/second refill. The edge limit is 2,000 API requests per five minutes per IP. Use returned limits rather than assuming every account has the default.
The current source stores and reports the organisation dailyRequests policy value, but the public request counter uses the global daily cap rather than that policy value. Do not treat a returned dailyRequests setting as an enforced per-organisation request budget. Monthly audio policy limits are a separate control.
Only operations declaring Idempotency-Key support it: in this edition, document creation, upload start, upload commit and sharing. Use a UUID for each logical action. For a retry keep the same credential, method, path/query, If-Match and exact body. Successful JSON responses up to 64 KiB are stored for 24 hours. Replay returns Idempotent-Replayed: true; a different request under that key gives 422 idempotency_mismatch. Do not blindly replay an OAuth refresh or a non-idempotent mutation after an uncertain outcome.

Revisions, sync and pagination#

Document metadata includes revision, contentSha256, contentLength, createdAt and updatedAt. Text is separate at /documents/{id}/content. Where supported, send the quoted revision ETag in If-Match to avoid overwriting someone else's edit. A 412 requires reconciliation, not an unconditional overwrite.
List pages have an opaque cursor; pass it unchanged until null. Default page size is 50, maximum 100. Ordinary document listing is newest first. Incremental listing with since=<ISO timestamp> is oldest first and includes deletion tombstones for 30 days. Keep a sync checkpoint only after every page is successfully processed. Deduplicate by document ID and revision; after a long offline period, rebuild the accessible inventory rather than expecting expired tombstones. shared=true lists shared documents; normal listing covers owned documents.
Search indexes titles and the first 50 KB of text, accepts up to eight terms, ignores case/accents and supports match=all|any. It is not full-text indexing of arbitrarily large files. Export includes owned documents with text, not original file bytes; settings are included only with settings:read.

Files and extraction#

Text limit: 4 MB. File upload limit: 50,000,000 bytes. Start an upload, PUT the bytes to the returned URL using exactly the returned headers, then commit its uploadId and matching file metadata. Never send your API credential to the storage URL. Upload links last 900 seconds; original-file download links last 300 seconds. Treat them as bearer access to the file.
Supported file types are enumerated in the API schema: PDF, PNG, JPEG, HEIC, WebP, TIFF, plain text, Markdown and DOCX. Acceptance as an original file does not guarantee text extraction. Extraction supports text/Markdown, DOCX paragraphs, text-layer PDFs up to 500 pages, and OCR for images and single-page scans up to 10 MB. HEIC and multi-page scanned PDFs are not supported by this extraction path. A commit may succeed while its extracted.error reports failure; retain the file and offer a manual text correction path. Explicit extraction replaces text and should use If-Match.

Sharing and notifications#

Only an owner grants or changes access; document roles are viewer and editor. The owner is implicit. Principal values must be URL encoded in paths. Email sharing always returns a pending invitation (202), whether the address already has an account or not. Invitations expire 14 days after the last invite and require the matching verified email to accept. Do not use email sharing as account lookup. The current implementation does not send invitation email; recipients find the invitation after signing in with that verified address.
Webhooks are server-to-server HTTPS, up to 10 endpoints. Verify Handwrite-Signature over the exact raw body before parsing: HMAC-SHA256 of <t>.<body> with the endpoint secret. Compare signatures in constant time and reject timestamps outside a five-minute window. Deduplicate Handwrite-Event-Id. Return 2xx within 10 seconds and process durable work asynchronously. There are eight delivery attempts total, with exponential delays of approximately 1, 2, 4, 8, 16, 32 and 64 minutes; redirects are failures. Recover missed notifications through document sync rather than treating webhooks as the sole source of truth.

Audio and app preferences#

Choose an actual voice ID from /voices. /audio/stream returns binary audio, up to 12,000 characters per response, and accepts text up to 200,000 characters. Continue at Handwrite-Next-Offset while present. /audio/speech returns base64 audio and word timings: explicit text is limited to 1,500 characters; document requests use chunks of about 1,400. Continue with nextOffset until null. Offsets and word indices are UTF-16 code units, not bytes. Timings are seconds relative to that audio chunk.
Whole-document audio is limited to 30,000 characters and cached for 30 days by the generated variant and document revision. An uncached generation consumes characters; an existing cached result is free. Read /quota and handle quota failure. Supplying a document ID for speech also requires documents:read and document access.
Settings use a version ETag for reads and per-key last-writer-wins timestamps for writes. PATCH only changed keys; inspect applied and kept in the response. Unknown, local-only and policy-locked settings must not be silently retried. Device storage, local sync switches and app lock settings are not remotely configurable through this endpoint.

OAuth for third-party apps#

Register an app at Account → Developer. Use a confidential client only on a server that can keep its secret; use a public client with PKCE for native/distributed apps. Authorization is at https://handwrites.io/oauth/authorize, with response_type=code, client_id, exact registered redirect_uri, space-separated scope, random state, S256 code_challenge and code_challenge_method=S256.
Check callback state and iss=https://handwrites.io before exchanging the code. Codes last 60 seconds and are single-use. Access tokens last 3,600 seconds. Refresh tokens rotate on every use, expire after 30 idle days, and cannot extend the authorization beyond 365 days. Serialize refreshes and atomically store the replacement token: replaying a used refresh token revokes the whole authorization. There is no implicit, password or client-credentials grant for personal data. Revoking a refresh token revokes its authorization; revoking an access token revokes that token.

Scope of this edition#

The Regular Accounts spec includes personal API routes plus the live-session admission route, which has additional plan eligibility. It omits /api/v1/orgs/**, internal worker callbacks, staff administration and same-origin portal endpoints. The unauthenticated /openapi.json endpoint returns the full upstream spec, not this filtered edition. Machine-readable schemas describe the current contract; use the integration notes for additional policy/plan failures not exhaustively enumerated in every operation response list.

Webhook compatibility snapshot — 2026-10-26#

The published OpenAPI still lists personal webhook registration. Current working-tree code instead rejects new personal receivers with 403 webhooks_moved; new receivers belong to teams/classes and are managed by an administrator in the organisation Developer tab. Existing personal receivers continue to receive events in that code. Deployment of this change and live runtime behavior were not verified here. Treat the registration reference as the published contract, not proof that new personal registration is available. The Team and Classroom editions describe the organisation workflow.
Modified at 2026-10-26 13:23:58
Previous
Start here — Handwrite for outside developers
Next
Socket specification notes
Built with