AI text watermarking with SynthID: how generation and detection work

An AI text watermark is a signal deliberately introduced into generated text. For a publisher, the useful question is specific: can we later recognize output from a generation system that we control?

SynthID Text addresses that question. It does not determine whether an arbitrary article was written by AI. Its detector tests for a configured statistical pattern, and an unsuccessful test leaves the text’s origin unresolved. The SynthID reference repository describes both watermark generation and detection.

This guide walks through a local Python example, then examines the harder cases: multiple watermarks, rewriting with a new watermark, and checking text when the original keys are unknown.

What a text watermark actually changes

A language model assigns probabilities to possible next tokens. Tokens can represent words, parts of words, or punctuation. A generative watermark changes how the next token is selected. The resulting choices carry evidence that a detector can measure later.

SynthID uses a process called tournament sampling. Candidate tokens compete under pseudorandom scoring functions. Recent token context and watermark keys determine the scores. Repeating the selection through several layers favors tokens with a measurable statistical pattern. The detector reconstructs these scores from the finished text. It does not need to run the original language model. See the SynthID research paper .

1. Predict possible next tokens

The language model produces a distribution over its vocabulary.

2. Apply the configured watermark

Keyed scoring functions influence token selection during generation.

3. Publish ordinary text

The statistical signal travels in the selected token sequence.

4. Reconstruct and score

The detector uses compatible tokenization and the matching configuration to look for that signal.

Longer passages generally provide more evidence. Highly predictable output leaves fewer alternative tokens available for watermarking. More layers can help, but the paper reports diminishing returns as layers consume the available variation in token selection.

Statistical watermarks and hidden characters are different

Before choosing a detector, identify the signal you intend to find.

MethodWhat carries the signal?What should you test?
SynthID TextStatistical token choices made during generationEvidence under a matching watermark configuration
Hidden-character markerSpecific characters inserted into a stringThe character sequence and its documented encoding
Document metadataFields attached to a document or containerThe metadata, its origin, and any signature
AI-writing classifierPatterns learned from example textsGeneralization and false positives on the intended audience

A Unicode scanner cannot substitute for a SynthID detector. Finding an unusual character also does not establish AI authorship. Our introduction to AmICited’s free tools discusses character-based watermark signals and their limits. The experiment below concerns statistical watermarking.

Logo

Ready to Monitor Your AI Visibility?

Track how AI chatbots mention your brand across ChatGPT, Perplexity, and other platforms.

Generate watermarked text in Python

The Google repository is a research reference and explicitly advises against using its model subclasses in production. It points to the implementation in Hugging Face Transformers. The reference also provides mean, weighted-mean, and trained Bayesian detection approaches. Repository usage notes

The following example uses Python 3.13, Transformers 4.57.1, and PyTorch 2.8.0. These are reproducible example versions, not a claim that they are the newest releases. It runs on CPU and downloads the public distilbert/distilgpt2 model on first use. That small model keeps the experiment accessible; its writing quality is not representative of current assistants.

Create an isolated environment:

python3.13 -m venv .venv
source .venv/bin/activate
python -m pip install "transformers==4.57.1" "torch==2.8.0"

Save the Python blocks in this guide, in order, in watermark_demo.py. Start with generation:

import torch
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    SynthIDTextWatermarkingConfig,
    SynthIDTextWatermarkLogitsProcessor,
    set_seed,
)

MODEL_ID = "distilbert/distilgpt2"
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
model = AutoModelForCausalLM.from_pretrained(
    MODEL_ID, use_safetensors=True
).to("cpu").eval()

# Public demonstration keys. Never use these as a private production mark.
watermark = SynthIDTextWatermarkingConfig(
    keys=[104729 + 7919 * i for i in range(30)],
    ngram_len=5,
    sampling_table_size=65536,
    sampling_table_seed=42,
    context_history_size=1024,
    skip_first_ngram_calls=True,
)

inputs = tokenizer(
    "A community garden can change a neighborhood in several ways. ",
    return_tensors="pt",
)

def generate(config):
    set_seed(17)
    with torch.inference_mode():
        output = model.generate(
            **inputs,
            watermarking_config=config,
            do_sample=True,
            max_new_tokens=384,
            temperature=0.9,
            top_k=50,
            pad_token_id=tokenizer.eos_token_id,
        )
    # Keep the continuation; the supplied prompt was not watermarked.
    return output[:, inputs["input_ids"].shape[1]:]

