LLM Guides

How to Detect Plagiarism Using JavaScript

Aug 03, 2026

Plagiarism detection sounds simple until you try to build it.

You take a chunk of text. You compare it against other text. You return a similarity score. Done, right?

Then real content walks in wearing muddy shoes.

A student changes every third word. A blog post copies three paragraphs from five different articles. A freelancer paraphrases just enough to dodge exact matching. A product description gets reused across 40 ecommerce pages. Someone pastes AI-generated text that includes suspiciously familiar phrasing from public sources. Someone uploads a PDF instead of plain text, because of course they do.

So yes, you can detect plagiarism using JavaScript.

But a useful plagiarism checker needs more than one string comparison function. You usually need an API-powered workflow, text preprocessing, similarity scoring, source matching, report handling, thresholds, and review logic.

In this guide, we’ll build a simple JavaScript workflow for checking copied or reused content. We’ll use a plagiarism detection API for the heavy lifting, then add local similarity checks, validation, and a cleaner backend structure your app can actually use.

What does plagiarism detection mean?

Plagiarism detection means checking whether text matches, copies, closely paraphrases, or reuses content from another source without proper attribution.

That can include:

TypeExample
Exact copyingCopy-pasting a paragraph from a website
PatchwritingChanging small parts of copied text
Paraphrased plagiarismRewriting the same idea in different words
Source mosaicCombining copied sections from several places
Self-plagiarismReusing your own old content without disclosure
Code plagiarismReusing code with small changes
AI-assisted rewritingUsing AI to rewrite copied content
Duplicate SEO contentReusing pages across domains or product listings

A basic checker can catch exact matches.

A stronger checker can find paraphrased or semantically similar content too.

That difference matters because plagiarism is not always a clean copy-paste job. Recent research agrees with that. A 2025 PLOS One review of text-based plagiarism detection techniques analyzed 189 papers from 2019 to 2024 and showed how modern plagiarism detection spans lexical, semantic, stylometric, and machine learning approaches. Another 2025 Frontiers survey on plagiarism types and detection methods also breaks plagiarism into different forms, including literal copying, obfuscation, paraphrasing, and source-code similarity.

The quick lesson: one similarity score is rarely enough.

What are we building?

We’ll build a Node.js backend that can:

  1. Accept text from a user.
  2. Validate the input.
  3. Run a simple local similarity check.
  4. Send the text to a plagiarism checker API.
  5. Normalize the result.
  6. Return a clean response to the frontend.
  7. Flag content that needs review.

The workflow looks like this:

user text
→ JavaScript backend
→ local similarity checks
→ plagiarism API
→ normalized report
→ review/pass decision

For the API layer, we’ll show a Copyleaks-style workflow because Copyleaks API docs include JavaScript SDK support and plagiarism detection endpoints. We’ll also talk about alternatives like PlagiarismCheck.org for developers and Originality.ai API, because your best provider depends on whether you’re building for education, publishing, SEO, or content operations.

Why use JavaScript for plagiarism detection?

JavaScript is a good fit when plagiarism checking lives inside a web app.

You can use it for:

  1. Backend APIs with Node.js.
  2. CMS originality checks.
  3. LMS assignment workflows.
  4. SEO content review.
  5. Blog publishing tools.
  6. Marketplace listing moderation.
  7. Internal writing QA.
  8. Document upload workflows.
  9. Editor plugins.
  10. Batch content checks.

A browser-only plagiarism checker is usually not enough because API keys, document storage, and paid scanning should stay on the backend.

A safer architecture looks like this:

frontend
→ your Node.js backend
→ plagiarism provider API
→ normalized report
→ frontend result

Keep the provider API key on the server. Do not ship it to the browser. Future-you does not need that security headache.

Why we can write this guide

We’ve spent around 6 years working with AI APIs, NLP workflows, text similarity, document parsing, content automation, and developer tutorials. We also checked current provider documentation for Copyleaks, PlagiarismCheck.org, and Originality.ai, plus recent research on semantic plagiarism detection.

