Skip to content

Verifying

There are two ways to verify an MC4P certificate. Call the verify endpoint and read the verdict, or fetch the public keys once and check the signature yourself.

Post the page URL you loaded, and the MC4P document you found on it.

Terminal window
curl -X POST https://api.mintall.ai/api/v1/mc4p/products/$PRODUCT_ID/verify \
-H "Content-Type: application/json" \
-d '{
"productUrl": "https://merchant.example/products/pendant",
"document": {
"productID": "...",
"url": "https://merchant.example/products/pendant",
"mintall:issuedFor": { "domain": "merchant.example" },
"proof": { "jwt": "eyJ..." }
}
}'

productUrl is required. document is optional: send it and the signature checks run, omit it and only the domain checks run.

Send the page’s MC4P Product node as a nested object, not the whole JSON-LD graph. Unknown keys are ignored, so passing the node through untouched is fine. A stringified node is parsed for you and verifies normally. A payload that isn’t a single node, such as a graph array, isn’t rejected with a 422. The document is dropped and the verdict is unverified, with a warning saying why.

{
"status": "verified",
"productId": "...",
"issuedFor": "merchant.example",
"checks": {
"domainKnown": true,
"domainVerified": true,
"boundToDomain": true,
"boundToUrl": true,
"productIdMatches": true,
"signatureValid": true,
"certificateIntegrity": true,
"timestampValid": true,
"trustDataCurrent": true
},
"warnings": [],
"errors": []
}
status Meaning
verified A signed document, and every load-bearing check passed.
failed A signed document, and at least one load-bearing check did not pass.
unverified No signed claim was submitted.

unverified is the verdict for a certificate with no proof or a request with no document at all.

Six checks are load bearing. All six must be true for verified:

  • signatureValid — the RS256 signature matches a Mintall public key
  • certificateIntegrity — the JWT decodes and its claims are intact
  • timestampValidiat and exp are in range
  • boundToDomain — the signed domain claim matches the page’s domain
  • productIdMatches — the signed productId matches the product being verified
  • boundToUrl — the signed url matches the page the document was found on

boundToUrl is the anti-copy check. A certificate lifted onto another listing still carries the URL it was issued for, so it fails here without anyone needing to trust the page.

domainKnown, domainVerified, and trustDataCurrent are advisory and do not decide the verdict. trustDataCurrent compares the coverage counts signed into the certificate against the counts now: false means the merchant’s catalog moved since issuance, not that the certificate is invalid.

Fetch and cache the RFC 7517 key set to verify certificates locally. Use a remote JWKS client to refresh the keys automatically when the cache expires or a certificate references a new key.

Terminal window
curl https://api.mintall.ai/api/v1/.well-known/jwks.json
import { createRemoteJWKSet, jwtVerify } from "jose";
const jwks = createRemoteJWKSet(
new URL("https://api.mintall.ai/api/v1/.well-known/jwks.json"),
);
const { payload } = await jwtVerify(certificate.proof.jwt, jwks);
if (payload.domain !== new URL(pageUrl).hostname) {
throw new Error("Certificate is not authorized for this domain");
}
if (payload.url && payload.url !== pageUrl) {
throw new Error("Certificate was issued for a different page");
}

Verifying the signature gets you these claims. Everything here is signed, so none of it depends on trusting the page.

Claim Meaning
iss Issuing API base URL
iat / exp Issued at, and 7 days later
domain The domain the certificate is authorized for
url The product page URL it was issued for
productId Mintall’s product ID
externalProductId The merchant platform’s own product ID
productName Product name at issuance
storeName / storeUrl Merchant identity at issuance
certifiedAssets Images with a C2PA certification
totalAssets Images on the product
coveragePercent certifiedAssets over totalAssets

Doing it yourself skips domainKnown, domainVerified, and trustDataCurrent: those compare against live Mintall state and need the verify endpoint.