Soft binding
A C2PA manifest can be stripped by re-encoding, transcoding, or any pipeline that drops metadata. A soft binding is a perceptual code computed from the content itself, not the metadata. When the embedded manifest is gone, a verifier recomputes the code from the bytes it has and looks it up in the registry to recover the original manifest. The signal lives in the content, so it survives when the manifest doesn't.
A soft binding is identified by its algorithm (alg) and carries an
algorithm-specific value.
Attaching soft bindings at sign time
The persisted storage modes carry a bindings: SoftBinding[], the soft bindings to
embed and index. Each is an { alg, value }, and a manifest can carry several (one per
algorithm), so a copy can recover by any of them.
interface SoftBinding {
alg: string; // soft-binding algorithm id, e.g. "io.iscc.v0"
value: string; // algorithm-specific value, e.g. an "ISCC:…" code
}
It is the same SoftBinding shape a verifier binder produces, so a binding attached at
sign feeds Binder.held at verify verbatim. Bindings live on the persisted modes only:
{ mode: "embedded" } has no bindings field, so the pairing is impossible by
construction. Each becomes its own c2pa.soft-binding assertion and its own recovery
index entry.
Computing an image ISCC with Binding.iscc
For images, compute the ISCC Image-Code with Binding.iscc(image) (the
producer-side mirror of the verifier's Binder.iscc()) and add it to the storage's
bindings:
import { AssetSdk, Binding, embed } from "@limboai/verifox-sdk-issuer";
const iscc = Binding.iscc(image); // { alg: "io.iscc.v0", value } | undefined
const { sidecar } = await sdk.sign(image, "photo.jpg", spec, {
storage: { mode: "both", bindings: iscc ? [iscc] : [] },
});
Binding.iscc returns undefined for a non-image or an undecodable input, so you
decide whether to include it. verify(bytes, [Binder.iscc()]) recomputes the same
code to recover.
ISCC (video)
For video masters, the soft binding is an ISCC Video-Code
(alg: "io.iscc.v0"); it survives transcoding because the same visual content
produces the same code, regardless of codec or container. The SDK computes image
codes only; a master's ISCC is derived out of band (the wasm build has no
video-ISCC path, and hashing a multi-GB master belongs in your pipeline). Pass the
code you computed as a binding, and recover a transcoded copy with Binder.held at
verify.
import { AssetSdk, embed } from "@limboai/verifox-sdk-issuer";
const spec = { origin: { sourceType: "digitalCapture" } };
const { sidecar } = await sdk.sign(
source, // RangeSource or Uint8Array
"master.mov", // title
spec, // manifest
{
// The video ISCC you computed out of band.
storage: { mode: "both", bindings: [{ alg: "io.iscc.v0", value: "ISCC:..." }] },
format: "video/quicktime",
},
);
const signed = embed(bytes, sidecar, "video/quicktime");
Multiple bindings
Because bindings is a list, an asset can carry more than one perceptual code,
each recoverable independently:
const bindings = [];
const iscc = Binding.iscc(image);
if (iscc) bindings.push(iscc);
await sdk.sign(image, "photo.jpg", spec, { storage: { mode: "both", bindings } });
A watermark is not attached here. Unlike a perceptual code you compute, a
watermark is embedded in the pixels, so it is applied by a separate
watermark call that watermarks the signed
image, indexes it under com.imatag.lamark.v1 (recording a c2pa.watermarked.bound
action), and pairs it with an ISCC so the manifest recovers by either. There is no
watermark value for you to attach at sign time; recovery is detection-driven. See
Recovery.
Soft binding vs. watermark
Both let a copy recover its manifest, by different but complementary means:
| ISCC (soft binding) | Watermark (watermark) | |
|---|---|---|
| Signal | perceptual content hash | invisible pixel-domain watermark |
| Survives | transcode (same visual content) | re-encode, screenshot, metadata strip |
| Applies to | images (via Binding.iscc) and video masters (out-of-band code) | images only |
| Added | a c2pa.soft-binding assertion at sign time | a watermark call after signing |
Recovery
When a copy turns up with no embedded manifest, the verifier recomputes the
soft-binding code and looks it up in the registry to recover and validate the
original manifest. For images you no longer need a manual detect round-trip:
pass [Binder.iscc()] and verify recomputes the ISCC Image-Code itself,
recovering by it when the exact content hash misses:
const results = await verifier.verify(image, [Binder.iscc()]);
An out-of-band binding you already hold, a video master's ISCC computed at
sign time, is passed as Binder.held:
const results = await verifier.verify(master, [Binder.held({ alg: "io.iscc.v0", value })]);
A watermark is recovered by detection, not a value you hold: Binder.watermark({ endpoint, token }) posts the image to the detect service and gets its binding back.
See Recovery.
See also
- File streaming: streaming sign and embed for huge masters
- Watermarking: the image-only durability mechanism
- Recovery: verifier-side soft-binding lookup