Skip to Content
APISoft binding

Soft binding

A hard binding ties a C2PA manifest to the exact bytes of an asset: change one byte and it breaks. A soft binding ties a manifest to a value derived from the content, so a verifier holding only the asset can ask a registry “who signed this?” without carrying the manifest along.

Monolith exposes that registry at /api/c2pa/*, in the shape C2PA 2.2 resolution expects. The lookup endpoints are public — no bearer token, by design, since a verifier is usually a stranger. Only POST /api/c2pa/manifests, which mints a manifest, is authenticated.

The binding value

The soft binding value is the SHA-256 of the asset — the same digest as the 0x… fingerprint used everywhere else in this API, encoded base64 instead of hex.

# 0xab… -> q83v… openssl dgst -binary -sha256 asset.jpg | base64
const value = Buffer.from(fingerprint.replace(/^0x/, ''), 'hex').toString('base64')

The algorithm identifier is com.joinmonolith.sha256. Two legacy names, org.monolith.sha256 and sha256, are still accepted on lookup so manifests signed before 2026-05-29 keep resolving; new integrations should send the canonical name.

curl https://api.joinmonolith.com/api/c2pa/services/supportedAlgorithms
{ "fingerprints": [{ "alg": "com.joinmonolith.sha256" }] }

Resolving a fingerprint

GET /api/c2pa/matches/byBinding takes the algorithm and the base64 value:

curl -G https://api.joinmonolith.com/api/c2pa/matches/byBinding \ --data-urlencode "alg=com.joinmonolith.sha256" \ --data-urlencode "value=q83vEjRWeJCrze8SNFZokKvN7xI0VmiQq83vEjRWeJA="
{ "matches": [ { "manifestId": "550e8400-e29b-41d4-a716-446655440000", "endpoint": "https://cdn.joinmonolith.com/manifests/550e8400.json", "similarityScore": 100 } ] }

POST to the same path carries { "alg", "value" } as a JSON body instead — identical resolution, useful when a proxy logs query strings.

similarityScore is always 100: SHA-256 matching is exact, never fuzzy. A fingerprint nobody registered returns { "matches": [] }, not a 404.

Where matches come from

Resolution is two-stage. Artifacts registered through Monolith are matched first. If none exist, the on-chain registry is consulted — third parties can write bindings straight to the contract, and those entries have no Monolith record. On-chain matches carry two extra fields:

FieldMeaning
sourcecurated or public. Curated entries win; public writes are permissionless and could otherwise shadow a curated record.
registeredByWallet that wrote the on-chain entry.

Matches served from Monolith’s own records omit both.

Reading the same registry directly from the chain — the ABI, the resolver functions, and the event log — is covered in Contracts → Soft binding.

An RPC outage degrades to an empty matches array rather than a 5xx — resolution is a public read path and verifiers should never see it fail hard. An empty result therefore means “nothing found right now”, not “provably unregistered”.

Letting the server hash

If the caller can’t hash locally, POST /api/c2pa/matches/byContent takes the file and computes the digest server-side:

curl -X POST https://api.joinmonolith.com/api/c2pa/matches/byContent \ -F "file=@asset.jpg"

The response is the same matches array. The bytes are hashed in memory and never stored.

Fetching a manifest

GET /api/c2pa/manifests/{manifestId} resolves the ID from a match (or from a create response) to where the manifest lives:

{ "manifestUrl": "https://cdn.joinmonolith.com/manifests/550e8400.json", "manifestBinaryUrl": "https://cdn.joinmonolith.com/manifests/550e8400.c2pa" }

manifestBinaryUrl is the raw JUMBF sidecar, when one was produced. A manifest whose Artifact is still pending has no published URL yet and returns 404 — retry once it leaves that state.

Creating a manifest

POST /api/c2pa/manifests signs a manifest for an asset, publishes it, and queues its registration on chain. This is the only authenticated route on this surface — send Authorization: Bearer <token>, either a Monolith JWT or a monolith_… API key.

With the asset (multipart/form-data) — the fingerprint is derived from the bytes, so the caller hashes nothing:

curl -X POST https://api.joinmonolith.com/api/c2pa/manifests \ -H "Authorization: Bearer monolith_AbCd…wx12" \ -F "markId=YOUR_MARK_UUID" \ -F "file=@asset.jpg"

Embeddable images get the manifest embedded; every other media type gets a signed drunken-bishop card. Sending fingerprint alongside the file is allowed, but a value that doesn’t match the bytes is a 400.

Without it (application/json) — fingerprint and filename are then required, and the manifest is fingerprint-only, exactly like POST /api/v1/artifacts:

curl -X POST https://api.joinmonolith.com/api/c2pa/manifests \ -H "Authorization: Bearer monolith_AbCd…wx12" \ -H "Content-Type: application/json" \ -d '{ "markId": "YOUR_MARK_UUID", "fingerprint": "0xab83ef12345678...", "filename": "asset.jpg" }'

Either way the response closes the loop — softBinding.value is exactly what byBinding expects, so no follow-up read is needed:

{ "manifestId": "550e8400-e29b-41d4-a716-446655440000", "fingerprint": "0xab83ef12345678...", "softBinding": { "alg": "com.joinmonolith.sha256", "value": "q83vEjRWeJCrze8SNFZokKvN7xI0VmiQq83vEjRWeJA=" }, "manifestUrl": "https://cdn.joinmonolith.com/manifests/550e8400.json", "manifestBinaryUrl": "https://cdn.joinmonolith.com/manifests/550e8400.c2pa", "signedAssetUrl": "https://cdn.joinmonolith.com/assets/550e8400.jpg", "status": "pending", "transactionHash": null, "createdAt": "2026-05-03T17:00:00Z" }

status is normally pending at creation: the on-chain write is queued, and transactionHash fills in once it lands.

Optional fields: mediaType, size, lastModified, and previousFingerprint — the last links the previous version as a C2PA parentOf ingredient.

What applies

The same rules as any Artifact creation: the caller must be a member of the Mark, tier claim limits are enforced, and an API key may only write into its own organization’s Marks.

StatusCause
400Invalid body, or an upload that doesn’t match the supplied fingerprint
401Missing or invalid bearer token
403Not a member of the Mark, tier limit reached, or API_KEY_ORG_MISMATCH
404Mark not found
409A manifest already exists for this fingerprint

See Errors for the full code list and Tiers & limits for the caps.

Endpoints at a glance

MethodPathAuthPurpose
GET/api/c2pa/services/supportedAlgorithmsnoneAlgorithms this registry resolves
GET/api/c2pa/matches/byBindingnoneResolve a base64 fingerprint (query params)
POST/api/c2pa/matches/byBindingnoneResolve a base64 fingerprint (JSON body)
POST/api/c2pa/matches/byContentnoneUpload the asset, server hashes it
GET/api/c2pa/manifests/{manifestId}noneManifest URLs for an ID
POST/api/c2pa/manifestsbearerSign, publish and register a manifest

The full schema-aware reference with try-it-out lives at /api/reference.

Last updated on