marked_ids = generate(watermark)
unmarked_ids = generate(None)
print(tokenizer.decode(marked_ids[0], skip_special_tokens=True))

Generation must expose a compatible sampling pipeline. Passing a finished response from a hosted API into this configuration cannot retroactively mark it. A separate model would have to generate a new version.

In production, create unique random keys and keep the configuration private. Retain the tokenizer revision, model revision, configuration version, and associated detector. A key rotation creates a new configuration; keep the old detection configuration if you still need to recognize older output. Google’s implementation guidance

How watermark detection computes evidence

Detection starts by tokenizing the candidate text and reconstructing a score for each eligible token window and watermark layer. In the implementation used here, these binary scores are called g values. We average them after excluding repeated contexts and positions at or after the end-of-sequence token. The first four tokens of a continuation do not form a complete five-token window on their own.

The following function uses the Transformers scoring and masking methods . It accepts one unpadded sequence at a time and returns a raw mean, not an authorship probability.

def score_ids(token_ids, config):
    if token_ids.ndim != 2 or token_ids.shape[0] != 1:
        raise ValueError("Pass one unpadded sequence with shape [1, length].")
    if token_ids.shape[1] < config.ngram_len:
        return {"mean_g": None, "usable_windows": 0}

    processor = SynthIDTextWatermarkLogitsProcessor(
        **config.to_dict(), device=torch.device("cpu")
    )
    with torch.inference_mode():
        values = processor.compute_g_values(token_ids).float()
        unique = processor.compute_context_repetition_mask(token_ids)
        before_eos = processor.compute_eos_token_mask(
            token_ids, tokenizer.eos_token_id
        )[:, config.ngram_len - 1:]
        mask = unique & before_eos.bool()
        usable = int(mask.sum().item())
        if usable == 0:
            return {"mean_g": None, "usable_windows": 0}
        # Boolean indexing retains the layer dimension.
        mean_g = float(values[mask].mean().item())
    return {"mean_g": mean_g, "usable_windows": usable}

print("marked:", score_ids(marked_ids, watermark))
print("unmarked:", score_ids(unmarked_ids, watermark))

def score_text(text, config):
    ids = tokenizer(
        text, add_special_tokens=False, return_tensors="pt"
    )["input_ids"]
    return score_ids(ids, config)

published_text = tokenizer.decode(
    marked_ids[0],
    skip_special_tokens=True,
    clean_up_tokenization_spaces=False,
)
print("published text:", score_text(published_text, watermark))
print("empty text:", score_text("", watermark))

The direct token-ID check isolates generation from text serialization. The second check exercises the path a detector sees after publication. Evaluate your actual publishing and text-extraction pipeline because it may change the input.

A score of 0.60 does not mean a 60% probability that AI wrote the text. This function has no calibrated decision threshold. A short passage can score highly by chance. Report an inconclusive result when the evidence is insufficient.

For a useful detector, collect representative human text, unwatermarked model output, and watermarked output. Keep calibration data separate from final evaluation data. Choose a threshold for an acceptable false-positive rate, evaluate detection rates on held-out samples, and break the results down by language, length, and content type. A single marked/unmarked pair only checks that the example runs.

For a trained alternative, Hugging Face supplies a Bayesian detector and a detector-training workflow . A detector trained for unrelated keys is not a drop-in replacement for your detector. A randomly initialized or demonstration checkpoint does not establish detection accuracy.

Can you detect ANY SynthID watermark without its keys?

The published detection approach requires a matching configuration. You also need compatible tokenization. Knowing that a provider uses the SynthID algorithm does not give you its configuration, and the public repository is not a detector for every provider’s output.

Google describes private detectors, detectors exposed through an API, and publicly distributed detectors. A provider could offer verification without disclosing its private configuration. Detection and verifiability

For our own system, we can check a registry of known configurations. Append this example to compare the correct keys with unrelated demonstration keys:

other_settings = watermark.to_dict()
other_settings["keys"] = [key + 1_000_003 for key in watermark.keys]
other_watermark = SynthIDTextWatermarkingConfig(**other_settings)

known_configs = {
    "demo-original-v1": watermark,
    "demo-other-v1": other_watermark,
}
for name, config in known_configs.items():
    print(name, score_text(published_text, config))

