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
- Give each recipient an id in your own system, such as
1042. The copy only ever carries the id, never a name. - Make one copy per recipient. Call
POST /api/watermark/encodewith a channel-bound token and a secret such asr=1042;d=q3-board. The id is encrypted with your channel key, so nobody else can read or forge it. - Send each person their copy by email, in your portal or as an attachment.
- When a text leaks, read the id.
POST /api/watermark/decodereturnsr=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
| Item | Value |
|---|---|
| One recipient copy | 1 credit |
| One lookup | 1 credit when an id is found; free otherwise |
| Minimum length | about (payload bytes + 30) × 8 visible characters; 376 characters for r=1042;d=q3-board |
| Signing channels | Free: none · Starter: 3 · Pro: 20 |
| Example volume | 40 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?
Could someone plant another recipient's id to blame them?
Should we tell recipients that copies are traced?
How long must the document be?
What does it cost?
Related
- Prove authorship and detect edits: show that a text is your unchanged original
- Let your agents sign what they write: the same channel key for agent output
- Signing channels: create, list and rotate channels