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.
Call the verify endpoint
Section titled “Call the verify endpoint”Post the page URL you loaded, and the MC4P document you found on it.
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": []}The three verdicts
Section titled “The three verdicts”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.
Reading the checks
Section titled “Reading the checks”Six checks are load bearing. All six must be true for verified:
signatureValid— the RS256 signature matches a Mintall public keycertificateIntegrity— the JWT decodes and its claims are intacttimestampValid—iatandexpare in rangeboundToDomain— the signeddomainclaim matches the page’s domainproductIdMatches— the signedproductIdmatches the product being verifiedboundToUrl— the signedurlmatches 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.
Verify offline with JWKS
Section titled “Verify offline with JWKS”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.
curl https://api.mintall.ai/api/v1/.well-known/jwks.jsonimport { 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");}import jwtfrom jwt import PyJWKClient
jwks = PyJWKClient("https://api.mintall.ai/api/v1/.well-known/jwks.json")key = jwks.get_signing_key_from_jwt(certificate["proof"]["jwt"])
payload = jwt.decode( certificate["proof"]["jwt"], key.key, algorithms=["RS256"], issuer="https://api.mintall.ai",)
if payload["domain"] != urlparse(page_url).hostname: raise ValueError("Certificate is not authorized for this domain")Signed claims
Section titled “Signed claims”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.