Skip to main content

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.

sign-master.ts
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)
Signalperceptual content hashinvisible pixel-domain watermark
Survivestranscode (same visual content)re-encode, screenshot, metadata strip
Applies toimages (via Binding.iscc) and video masters (out-of-band code)images only
Addeda c2pa.soft-binding assertion at sign timea 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