Quickstart
Read this if you have installed Laidline and want to see it work end to end. You will write a starter configuration, mark an image, read the result back, run the HTTP sidecar and mark from Python, in under 15 minutes. Skip if you have not installed it yet (Install) or you are setting up a deployment for real traffic (Production). Time: 15 minutes. Needs:
pip install "laidline[image,audio,server]", the model weights (laidline models fetch, or a first mark that waits for the download), and outbound HTTP totimestamp.digicert.com.
Before you start
-
Laidline installed with the
image,audioandserverextras (Install). Check it:laidline --versionExpected output:
1.0.1. -
An empty working directory with one photo in it, named
in.png(a PNG or JPEG of at least 128 pixels a side; a real photo, not a flat colour). The outputs below come from a 640x426 photo. -
Outbound HTTP (port 80) to
timestamp.digicert.com, withtimestamp.sectigo.comas the fallback. Every mark carries a trusted timestamp, and marking stops when no timestamp server answers.
1. Write a starter configuration
laidline init --system-name my-image-model --org "Yourcompany Inc"
Expected output (your issuer number differs):
wrote laidline.yaml and ./certs/ (development only; use your KMS and a C2PA trust-list certificate in production) One usage report a month (counts only, no media) is sent to your issuer; set issuer.usage_report: false to turn it off issuer 49045 is a trial number, so marks are shared marks (grade D, provider not identified). laidline register --email <you@yourdomain> --display-name "<Your Company Inc>" --plan registered_free gets a registered number, ...
What just happened: Laidline wrote three things into the directory. laidline.yaml is the configuration, with a
random trial issuer number (32,768 to 65,279) and every section filled in. payload-k1.key (mode 0600) is your
private payload key. ./certs/ holds a development CA and a signing key it certified. --system-name and
--system-version go into every Content Credentials manifest; --org names the organisation in the development
certificate and the evidence log.
Every command, and Marker.from_config() in Python, finds the configuration in this order: the path you pass
(--config), then LAIDLINE_CONFIG, then ./laidline.yaml, then /etc/laidline/config.yaml; never under
LAIDLINE_HOME, which holds the model weights. Relative paths inside
the file resolve against the file's own directory, not the working directory. laidline init refuses to overwrite an
existing laidline.yaml or payload-k1.key; --no-certs skips the development CA, and --path writes the file
elsewhere. Run init before laidline keys dev-cert, never after: init replaces a development certificate in
./certs/. The starter file has no upstream or assets section; the proxy needs them (below). The one usage report a month is counts only and goes to Laidline only once you register
(The usage report).
2. Mark an image
laidline mark in.png
Expected output:
{
"out": "in.marked.png",
"asset_id": "ast_01M5W56KVWNM77W9ND78K138PK",
"manifest_id": "urn:c2pa:0d51f50d-33e5-4087-9458-6b2be33cbbf8",
"entry_hash": "a2bd1866c554d6c86ac7ae9b60fb73c32f546a9cf06315fb739b413d5a4c97ef",
"warnings": []
}
What just happened: Laidline did three things to one file and wrote in.marked.png. It embedded a PixelSeal
watermark in the pixels, carrying your issuer number. It signed a Content Credentials manifest over the final bytes
with your development key, with an RFC 3161 timestamp from DigiCert. It appended an evidence entry to the hash-chained
log in ./evidence/. The asset_id names the mark everywhere, and entry_hash is the evidence entry. If you skipped
laidline models fetch, this first mark waited for the 1.24 GB PixelSeal download, without progress output.
3. Read the mark back
laidline detect in.marked.png
Expected output:
verdict: marked_by_this_system grade: A
c2pa present=True grade=A status=valid_trusted
watermark present=True grade=D status=shared mark
issuer: shared mark, provider not identified
note: Absence of a signal is not evidence that content is human-made.
note: Revocation status was not checked (OCSP disabled in this deployment).
What just happened: detect found both layers. The Content Credentials manifest verifies and chains to your own
development CA, which your deployment trusts as its own anchor, so that signal is grade A and the verdict is
marked_by_this_system. The watermark is found too, and reads as a grade D shared mark. laidline detect --json
prints the full laidline.detect/1 result; Grading explains every field.
Why the first marks read as a grade D shared mark
Until you register, your marks carry the trial number laidline init drew and the shared pool key that every
unregistered deployment uses. A reader can confirm that the watermark is a Laidline mark, but not whose: anyone could
have made it. That is what grade D and "provider not identified" say. The manifest, which names you, is easy to remove:
a re-encode, a resize or a screenshot drops it. Make a copy without it and read it:
python -c "from PIL import Image; Image.open('in.marked.png').convert('RGB').save('copy.jpg', quality=85)" laidline detect copy.jpg
Expected output:
verdict: signals_present_unverified grade: D
c2pa present=False grade=none status=absent
watermark present=True grade=D status=shared mark
issuer: shared mark, provider not identified
note: Absence of a signal is not evidence that content is human-made.
note: Revocation status was not checked (OCSP disabled in this deployment).
note: A watermark shown as present but not verified is not evidence of origin: ...
What just happened: with the manifest gone, only the watermark is left, and a shared mark cannot say who made the
file. After laidline register your marks carry a registered issuer number and your private payload key, and a copy
like this one reads as "registered to" your name, at grade B once it matches your evidence log
(Production). The development certificate has the same limit on the
manifest side: only your own deployment trusts it, and other validators report the manifest valid but not trusted
until you sign with a certificate from a C2PA trust-list CA (Certificates).
4. Run the sidecar
Start the HTTP sidecar in this directory and leave it running:
laidline serve
Expected output (the lines that matter, after a few seconds):
... INFO laidline.cli listening on 127.0.0.1:8080 (loopback only) ... INFO laidline.server.app admin routes disabled: LAIDLINE_ADMIN_TOKEN is not set ... INFO laidline.server.app inbound token off: server.mark_token_env is not set ... INFO laidline.watermark.image.pixelseal loading PixelSeal on cpu INFO: Application startup complete. INFO: Uvicorn running on http://127.0.0.1:8080 (Press CTRL+C to quit)
In a second terminal, in the same directory, wait for it to be ready:
curl -s localhost:8080/readyz
Expected output:
{"ready":true,"error":null}
Mark the photo over HTTP:
curl -s -F file=@in.png localhost:8080/v1/mark/image -o served.png -D -
Expected output (the response headers; the marked image is in served.png):
HTTP/1.1 200 OK ... x-laidline-asset-id: ast_01M5W59GCNVMCZ1E284CK6DHBM x-laidline-manifest-id: urn:c2pa:bd13b89a-8705-41e4-93ad-63bc981867a6 x-laidline-entry-hash: d16244ed73dfdf9cebb71c99d68c9d7c39b99778df9d87e676c6c36923839d82 x-laidline-warnings: content-type: image/png
And read it back:
curl -s -F file=@served.png localhost:8080/detect | python -c "import json, sys; r = json.load(sys.stdin); print(r['verdict'], r['grade'])"
Expected output:
marked_by_this_system A
What just happened: laidline serve loaded the same configuration and ran a self-test mark (not counted in your
credits) before /readyz turned ready. It listens on 127.0.0.1 only, so nothing outside this machine can reach it;
laidline serve --host 0.0.0.0 listens on every interface, which a container needs. The same process serves
/v1/mark/image, /v1/mark/audio, /v1/mark/video (asynchronous), /detect, /healthz, /readyz, /metrics and
an OpenAI-compatible proxy. The mark routes are open while no inbound token is set; set one before the port is
reachable from anywhere else (Production). Every route is in the
HTTP API.
5. Mark from Python
In your own worker, load the configuration once and mark each output. Save this as mark_one.py:
from laidline import Marker, MarkContext marker = Marker.from_config() # ./laidline.yaml, or the file LAIDLINE_CONFIG names marked = marker.mark_image("in.png", fmt="png", ctx=MarkContext(request_id="job-1")) with open("library.marked.png", "wb") as f: f.write(marked.data) print(marked.asset_id, len(marked.data), "bytes")
python mark_one.py
Expected output:
ast_01M5W5AZ4W4MGS9BVQ0MQB7BWK 519156 bytes
What just happened: Marker did in-process what laidline mark did: watermark, signed manifest, evidence entry.
mark_image takes a PIL image, bytes or a path and encodes to PNG, JPEG (quality 95, 4:4:4) or WebP (fmt).
MarkContext carries your request id into the evidence entry. Load the Marker once per process: the first call
loads the models, later calls reuse them. A worker handler looks like this:
marker = Marker.from_config() def handler(job): image = pipe(job["input"]["prompt"]).images[0] marked = marker.mark_image(image, fmt="png", ctx=MarkContext(request_id=job["id"])) return {"image_b64": marked.b64(), "asset_id": marked.asset_id}
A host that must not hold keys calls the sidecar instead, with the same methods. Save this as client_one.py:
from laidline import MarkClient, MarkContext client = MarkClient.from_config() # reads LAIDLINE_URL marked = client.mark_image("in.png", fmt="png", ctx=MarkContext(request_id="job-2")) result = client.detect(marked.data, mime="image/png") print(marked.asset_id, result.verdict, result.grade)
LAIDLINE_URL=http://127.0.0.1:8080 python client_one.py
Expected output:
ast_01M5W5BH0AFAA5YS3JFTXRK486 marked_by_this_system A
What just happened: MarkClient sent the photo to /v1/mark/image and the result to /detect; the sidecar holds
the key and the evidence log. When the sidecar requires an inbound token, set LAIDLINE_API_KEY to it and
MarkClient sends it as Authorization: Bearer. A detect credential for a keyed sidecar's full response tier is
separate: LAIDLINE_DETECT_TOKEN, sent only to /detect in the X-Laidline-Detect-Token header
(Keys).
Check it worked
Every mark is in the evidence log, and the log is the meter. Ask for this month's count:
laidline licence status
Expected output:
state: free_within_cap plan: none allowance: none credits this month: image 4, video 0, audio 0 (total 4) suggested band: registered_free marks this month: private 0, shared 4 self-test marks: 1 issuer: unregistered shared number 49045 ...
Four images: one from laidline mark, one over HTTP and two from Python. The sidecar's self-test is listed apart and
not counted. All four are shared marks, because the deployment is not registered.
Try audio and video
The audio extra is installed, so audio marks need nothing new. With a WAV file named speech.wav:
laidline mark speech.wav laidline detect speech.marked.wav
Expected output of the second command:
verdict: marked_by_this_system grade: A
c2pa present=True grade=A status=valid_trusted
watermark present=True grade=D status=shared mark
issuer: shared mark, provider not identified
...
What just happened: Laidline embedded an AudioSeal watermark, signed a manifest into the WAV and logged the mark.
The audio keeps its container unless --fmt names another (wav, flac, mp3 and m4a carry a manifest); raw PCM is
refused. The first audio mark loads AudioSeal, and on a CPU a 10-second clip can take a minute. From Python,
marker.mark_audio(samples, sample_rate, fmt="mp3", ctx=...) marks samples or encoded bytes, and
marker.audio_stream(...) marks streamed speech chunk by chunk (Audio).
Video marking is off by default (watermark.video.enabled: false), because VideoSeal re-encodes every frame and wants
a GPU. Turn it on for one command with the environment override, after pip install "laidline[video]":
LAIDLINE_WATERMARK__VIDEO__ENABLED=true laidline mark clip.mp4 LAIDLINE_WATERMARK__VIDEO__ENABLED=true laidline detect clip.marked.mp4
The second command prints verdict: marked_by_this_system grade: A with the watermark line grade=D status=shared mark. Without the override, detect skips the video watermark and grades the manifest alone. Use natural footage:
VideoSeal cannot hide a mark in noise or flat colour. On a CPU it takes seconds per second of video (11 s for a 5 s
352x288 clip on a Ryzen 7 7800X3D); for production use a GPU and the asynchronous POST /v1/mark/video route
(Video). Audio counts 0.05 credits per second and video 0.2 credits per second
(Licensing).
Where to go next
| You produce outputs with | Go to | What to know first |
|---|---|---|
| An OpenAI-compatible server | OpenAI-compatible proxy | Set upstream.base_url and point clients at port 8080: image generations, edits and variations come back marked, /v1/audio/speech comes back marked, every other /v1 route passes through (x-laidline-speech-mode: streaming streams the speech). url responses are stored under assets.path (default /var/lib/laidline/assets, which a non-root user cannot create: set assets: {path: ./assets}). Marking speech needs the audio extra; without it the route answers 501 audio_unavailable. |
| Another vendor's JSON API | Generic JSON adapter | One adapter.rules entry per route: where the images sit (JSONPath) and whether they are base64, URLs or data URIs. |
| vLLM or transformers (text) | vLLM sidecar | laidline init writes watermark.text.enabled: true with a random key_seed; install laidline-vllm into the vLLM environment and put the sidecar in front. Sampled completions are watermarked (SynthID has no effect at temperature 0), each with an evidence entry. A host that detects text needs the text extra and the same key_seed. |
| ComfyUI | ComfyUI | pip install laidline-comfyui and use the LaidlineSaveImage node instead of Save Image. |
| diffusers | diffusers | laidline_diffusers.attach(pipe, marker) marks every image the pipeline returns. |
| A voice agent | Pipecat, LiveKit, Audio | The unkeyed streaming mark survives an offline Opus transcode but not a simulated phone leg (G.711); a keyed stream's payload reads through it (Voice calls). |
| Files landing in a bucket | AWS S3 with SQS, AWS Lambda, Google Cloud Run | laidline ingest marks objects in place, from any provider; Lambda and Cloud Run are proven live. |
| Real traffic | Production | Register, a managed signing key, a shared evidence sink and the security settings. |
Limits
- A development certificate is trusted only by your own deployment; other validators show the manifest as valid but not trusted (Certificates).
- Unregistered marks are shared marks: grade D, provider not identified, whatever survives (Licensing).
- The image watermark read 100 of 100 marked COCO photos after each of a 5° rotation, a quarter or half turn, a screenshot and a 75 % centre crop (Grading).
- A PixelSeal mark of a 1024 px photo took 980 ms at the median on 2 CPU threads, with a timestamp per asset (Benchmarks).
- With
signing.tsa.required: true(the default) a mark fails when no timestamp server answers.
Troubleshooting
| Message | Cause | Fix |
|---|---|---|
error: tsa_unavailable: ... and the timestamp server did not answer: ... |
Neither DigiCert nor Sectigo answered | Allow outbound HTTP to timestamp.digicert.com and timestamp.sectigo.com, or set signing.tsa.required: false to sign without a timestamp (the manifest then grades B). LAIDLINE_OFFLINE=1 does not help: it only forbids model downloads. |
laidline.yaml already exists; not overwriting |
laidline init ran in a directory that has a configuration |
Use another directory, or --path for another file name. |
no configuration file found; pass a path, set LAIDLINE_CONFIG, or run laidline init (looked at: ...) |
The command ran outside the directory with laidline.yaml |
cd into it, pass --config, or set LAIDLINE_CONFIG. |
signing.backend=local is not allowed in production |
LAIDLINE_ENV=production is set in your shell, and the starter configuration signs with a local key |
Unset LAIDLINE_ENV for this walkthrough; production signs with a KMS or Vault key (Production). |
{"ready":false,"error":"tsa_unavailable: MarkError"} from /readyz |
The sidecar's self-test could not reach a timestamp server | Fix the network; the probe retries on its own (Production). |
curl: (7) Failed to connect from another machine |
laidline serve listens on 127.0.0.1 |
Start it with --host 0.0.0.0, after setting the inbound token. |
401 with a valid Authorization: Bearer token is required (server.mark_token_env) |
The sidecar requires an inbound token | Send Authorization: Bearer <token>, or set LAIDLINE_API_KEY for MarkClient. |
ConfigError: LAIDLINE_URL is not set (URL of the laidline sidecar) |
MarkClient.from_config() without the URL |
export LAIDLINE_URL=http://127.0.0.1:8080. |
ModuleNotFoundError: No module named 'uvicorn' |
The server extra is missing |
pip install "laidline[image,audio,server]". |
error: video_disabled: watermark.video.enabled is false |
Video marking is off by default | Set watermark.video.enabled: true in laidline.yaml, or prefix the command with LAIDLINE_WATERMARK__VIDEO__ENABLED=true. |
What was proven
On 19 Oct 2026 the steps above were run from a fresh environment built from the package, on CPU and on a GPU: an image, an audio clip and a video were marked and then detected.
Reference
- Install: extras, PyTorch builds and the model weights
- Production: what to change before real traffic
- Grading: every verdict, grade and caveat in a detect result
- Configuration: every key in
laidline.yaml - Command line: every command and option
- HTTP API: the sidecar's routes, headers and errors
- Licensing: credits, the free allowance and registration