Check a Listing
curl --request POST \
--url https://api.signa.so/v1/screening/listings \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.signa.so/v1/screening/listings"
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/screening/listings', 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/screening/listings",
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/screening/listings"
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/screening/listings")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.signa.so/v1/screening/listings")
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_bodyScreening
Check a Listing
Screen a whole product listing for trademark conflicts (alpha, internal testing)
POST
/
v1
/
screening
/
listings
Check a Listing
curl --request POST \
--url https://api.signa.so/v1/screening/listings \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.signa.so/v1/screening/listings"
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/screening/listings', 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/screening/listings",
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/screening/listings"
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/screening/listings")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.signa.so/v1/screening/listings")
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. Listing checks are not open to customers yet. 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.
A listing check returns a risk signal, not legal advice.
Known limitation (alpha). The current build over-flags listings that
contain common dictionary words, so it returns
caution for the large
majority of ordinary listings. A verdict-separation redesign is in progress
to restore precision. Until it lands, treat a caution verdict as no-signal,
not evidence of risk, and do not rely on the endpoint for automated
enforcement decisions. Real-world validation against the full production
register superseded the earlier precision numbers measured on a small
synthetic test index.Overview
POST /v1/screening/listings answers “is this LISTING risky?” — one level up
from screening a name. You send the
commerce text of a product listing (title, brand, description, keywords) and it:
- Extracts candidate marks deterministically (no LLM at request time), with provenance (source field + character span per candidate).
- Fans out each candidate through the screening core.
- Aggregates to one listing-level verdict (
clear/caution/high_risk), deduplicating conflicts across candidates.
Request
{
"title": "Vintage AirPods Case — Handmade Leather",
"brand": "LeatherCraft Co",
"description": "Compatible with AirPods Pro. Genuine leather.",
"keywords": ["airpods case", "leather", "handmade"],
"nice_classes": [18],
"jurisdictions": ["US"],
"sensitivity": "standard"
}
title is required. Fields are generic — there are no marketplace-specific
fields (Etsy/Amazon mappings live in docs recipes, not the schema).
| Field | Type | Notes |
|---|---|---|
title | string | Required. Split on hard delimiters, then n-grammed. |
brand | string | Always screened, never suppressed. |
description | string | High-precision markers only (quoted / ™®-adjacent / “compatible with X”). |
keywords | string[] | Platform-neutral tags/keywords/search terms. |
category | string | Context only — never itself a candidate. |
nice_classes | number[] | Hard filter + class-conditioned descriptive suppression. |
jurisdictions | string[] | Territory codes (e.g. US, EU). Protection-scope, applied unconditionally as in Screen a Name: a country request pulls in EU-wide rights that protect there, and a regional request pulls in every member state’s national register. There is no territory_match opt-out on screening. |
offices | string[] | Registering office codes, uppercase ST.3 (e.g. US). |
include | live | all | live (default) keeps live marks. |
sensitivity | strict | standard | broad | Primary-tier threshold; the title/keyword tier is always strict. |
Response
A bespoke envelope: a leadinglisting decision block, an evidence-first
matches[] payload, the back-compatible data[] of
listing_candidate (each conflict is byte-identical to a
screening hit), and a trailing
suppressed[] audit list explaining why candidates were not screened.
{
"object": "list",
"listing": {
"verdict": "high_risk",
"summary": { "high": 1, "medium": 0, "low": 0, "informational": 0 },
"candidates_screened": 3,
"rules_version": "screening-v1",
"verdict_version": "listing-verdict-v8",
"extractor_version": "listing-extractor-v1:…",
"selector_version": "listing-selector-v1:…",
"as_of": "2026-07-01T00:00:00Z",
"warnings": [],
"analysis_status": "complete",
"auto_approve_eligible": false,
"class_provenance": { "source": "provided_unverified", "confidence": null },
"resolved_nice_classes": [18],
"dictionaries_version": "…",
"decision_asset_version": "a1b2c3d4e5f60718"
},
"matches": [
{
"object": "listing_match",
"disposition": "actionable",
"reason_codes": ["exact_mark", "live_status"],
"rights_domain": "trademark",
"goods_services_text_available": true,
"candidate": { "term": "AirPods", "normalized": "airpods", "tier": "S", "field": "keywords" },
"registration": { "object": "screening_hit", "risk_level": "high", "…": "…" }
}
],
"data": [
{
"object": "listing_candidate",
"term": "AirPods",
"normalized": "airpods",
"tier": "S",
"sources": [{ "field": "keywords", "span": [0, 7], "keyword_index": 0 }],
"verdict": "high_risk",
"nice_classes_used": [18],
"hits": [ { "object": "screening_hit", "risk_level": "high", "…": "…" } ]
}
],
"suppressed": [
{ "object": "suppressed_candidate", "term": "vintage", "field": "title", "span": [0, 7], "reason": "jargon" }
],
"has_more": false,
"pagination": { "cursor": null },
"request_id": "req_…"
}
Reading the result
analysis_status is a closed completeness enum and is orthogonal to the
tri-state verdict. Automated approval must key off
auto_approve_eligible (equivalent to verdict === "clear" and
analysis_status === "complete") and must not use verdict alone. Incomplete
or unsupported analysis is never auto-approvable.
class_provenance explains where the class set came from;
provided_unverified means the caller supplied it and Signa has not verified it
against a platform taxonomy. resolved_nice_classes is the listing-level set
actually screened. dictionaries_version and decision_asset_version make the
deterministic decision assets auditable. There is no public risk_score.
Listing checks cover English only. When the title and brand are majority non-Latin
(CJK, Thai, Arabic, or Cyrillic), the endpoint returns
analysis_status: "unsupported_language", floors the verdict to caution, and
does not run the text through English screening.
Safety guarantees
- Never a false clear. Any search-backend error →
503(never a partial “clear”). If the candidate budget is exceeded, the listing can never beclear— it is forced tocautionwith acandidate_budget_cappedwarning. - Coverage-gated clear. A would-be
clearis forced tocaution(with anincomplete_office_coveragewarning) when a requested territory’s register is not covered live in Signa, on the same protection-scope basis as Screen a Name. The listing verdict enum staysclear/caution/high_risk, so the incomplete-coverage case surfaces ascautionhere rather than a distinct verdict. - Informational-only conflicts. A tier-S identical name in an unrelated
class (e.g. a famous word used descriptively) is surfaced with
"informational": truebut does not escalate the listing verdict.
SDK
const result = await signa.screening.checkListing({
title: "Vintage AirPods Case — Handmade Leather",
nice_classes: [18],
});
if (!result.listing.auto_approve_eligible) {
for (const match of result.matches) {
console.log(match.disposition, match.reason_codes, match.registration);
}
}