Search and filters
The company filter AST, the field vocabulary that defines it, and how canonical hashing makes an identical search free for 24 hours.
The vocabulary comes first
GET /api/v1/catalog?section=filters is the grammar for everything on this page: every filterable
field with its type, the operator ids it accepts, closed enum domains where they exist,
and the rule parameters it takes. Operator definitions are listed once under operators
and referenced by id.
It is projected from the same filter schema the server validates and evaluates against, so nothing it lists is unsupported and nothing it omits will be accepted. Free, cacheable, and safe to poll.
curl https://www.spore.ee/api/v1/catalog?section=filters \
-H "Authorization: Bearer $SPORE_API_KEY"The filter AST
A filter is a tree of two node kinds:
- a group —
{ "type": "group", "logic": "AND" | "OR", "children": [...] } - a rule —
{ "type": "rule", "fieldId": ..., "operator": ..., "value": ... }
Groups nest, so any boolean combination is expressible. fieldId and operator must both
come from the catalog.
Searching
POST /api/v1/search runs the AST against the live company population:
{
"filter": {
"type": "group",
"logic": "AND",
"children": [
{
"type": "rule",
"fieldId": "countryCode",
"operator": "in",
"value": ["EE"]
}
]
},
"sortBy": "revenue",
"sortOrder": "desc",
"maxCredits": 5
}The response has three parts:
count— how many Legal entities matched.analytics— the full market-intelligence aggregate over every match, not just the sample.sample— up to 100 Legal entities, capped server-side, minted as Companies of your organization and unlocked from birth. Each row is a Company with its live registry section, exactly asGET /api/v1/companies/{companyId}returns it.
The response also carries the immutable searchResultId, filterHash, newlyUnlocked,
alreadyUnlocked, charged, balance, replayed, and expiresAt fields. newlyUnlocked
and alreadyUnlocked are opaque Company ids.
The cap is deliberate. Aggregate intelligence over an entire population is what search is
for; the full roster leaves search through the receipt's own door,
POST /api/v1/search/{searchId}/unlock, which mints and unlocks every match at one credit
per Legal entity you have not unlocked before. See
Segments and the loop.
The canonical hashes
Every response carries filterHash: a SHA-256 over a canonical serialization of the
filter in which object keys are sorted. Two ASTs that mean the same thing therefore hash
the same, whatever order you wrote their keys in.
Array order is still significant — ["EE", "LV"] and ["LV", "EE"] are different
serializations, so normalize your own lists if you want them to dedupe.
The 24-hour replay identity is a separate request hash over normalized filter, sortBy,
sortOrder, and sampleSize. Changing the sort or sample size is a distinct search even
when filterHash is unchanged. The returned searchResultId is the receipt for that
exact request.
Charging and free replay
A search charges max(5, newly unlocked sample ids), never more than its quote,
max(5, sampleSize), which maxCredits and the key's spend bounds are checked against.
"dryRun": true answers { "dryRun": true, "quote": … } and nothing else: it reads no
registry data, mints nothing, and charges nothing. Repeating the identical request
within 24 hours is free. The response says which happened:
| Field | Meaning |
|---|---|
charged | credits actually deducted — 0 on a replay |
replayed | true when the 24-hour request identity matched an unexpired receipt |
balance | the organization's balance after the call |
The window is per organization, not per key: a search one key paid for is free for every other key in the organization until the receipt expires.
Errors
A 422 means the filter itself was refused. The top-level error is lowercase, such as
unknown_field, unsupported_operator, missing_param, invalid_value, or
invalid_filter; filter refusals also carry field and, when applicable, operator at
the top level. A 402 max_credits_exceeded refusal carries requiredCredits and
maxCredits, so raise the ceiling and retry. A 503 means the company search backend is
unavailable; every other status, and the envelope they all share, is in
Conventions.