The practical lesson is simple: plagiarism detection works best as a layered workflow.

Use local similarity checks for quick comparisons against your own known content. Use a plagiarism API to search wider web, academic, or internal databases. Use semantic similarity when paraphrasing matters. Use LLMAPI after detection when you need summaries, review notes, routing, or human-readable explanations.

That gives you a tool that helps reviewers act, not just stare at a percentage and guess what it means.

Quick note: similarity is not the same as plagiarism

This matters.

A high similarity score can mean plagiarism.

It can also mean:

  1. Properly quoted text.
  2. Public domain text.
  3. Legal boilerplate.
  4. Product specs.
  5. Standard definitions.
  6. Common phrases.
  7. Citations and references.
  8. Template language.
  9. Reused brand copy.
  10. Assignment prompt text.

So your app should avoid saying:

This is plagiarized.

A safer label:

This content has high similarity and needs review.

That language is better for education, publishing, SEO, and compliance workflows.

The checker finds evidence. A person or policy decides what it means.

Step 1: Create a Node.js project

Create a new project:

mkdir plagiarism-checker-js
cd plagiarism-checker-js
npm init -y

Install dependencies:

npm install express dotenv axios zod string-similarity

We’ll use:

PackageWhy
expressBuild the backend API
dotenvLoad environment variables
axiosCall external plagiarism APIs
zodValidate request bodies and responses
string-similarityRun quick local similarity checks

Add this to package.json:

{
  "type": "module"
}

Create .env:

PORT=3000
[email protected]
COPYLEAKS_API_KEY=your_copyleaks_api_key

Use the provider variables for whichever API you pick.

Step 2: Build a basic Express server

Create server.js:

import express from "express";
import dotenv from "dotenv";

dotenv.config();

const app = express();

app.use(express.json({
  limit: "1mb"
}));

app.get("/health", (req, res) => {
  res.json({
    status: "ok"
  });
});

const port = process.env.PORT || 3000;

app.listen(port, () => {
  console.log(`Plagiarism checker API running on http://localhost:${port}`);
});

Run:

node server.js

Test:

curl http://localhost:3000/health

Expected response:

{
  "status": "ok"
}

Now we have the backend shell.

Step 3: Validate incoming text

Plagiarism checking costs money and time, so validate text before sending it anywhere.

Create schemas.js:

import { z } from "zod";

export const PlagiarismCheckRequestSchema = z.object({
  text: z
    .string()
    .min(100, "Text must be at least 100 characters.")
    .max(50000, "Text is too long for this endpoint."),
  title: z
    .string()
    .max(200)
    .optional(),
  language: z
    .string()
    .default("en"),
  checkAgainstLocalSources: z
    .boolean()
    .default(true)
});

Then add a helper:

export function validateRequest(schema, body) {
  const result = schema.safeParse(body);

  if (!result.success) {
    return {
      ok: false,
      errors: result.error.issues.map((issue) => ({
        path: issue.path.join("."),
        message: issue.message
      }))
    };
  }

  return {
    ok: true,
    data: result.data
  };
}

This prevents weird requests from reaching the API provider.

Step 4: Add a quick local similarity check

Before using a paid plagiarism API, you may want to compare user text against your own known content.

Examples:

  1. Existing blog posts.
  2. Old assignments.
  3. Product pages.
  4. Internal documentation.
  5. Previously submitted essays.
  6. Marketplace listings.

This catches obvious reuse inside your own system.

Create localSimilarity.js:

import stringSimilarity from "string-similarity";

const localSources = [
  {
    id: "blog_001",
    title: "How to Build an AI Backend",
    text: "Building an AI backend requires routing, validation, logging, fallback, and reliable model calls."
  },
  {
    id: "docs_042",
    title: "Refund Policy",
    text: "Customers can request refunds within 30 days if the product has not been used."
  }
];

