Trace a leaked document to its recipient

For publishers, research providers and companies that share confidential text with many people. Every recipient gets a copy that looks identical but carries an invisible, encrypted recipient id. When a copy turns up in a forum, a chat or a competitor's inbox, one call tells you whose copy it was.

Endpoints: POST /api/watermark/encode, /decode with a channel-bound token  ·  MCP tools: watermark_encode, watermark_decode  ·  Plan: Starter or Pro (signing channels)  ·  Cost: 1 credit per copy

The problem

Board updates, pre-release announcements, paid research, contract drafts and subscriber newsletters leak. Most leaks are not screenshots: someone copies the text and pastes it into a message or a post. When every recipient received the same text, the leaked version says nothing about where it came from, and the only options are to accuse nobody or to suspect everybody.

How it works

  1. Give each recipient an id in your own system, such as 1042. The copy only ever carries the id, never a name.
  2. Make one copy per recipient. Call POST /api/watermark/encode with a channel-bound token and a secret such as r=1042;d=q3-board. The id is encrypted with your channel key, so nobody else can read or forge it.
  3. Send each person their copy by email, in your portal or as an attachment.
  4. When a text leaks, read the id. POST /api/watermark/decode returns r=1042;d=q3-board. Look up 1042 in your list.

Add it to your workflow

The examples read the channel-bound token from TEXTWATERMARKING_SIGNING_TOKEN. Each example below is run against the API before this page is published.

Make the copies (Python)

Reads board-update.txt and writes one file per recipient into copies/. In a mailing system, call copy_for for each recipient instead of writing files.

make_copies.py

# Make one copy of a document per recipient, each with its own hidden recipient id (Python 3.10+, pip install requests).
# 1 credit per copy. Uses a token bound to your signing channel, so only you can read or forge the ids.
import os
import pathlib

import requests

API = "https://textwatermarking.com/api/watermark"
HEADERS = {"Authorization": f"Bearer {os.environ['TEXTWATERMARKING_SIGNING_TOKEN']}"}


def copy_for(document: str, recipient_id: str, doc_id: str) -> str:
    r = requests.post(f"{API}/encode", headers=HEADERS, timeout=10,
                      json={"text": document, "secret": f"r={recipient_id};d={doc_id}"})
    if r.status_code == 400 and "required_length" in r.json():
        raise ValueError(f"Document too short: needs {r.json()['required_length']} visible characters")
    r.raise_for_status()
    return r.json()["encoded"]


if __name__ == "__main__":
    document = pathlib.Path("board-update.txt").read_text(encoding="utf-8")
    # Keep this list in your own system: the copy only carries the id, never the name.
    recipients = {"1041": "A. Demir", "1042": "J. Smith", "1043": "M. Rossi"}
    out = pathlib.Path("copies")
    out.mkdir(exist_ok=True)
    for recipient_id in recipients:
        marked = copy_for(document, recipient_id, doc_id="q3-board")
        (out / f"board-update-{recipient_id}.txt").write_text(marked, encoding="utf-8")
        print(f"copies/board-update-{recipient_id}.txt")

Find whose copy leaked (Python)

Paste the leaked text into a file and run the script on it. Leaks often arrive below a forwarding header, so the script also tries the text from each line start; attempts that find nothing are free.

who_leaked.py

# Read the recipient id from a leaked copy (Python 3.10+, pip install requests).
# 1 credit when the id is found; attempts that find nothing are free.
import os
import sys

import requests

API = "https://textwatermarking.com/api/watermark"
HEADERS = {"Authorization": f"Bearer {os.environ['TEXTWATERMARKING_SIGNING_TOKEN']}"}


def starts(text: str):
    """The whole text, then the text from each line start: leaks often arrive below a forwarding header."""
    yield text
    for i, ch in enumerate(text):
        if ch == "\n" and text[i + 1:].strip():
            yield text[i + 1:]


def recipient_of(leaked: str) -> str | None:
    """The recipient id hidden in the leaked text, or None if no copy of yours starts in it."""
    for candidate in starts(leaked):
        r = requests.post(f"{API}/decode", json={"text": candidate}, headers=HEADERS, timeout=10)
        if r.status_code == 422:      # nothing from your channel starts here (free)
            continue
        r.raise_for_status()
        fields = dict(p.split("=", 1) for p in r.json()["decoded"].split(";") if "=" in p)
        return fields.get("r")
    return None


if __name__ == "__main__":
    # Usage: python who_leaked.py < leaked.txt
    print(recipient_of(sys.stdin.read()))

Ad hoc, from an AI assistant

With the MCP server connected using the channel-bound token, paste the leaked text into Claude or Cursor and ask: "Read the watermark in this text with watermark_decode and tell me the recipient id."

What it costs

ItemValue
One recipient copy1 credit
One lookup1 credit when an id is found; free otherwise
Minimum lengthabout (payload bytes + 30) × 8 visible characters; 376 characters for r=1042;d=q3-board
Signing channelsFree: none · Starter: 3 · Pro: 20
Example volume40 recipients × 12 documents a month = 480 credits (Starter: 5,000 a month)

What it does not do

  • Screenshots, photos, retyping and rewording carry no id. Neither does text run through a tool that strips invisible characters.
  • An excerpt that leaves out the beginning cannot be traced. The id starts at the first character of the copy. Lines added above or below it are fine.
  • A careful leaker can remove it. Comparing two copies character by character reveals that they differ. The method catches the common case of a copy pasted as it is.
  • Some formats drop invisible characters. Plain text, HTML email and most editors keep them; some PDF exporters and word processors do not. Test the format you send.
  • A found id is evidence, not a verdict. A recipient may have forwarded their copy to someone else, or had their account compromised.

Frequently asked questions

Can a recipient see or remove the id?
They cannot see it, and because it is encrypted with your channel key, they cannot read it even with their own TextWatermarking account. They can remove it, for example by retyping the text, running it through a tool that strips invisible characters, or by comparing two copies character by character. The method catches the common case of a copy pasted as it is; it does not stop a careful leaker.
Could someone plant another recipient's id to blame them?
Not without a token on your channel. Only tokens bound to the channel can create marks that decrypt with its key, and the recipient never sees any id in readable form.
Should we tell recipients that copies are traced?
In most cases yes. Telling people that each copy is individual deters leaks on its own, and data-protection rules often expect it. This page is not legal advice; check what applies to you.
How long must the document be?
About 8 visible characters per byte of the payload plus 30 bytes of overhead. The id used here, r=1042;d=q3-board, needs 376 characters, roughly 60 words. Most reports, announcements and newsletters are longer than that.
What does it cost?
One credit per copy, and one credit for each lookup that finds an id. Lookups that find nothing are free. Forty recipients and twelve documents a month is 480 credits, which fits the Starter plan.