Compare Marks
curl --request POST \
--url https://api.signa.so/v1/compare \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.signa.so/v1/compare"
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.signa.so/v1/compare', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.signa.so/v1/compare",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.signa.so/v1/compare"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.signa.so/v1/compare")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.signa.so/v1/compare")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_bodyCompare
Compare Marks
Score one candidate against up to 10 specific trademark conflicts (alpha, internal testing)
POST
/
v1
/
compare
Compare Marks
curl --request POST \
--url https://api.signa.so/v1/compare \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.signa.so/v1/compare"
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.signa.so/v1/compare', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.signa.so/v1/compare",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.signa.so/v1/compare"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.signa.so/v1/compare")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.signa.so/v1/compare")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_bodyAlpha, internal testing. Compare is not open to customers yet, and the risk bands are
still being calibrated. These endpoints run on internal and design-partner keys during internal testing and their
request and response shapes can still change. Email
support@signa.so to join the early access list.
Compare returns an informational comparison, not legal advice or a registrability
determination.
Overview
Screening finds possible conflicts for a candidate. Compare scores pairs you already chose: one candidate against 1–10 register records or inline marks. Each request-ordered result includes the same coarse risk-band vocabulary used by served screening, plus numeric string, phonetic, and goods/services measurements. Compare does not return an overall or confusion-probability score. The endpoint is synchronous and deterministic within the reported rule, mapping, resolver, and corpus versions. Each request is billed at 10 units, regardless of the number of conflicts (up to 10). Optionally supply anIdempotency-Key; retrying the same body with the same key replays the
original response without billing the comparison again.
Request
Supply the candidate either as an existingtrademark_id or as an inline
{ mark, use } object. Each conflict is either a trademark_id or an inline
{ mark, nice_classes, goods_services?, status? } object. These alternatives
are mutually exclusive.
curl --request POST https://api.signa.so/v1/compare \
--header "Authorization: Bearer $SIGNA_API_KEY" \
--header "Idempotency-Key: compare-kofee-001" \
--header "Content-Type: application/json" \
--data '{
"candidate": {
"mark": "KOFEE",
"use": {
"nice_classes": [30, 43],
"business_description": "Coffee beans and cafe services"
}
},
"conflicts": [
{ "trademark_id": "tm_abc123" },
{
"mark": "COFFEE HOUSE",
"nice_classes": [30, 43],
"goods_services": "Coffee and restaurant services",
"status": "active"
}
],
"jurisdictions": ["US"],
"offices": ["US"]
}'
| Field | Type | Notes |
|---|---|---|
candidate | object | Required. Exactly one of { trademark_id } or { mark, use }. Mark text is 2–200 characters. |
candidate.use.nice_classes | number[] | Intended Nice classes, 1–45. |
candidate.use.goods_services | object[] | Structured lines shaped as { nice_class?, text }; text is at most 500 characters. |
candidate.use.business_description | string | Plain-English intended use, up to 2,000 characters. |
conflicts | object[] | Required. 1–10 items. Each is exactly { trademark_id } or an inline conflict. |
conflicts[].nice_classes | number[] | Required for inline conflicts; at least one class, 1–45. |
conflicts[].goods_services | string | Optional goods/services text, up to 2,000 characters. |
conflicts[].status | string | Optional closed vocabulary: active, pending, inactive, or unknown; defaults to unknown. Unknown is treated as live so missing status never lowers risk. |
jurisdictions | string[] | Optional ISO 3166-1 alpha-2 territory codes such as US and EU; trimmed, uppercased, maximum 30. |
offices | string[] | Optional office codes such as US and EM; trimmed, maximum 10 characters each and 30 items. ST.3 codes and legacy internal codes (uspto, euipo) resolve to one canonical office identity and duplicates are collapsed; unrecognized tokens pass through as-is. |
Response
{
"object": "list",
"comparison": {
"candidate": {
"mark": "KOFEE",
"normalized": "kofee",
"effective_nice_classes": [30, 43]
},
"rules_version": "compare-v1.1",
"band_rules_version": "screening-v1",
"mapping_version": "mark-search-v3",
"resolver": {
"engine_version": "resolver-v1",
"ruleset_version": "rules-v1",
"catalog_version": "nice-12"
},
"as_of": "2026-07-13T00:00:00.000Z",
"warnings": [],
"disclaimer": "This comparison is informational, not legal advice or a registrability determination; the band table is under active recalibration."
},
"data": [
{
"object": "comparison_result",
"comparison_status": "ok",
"review_recommended": true,
"warnings": [],
"conflict": {
"trademark_id": "tm_abc123",
"mark": "COFFEE",
"nice_classes": [30],
"status_as_evaluated": "active",
"record_as_of": "2026-07-13T00:00:00.000Z"
},
"risk_level": "medium",
"reason_codes": ["phonetic_mark", "same_nice_class"],
"mark_match": {
"level": "strong",
"matched_levels": ["phonetic_strong"]
},
"goods_services": {
"level": "same_class",
"overlap_classes": [30],
"text_similarity": 0.8
},
"similarity": { "string": 0.67, "phonetic": 1, "semantic": null }
}
],
"summary": { "high": 0, "medium": 1, "low": 0, "none": 0, "not_comparable": 0, "review_recommended": 1 },
"has_more": false,
"pagination": { "cursor": null },
"request_id": "req_…"
}
comparison_status, risk levels, and match levels are closed vocabularies.
reason_codes, data[].warnings, and comparison.warnings are open vocabularies; tolerate new
values. matched_levels names engine clauses for the pair. Screening calls its
corresponding post-sensitivity-gate field admitted_match_levels; Compare has
no sensitivity admission gate. similarity.semantic is reserved and currently
always null.
review_recommended is true exactly when the comparison succeeded and at
least one mark-similarity clause fired. It deliberately includes dead-mark and
below_screening_band pairs: it is a conservative human-review triage bit,
independent of risk_level. summary.review_recommended counts those results.
An unknown conflict ID degrades to not_found. A conflict whose database
record exists but cannot be compared, because it is design-only or its search
document is still being indexed, degrades to not_comparable; index lag is
reported in that item’s warnings. The other batch items still succeed. The
The five risk/status buckets in summary count every requested conflict exactly
once; review_recommended is an additional, overlapping count.
Error codes
Compare can returninsufficient_use_profile when the candidate’s intended use
cannot be resolved, batch_too_large when more than 10 conflicts are supplied,
or not_screenable when a candidate trademark has no screenable mark text.
Why can risk_level be null?
An ok comparison has no band only when no mark-similarity clause fired. Those
pairs count in summary.none:
| Reason code | risk_level | Meaning |
|---|---|---|
no_mark_similarity | null | The marks did not match any meaningful name-similarity clause, so assigning a risk band would invent a signal. |
below_screening_band | low | A clause matched, but the pair’s mark and goods/services combination falls below the served screening band table; review is still recommended. |
Suggest → screen → compare
Use classification suggestion to turn plain English into classes, screening to find candidates, then Compare to inspect a selected pair. This sequence usesjq to pass results between calls:
# 1. Turn the intended use into suggested Nice classes.
curl --silent --request POST https://api.signa.so/v1/classifications/suggest \
--header "Authorization: Bearer $SIGNA_API_KEY" \
--header "Content-Type: application/json" \
--data '{"description":"Coffee beans and cafe services"}' > /tmp/signa-classes.json
CLASSES=$(jq -r '[.classes[].class_number] | join(",")' /tmp/signa-classes.json)
# 2. Find possible conflicts.
curl --silent --get https://api.signa.so/v1/screening \
--header "Authorization: Bearer $SIGNA_API_KEY" \
--data-urlencode "q=KOFEE" \
--data-urlencode "nice_classes=$CLASSES" \
--data-urlencode "jurisdictions=US" > /tmp/signa-screen.json
CONFLICT_ID=$(jq -r '.data[0].trademark.id' /tmp/signa-screen.json)
CLASS_JSON=$(jq '[.classes[].class_number]' /tmp/signa-classes.json)
# 3. Score the selected pair.
jq -n --arg id "$CONFLICT_ID" --argjson classes "$CLASS_JSON" '{
candidate: {
mark: "KOFEE",
use: {nice_classes: $classes, business_description: "Coffee beans and cafe services"}
},
conflicts: [{trademark_id: $id}],
jurisdictions: ["US"]
}' | curl --request POST https://api.signa.so/v1/compare \
--header "Authorization: Bearer $SIGNA_API_KEY" \
--header "Content-Type: application/json" \
--data-binary @-
Intent-to-use candidates
You do not need to know the Nice system before comparing a proposed mark. Describe the planned business in ordinary language, call Suggest Classifications, review its ranked classes, then place those class numbers and the original description incandidate.use. Compare resolves that intent into the effective classes echoed
in comparison.candidate.effective_nice_classes before scoring every pair.
SDK
const result = await signa.compare.compare({
candidate: {
mark: 'KOFEE',
use: {
nice_classes: [30, 43],
business_description: 'Coffee beans and cafe services',
},
},
conflicts: [{ trademark_id: 'tm_abc123' }],
jurisdictions: ['US'],
});
console.log(result.data[0]?.risk_level, result.data[0]?.similarity);
Limitations
- Compare v1 is text-only; it does not compare logos, designs, or images.
- Numeric dimensions are measurements beneath the band, not a legal conclusion or an overall confusion probability.
- The band table is under active recalibration.
Compare vs Screening divergence FAQ
Compare and Screening use the same mark and goods/services levels. Compare must never return a less alarming band for a pair that the equivalent broad screen surfaced. A few differences are expected because Screening searches a corpus while Compare evaluates a chosen pair.Why can Screening say medium when Compare says high?
Screening can demote a weak or crowded mark using corpus-wide evidence that a
pair-only comparison cannot see. The Screening hit then includes
weak_mark_demoted or crowded_field. Compare deliberately gives the more
conservative reading, and it can be only one band higher.
SCREEN=$(curl -sG https://api.signa.so/v1/screening \
-H "Authorization: Bearer $SIGNA_API_KEY" \
--data-urlencode "q=YOUR_MARK" --data-urlencode "nice_classes=30" \
--data-urlencode "sensitivity=broad" --data-urlencode "include=all")
ID=$(jq -r '.data[0].trademark.id' <<<"$SCREEN")
jq -n --arg id "$ID" '{candidate:{mark:"YOUR_MARK",use:{nice_classes:[30]}},conflicts:[{trademark_id:$id}]}' |
curl -s https://api.signa.so/v1/compare -H "Authorization: Bearer $SIGNA_API_KEY" \
-H "Content-Type: application/json" --data-binary @-
Why does Compare score a pair that default Screening did not show?
Compare evaluates every match tier. Screening defaults tostandard, while a
fuzzy_2-only match is admitted only at broad sensitivity.
curl -sG https://api.signa.so/v1/screening \
-H "Authorization: Bearer $SIGNA_API_KEY" \
--data-urlencode "q=QUORUM" --data-urlencode "nice_classes=9" \
--data-urlencode "sensitivity=broad" --data-urlencode "include=all"
curl -s https://api.signa.so/v1/compare \
-H "Authorization: Bearer $SIGNA_API_KEY" -H "Content-Type: application/json" \
-d '{"candidate":{"mark":"QUORUM","use":{"nice_classes":[9]}},"conflicts":[{"mark":"QUXRUZ","nice_classes":[9]}]}'
sensitivity=broad&include=all when validating Screening
against Compare.
Why does a Madrid-family result change when I add a jurisdiction?
Madrid families can contain both live and dead territories. An unscoped call uses family-wide liveness; a scoped call evaluates only the requested territories. Passing a jurisdiction to one surface but not the other therefore changes the record state being evaluated.curl -sG https://api.signa.so/v1/screening \
-H "Authorization: Bearer $SIGNA_API_KEY" \
--data-urlencode "trademark_id=$CANDIDATE_ID" \
--data-urlencode "jurisdictions=EU" --data-urlencode "sensitivity=broad" \
--data-urlencode "include=all"
curl -s https://api.signa.so/v1/compare \
-H "Authorization: Bearer $SIGNA_API_KEY" -H "Content-Type: application/json" \
-d "{\"candidate\":{\"trademark_id\":\"$CANDIDATE_ID\"},\"conflicts\":[{\"trademark_id\":\"$CONFLICT_ID\"}],\"jurisdictions\":[\"EU\"]}"
jurisdictions (and offices, when used) to both
calls.
Why can Compare score an identical mark in an unrelated class?
Supplyingnice_classes to Screening is a retrieval hard filter, so a record
outside those classes is never surfaced. Compare does not use class as a
retrieval filter: it evaluates the supplied pair and an identical, live mark in
an unrelated class can score medium.
curl -sG https://api.signa.so/v1/screening \
-H "Authorization: Bearer $SIGNA_API_KEY" \
--data-urlencode "q=CLASHSCOPE" --data-urlencode "nice_classes=30" \
--data-urlencode "sensitivity=broad" --data-urlencode "include=all"
curl -s https://api.signa.so/v1/compare \
-H "Authorization: Bearer $SIGNA_API_KEY" -H "Content-Type: application/json" \
-d '{"candidate":{"mark":"CLASHSCOPE","use":{"nice_classes":[30]}},"conflicts":[{"mark":"CLASHSCOPE","nice_classes":[9]}]}'