export function checkLocalSimilarity(inputText) {
  const normalizedInput = normalizeText(inputText);

  const results = localSources.map((source) => {
    const score = stringSimilarity.compareTwoStrings(
      normalizedInput,
      normalizeText(source.text)
    );

    return {
      source_id: source.id,
      title: source.title,
      similarity_score: Number(score.toFixed(4))
    };
  });

  return results.sort((a, b) => b.similarity_score - a.similarity_score);
}

function normalizeText(text) {
  return text
    .toLowerCase()
    .replace(/\s+/g, " ")
    .replace(/[^\p{L}\p{N}\s]/gu, "")
    .trim();
}

The string-similarity package uses Dice’s coefficient to compare strings. It is useful for quick local checks, but it should not be your whole plagiarism system.

Why?

Because surface similarity can miss paraphrasing.

Example:

Original: The app reduces manual editing work by removing image backgrounds automatically.
Paraphrase: The tool cuts down editing time by automatically deleting backgrounds from images.

These are similar in meaning, but not identical in wording.

That is why API-powered and semantic approaches matter.

Step 5: Create the local check endpoint

Update server.js:

import express from "express";
import dotenv from "dotenv";
import { PlagiarismCheckRequestSchema, validateRequest } from "./schemas.js";
import { checkLocalSimilarity } from "./localSimilarity.js";

dotenv.config();

const app = express();

app.use(express.json({
  limit: "1mb"
}));

app.get("/health", (req, res) => {
  res.json({
    status: "ok"
  });
});

app.post("/check-local", (req, res) => {
  const validation = validateRequest(
    PlagiarismCheckRequestSchema,
    req.body
  );

  if (!validation.ok) {
    return res.status(400).json({
      error: "Invalid request.",
      details: validation.errors
    });
  }

  const localMatches = checkLocalSimilarity(validation.data.text);

  return res.json({
    status: "success",
    local_matches: localMatches.slice(0, 5)
  });
});

const port = process.env.PORT || 3000;

app.listen(port, () => {
  console.log(`Plagiarism checker API running on http://localhost:${port}`);
});

Test:

curl -X POST http://localhost:3000/check-local \
  -H "Content-Type: application/json" \
  -d '{"text":"Building an AI backend requires routing, validation, logging, fallback, and reliable model calls. This paragraph is long enough for the validation rule."}'

This gives you a fast local signal before doing a wider scan.

Step 6: Add Copyleaks plagiarism checking

For a full plagiarism workflow, you usually want a provider that can compare text against web pages, academic sources, or private repositories.

Copyleaks is one option. Its Plagiarism Checker API docs describe checks across web pages, academic journals, and your own document libraries. The Copyleaks JavaScript SDK quickstart also shows JavaScript/TypeScript usage through the plagiarism-checker package.

Install the SDK:

npm install plagiarism-checker

Create copyleaksClient.js:

import {
  Copyleaks,
  CopyleaksFileSubmissionModel
} from "plagiarism-checker";

const copyleaks = new Copyleaks();

let cachedToken = null;

export async function loginToCopyleaks() {
  if (cachedToken) {
    return cachedToken;
  }

  cachedToken = await copyleaks.loginAsync(
    process.env.COPYLEAKS_EMAIL,
    process.env.COPYLEAKS_API_KEY
  );

  return cachedToken;
}

export async function submitTextToCopyleaks({
  scanId,
  text,
  title = "Untitled document"
}) {
  const token = await loginToCopyleaks();

  const base64Text = Buffer.from(text, "utf8").toString("base64");

  const submission = new CopyleaksFileSubmissionModel(
    base64Text,
    title
  );

  submission.properties.setWebhooks({
    status: `${process.env.PUBLIC_WEBHOOK_URL}/webhooks/copyleaks/status/{STATUS}/scan/${scanId}`,
    newResult: `${process.env.PUBLIC_WEBHOOK_URL}/webhooks/copyleaks/result/scan/${scanId}`,
    completed: `${process.env.PUBLIC_WEBHOOK_URL}/webhooks/copyleaks/completed/scan/${scanId}`
  });

  await copyleaks.submitFileAsync(
    token,
    scanId,
    submission
  );

  return {
    scan_id: scanId,
    status: "submitted"
  };
}