Run the assembled file with python watermark_demo.py. This comparison produces evidence for two known configurations. It cannot discover an arbitrary unknown key, and it intentionally does not label either score as a detection.

Checking many configurations also means performing many statistical tests. If each test permits false positives, accepting any positive can increase the overall false-positive rate. Calibrate the complete search procedure. Where individual tests provide valid p-values, a conservative option is to divide the total false-positive allowance by the number of tests. Do not apply that arithmetic directly to the raw mean_g score.

If no known configuration matches, the appropriate result is “No supported watermark detected.” The possibilities still include human writing, unwatermarked AI output, an unknown watermark, and an edited passage whose signal is too weak.

Can the same text carry multiple SynthID watermarks?

There are two different designs to consider.

Multiple layers in one configuration

SynthID already supports a list of keys. In the example, 30 keys create 30 layers within one configured watermark. The detector combines their evidence. Those keys do not automatically represent 30 separately attributable authors or organizations. Watermark configuration

Independently detectable platform and customer marks

Suppose you want a platform-wide detector and a separate customer-specific detector to recognize the same passage. Allocating groups of layers to those purposes is an experimental design option, not a documented guarantee of the library.

Simply concatenating two key lists does not demonstrate that both original detectors still work. Each detector would need evaluation on the combined generation process, including its position in the layer sequence. Measure each mark’s detection rate, cross-matches against other customers, and behavior after edits. This is an engineering proposal that needs experiments, rather than a claim that SynthID provides independent multi-owner attribution out of the box.

If a central service only needs to identify a customer, a simpler design is one configuration per customer with an internal registry. That design still needs calibration across the full registry. It does not provide a separate platform-wide mark that can be checked without searching customer configurations.

Can you replace an existing watermark?

SynthID does not store a field such as watermark_id in the text. There is no metadata value to overwrite. The original signal is distributed across token choices.

If you rewrite a passage using a second watermarked generator, the new generation process introduces its own signal. The effect on the earlier signal depends on the resulting token sequence. Google reports that mild edits can preserve detectability, while thorough rewriting or translation can substantially reduce confidence. Documented transformation limits

It follows that a second watermark does not guarantee erasure of the first. Both signals may remain detectable, or one or both may fall below their detection thresholds. This is an inference about a combined workflow; the cited documentation does not establish a survival rate for every pair of watermarks.

An unknown first configuration creates a practical limit. Our own detector can evaluate our mark, but it cannot establish that the original mark disappeared. That requires access to the original matching detector. Even a negative result means the detector did not find enough evidence under its settings, not proof that every trace was removed.

How to test overlapping watermarks

Before claiming that marks coexist or that one replaces another, use a test matrix. The entries below describe experiments to run, not measured outcomes.

Sample groupDetector ADetector BWhat the comparison checks
Human and unwatermarked AI textRunRunBaseline false positives
Text generated with A onlyRunRunA detection and accidental B matches
Text generated with B onlyRunRunB detection and accidental A matches
Text generated with an experimental combined configurationRunRunWhether each intended mark is detectable
A text rewritten under BRunRunA survival and B detection after rewriting
Short excerpts and edited versions of each groupRunRunSensitivity to the publishing workflow

Use held-out prompts and report usable text lengths, sample counts, detector versions, thresholds, and false-positive rates. Include uncertainty around the reported rates. Repeat the evaluation when you change keys, tokenization, model versions, or generation settings. Until those checks exist, describe coexistence and replacement as unverified.

What this means for publishers and AI visibility

Watermark detection and AI citation monitoring answer different questions. A watermark test asks whether a passage carries evidence of a particular generation configuration. Citation monitoring checks which sources appear in AI answers.

Do not present a missing watermark as proof of human authorship, or a detected watermark as proof that a particular website was cited. For publishing workflows, keep generation records and editorial attribution alongside any statistical detector. Each provides evidence the others do not.

Frequently asked questions

Viktor Zeman is a co-owner of QualityUnit. Even after 20 years of leading the company, he remains primarily a software engineer, specializing in AI, programmatic SEO, and backend development. He has contributed to numerous projects, including LiveAgent, PostAffiliatePro, FlowHunt, UrlsLab, and many others.

Viktor Zeman
Viktor Zeman
CEO, AI Engineer

Track where AI cites your content

Use citation monitoring to understand which pages AI answers reference. Watermark detection answers a different question about text provenance.