Skip to main content

Source Match API

The Source Match endpoint compares a text with up to 20 source documents that you supply, such as reference answers, earlier submissions, the pages a draft was researched from, or an internal corpus. For each source, it returns every run of 8 or more words the two share word for word, as character offsets into your text, and an overall similarity: the share of the text's words copied from any source.

Sapling doesn't search the web for sources. You send the documents to compare against, and nothing leaves Sapling to find more.

It detects copying, not paraphrase or authorship. To ask whether a text was written by AI, use the AI Detector API. You can call both and show the results side by side.

tip

This endpoint is free of charge.

Sample Code​

curl -X POST https://api.sapling.ai/api/v1/source-match \
-H "Content-Type: application/json" \
-d '{"key":"<api-key>", "text":"Our caching layer stores rendered pages for five minutes. When a page changes, the editor service publishes an invalidation event to every edge node, which drops the stale copy before the next request arrives. We chose this design because it keeps reads fast.", "sources":[{"id":"ref-answer", "text":"A good answer explains that the editor service publishes an invalidation event to every edge node, which drops the stale copy before the next request arrives, and discusses the trade-offs."}, {"text":"Caching is hard. Most teams start with a time-based expiry."}]}'

Request Parameters​

https://api.sapling.ai/api/v1/source-match

HTTP method: POST

key: String
32-character API key. Can also be supplied via the Authorization header as a bearer token; if both are provided, the key parameter takes precedence.

text: String
The text to check, as plain text. Up to 50,000 characters. It is compared exactly as sent (HTML isn't stripped), so span offsets line up with your own copy.

sources: List[Object]
1 to 20 documents to compare against. Each is an object with:

  • text (String, required): the source's plain text, up to 100,000 characters. All sources together can be up to 300,000 characters.
  • id (String, optional): your label for the source, up to 128 characters and unique within the request. It is echoed back on the matching source.

min_words: Integer Optional
The shortest shared run of words to report, from 6 to 50. Defaults to 8.

exclude_quotes: Boolean Optional
Ignore quoted passages, as plagiarism checkers do. Words inside double quotation marks (straight "…" or curly “…”, German „…“, guillemets «…», and CJK corner brackets 「…」 and 『…』) or on block-quote lines starting with > never count as matched, and no span runs through them. Single quotes aren't treated as quotation marks, because they double as apostrophes. A quote still open at a blank line is ignored, so one unbalanced mark can't hide the rest of the text. Defaults to false.

Response Parameters​

The response for the cURL request above:

{
"similarity": 0.488,
"matched_words": 21,
"text_words": 43,
"quoted_words": 0,
"sources_checked": 2,
"sources": [
{
"index": 0,
"id": "ref-answer",
"matched_words": 21,
"matched_fraction": 0.488,
"spans": [
{
"start": 79,
"end": 208,
"words": 21,
"text": "the editor service publishes an invalidation event to every edge node, which drops the stale copy before the next request arrives"
}
]
}
],
"min_words": 8,
"exclude_quotes": false,
"version": "0.2.0"
}

The second source shares no run of 8 words with the text, so it isn't listed.

similarity: Float
The share of the text's words that are inside a span from any source, from 0 to 1. Words copied from more than one source count once.

matched_words, text_words: Integer
The number of the text's words inside any span, and the text's total word count.

quoted_words: Integer
The number of words set aside by exclude_quotes, or 0 when it is off. They still count in text_words.

sources_checked: Integer
The number of sources in the request.

sources: List[Object]
The sources that share at least one span with the text, most matched words first. Sources with no span are left out. Each has:

  • index: the source's position in the request's sources list.
  • id: the id you sent, or null if you didn't send one.
  • matched_words and matched_fraction: how many of the text's words, and what share of them, are inside a span from this source.
  • spans: the shared passages, each with start and end (character offsets into text, end exclusive), words, and text (the passage as it appears in your text).

min_words: Integer
The minimum span length used.

exclude_quotes: Boolean
Whether quoted passages were ignored.

version: String
The version of the matching algorithm.

How matching works​

A span is a run of at least min_words consecutive words that appears in both the text and a source. Matching ignores case and punctuation, and treats curly and straight apostrophes as the same, so a passage still matches after its punctuation is edited. Technical terms such as C++ and Node.js stay whole.

The default of 8 words is the usual floor for copy detection. Shorter runs, such as "one of the most important things", turn up in unrelated documents and would inflate similarity. Raise min_words to report only longer passages. Lower it, down to 6, for short texts such as answers to a single question.

The same inputs give the same result. No AI model is involved, so responses are fast.

Quoted passages​

Suppose the middle sentence of the sample text above quoted the reference answer instead, making the text:

Our caching layer stores rendered pages for five minutes. As the reference answer puts it, "the editor service publishes an invalidation event to every edge node, which drops the stale copy before the next request arrives." We chose this design because it keeps reads fast.

The same 21-word span is reported, and similarity is 0.467 (21 of the 45 words). With "exclude_quotes": true, those 21 words are set aside: quoted_words is 21, no source is listed, and similarity is 0. Quoted words still count in text_words, so quoting a source lowers similarity rather than raising it.

Interpreting the result​

  • Copying is not paraphrase. A passage reworded so that no run of min_words words survives isn't reported. A low similarity doesn't show that a text is original.
  • Copying is not always misconduct. Quotations, shared boilerplate, and a question repeated in its answer all produce spans. Set exclude_quotes to drop quoted passages, and read the spans before acting on similarity.
  • Results depend on the sources you send. Only the documents in the request are checked.
  • It is tuned for languages written with spaces between words. Text in Chinese, Japanese or Thai, for example, matches poorly.

Errors​

StatusCause
400text or sources is missing or empty, a value has the wrong type, a length or count limit is exceeded, a text contains a NUL character, an id is empty, too long or repeated, min_words is outside 6–50, or exclude_quotes isn't a boolean.
401The API key is missing or invalid.
402The account can't make API requests, for example because its quota is used up.
413The request body is too large.
429Rate limited. The text and every source count against your key's rate limit.

The 400 and 401 responses have a msg field with the reason.

Notes​

  • Offsets count Unicode code points (Python str indexing), not UTF-16 code units or bytes. In JavaScript, use Array.from(text) if the input may contain characters outside the Basic Multilingual Plane.
  • This endpoint doesn't save the text or the sources, and its usage events record only their sizes, how many sources were sent and matched, and the resulting similarity.
  • To compare PDF or Word documents, extract their text first with the file-to-text routes.
  • For a resume against the job posting it was submitted for, use the Job Description Match API, which also reports keyword coverage.