A quick note: Copyleaks plagiarism scans are commonly asynchronous. You submit a scan, then receive webhooks or fetch/export results after processing.

That means your app should think in jobs:

submit scan
→ store scan ID
→ wait for webhook
→ fetch/export report
→ show result

This is different from a tiny “send text, get score instantly” endpoint.

Step 7: Create a scan submission endpoint

Install UUID:

npm install uuid

Update server.js:

import { v4 as uuidv4 } from "uuid";
import { submitTextToCopyleaks } from "./copyleaksClient.js";

Add the route:

app.post("/check-plagiarism", async (req, res) => {
  const validation = validateRequest(
    PlagiarismCheckRequestSchema,
    req.body
  );

  if (!validation.ok) {
    return res.status(400).json({
      error: "Invalid request.",
      details: validation.errors
    });
  }

  try {
    const { text, title, checkAgainstLocalSources } = validation.data;

    const scanId = `scan-${uuidv4()}`;

    const localMatches = checkAgainstLocalSources
      ? checkLocalSimilarity(text).slice(0, 5)
      : [];

    const providerSubmission = await submitTextToCopyleaks({
      scanId,
      text,
      title: title || "Submitted text"
    });

    return res.json({
      status: "submitted",
      scan_id: scanId,
      local_matches: localMatches,
      provider: "copyleaks",
      provider_submission: providerSubmission,
      message: "Plagiarism scan submitted. Wait for webhook or poll status."
    });
  } catch (error) {
    return res.status(502).json({
      error: "Plagiarism provider request failed.",
      details: error.message
    });
  }
});

Now your app can submit a text scan.

Step 8: Add webhook endpoints

Your provider needs a public URL to send scan results.

For local testing, you can use a tunnel like ngrok or a deployment preview.

Add simple webhook routes:

app.post("/webhooks/copyleaks/status/:status/scan/:scanId", (req, res) => {
  const { status, scanId } = req.params;

  console.log("Copyleaks status webhook:", {
    scanId,
    status,
    body: req.body
  });

  res.sendStatus(200);
});

app.post("/webhooks/copyleaks/result/scan/:scanId", (req, res) => {
  const { scanId } = req.params;

  console.log("Copyleaks new result webhook:", {
    scanId,
    body: req.body
  });

  res.sendStatus(200);
});

app.post("/webhooks/copyleaks/completed/scan/:scanId", (req, res) => {
  const { scanId } = req.params;

  console.log("Copyleaks completed webhook:", {
    scanId,
    body: req.body
  });

  res.sendStatus(200);
});

In production, do more than console.log.

Store scan status in your database:

{
  "scan_id": "scan-123",
  "status": "completed",
  "provider": "copyleaks",
  "created_at": "2026-08-17T18:00:00Z",
  "completed_at": "2026-08-17T18:02:12Z"
}

You’ll also want to verify webhook signatures or provider authentication if available. Do not trust random internet POST requests.

Step 9: Normalize the plagiarism report

Every provider returns reports differently.

Your app should normalize results into one schema.

Example normalized output:

{
  "scan_id": "scan-123",
  "status": "completed",
  "similarity_score": 0.34,
  "risk_label": "review_recommended",
  "matches": [
    {
      "source_url": "https://example.com/article",
      "source_title": "Example Article",
      "matched_percent": 0.18,
      "matched_text": "Copied or similar text snippet...",
      "source_type": "web"
    }
  ],
  "recommendation": "Review the matched sources before publishing."
}

Create normalizeReport.js:

