AI Watermark¶
Every AI-generated response is stamped with a latent HMAC-SHA256 watermark recorded in a log, so later you can answer "did this deployment write this text?" — and detect whether it was edited since. One endpoint does the checking, in three modes depending on what you still have: the text, the identifiers, or both.
Endpoints¶
POST /v1/glad/watermark/verify¶
What it does. Answers "did this deployment generate this text?" One endpoint, three modes, chosen automatically by what you send:
| You send | Mode | What it proves |
|---|---|---|
response_text (or text) + call_id + watermark_id | live | The watermark matches this exact text. Detects tampering. |
call_id + watermark_id, no text | lookup | The watermark is registered for that call. Does not check text integrity — suitable for a third-party auditor who has the identifiers but not the content. |
response_text (or text) alone | search | Scans the watermark log for an entry matching the text. This is the paste-a-snippet path. |
Sending none of those returns 400.
# live — full integrity check
curl -s -X POST http://localhost:8080/v1/glad/watermark/verify \
-H "Content-Type: application/json" \
-d '{
"call_id": "call_abc123",
"watermark_id": "a8b3c1d4e5f6…",
"response_text": "The capital of France is Paris."
}' | jq
# search — just paste the text
curl -s -X POST http://localhost:8080/v1/glad/watermark/verify \
-H "Content-Type: application/json" \
-d '{"text": "The capital of France is Paris."}' | jq '{valid, call_id, match_mode}'
import httpx
def verify(text: str, call_id: str | None = None, watermark_id: str | None = None):
body = {"response_text": text}
if call_id and watermark_id:
body |= {"call_id": call_id, "watermark_id": watermark_id}
r = httpx.post("http://localhost:8080/v1/glad/watermark/verify", json=body, timeout=60)
r.raise_for_status() # 400 when the body identifies nothing
return r.json()
res = verify("The capital of France is Paris.", "call_abc123", "a8b3c1d4e5f6…")
print(res["mode"], res["valid"])
print(res["message"])
async function verify(text: string, callId?: string, watermarkId?: string) {
const body: Record<string, string> = { response_text: text }
if (callId && watermarkId) { body.call_id = callId; body.watermark_id = watermarkId }
const r = await fetch("http://localhost:8080/v1/glad/watermark/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
})
if (!r.ok) throw new Error(await r.text())
return r.json()
}
const res = await verify("The capital of France is Paris.", "call_abc123", "a8b3c1d4e5f6…")
console.log(res.mode, res.valid, res.message)
What comes back¶
{
"verification_id": "VRF-3F9A1C7B2E5D8046A1B2",
"valid": true,
"call_id": "call_abc123",
"watermark_id": "a8b3c1d4e5f6…",
"method": "hmac_sha256_v1",
"mode": "live",
"timestamp": "2026-06-10T10:23:45Z",
"verified_at": "2026-06-18T09:00:00+00:00",
"applicable_laws": ["EU_AI_ACT", "CA_SB_942"],
"lang": "en",
"message": "Verification successful. The watermark_id matches the response text. …",
"provider": "Geodesia S.R.L.",
"product_version": "…"
}
| Field | Description |
|---|---|
valid | Whether the check passed. |
mode | Which of the three paths ran: live, lookup or the search path. |
verification_id | A VRF-… id for this verification act, so the check itself can be cited. |
timestamp | When the watermarked call was generated (null if the log entry is gone). |
verified_at | When this verification ran. |
applicable_laws | The frameworks the original call was recorded under. |
message | A human-readable verdict, suitable for showing to a non-technical auditor. |
provider / product_version | Who issued the watermark and with which build. |
Request fields¶
| Field | Type | Description |
|---|---|---|
response_text | string | The text to verify. |
text | string | Alias for response_text. |
call_id | string | The call the text came from. |
watermark_id | string | The HMAC recorded for that call. |
session_id | string | Improves the live check when the original call belonged to a session. |
Search mode survives copy-paste
A strict HMAC is computed over the exact bytes, so text copied out of a rendered chat — with end-of-turn markers and thinking blocks stripped — will not match. Search mode tries the exact HMAC first and then falls back to a normalised comparison against the stored response, which is what makes "paste the paragraph you received" actually work.
A failed verification is not proof of forgery
valid: false means this deployment's log does not vouch for this text as given. The text may have been edited, or the identifiers may be wrong, or it may have come from a different deployment. Read message before drawing a conclusion.
What Is an AI Watermark?¶
An AI watermark is a signal embedded in the text output of a language model that allows the origin of the text to be verified. Unlike visible watermarks, Geodesia's watermark is latent — it is not visible in the text but is recoverable through the verification endpoint.
This satisfies:
- EU AI Act Article 50 — AI-generated content must be marked in a way that is detectable
- California SB 942 — AI-generated content must carry detectable disclosure
- Italy 132/2025 — AI content marking requirements
The watermark is a cryptographic HMAC computed over the generation parameters and embedded as metadata attached to the response. The manifest watermark (explicit disclosure in the geodesia.watermark response field) is delivered alongside the latent one.
How It Works¶
When Geodesia generates a response, it:
- Computes
HMAC-SHA256(watermark_key, session_id + call_id + timestamp + model_id) - Attaches the result as the
watermark_tokenin the response metadata - Records the watermark in the audit chain
To verify, you provide the watermark_token and the relevant metadata. The verifier recomputes the HMAC and checks if the provided token matches.
The watermark_key is derived from your deployment's license token. It never leaves the server.
Response Disclosure¶
Every Geodesia G-1 response includes a manifest watermark in the geodesia.watermark field:
{
"geodesia": {
"watermark": {
"token": "hmac:v1:a8b3c1d4e5f6...",
"generated_by": "Geodesia G-1",
"call_id": "call_abc123",
"timestamp": "2026-06-10T10:23:45Z",
"disclosure": "This content was generated by an AI system."
}
}
}
| Field | Description |
|---|---|
token | The HMAC token used for verification |
generated_by | System identifier |
call_id | The call ID — can be cross-referenced with the audit chain |
timestamp | When the response was generated |
disclosure | Plain-language AI disclosure string |
Watermark Configuration¶
Configure watermarking in config.yaml:
watermark:
enabled: true
algorithm: "hmac_sha256"
include_in_response: true # Include watermark in all API responses
disclosure_text: "This content was generated by an AI system."
custom_disclosure: null # Override with jurisdiction-specific text
Regulatory Coverage¶
| Law | Requirement | Coverage |
|---|---|---|
| EU AI Act Art. 50(1) | Disclose that content is AI-generated | Manifest disclosure in every response |
| EU AI Act Art. 50(2) | Detectable machine-readable marker | Latent HMAC-SHA256 token |
| CA SB 942 §22757(a) | Watermark or tag AI-generated content | Latent + manifest watermark |
| Italy 132/2025 Art. 4 | Mark AI-generated content | Disclosure field + latent token |
| UK DUAA 2025 | Transparency of AI content | Manifest disclosure |