DraftFilterDeveloper API

Developer reference

Synchronous Scan API

Synchronous scans are the interactive hot path: submit one text and receive the document score, band, confidence, and per-sentence results in a single response. 0 means 0% AI-like, and 100 means 100% AI-like. Sentence scores are computed in document context, and every scan runs at sentence granularity.

Create a scan:

text
POST /v1/scans

Request body:

json
{
  "text": "The onboarding checklist now includes an owner for every launch task. A shorter release window should reduce customer support handoffs."
}

Scan request limits from the OpenAPI contract:

text
text: per-scan character cap by plan (over the cap: 413 scan_input_too_large)
free plan: at most 15,000 characters; paid plans: at most 50,000 characters
anonymous App Check scans: at most 5,000 characters and 5 scans per day

Texts over the plan's per-scan character cap (15,000 on the free plan, 50,000 on paid plans) are rejected with 413 scan_input_too_large; use the asynchronous score job API (POST /v1/score-jobs) for longer documents or batches. Scans accept the same credentials as score jobs — API keys, Firebase ID tokens, and anonymous App Check tokens — and require the score_jobs:create scope. Scan input counts against the same quotas as score jobs: the anonymous daily scan quota and the free-plan monthly word allowance.

Successful response:

json
{
  "scanId": "scan_000301",
  "is_ai": 47,
  "band": "mixed",
  "confidence": 0.62,
  "sentences": [
    {
      "text": "The onboarding checklist now includes an owner for every launch task.",
      "start": 0,
      "end": 70,
      "is_ai": 22,
      "band": "likely_human"
    },
    {
      "text": "A shorter release window should reduce customer support handoffs.",
      "start": 71,
      "end": 137,
      "is_ai": 68,
      "band": "likely_ai"
    }
  ],
  "createdAt": "2026-07-06T18:00:00Z"
}

Sentence granularity returns submitted sentence text with exact start/end offsets into the submitted text. The most AI-like sentences carry the likely_ai band.

If the scan backend is temporarily unreachable, the API responds with 503 scan_backend_unavailable; retry after a few seconds or fall back to the asynchronous score job API. Synchronous scans have no job lifecycle — there is nothing to poll or cancel.

Shared Scan Results

Paid dashboard accounts can create read-only public links for stored scan results from dashboard history. Share creation is handled by the internal dashboard API and requires a Plus or Pro plan. Firestore remains deny-all for direct clients; public reads go through the API.

Read a shared scan:

text
GET /v1/shared/{shareId}

The shareId is an unguessable capability identifier such as share_0123456789abcdef0123456789abcdef. This endpoint does not require an API key, Firebase ID token, or App Check token.

Successful response:

json
{
  "shareId": "share_0123456789abcdef0123456789abcdef",
  "is_ai": 47,
  "band": "mixed",
  "confidence": 0.62,
  "sentences": [
    {
      "text": "The onboarding checklist now includes an owner for every launch task.",
      "start": 0,
      "end": 70,
      "is_ai": 22,
      "band": "likely_human"
    }
  ],
  "createdAt": "2026-07-06T18:00:00Z"
}

When the owner enables text redaction, sentence text fields are omitted but offsets, scores, and bands remain so clients can render a shape-only heatmap. Revoked shares and unknown share IDs both return the same 404 shared_scan_not_found response. There is no published request-rate quota for this unauthenticated endpoint; capability unguessability, uniform 404s, and revocation are the primary controls.

File Upload Scans

Pro accounts can upload a PDF, DOCX, or TXT file and scan the server-extracted text. The raw file is retained in the API artifacts bucket at uploads/{uploadId}. Extracted text and the scan or score result are persisted through the normal scans or score_jobs collections.

Create an upload:

text
POST /v1/uploads
json
{
  "filename": "draft.pdf",
  "contentType": "application/pdf",
  "sizeBytes": 4096
}

Successful response:

json
{
  "uploadId": "upl_000301",
  "signedUrl": "https://storage.googleapis.com/draftfilter-dev-api-artifacts/uploads/upl_000301?...",
  "expiresAt": "2026-07-06T18:15:00Z"
}

Upload the file bytes to signedUrl with PUT. Use the same Content-Type sent in the create request and include the signed x-goog-content-length-range: 0,10485760 header. The API also re-checks the stored object size before extraction.

Trigger extraction and scoring:

text
POST /v1/uploads/{uploadId}/scan

By default, small extracted files use the synchronous scan path and larger files queue a sentence-granularity score job. Batch clients can force every upload through the async job path:

json
{
  "forceAsync": true
}

For extracted text up to 15,000 characters, the API returns the synchronous scan result plus pages when the source was a PDF:

json
{
  "uploadId": "upl_000301",
  "status": "scanned",
  "pages": [
    {
      "page": 1,
      "start": 0,
      "end": 70
    }
  ],
  "scanId": "scan_000301",
  "is_ai": 47,
  "band": "mixed",
  "confidence": 0.62,
  "sentences": [
    {
      "text": "The onboarding checklist now includes an owner for every launch task.",
      "start": 0,
      "end": 70,
      "is_ai": 22,
      "band": "likely_human"
    }
  ],
  "createdAt": "2026-07-06T18:00:00Z"
}

For extracted text over 15,000 characters, or when forceAsync is true, file scans create a sentence-granularity score job instead of returning 413 scan_input_too_large:

json
{
  "uploadId": "upl_000302",
  "status": "queued",
  "jobId": "job_000302",
  "state": "queued",
  "pages": []
}

PDF pages are one-based and use start/end offsets into the extracted text, so clients can group sentence heatmaps by page. DOCX and TXT uploads return an empty pages list.

Failure codes:

text
402 upgrade_required: account is not on Pro
413 file_too_large: file is over 10 MB
422 file_unsupported: not PDF, DOCX, or TXT
422 file_encrypted: encrypted PDF
422 file_too_complex: DOCX expands too large, PDF has too many pages, or extraction times out
422 file_no_text: scanned/image-only file or empty extraction

Share Links

Completed scans you own can be published as read-only share links and managed entirely via the API (scope shares:manage; requires a Plus or Pro plan; these are the same endpoints the dashboard uses):

text
POST   /v1/scans/{scanId}/share    create (or return the existing active share)
PATCH  /v1/scans/{scanId}/share    update text redaction: {"redactText": true}
DELETE /v1/scans/{scanId}/share    revoke the link
GET    /v1/shared/{shareId}        public, unauthenticated read
json
{
  "share": {
    "shareId": "share_9f2a41c07b5e4d33a1b2c3d4e5f60718",
    "scanId": "scan_000042",
    "redactText": false,
    "revoked": false,
    "createdAt": "2026-07-08T00:00:00Z",
    "updatedAt": "2026-07-08T00:00:00Z"
  }
}

With redactText enabled the public share keeps sentence offsets, scores, and bands but omits sentence text. Revoking a share makes the public link return 404 shared_scan_not_found; revocation works on any plan so downgraded owners can always kill an existing public link. Free-plan share creation and redaction updates return 402 upgrade_required.