export function normalizePlagiarismReport({
  scanId,
  provider,
  providerReport,
  localMatches = []
}) {
  const providerMatches = extractProviderMatches(providerReport);

  const similarityScore = calculateOverallSimilarity({
    providerReport,
    providerMatches,
    localMatches
  });

  return {
    scan_id: scanId,
    provider,
    status: "completed",
    similarity_score: similarityScore,
    risk_label: getRiskLabel(similarityScore),
    local_matches: localMatches,
    matches: providerMatches,
    recommendation: getRecommendation(similarityScore)
  };
}

function extractProviderMatches(providerReport) {
  const results = providerReport?.results || providerReport?.matches || [];

  return results.map((item) => ({
    source_url: item.url || item.sourceUrl || null,
    source_title: item.title || item.sourceTitle || null,
    matched_percent: item.matchedPercent || item.score || null,
    matched_text: item.matchedText || item.text || null,
    source_type: item.type || "unknown"
  }));
}

function calculateOverallSimilarity({
  providerReport,
  providerMatches,
  localMatches
}) {
  const providerScore =
    providerReport?.similarityScore ??
    providerReport?.score ??
    maxMatchScore(providerMatches);

  const localScore = maxMatchScore(localMatches);

  const maxScore = Math.max(
    Number(providerScore || 0),
    Number(localScore || 0)
  );

  return Number(maxScore.toFixed(4));
}

function maxMatchScore(matches) {
  if (!matches || matches.length === 0) {
    return 0;
  }

  return Math.max(
    ...matches.map((match) =>
      Number(match.similarity_score || match.matched_percent || 0)
    )
  );
}

function getRiskLabel(score) {
  if (score >= 0.5) {
    return "high_similarity_review_required";
  }

  if (score >= 0.25) {
    return "review_recommended";
  }

  return "low_similarity_detected";
}

function getRecommendation(score) {
  if (score >= 0.5) {
    return "High similarity detected. Review matched sources before accepting or publishing."
  }

  if (score >= 0.25) {
    return "Some similarity detected. Manual review is recommended."
  }

  return "No strong similarity signal found. This is not proof of originality."
}

This normalizer is intentionally generic.

Once you choose a provider, update the mappings to match that provider’s real response exactly.

Step 10: Add a status endpoint

Your frontend needs to ask:

Is my plagiarism report ready yet?

Create a simple in-memory store for demo purposes.

In production, use a database.

const scans = new Map();

function saveScan(scanId, data) {
  scans.set(scanId, {
    ...scans.get(scanId),
    ...data,
    updated_at: new Date().toISOString()
  });
}

function getScan(scanId) {
  return scans.get(scanId);
}

When submitting:

saveScan(scanId, {
  scan_id: scanId,
  status: "submitted",
  provider: "copyleaks",
  local_matches: localMatches,
  created_at: new Date().toISOString()
});

Add endpoint:

app.get("/check-plagiarism/:scanId", (req, res) => {
  const scan = getScan(req.params.scanId);

  if (!scan) {
    return res.status(404).json({
      error: "Scan not found."
    });
  }

  return res.json(scan);
});

Now your frontend can poll:

GET /check-plagiarism/scan-123

For production, prefer webhooks plus database updates, then polling or WebSockets for frontend status.

Step 11: Add direct API support for other providers

Copyleaks is not the only option.

PlagiarismCheck.org for developers offers a plagiarism checker API and AI detector API, with examples for sending plain text or files. Originality.ai’s API help page says its API can bring AI detection and plagiarism checking directly into a platform, while its API documentation covers scan workflows.

A generic provider wrapper can look like this:

import axios from "axios";

export async function submitToGenericPlagiarismApi({
  endpoint,
  apiKey,
  text,
  title
}) {
  const response = await axios.post(
    endpoint,
    {
      text,
      title
    },
    {
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json"
      },
      timeout: 60000
    }
  );

  return response.data;
}

Then normalize the response into your internal schema.

The pattern stays the same:

