Supplier quickstart
You send an image. CheckMerch tells you whether it matches artwork registered in the rights registry, and whether the account that submitted it holds a license covering that artwork. One HTTP call, before anything goes on a press.
Base URL: https://checkmerch.io. Contact: help@checkmerch.io.
Every request and response on this page is real. The identifiers in them
(ver_..., wrk_..., grant_...) are from our demo catalog, so substitute
your own where a command takes one. The artwork and rights holders in the
examples are invented.
If a coding agent writes your integration, hand it the agent integration prompt instead: the same contract as this page, packaged as one self-contained prompt to paste.
1. Get a key
There is no signup form. Email help@checkmerch.io with your company name,
what you produce, and who should receive the key. We reply with an API key that
looks like mrc_... and carries the verify scope, which is all you need for
everything on this page. Verification is free.
Keep the key server-side. It is a bearer credential: anyone holding it can verify as you.
export CHECKMERCH_KEY=mrc_your_key_here
2. Make the call
curl -X POST https://checkmerch.io/v1/verify \
-H "Authorization: Bearer $CHECKMERCH_KEY" \
-F "image=@artwork.png"
{
"id": "ver_01m009avh5dyfcyjfay7fkekhf",
"verdict": "no_match",
"fingerprint_version": "fp-v1",
"matches": [],
"grant": null,
"created_at": "2026-08-14T14:03:08Z"
}
That is a successful call. no_match means nothing in the registry resembled
the image, which is the normal result for original artwork.
If you have the artwork on a URL we can fetch instead of a file to upload, send JSON:
curl -X POST https://checkmerch.io/v1/verify \
-H "Authorization: Bearer $CHECKMERCH_KEY" \
-H "Content-Type: application/json" \
-d '{"image_url": "https://files.yourshop.example/jobs/40871/art.png"}'
Use one or the other. image is a multipart file upload, image_url must be
reachable from the public internet.
3. Read the verdict
verdict is the single field to branch on. It has four values.
| Verdict | What we found |
|---|---|
no_match |
Nothing in the registry matched above your confidence floor. |
match_authorized |
The image matches registered artwork, and the license presented with the request covers it. |
match_unauthorized |
The image matches registered artwork, and no license covering it was presented on this request. |
match_reference |
The image matches known third-party IP held in our reference index. No authorization data exists for it. |
Verdicts are advisory. CheckMerch reports what it found and never tells you what to produce. What you do with each verdict is your policy, and most shops write it down once: which verdicts pass straight through, which pause for a human, who that human is.
match_unauthorized
{
"id": "ver_01m009emkpgnn41ngg438ncrc7",
"verdict": "match_unauthorized",
"fingerprint_version": "fp-v1",
"matches": [
{
"id": "mtch_01m009emknc9sg57tjmmqyqyd3",
"tier": 1,
"confidence": "exact",
"scores": {
"phash_distance": 0,
"embedding_similarity": 1,
"content_hash_match": true
},
"authorized": false,
"work": {
"id": "wrk_01m009dwhhwj72wqhwccecsq17",
"title": "Haunted Ramen",
"holder": {
"id": "rh_01m009dw8zfaa0m8t9ky406th5",
"name": "Aster & Vane Studio"
}
},
"licensing": {
"license_available": true,
"contact": "licensing@asterandvane.example",
"inquiry_url": "https://checkmerch.io/inquiries/new?work=wrk_01m009dwhhwj72wqhwccecsq17"
}
}
],
"grant": null,
"created_at": "2026-08-14T14:05:12Z"
}
Knowable: which registered work was matched, how closely, who registered it,
and whether they are open to licensing. When licensing.license_available is
true, contact and inquiry_url are a route to a license rather than a dead
end, which is often the fastest way to unblock a customer's order.
Not knowable: whether your customer holds a license we were not shown.
match_unauthorized means "no valid grant covering this submitter was
presented on this request," not "no license exists." If your customer says they
are licensed, section 5 is what to do about it.
match_reference
{
"verdict": "match_reference",
"matches": [
{
"tier": 2,
"confidence": "exact",
"reference": {
"id": "ref_01m009dwvdcytpbpnygwb8jjqe",
"ip_name": "Ferro the Tin Sentinel (character artwork)",
"disputed": false,
"provenance_summary": "Curated 2026-08 from publicly released catalog imagery.",
"dispute_url": "https://checkmerch.io/disputes/new?ref=ref_01m009dwvdcytpbpnygwb8jjqe"
},
"advisory": "This image matches a reference entry for widely recognized third-party IP. The rights holder does not participate in this registry and no authorization data is available. Treat as high risk."
}
],
"grant": null
}
The reference index covers widely recognized third-party IP whose owners are
not part of CheckMerch. Because they are not part of it, there is no
authorization data to check, so this verdict can never become
match_authorized no matter what your customer presents. It is a similarity
report plus a risk signal, not a legal finding. CheckMerch makes no legal
determination about your order, and the named party neither participates in
the registry nor has any view on it.
Knowable: the image is a close match for imagery associated with that IP, and
how we sourced the entry (provenance_summary).
Not knowable: everything about permission. Your customer may hold a license directly from that owner. We would not see it.
disputed: true means someone has contested the entry and the dispute is open.
The entry still matches while that is being worked out. If you believe an entry
is wrong, dispute_url is the way to say so.
match_authorized
{
"verdict": "match_authorized",
"matches": [
{
"tier": 1,
"confidence": "exact",
"authorized": true,
"work": {
"id": "wrk_01m00f68tc9zz52xe1wd58n0jv",
"title": "Haunted Ramen",
"holder": { "id": "rh_01m00f68jn5ez7a4wn9ddz7b6f", "name": "Aster & Vane Studio" }
}
}
],
"grant": {
"presented_as": "delegation",
"valid": true,
"authorizing": true,
"ordered_by": { "account": "acct_01m00f68jvv0mp0sd9aceg88ab", "name": "Meridian Merch Co." },
"unchecked": [],
"works_covered": ["wrk_01m00f68tc9zz52xe1wd58n0jv"],
"grants_applied": [
{
"id": "grant_01m00f692v7t17tks8cey8fnf6",
"granted_by": { "id": "rh_01m00f68jn5ez7a4wn9ddz7b6f", "name": "Aster & Vane Studio" },
"expires_at": "2027-08-14T15:45:29Z"
}
]
},
"created_at": "2026-08-14T15:45:48Z"
}
This is what a job for a licensed customer looks like once they have authorized
you, which is section 5. works_covered lists which of the matched works the
license actually covers, grants_applied names the licenses that did it, and
unchecked lists conditions we could not evaluate because they were not sent.
4. The confidence floor, and the matches you did not expect
min_confidence defaults to high. Accepted values, weakest to strongest:
possible, high, exact.
The floor decides the verdict only. Matches below it are still returned in
matches[], flagged below_floor: true. This surprises people, so it is worth
seeing:
curl -X POST https://checkmerch.io/v1/verify \
-H "Authorization: Bearer $CHECKMERCH_KEY" \
-F "image=@recolored-crop.png"
{
"verdict": "no_match",
"matches": [
{
"tier": 1,
"confidence": "possible",
"scores": { "embedding_similarity": 0.8937 },
"below_floor": true,
"authorized": false,
"work": {
"id": "wrk_01m009dwhhwj72wqhwccecsq17",
"title": "Haunted Ramen",
"holder": { "id": "rh_01m009dw8zfaa0m8t9ky406th5", "name": "Aster & Vane Studio" }
},
"licensing": {
"license_available": true,
"contact": "licensing@asterandvane.example",
"inquiry_url": "https://checkmerch.io/inquiries/new?work=wrk_01m009dwhhwj72wqhwccecsq17"
}
}
],
"grant": null
}
The verdict is no_match and matches[0].authorized is false. Those two
facts are not in conflict, and the first one is the answer.
Branch on verdict. Do not branch on matches[0].authorized. A
below-floor match carries authorized: false and a full licensing block
exactly like a real one, so an integration that reads the first match instead
of the verdict will flag clean jobs.
Lowering the floor to possible on the same image turns that entry into a
match_unauthorized and drops the below_floor flag. Raising it to exact
narrows to near-identical files only. high is the default because it is the
level at which recolored, resized, and re-encoded copies of registered
artwork still match.
Keep the below-floor entries out of your operator's way, but they are useful in a log: a run of them on one customer's orders is worth a look.
5. When your customer is licensed
Your customer holds a license from the rights holder. For you to get a green answer on their work, they authorize you once, and from then on you verify by saying who the order came from.
They do this, once, on their own account, either on the Printers page of their CheckMerch dashboard (they enter the email your account is under) or with one call:
curl -X POST https://checkmerch.io/v1/suppliers \
-H "Authorization: Bearer $THEIR_KEY" \
-H "Content-Type: application/json" \
-d '{"supplier_account": "acct_01m00f68jy5qhffb6vj0mjf8rx", "external_ref": "VENDOR-118"}'
You can see who has done it on the Customers page of your dashboard, or over the API, which is also where you get the account ids you will be sending:
curl https://checkmerch.io/v1/customers \
-H "Authorization: Bearer $CHECKMERCH_KEY"
{
"data": [
{
"id": "lsl_01m00f6kgex6b10t61172r9eab",
"ordered_by": { "account": "acct_01m00f68jvv0mp0sd9aceg88ab", "name": "Meridian Merch Co." },
"external_ref": "VENDOR-118",
"active": true,
"started_at": "2026-08-14T15:45:40Z"
}
]
}
Then every job for that customer carries ordered_by:
curl -X POST https://checkmerch.io/v1/verify \
-H "Authorization: Bearer $CHECKMERCH_KEY" \
-F "image=@artwork.png" \
-F "ordered_by=acct_01m00f68jvv0mp0sd9aceg88ab" \
-F "context[intended_use]=print" \
-F "context[external_ref]=JOB-40871"
{
"id": "ver_01m00f6vf9ghe9mpr71ysxefdw",
"verdict": "match_authorized",
"matches": [
{
"tier": 1,
"confidence": "exact",
"authorized": true,
"work": {
"id": "wrk_01m00f68tc9zz52xe1wd58n0jv",
"title": "Haunted Ramen",
"holder": { "id": "rh_01m00f68jn5ez7a4wn9ddz7b6f", "name": "Aster & Vane Studio" }
}
}
],
"grant": {
"presented_as": "delegation",
"valid": true,
"authorizing": true,
"ordered_by": { "account": "acct_01m00f68jvv0mp0sd9aceg88ab", "name": "Meridian Merch Co." },
"unchecked": [],
"works_covered": ["wrk_01m00f68tc9zz52xe1wd58n0jv"],
"grants_applied": [
{
"id": "grant_01m00f692v7t17tks8cey8fnf6",
"granted_by": { "id": "rh_01m00f68jn5ez7a4wn9ddz7b6f", "name": "Aster & Vane Studio" },
"expires_at": "2027-08-14T15:45:29Z"
}
]
},
"created_at": "2026-08-14T15:45:48Z"
}
Note what you did not have to do. You did not hold anything of your customer's,
and you did not have to know which of their licenses was the relevant one. You
sent an account id that is already in your system, mapped once when you set the
customer up. The registry checked that they authorized you, looked across every
license they hold, and reported in grants_applied which one covered the work
and who issued it.
That is the whole integration for licensed work: one mapping per customer, not one token per order.
Your customer can end the authorization at any time, and the rights holder can revoke the underlying license at any time. Either one takes effect on your very next call, so nothing to expire on your side and nothing to renew when a license rolls over.
If a job comes back match_unauthorized on a customer you expected to be
covered, the grant object says which of the two is missing. No relationship
gives you "valid": false and an advisory saying so. A live relationship with
no covering license gives you "valid": true with "works_covered": [], which
means your customer is authorized for other things but not this design.
Checking a grant id
Your customer may also send you a bare grant id on the paperwork. You can look it up, and it will tell you what that license covers and who holds it:
curl -X POST https://checkmerch.io/v1/tokens/introspect \
-H "Authorization: Bearer $CHECKMERCH_KEY" \
-H "Content-Type: application/json" \
-d '{"grant_id": "grant_01m00f692v7t17tks8cey8fnf6"}'
You can also send grant_id on a verify call and it will report coverage the
same way. What it will not do is change the verdict:
{
"id": "grant_01m00f692v7t17tks8cey8fnf6",
"valid": true,
"presented_as": "id",
"granted_by": { "id": "rh_01m00f68jn5ez7a4wn9ddz7b6f", "name": "Aster & Vane Studio" },
"granted_to": { "account": "acct_01m00f68jvv0mp0sd9aceg88ab", "name": "Meridian Merch Co." },
"works_covered": ["wrk_01m00f68tc9zz52xe1wd58n0jv"],
"authorizing": false,
"advisory": "A grant id identifies a license and reports what it covers. It does not establish that this account may verify on the licensee's behalf, so on a verification it does not change the verdict. The licensee authorizes the suppliers who print for them with POST /v1/suppliers; those suppliers then verify by sending ordered_by."
}
A grant id travels on purchase orders and job tickets and is not a secret, so it identifies a license rather than proving anything about who is calling. Treat it as useful context at intake, and get your customer to authorize you for the answer that counts.
There is also a status-only check that needs no key and no account:
curl https://checkmerch.io/v1/grants/grant_01m00f692v7t17tks8cey8fnf6/status
{ "status": "active", "revoked_at": null }
6. Tie a verification to your job
context is optional. It sharpens the answer and connects it to your paperwork:
external_ref: your order number, job ticket, or line item, up to 255 characters.intended_use:print,sell, ordisplay.territory: a two-letter country code.
curl -X POST https://checkmerch.io/v1/verify \
-H "Authorization: Bearer $CHECKMERCH_KEY" \
-F "image=@artwork.png" \
-F "context[external_ref]=JOB-40871" \
-F "context[intended_use]=print"
Store the returned id against your job. It is how you read the verification
back later, and it is what a rights holder will ask for if a job is ever
questioned:
curl https://checkmerch.io/v1/verifications/ver_01m00dg3ztf8d739t89rbexycv \
-H "Authorization: Bearer $CHECKMERCH_KEY"
{
"id": "ver_01m00dg3ztf8d739t89rbexycv",
"verdict": "match_authorized",
"fingerprint_version": "fp-v1",
"min_confidence": "high",
"context": { "external_ref": "JOB-40871", "intended_use": "print" },
"grant": { "id": "grant_01m00dfk2gmq2qr80b7c52t61m", "presented_as": "id" },
"created_at": "2026-08-14T15:15:55Z",
"matches": [
{
"tier": 1,
"confidence": "exact",
"authorized": true,
"work": { "id": "wrk_01m00dfjt096cmkekpeff96zb2", "title": "Haunted Ramen" }
}
]
}
That record is the answer to "prove you checked": the verdict, your own job reference, and which grant it was made against.
Two limits worth designing around. There is no lookup by external_ref, so if
you lose the id you cannot find the verification again: write it down at the
moment you get it. And there is no idempotency key, so a retried call creates a
second verification with a new id rather than returning the first. Both are
survivable if you record the id on first success and treat retries as new
checks.
7. Errors
Every error uses the same envelope, so one handler covers all of them:
{ "error": { "code": "image_unreadable", "message": "The submitted image could not be decoded." } }
| Status | Code | What it means | What to do |
|---|---|---|---|
| 503 | fingerprint_unavailable |
The registry is up, the image is fine, and fingerprinting is temporarily unavailable. | Retry. The response carries Retry-After: 30. Back off and try again; do not treat the job as checked. |
| 422 | image_unreadable |
The bytes we received are not a decodable image. | Do not retry. Wrong file, wrong format, or a truncated upload. See section 8. |
| 422 | blank_image |
The image decoded but has nothing to fingerprint: fully transparent, or one flat color. | Do not retry. Usually the wrong layer or an empty export. |
| 422 | invalid_request |
The request is malformed: no image, an image_url we could not fetch, or a bad field value. |
Do not retry unchanged. message names the field. |
| 401 | unauthorized |
Missing, malformed, or revoked key. | Check the Authorization header. Do not retry. |
| 403 | forbidden |
The key is valid but lacks the verify scope, or the account is suspended. |
Contact help@checkmerch.io. |
| 404 | not_found |
No such verification under your account. | Check the id. |
| 500 | matcher_version_mismatch |
A misconfiguration on our side. Not your request. | Retry later, and tell us if it persists. |
The important distinction is between fingerprint_unavailable and the two
image errors. 503 says nothing about the artwork. A queue that treats it as
"this image is bad" will reject clean jobs whenever we have a brief outage, and
a queue that treats it as a pass has not checked anything. Retry it, and if it
does not clear, hold the job in whatever state your policy uses for "not yet
verified."
An unfetchable image_url currently comes back as invalid_request, the same
code as a genuinely malformed request. If you use the URL form, check that your
files are reachable before you conclude the request was wrong.
8. What to send
Send the production artwork, the file that goes to the press. Raster formats
decode: PNG, JPEG, TIFF, GIF, and BMP are all accepted. Format and resolution
are not matched on, so a downscaled JPEG export of a PNG-registered design
still comes back exact. Send whatever you already have.
PDF, AI, EPS, and SVG are not accepted and return image_unreadable. Most
shops already rasterize for proofing, so the usual fix is to verify the proof
raster rather than the vector file. Flatten to a single image first: a
transparent or empty layer exported on its own returns blank_image.
Uploads are capped at 100 MB. The image_url form has a lower cap of 50 MB,
and a file over it comes back as invalid_request rather than a size error, so
prefer the upload form for large production files.
Matching survives ordinary production edits. Recolors, resizes, re-encodes,
mirroring, and single-ink versions of registered artwork still match, which
is the point. Cropped artwork, and artwork recomposed into a larger layout,
can fall to possible or below the default floor, which is where section 4
matters: those entries still show up in matches[], and a run of them on one
customer's orders is worth a look.
Set your client timeout to at least 30 seconds. A verify call does real work on the image, and a tight timeout turns a normal call into a retry that costs more than the wait.
9. Webhooks
You can register an HTTPS endpoint:
curl -X POST https://checkmerch.io/v1/webhooks \
-H "Authorization: Bearer $CHECKMERCH_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://yourshop.example/hooks/checkmerch", "events": ["match.created"]}'
The response includes a secret shown exactly once. Deliveries are signed with
it in a CheckMerch-Signature: t=<timestamp>,v1=<hmac> header, where the HMAC
is SHA-256 over <timestamp>.<raw body>.
Be aware of what this does and does not give a supplier today. Events are
delivered to the account they concern: match.created goes to the rights
holder whose work was matched, and grant.revoked goes to the license holder
and the rights holder. An account that only verifies is not the subject of
either, so registering a webhook on a verify-only key will not produce
deliveries. Verification is synchronous: the answer you need is in the
response to your own call. Register an endpoint if you also hold grants, and
talk to us if you want notification for something else.
10. Getting help
Email help@checkmerch.io. Include the verification id if you have one, or
the request you sent if you do not. If a verdict looks wrong, send us the id
and the artwork and we will tell you what matched and why.