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 | base64const 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:
| Field | Meaning |
|---|---|
source | curated or public. Curated entries win; public writes are permissionless and could otherwise shadow a curated record. |
registeredBy | Wallet 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.
| Status | Cause |
|---|---|
400 | Invalid body, or an upload that doesn’t match the supplied fingerprint |
401 | Missing or invalid bearer token |
403 | Not a member of the Mark, tier limit reached, or API_KEY_ORG_MISMATCH |
404 | Mark not found |
409 | A manifest already exists for this fingerprint |
See Errors for the full code list and Tiers & limits for the caps.
Endpoints at a glance
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /api/c2pa/services/supportedAlgorithms | none | Algorithms this registry resolves |
GET | /api/c2pa/matches/byBinding | none | Resolve a base64 fingerprint (query params) |
POST | /api/c2pa/matches/byBinding | none | Resolve a base64 fingerprint (JSON body) |
POST | /api/c2pa/matches/byContent | none | Upload the asset, server hashes it |
GET | /api/c2pa/manifests/{manifestId} | none | Manifest URLs for an ID |
POST | /api/c2pa/manifests | bearer | Sign, publish and register a manifest |
The full schema-aware reference with try-it-out lives at /api/reference.