provider-specific request
→ provider-specific response
→ your normalized report

Do not let provider JSON leak all over your frontend. It gets ugly fast.

API providers to consider

Here is the practical shortlist.

ProviderBest fit
CopyleaksPlagiarism, AI detection, code/content authenticity workflows
PlagiarismCheck.orgEducation, LMS workflows, plagiarism + AI detection
Originality.aiSEO, publishing, AI detection, plagiarism, readability workflows
Custom local similarityInternal duplicate checks against your own content
Embedding-based searchSemantic reuse and paraphrase-style similarity
LLMAPIReview summaries, routing, explanation, and workflow automation

Use the provider that matches your product.

An LMS assignment checker, SEO publishing platform, and marketplace moderation tool will not have the same needs.

Step 12: Add semantic similarity for paraphrasing

Exact matching catches copy-paste.

Semantic similarity helps with paraphrasing.

A 2025 Springer paper on plagiarism detection using BERT and cosine similarity describes a system that combines BERT-based similarity and pairwise comparison for academic submissions. The 2025 PAN plagiarism detection task overview also notes that naive semantic similarity with embedding vectors produced promising recall, though precision remained limited.

That is the tradeoff.

Embeddings can catch meaning-level similarity, but they can also flag innocent similarity.

Example:

“Climate change increases extreme weather risk.”

and

“Global warming raises the chance of severe weather events.”

These are semantically similar. That does not automatically mean plagiarism.

So use semantic similarity as a review signal, not a final accusation.

A practical semantic workflow:

split document into chunks
→ create embeddings
→ search similar chunks in your database
→ compare top matches
→ flag high-similarity sections

This works well for checking against your own content library.

Step 13: Chunk long text before checking

Long documents are harder to analyze as one blob.

Split them into chunks.

export function chunkText(text, maxWords = 250) {
  const words = text.split(/\s+/);
  const chunks = [];

  for (let i = 0; i < words.length; i += maxWords) {
    chunks.push(words.slice(i, i + maxWords).join(" "));
  }

  return chunks;
}

Use chunking for:

  1. Long essays.
  2. Blog posts.
  3. Research articles.
  4. Documentation pages.
  5. Reports.
  6. Books or chapters.

Why chunk?

Because plagiarism may only appear in one section.

A full-document score can hide local copying.

Example:

Whole article similarity: 12%
One paragraph similarity: 91%

That paragraph needs review even if the whole document looks fine.

Step 14: Build a review-focused response

A good plagiarism checker should help humans review.

Return:

  1. Overall risk label.
  2. Matched sources.
  3. Matched snippets.
  4. Local matches.
  5. Chunk-level matches.
  6. Confidence or similarity scores.
  7. Recommendation.
  8. Caveats.

Example:

{
  "status": "completed",
  "risk_label": "review_recommended",
  "overall_similarity": 0.31,
  "summary": "Several sections are similar to public web sources.",
  "matches": [
    {
      "source_title": "How to Build an AI Backend",
      "source_url": "https://example.com/article",
      "matched_percent": 0.27,
      "matched_snippet": "A real AI backend usually needs retries, fallback models..."
    }
  ],
  "recommendation": "Review matched snippets and confirm whether they are quoted, cited, reused, or copied."
}

This is much more useful than:

31% plagiarized

That phrase can be misleading.

Use “similarity” unless a reviewer has confirmed misuse.

Step 15: Add LLMAPI for review summaries

Plagiarism APIs often return technical reports.

LLMAPI can help turn those reports into reviewer-friendly summaries.

Example report input:

{
  "risk_label": "review_recommended",
  "overall_similarity": 0.31,
  "matches": [
    {
      "source_title": "Example Article",
      "matched_percent": 0.27,
      "matched_snippet": "A real AI backend usually needs retries, fallback models..."
    }
  ]
}

LLMAPI can generate:

This submission has moderate similarity to one web source. The strongest match appears in a paragraph about backend retries and fallback models. Review whether the text is quoted, cited, or rewritten enough to be considered original.

Install:

npm install openai

Create llmapiReview.js:

import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.LLMAPI_API_KEY,
  baseURL: process.env.LLMAPI_BASE_URL || "https://api.llmapi.ai/v1"
});

export async function createPlagiarismReviewSummary(report) {
  const response = await client.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      {
        role: "system",
        content: `
You write careful plagiarism review summaries.
Do not accuse the author.
Use terms like "similarity", "matched text", and "needs review".
Do not invent sources or claims.
        `
      },
      {
        role: "user",
        content: JSON.stringify(report)
      }
    ],
    temperature: 0.2
  });

  return response.choices[0].message.content;
}

This is useful for:

  1. Teachers.
  2. Editors.
  3. SEO teams.
  4. Content managers.
  5. Compliance reviewers.
  6. Marketplace moderation teams.

LLMAPI should explain and route. It should not make the final plagiarism judgment by itself.

Where LLMAPI fits

LLMAPI fits after plagiarism detection when your app needs a human-readable workflow layer.

Use it for:

TaskExample
Review summaryExplain why a document needs review
Source explanationSummarize matched sources and snippets
Risk routingSend high-similarity cases to editors
Editor notesDraft careful internal review notes
Student feedbackCreate non-accusatory feedback language
SEO workflowRecommend rewrite/review before publishing
Batch reportsSummarize all flagged content this week
Policy mappingMap similarity results to internal rules
Rewrite supportSuggest how to cite or rewrite matched sections
Appeal reviewSummarize evidence for human review

A practical workflow:

text
→ plagiarism API
→ normalized similarity report
→ LLMAPI review summary
→ human decision

That keeps each piece in its lane.

The plagiarism API finds matches. LLMAPI explains the report. Humans or policy rules make final decisions.

Step 16: Add thresholds carefully

Thresholds depend on your use case.

A 15% match might be normal for academic writing with references.

A 15% match might be suspicious for a short product description.

A 60% match might be fine if the text is a legal template.

So do not blindly use one threshold everywhere.

Example thresholds:

export function classifySimilarity(score, context = "general") {
  if (context === "seo_article") {
    if (score >= 0.25) return "review_required";
    if (score >= 0.10) return "review_recommended";
    return "low_similarity";
  }

  if (context === "student_essay") {
    if (score >= 0.40) return "review_required";
    if (score >= 0.20) return "review_recommended";
    return "low_similarity";
  }

  if (context === "legal_template") {
    if (score >= 0.70) return "review_required";
    if (score >= 0.40) return "review_recommended";
    return "expected_similarity_possible";
  }

  if (score >= 0.50) return "review_required";
  if (score >= 0.25) return "review_recommended";
  return "low_similarity";
}

The right threshold is a product decision.

Test it on your real content.

Step 17: Handle uploaded files

Many plagiarism workflows need files, not just pasted text.

Support formats may include:

  1. .txt
  2. .docx
  3. .pdf
  4. .html
  5. .md
  6. .rtf

Some provider APIs accept files directly. Copyleaks supports file submission flows in its Authenticity API actions, including submitting files, URLs, OCR, and exports.

If you want to extract text yourself, use libraries:

File typeJavaScript library direction
TXTNative fs.readFile
PDFpdf-parse
DOCXmammoth
HTMLcheerio
MarkdownPlain text or parser
Images/OCRProvider OCR or OCR API

Example file upload setup:

npm install multer
import multer from "multer";

const upload = multer({
  dest: "uploads/",
  limits: {
    fileSize: 10 * 1024 * 1024
  }
});

app.post("/check-file", upload.single("file"), async (req, res) => {
  if (!req.file) {
    return res.status(400).json({
      error: "File is required."
    });
  }

  return res.json({
    message: "File uploaded.",
    filename: req.file.originalname,
    path: req.file.path,
    mimetype: req.file.mimetype
  });
});

In production, validate file type, scan uploads, delete temporary files, and never store documents longer than needed.

Step 18: Security and privacy notes

Plagiarism checking often involves sensitive text.

That can include:

  1. Student assignments.
  2. Unpublished articles.
  3. Legal drafts.
  4. Business proposals.
  5. Internal reports.
  6. Customer messages.
  7. Source code.
  8. Research manuscripts.

Add safeguards:

SafeguardWhy it matters
Backend-only API keysProtect provider credentials
File size limitsPrevent abuse
Text length limitsControl cost
User authenticationAvoid anonymous scanning abuse
Rate limitsControl API spend
Temporary file cleanupReduce sensitive storage
Redacted logsAvoid leaking document text
Data retention policyDecide how long reports stay
Access controlsKeep reports private
Provider reviewCheck data handling terms

Also decide whether submitted content becomes part of a provider’s private repository or comparison index. Some tools let you control indexing. Make that setting explicit in your product.

What production output should look like

A good API response should be clear and cautious.

Example:

{
  "scan_id": "scan-87e2",
  "status": "completed",
  "risk_label": "review_recommended",
  "overall_similarity": 0.31,
  "local_matches": [
    {
      "source_id": "blog_001",
      "title": "How to Build an AI Backend",
      "similarity_score": 0.42
    }
  ],
  "external_matches": [
    {
      "source_title": "Example Web Article",
      "source_url": "https://example.com/article",
      "matched_percent": 0.27,
      "matched_snippet": "A real AI backend usually needs retries..."
    }
  ],
  "review_summary": "Moderate similarity detected. Review the matched sections before publishing.",
  "recommendation": "Manual review recommended."
}

Use labels like:

  1. low_similarity_detected
  2. review_recommended
  3. high_similarity_review_required
  4. scan_failed
  5. insufficient_text
  6. provider_error

These are much safer than declaring plagiarism automatically.

Common mistakes

These are the classics.

MistakeBetter approach
Calling every match plagiarismUse “similarity” until reviewed
Relying only on exact matchingAdd semantic/API checks
Ignoring local duplicatesCheck your own database too
No chunk-level analysisSplit long documents
One threshold for every use caseTune by content type
No source snippetsShow reviewers matched text
API key in frontendKeep provider calls on backend
No webhook handlingTreat scans as async jobs
No report normalizationReturn one clean internal schema
No privacy planProtect submitted content

The biggest mistake is building a plagiarism checker that sounds more certain than it is.

A checker should support review, not replace judgment.

A simple production workflow

Here is the workflow we would actually ship:

user submits text/file
→ backend validates input
→ local similarity check
→ provider plagiarism scan
→ webhook/status handling
→ normalized report
→ LLMAPI review summary
→ human review if needed

For SEO content:

draft article
→ plagiarism scan
→ matched source review
→ rewrite/citation decision
→ publish approval

For education:

student submission
→ plagiarism scan
→ similarity report
→ instructor review
→ feedback if needed

For marketplaces:

new listing
→ duplicate content scan
→ similar listing review
→ approve, edit, or reject

That is how plagiarism checking becomes an actual product workflow.

The practical takeaway

You can detect plagiarism using JavaScript by building a Node.js backend that validates text, runs local similarity checks, submits content to a plagiarism detection API, normalizes the report, and returns review-friendly results.

A simple flow looks like this:

text/file
→ JavaScript backend
→ plagiarism API
→ similarity report
→ review decision

Use local string similarity for quick internal duplicate checks. Use APIs like Copyleaks, PlagiarismCheck.org, or Originality.ai for wider scans. Use semantic similarity when paraphrasing matters. Use LLMAPI after detection to summarize reports, route review cases, and create careful feedback.

The goal is not to yell “plagiarism” every time two texts overlap.

The goal is to give your app a fast, structured way to spot copied or reused content, show the evidence, and help the right person decide what happens next.

Deploy in minutes