As of 07.09.2026

SpecScout API Documentation

Welcome to the SpecScout API. This documentation provides detailed information about endpoints, parameters, and responses to help you integrate automated product data extraction into your e-commerce platform.

Authentication

Authentication is handled via API keys. A client must provide a valid API key with each request to protected endpoints.

Header: x_api_key: <API_KEY>

POST /api/v1/auth/validate_api_key Validate API key

Validate the API key sent in the x_api_key header and return its metadata. No request body - useful to test your integration.

Responses
  • 200 Successful Response, schema: #/components/schemas/ValidateAPIKeySuccessResponse
  • {
      "valid": true,
      "key_id": "uuid",
      "key_type": "user",
      "expires_at": null,
      "capabilities": {
        "search": true,
        "categories_read": true,
        "categories_write": true
      }
    }

Making Requests

Every request that carries a body must send a JSON Content-Type. The API rejects a JSON body sent under any other media type — including a missing header — with 422 Unprocessable Entity, before the endpoint runs.

Header: Content-Type: application/json

A charset parameter is accepted, so application/json; charset=utf-8 is fine. Requests without a body — most DELETEs, and POSTs that take their input from the path or query string — do not need the header.

Common mistake

Browser fetch() sends text/plain;charset=UTF-8 when given a JSON string body without an explicit header, which this API refuses. Set the header explicitly.

Accepted

POST /api/v1/auth/login
Content-Type: application/json

{"email": "...", "password": "..."}

Rejected — 422

POST /api/v1/auth/login
Content-Type: text/plain

{"email": "...", "password": "..."}

Errors & Status Codes

Errors raised by the API's own validation and business rules return a human-readable detail plus a stable, machine-readable code. Branch on code, not on the text. Request-schema violations caught before the endpoint runs (missing required field, wrong type, unknown enum value) use FastAPI's standard {"detail": [...]} list shape without a code.

Business / validation error

{
  "detail": "a specification named 'Weight' is already attached to category ...",
  "code": "specification_name_conflict_in_category"
}

Internal error (500)

{
  "detail": "internal server error",
  "code": "internal_server_error",
  "correlation_id": "3f9c..."   // also sent as X-Correlation-ID header; quote it when reporting a problem
}
StatusMeaningTypical codes
400Malformed input that is not a schema violation (invalid JSON in a query parameter, non-UUID category, nothing to search for)–
401Missing, invalid, expired or revoked API keyinvalid_or_missing_api_key, invalid_api_key
403Key valid but not allowed: account not activated or blocked, key scope insufficient, foreign private resourceaccount_not_activated, account_blocked, api_key_scope_insufficient, insufficient_privileges
404Resource does not exist or is not visible to youcategory_not_found, domain_blacklist_item_not_found, scope_term_not_found, ...
409Conflict with existing data or a capspecification_name_conflict_in_category, domain_whitelist_item_duplicate, scope_term_duplicate, scope_term_limit_exceeded, scope_term_last_undeletable
422Request understood but semantically invalidvalidation_error, invalid_scope_term, invalid_brand, or FastAPI's {"detail": [...]}
429Per-endpoint rate limit or monthly search quota exceeded–
500Unexpected failure; nothing about the cause is exposedinternal_server_error (+ correlation_id)

Users

GET /api/v1/users/me Get own account

Get your account information.

Rate limit: 20 requests per minute.

Responses
  • 200 Successful Response, schema: #/components/schemas/UserPublic
  • {
      "id": "uuid",
      "email": "jane.doe@example.com",
      "full_name": "Jane Doe",
      "is_activated": false,
      "created_at": "2026-01-01T00:00:00Z",
      "updated_at": "2026-01-01T00:00:00Z"
    }

Quotas

Check your quota limits and usage.

GET /api/v1/quotas/me Get own quota

Get the current user's quota limits and usage.

Responses
  • 200 Successful Response, schema: #/components/schemas/UserQuotaResponse
  • {
      "quota_key": "search",
      "limit": 500,
      "used": 254,
      "remaining": 246,
      "period_start": "2026-04-01T00:00:00Z",
      "period_end": "2026-05-01T00:00:00Z",
      "period_type": "monthly",
      "period_tz": "UTC",
      "source": "plan",
      "is_exempt": false
    }
GET /api/v1/quotas/monthly Get monthly usage history

Get your monthly search usage history, paginated.

Parameters

skip Optional integer. Default: 0.

limit Optional integer. Default: 12, max: 120.

sortBy Optional string. Default: period_start. Allowed: period_start, search_count, last_search_at.

sortDir Optional string. Default: desc. Allowed: asc, desc.

Responses
  • 200 Successful Response, schema: #/components/schemas/MonthlySearchUsageListResponse
  • {
      "items": [
        {
          "period_start": "2026-06-01T00:00:00Z",
          "period_end": "2026-07-01T00:00:00Z",
          "regular_count": 254,
          "demo_count": 0,
          "total_count": 254,
          "last_search_at": "2026-06-28T14:31:07Z"
        }
      ],
      "pagination": {"total": 6, "skip": 0, "limit": 12, "returned": 6, ...}
    }
  • 422 Validation Error, schema: #/components/schemas/HTTPValidationError

Specifications

Best Practices

The quality of extracted values depends directly on the quality of your specification definitions. Names and descriptions must be precise and unambiguous.

Good vs Bad Examples

Bad

  • name: "Size" (too vague)
  • description: "display size in inches" while unit: "inch" is already set
  • instance: "list" (not allowed - use sub_values for multi-value specs)

Good

  • name: "Display Size"
  • description: "Diagonal length of the active display area"
  • instance: "float", unit: "inch"
Validation Rules
  • Allowed instance values only: "float", "int", "str", "bool". Common aliases ("number", "double", "integer", "string", "text", "boolean") are accepted and normalized. There is no "list" instance; define sub_values when a spec has multiple values.
  • Use int only for counts (cores, gears). Physical quantities (weight, size, clock speed) are float, even if they are usually listed as whole numbers.
  • unit is optional. Leave it out for values without a unit; it reads back as null. Unit conversion only happens for int/float; on a str spec a unit acts as a format hint, on bool it does nothing.
  • No duplicated semantics: if unit is set, do not repeat it in the description. Avoid plausible example values in the description; they bias the extraction.
  • Multi-part values use sub_values: either an array of at least two per-element descriptions (e.g. ["width", "height", "depth"]; subvalue_elements is derived from the length), or a string describing the whole collection when the elements cannot be enumerated (e.g. "the materials of the fabric with their share"; subvalue_elements stays null for a variable count or states a fixed count ≥ 2). subvalues_ordered says whether each position has a fixed meaning; it defaults to true for the array form and false for the string form, and true with a string is rejected. All elements share one instance, unit, tolerance and options. Sub-values are verified as a unit: sources must agree on every element.
  • Tolerance defaults by instance: float 0.02, int 0, str 0.15 (recommended to keep near that), bool ignored. Value is a fraction (0.05 = 5%).
  • Use options for closed vocabularies: e.g. material, connector type. Keep them on one abstraction level, exhaustive for the domain, in one language.
  • Prefer specific names and keep one meaning per spec: e.g. Battery Capacity instead of Battery; do not combine multiple attributes in one field. Two specs that overlap should each exclude the other's domain in their description.
  • Names are unique per category (case-insensitive). Attaching or renaming a spec so that two specs in one category share a name is rejected with 409 specification_name_conflict_in_category.
POST /api/v2/specifications Create spec

Create a new specification for the current user.

Request Body

{
  "name": "Display Size",                     // required
  "description": "Diagonal of visible display area", // required
  "instance": "float",                        // required: "float", "int", "str", "bool"
  "unit": "inch",                             // optional; omit for values without a unit

  "tolerance": 0.05,                          // optional; default depends on instance (float 0.02, int 0, str 0.15)
  "options": null,                            // optional: array of allowed values (closed vocabulary)
  "sub_values": null,                         // optional: array of >= 2 element descriptions, or a string describing the collection
  "subvalue_elements": null,                  // optional: expected element count (derived from an array sub_values)
  "subvalues_ordered": true                   // optional: whether positions have a fixed meaning (default: true for array, false for string)
}

Required for creation: name, description, instance. All other fields are optional and used for fine-tuning; see the best practices above for the sub_values contract.
Note: Use options for filters (e.g. material: ["aluminum", "plastic"]).

Responses
  • 201 Successful Response, schema: #/components/schemas/SpecificationPublic
  • {
      "id": "uuid",
      "name": "Display Size",
      "description": "Diagonal of visible display area",
      "instance": "float",
      "unit": "inch",
      "tolerance": 0.05,
      "options": null,
      "sub_values": null,
      "subvalue_elements": null,
      "subvalues_ordered": true,
      "user_id": "uuid",
      "created_at": "2026-01-01T00:00:00Z",
      "updated_at": "2026-01-01T00:00:00Z"
    }
  • 422 Validation Error, schema: #/components/schemas/HTTPValidationError
  • {
      "detail": [
        {"loc": ["query", "field_name"], "msg": "Field required", "type": "missing"}
      ]
    }
GET /api/v2/specifications List all specs

List all specifications visible to the current user.

Parameters

search Optional string. Default: "".

scope Optional string. Default: all. Allowed: all (own + public), owned, public, mine.

user_id Optional UUID (nullable). Filter by owner.

skip Optional integer. Default: 0.

limit Optional integer. Default: 100, max: 1000.

sortBy Optional string. Default: created_at. Allowed: created_at, name, unit, instance.

sortDir Optional string. Default: desc. Allowed: asc, desc.

exclude_category_id Optional UUID (nullable). Exclude specifications that are attached to this category.

instance Optional, repeatable. Only specifications with one of the given instance types, e.g. instance=float&instance=int. Invalid values are rejected with 422.

unit Optional string (nullable). Only specifications with exactly this unit.

has_options Optional boolean (nullable). true: only specifications with options; false: only ones without.

has_sub_values Optional boolean (nullable). true: only specifications with sub_values; false: only ones without.

subvalues_ordered Optional boolean (nullable). Among sub-value specs only: filter by whether element order carries meaning. Plain specs never match either value.

has_subvalue_elements Optional boolean (nullable). Among sub-value specs only: true = element count is declared, false = count varies per product.

no_category Optional boolean (nullable). true: only specifications not attached to any category; false: only attached ones.

Responses
  • 200 Successful Response, schema: #/components/schemas/SpecificationListResponse
  • {
      "items": [
        {"id": "uuid", "name": "Display Size", "description": "...", "instance": "float", "unit": "inch"}
      ],
      "pagination": {
        "total": 128,
        "skip": 0,
        "limit": 100,
        "returned": 1,
        "has_prev": false,
        "has_next": true,
        "next_skip": 100,
        "prev_skip": null
      }
    }
  • 422 Validation Error, schema: #/components/schemas/HTTPValidationError
  • {
      "detail": [
        {"loc": ["query", "field_name"], "msg": "Field required", "type": "missing"}
      ]
    }
GET /api/v2/specifications/{spec_id} Get single spec

Get a single specification by ID.

Responses
  • 200 Successful Response, schema: #/components/schemas/SpecificationPublic
  • {
      "id": "uuid",
      "name": "Display Size",
      "description": "Diagonal of visible display area",
      "instance": "float",
      "unit": "inch",
      "tolerance": 0.05,
      "options": null,
      "sub_values": null,
      "subvalue_elements": null,
      "subvalues_ordered": true,
      "user_id": "uuid",
      "created_at": "2026-01-01T00:00:00Z",
      "updated_at": "2026-01-01T00:00:00Z"
    }
  • 422 Validation Error, schema: #/components/schemas/HTTPValidationError
  • {
      "detail": [
        {"loc": ["query", "field_name"], "msg": "Field required", "type": "missing"}
      ]
    }
PUT /api/v2/specifications/{spec_id} Update spec

Update a specification.

Request Body

{
  "name": "Display Size",                     // required
  "description": "Diagonal of visible display area", // required
  "instance": "float",                        // required: "float", "int", "str", "bool"
  "unit": "inch",                             // optional
  "tolerance": 0.05,                          // optional
  "options": null,                            // optional
  "sub_values": null,                         // optional (array or string, see best practices)
  "subvalue_elements": null,                  // optional
  "subvalues_ordered": true                   // optional
}

For updates, provide a full specification payload. name, description and instance are required; omitted optional fields are reset to their defaults.

Query Parameters

scope Optional string. Default: all_categories. Allowed: all_categories, this_category. With all_categories the specification is updated in place, affecting every category it is attached to. With this_category the change applies only to the category given in category_id: if the specification is attached to other categories as well, it is detached from that category and an edited copy is created and attached in its place - the original remains unchanged everywhere else.

category_id Optional UUID. Required when scope=this_category (otherwise 422).

Conflicts: renaming a spec to a name another spec in one of its categories already carries is 409 specification_name_conflict_in_category. With scope=this_category, forking inside a public category is refused with 409 specification_fork_into_public_category (duplicate the category first).

Responses
  • 200 Successful Response, schema: #/components/schemas/SpecificationPublic
  • {
      "id": "uuid",
      "name": "Display Size",
      "description": "Diagonal of visible display area",
      "instance": "float",
      "unit": "inch",
      "tolerance": 0.05,
      "options": null,
      "sub_values": null,
      "subvalue_elements": null,
      "subvalues_ordered": true,
      "user_id": "uuid",
      "created_at": "2026-01-01T00:00:00Z",
      "updated_at": "2026-01-01T00:00:00Z"
    }
  • 422 Validation Error, schema: #/components/schemas/HTTPValidationError
  • {
      "detail": [
        {"loc": ["query", "field_name"], "msg": "Field required", "type": "missing"}
      ]
    }
DELETE /api/v2/specifications/{spec_id} Delete spec
Responses
  • 204 Successful Response (no content)
  • 422 Validation Error, schema: #/components/schemas/HTTPValidationError
  • {
      "detail": [
        {"loc": ["query", "field_name"], "msg": "Field required", "type": "missing"}
      ]
    }
POST /api/v2/specifications/bulk Bulk create specs

Create up to 100 specifications in one request. Use mode to control atomicity: partial commits every valid row and reports failed rows individually; atomic creates all rows or none.

Request Body

{
  "mode": "partial",    // "partial" (default) or "atomic" - partial commits valid rows independently
  "items": [
    {
      "client_id": "my-ref-1",   // optional, echoed back in results
      "specification": {
        "name": "Display Size",
        "description": "Diagonal of visible display area",
        "instance": "float",
        "unit": "inch"
      }
    }
  ]
}
Responses
  • 201 Successful Response, every row created
  • 200 Partial success (mode: partial only): at least one row failed; summary.failed is greater than 0 and the failed rows carry an error. Check the summary, not just the status code.
  • {
      "mode": "partial",
      "summary": {"requested": 1, "created": 1, "attached": 0, "failed": 0},
      "results": [
        {
          "index": 0,
          "client_id": "my-ref-1",
          "status": "created",
          "specification": {"id": "uuid", "name": "Display Size", ...},
          "attached": null,
          "error": null              // on "failed": {"code": "specification_create_failed" | "category_attach_failed" | "validation_error", "message": "..."}
        }
      ]
    }
  • 422 validation_error in atomic mode if any item is invalid: nothing is created. Also the standard Validation Error for a malformed request (e.g. more than 100 items).
  • 409 atomic mode only: a conflict on any row fails the whole request and nothing is created. In partial mode the same conflict only fails that row (error.code is specification_create_failed, the cause is in error.message).
POST /api/v2/specifications/draft AI-draft a spec

Use AI to draft a specification from a natural-language prompt. Also returns similar existing specs to prevent duplicates before creating.

Request Body

{
  "prompt": "battery capacity in milliampere-hours",   // required, max length 200 characters
  "category_id": "uuid",                               // optional - for context
  "language": "en",                                    // optional, default "en"
  "existing_spec_ids": ["uuid", ...]                   // optional - for context
}
Responses
  • 200 Successful Response
  • {
      "draft": {
        "name": "Battery Capacity",
        "description": "Rated capacity of the battery cell",
        "instance": "int",
        "unit": "mAh",
        "tolerance": null,
        "options": null,
        "sub_values": null
      },
      "duplicate_candidates": [
        {"id": "uuid", "name": "Battery Capacity", "description": "...", "instance": "int", "unit": "mAh"}
      ],
      "warnings": []   // advisory codes: needs_manual_review, high_similarity_existing_spec, missing_unit_guess, unit_on_non_numeric_spec, subvalues_order_dropped
    }
  • 400 Invalid prompt
  • 403 Forbidden category access
  • 404 Category not found
  • 502 AI draft generation failed
  • 422 Validation Error
GET /api/v2/specifications/{spec_id}/categories List spec's categories

List all categories the specification is attached to.

Responses
  • 200 Successful Response - array of CategoryPublic
  • [
      {
        "id": "uuid",
        "name": "Laptops",
        "user_id": "uuid",
        "created_at": "2026-01-01T00:00:00Z",
        "updated_at": "2026-01-01T00:00:00Z"
      }
    ]
  • 422 Validation Error

Categories

Categories group related specifications.

GET /api/v2/categories List categories

List categories visible to the current user.

Parameters

search Optional string. Default: "".

scope Optional string. Default: all. Allowed: all (own + public), owned, public, mine.

user_id Optional UUID (nullable). Filter by owner.

skip Optional integer. Default: 0.

limit Optional integer. Default: 100, max: 1000.

sortBy Optional string. Default: created_at. Allowed: created_at, updated_at, name.

sortDir Optional string. Default: desc. Allowed: asc, desc.

Responses
  • 200 Successful Response, schema: #/components/schemas/CategoryListResponse
  • {
      "items": [
        {
          "id": "uuid",
          "name": "Laptops",
          "user_id": "uuid",
          "created_at": "2026-01-01T00:00:00Z",
          "updated_at": "2026-01-01T00:00:00Z"
        }
      ],
      "pagination": {
        "total": 42,
        "skip": 0,
        "limit": 100,
        "returned": 1,
        "has_prev": false,
        "has_next": false,
        "next_skip": null,
        "prev_skip": null
      }
    }
  • 422 Validation Error, schema: #/components/schemas/HTTPValidationError
  • {
      "detail": [
        {"loc": ["query", "field_name"], "msg": "Field required", "type": "missing"}
      ]
    }
GET /api/v2/categories/{category_id} Get category

Get a single category by ID.

Responses
  • 200 Successful Response, schema: #/components/schemas/CategoryPublic
  • {
      "id": "uuid",
      "name": "Laptops",
      "user_id": "uuid",
      "created_at": "2026-01-01T00:00:00Z",
      "updated_at": "2026-01-01T00:00:00Z"
    }
  • 422 Validation Error, schema: #/components/schemas/HTTPValidationError
  • {
      "detail": [
        {"loc": ["query", "field_name"], "msg": "Field required", "type": "missing"}
      ]
    }
GET /api/v2/categories/{category_id}/specifications List category specifications

Get all specifications that are attached to the given category.

Parameters

category_id Path parameter, required UUID.

skip Query parameter, optional integer. Default: 0.

limit Query parameter, optional integer. Default: 100, max: 1000.

sortBy Optional string. Default: created_at. Allowed: created_at, name, unit, instance.

sortDir Optional string. Default: desc. Allowed: asc, desc.

Responses
  • 200 Successful Response, schema: #/components/schemas/SpecificationListResponse
  • {
      "items": [
        {
          "id": "uuid",
          "name": "Screen Size",
          "description": "Display diagonal size",
          "instance": "float",
          "unit": "inch",
          "created_at": "2026-01-01T00:00:00Z",
          "updated_at": "2026-01-01T00:00:00Z"
        }
      ],
      "pagination": {
        "total": 12,
        "skip": 0,
        "limit": 100,
        "returned": 1,
        "has_prev": false,
        "has_next": false,
        "next_skip": null,
        "prev_skip": null
      }
    }
  • 422 Validation Error, schema: #/components/schemas/HTTPValidationError
  • {
      "detail": [
        {"loc": ["query", "field_name"], "msg": "Field required", "type": "missing"}
      ]
    }
POST /api/v2/categories Create category

Create a category. Keep category names unique; the API does not enforce this, but duplicate names make categories hard to tell apart.

Request Body

{
  "name": "Laptops"
}
Responses
  • 201 Successful Response, schema: #/components/schemas/CategoryPublic
  • 422 Validation Error, schema: #/components/schemas/HTTPValidationError
  • {
      "detail": [
        {"loc": ["query", "field_name"], "msg": "Field required", "type": "missing"}
      ]
    }
PUT /api/v2/categories/{category_id} Update category

Update a category.

Request Body

{
  "name": "New Category Name"
}
Responses
  • 200 Successful Response, schema: #/components/schemas/CategoryPublic
  • {
      "id": "uuid",
      "name": "Laptops",
      "user_id": "uuid",
      "created_at": "2026-01-01T00:00:00Z",
      "updated_at": "2026-01-01T00:00:00Z"
    }
  • 422 Validation Error, schema: #/components/schemas/HTTPValidationError
  • {
      "detail": [
        {"loc": ["query", "field_name"], "msg": "Field required", "type": "missing"}
      ]
    }
DELETE /api/v2/categories/{category_id} Delete category
Responses
  • 204 Successful Response (no content)
  • 422 Validation Error, schema: #/components/schemas/HTTPValidationError
  • {
      "detail": [
        {"loc": ["query", "field_name"], "msg": "Field required", "type": "missing"}
      ]
    }
POST /api/v2/categories/{category_id}/specs/{spec_id} Attach spec

Associates a specification with a category. Specification names must be unique within a category (case-insensitive). Attaching an already attached specification is a no-op.

Responses
  • 201 Successful Response: {"detail": "attached"}
  • 403 Forbidden: foreign private category, or a public/foreign spec into a public category
  • 404 Category or specification not found
  • 409 specification_name_conflict_in_category: another spec with the same name is already attached
  • 422 Validation Error, schema: #/components/schemas/HTTPValidationError
  • {
      "detail": [
        {"loc": ["query", "field_name"], "msg": "Field required", "type": "missing"}
      ]
    }
DELETE /api/v2/categories/{category_id}/specs/{spec_id} Detach spec

Removes a specification from a category.

Responses
  • 204 Successful Response (no content)
  • 422 Validation Error, schema: #/components/schemas/HTTPValidationError
  • {
      "detail": [
        {"loc": ["query", "field_name"], "msg": "Field required", "type": "missing"}
      ]
    }
POST /api/v2/categories/{category_id}/duplicate Duplicate category

Creates a copy of the category and all its attached specifications. No request body required.

Responses
  • 201 Successful Response - returns CategoryPublic of the new category
  • 422 Validation Error
POST /api/v2/categories/{category_id}/specifications/bulk Bulk create & attach specs

Bulk-creates specifications and attaches them to the category in one request. Same request schema as POST /api/v2/specifications/bulk. The attached field in each result indicates whether the attachment succeeded. In partial mode a name clash inside the category fails only that row: error.code is category_attach_failed and error.message names the conflict.

Responses
  • 201 Successful Response - same BulkSpecificationResponse schema; attached: true/false per result row
  • 200 Partial success (mode: partial only): at least one row failed, see summary.failed and the per-row error
  • 403 Forbidden: foreign private category
  • 404 Category not found
  • 409 specification_name_conflict_in_category, atomic mode only: the whole request fails and nothing is created
  • 422 validation_error in atomic mode if any item is invalid (nothing created); otherwise the standard Validation Error

Domain Rules (Blacklist & Whitelist)

Saved domain rules steer which websites a search uses. The blacklist excludes domains from web searches and scraping; the whitelist adds domains as sources and can declare how much to trust them. Each user holds at most one rule per domain per list. Every route below exists for both lists; {list} stands for blacklist or whitelist.

Global vs. scoped rules

  • A rule with no scope terms is global and applies to every search (source stored).
  • A scope term is one situation the rule applies in: {"category_id": ..., "brand": ...} with at least one part set. The parts of one term AND together; a rule's terms OR together.
  • A search activates a term only if its category / brand parameters match the term's parts. A missing part is never a wildcard.
  • Brands are normalized (trim, collapse whitespace, lowercase, max 128 chars). Responses carry both brand (identity) and brand_display (as entered).
  • Max 100 scope terms per rule.

Trust ladder (whitelist only)

  • normal - include the domain as a source, no further claim.
  • trusted - strong prior: values from this domain get a high per-value confidence and the domain is ranked higher. Values are still cross-source verified.
  • authoritative - the product's own source (e.g. the manufacturer). Everything trusted does, plus its pages get a second, independent extraction so specs only this source publishes can still be confirmed.

A rule has a base trust; a scope term may override it for its own situation (effective_trust). globally_active: true puts a scoped whitelist rule into the stored pool at its base trust while its terms keep acting as overrides.

source_kind (manufacturer | comparison_portal) is a purely descriptive label available on both lists.

How a search picks rules up is controlled per request by blacklist_sources / whitelist_sources on Search. Error responses from this family carry a machine-readable code (see Errors).

GET /api/v1/users/search-settings/{list} List rules

List the current user's saved rules for one list. Without sources this is a plain listing. With sources it becomes the effective-set view: only rules that a search with the given category_id / brand context would pick up from those pools, each with its activation reason in source and a probe. Add is_active=true to preview exactly what a search would use.

Parameters

search Optional string. Substring match on the domain.

is_active Optional boolean. Filter by active state.

category_id Optional UUID. Rules with a scope term naming this category. Also the category context for the effective-set view.

brand Optional string. Rules with a scope term naming this brand (normalized server-side). Also the brand context for the effective-set view.

sources Optional, repeatable. Turns on the effective-set view. Allowed: stored, category, brand (e.g. ?sources=stored&sources=category&sources=brand). provided and none are rejected with 422. Requesting only scoped sources with no category_id/brand context is a 422 (guaranteed-empty view).

source_kind Optional string. Allowed: manufacturer, comparison_portal, unset (rules without a classification).

trust Optional string, whitelist only. Exact match on the rule's base trust: normal, trusted, authoritative.

skip Optional integer. Default: 0.

limit Optional integer. Default: 100, max: 1000.

sortBy Optional string. Default: created_at. Allowed: created_at, updated_at, domain, is_active; whitelist additionally trust (ordered by ladder rank, not alphabetically).

sortDir Optional string. Default: desc. Allowed: asc, desc.

Responses
  • 200 Successful Response, schema: DomainBlacklistItemListResponse / DomainWhitelistItemListResponse
  • {
      "items": [
        {
          "id": "uuid",
          "user_id": "uuid",
          "domain": "bosch.com",
          "trust": "trusted",                 // whitelist only: "normal" | "trusted" | "authoritative"
          "source_kind": "manufacturer",      // both lists: "manufacturer" | "comparison_portal" | null
          "globally_active": false,           // whitelist only
          "is_active": true,
          "created_at": "2026-01-01T00:00:00Z",
          "updated_at": "2026-01-01T00:00:00Z",
          "category_count": 1,                // distinct categories across the rule's scope terms
          "brand_count": 1,                   // distinct brands across the rule's scope terms
          "scope_term_count": 2,              // number of scope terms; 0 = global rule
          "source": null,                     // only set in the sources= view: "stored" | "category" | "brand" | "category_and_brand"
          "probe": null                       // only set when probing (see single-rule detail)
        }
      ],
      "pagination": {"total": 5, "skip": 0, "limit": 100, "returned": 1, "has_prev": false, "has_next": false, "next_skip": null, "prev_skip": null}
    }
  • 422 Validation Error
POST /api/v1/users/search-settings/{list} Create rule

Create a rule for a domain. Without scopes the rule is global; each scopes entry becomes one scope term. Creating a domain that already has a rule on this list is always a 409. There is no upsert; use ensure for an idempotent "make sure this domain is covered" call.

Request Body

{
  "domain": "bosch.com",                       // required, 1-2048 chars
  "source_kind": "manufacturer",               // optional, both lists: "manufacturer" | "comparison_portal"
  "trust": "trusted",                          // whitelist only, optional, default "normal"
  "globally_active": false,                    // whitelist only, optional; true requires at least one scope entry
  "scopes": [                                  // optional, max 100 entries; omit or [] for a global rule
    {"category_id": "uuid"},                   // applies to searches in this category
    {"brand": "Bosch", "trust": "authoritative"},  // applies to searches for this brand; per-term trust is whitelist only
    {"category_id": "uuid", "brand": "Bosch"}  // both parts must match (AND)
  ]
}

Sending trust inside a scope entry or globally_active on a blacklist rule is rejected with 422.

Responses
  • 201 Successful Response, schema: DomainBlacklistItemPublic / DomainWhitelistItemPublic (see list example above)
  • 404 category_not_found for an unknown category_id in scopes
  • 409 domain_blacklist_item_duplicate / domain_whitelist_item_duplicate if the domain already has a rule; domain_blacklist_limit_exceeded / domain_whitelist_limit_exceeded at the per-user rule cap
  • 422 invalid_scope_term for an empty scope entry; validation_error for a blacklist globally_active or globally_active: true without scopes; invalid_domain_blacklist_entry / invalid_domain_whitelist_entry for an invalid domain
GET /api/v1/users/search-settings/{list}/{item_id} Get rule (with scope terms)

Single rule with its scope terms embedded. scopes is the authoritative list; categories and brands are derived per-dimension views of the same terms. Pass sources to additionally probe the rule: "would a search with this context see this rule, and at which trust?" (computed with the same logic the search uses).

Parameters

item_id Path parameter, required UUID.

sources Optional, repeatable. Same values as on the list route. Turns probing on; without it probe is null.

category_id Optional UUID. Probe context. Only allowed together with sources (422 otherwise); unknown category is 404.

brand Optional string. Probe context. Only allowed together with sources.

Responses
  • 200 Successful Response, schema: DomainBlacklistItemDetail / DomainWhitelistItemDetail. Blacklist responses carry no trust fields at all.
  • {
      "id": "uuid", "user_id": "uuid", "domain": "bosch.com",
      "trust": "trusted", "source_kind": "manufacturer", "globally_active": false, "is_active": true,
      "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z",
      "category_count": 1, "brand_count": 1, "scope_term_count": 2, "source": null,
      "scopes": [                          // authoritative list of scope terms; "id" is what DELETE .../scopes/{term_id} takes
        {"id": "uuid", "category_id": "uuid", "category_name": "Power Tools", "brand": null, "brand_display": null,
         "trust": null, "effective_trust": "trusted", "attached_at": "2026-01-01T00:00:00Z"},
        {"id": "uuid", "category_id": null, "category_name": null, "brand": "bosch", "brand_display": "Bosch",
         "trust": "authoritative", "effective_trust": "authoritative", "attached_at": "2026-01-01T00:00:00Z"}
      ],
      "categories": [{"category_id": "uuid", "category_name": "Power Tools", "attached_at": "2026-01-01T00:00:00Z"}],
      "brands": [{"brand": "bosch", "brand_display": "Bosch", "attached_at": "2026-01-01T00:00:00Z"}],
      "probe": {                           // only when ?sources=... was given, otherwise null
        "activated": true,
        "effective_trust": "authoritative", // whitelist only; null when not activated
        "matched_term_ids": ["uuid"],
        "deciding_term_id": "uuid"          // whitelist only
      }
    }
  • 404 domain_blacklist_item_not_found / domain_whitelist_item_not_found
  • 422 Validation Error
PATCH /api/v1/users/search-settings/{list}/{item_id} Update rule

Partial update: an omitted key leaves the stored value untouched. Scope terms are managed through the term routes below, not here.

Request Body

{
  "is_active": false,           // optional: disable/enable without deleting
  "trust": "authoritative",     // whitelist only, optional: raises or lowers the rule's base trust (null = no-op)
  "source_kind": null,          // optional, both lists; explicit null clears the classification
  "globally_active": true       // whitelist only, optional; true requires at least one stored scope term
}
Responses
  • 200 Successful Response, returns the updated DomainBlacklistItemPublic / DomainWhitelistItemPublic
  • 404 domain_blacklist_item_not_found / domain_whitelist_item_not_found
  • 422 validation_error for globally_active on a blacklist rule or on a rule without scope terms
DELETE /api/v1/users/search-settings/{list}/{item_id} Delete rule

Deletes the rule together with all its scope terms.

Responses
  • 204 Successful Response (no content)
  • 404 domain_blacklist_item_not_found / domain_whitelist_item_not_found
  • 422 Validation Error
POST /api/v1/users/search-settings/{list}/ensure Ensure rule covers a scope (idempotent)

Goal-directed "block this source" / "trust this source" call. After it returns, a rule for the domain exists, is active, and covers at least the requested scope (or the whole domain if no scope is given). It never narrows an existing rule and never lowers trust. At most one scope per call.

Request Body

{"domain": "annoying-shop.de", "category_id": "uuid"}                 // scope: one category
{"domain": "annoying-shop.de", "brand": "Sony"}                       // scope: one brand
{"domain": "annoying-shop.de", "category_id": "uuid", "brand": "Sony"} // scope: category AND brand
{"domain": "annoying-shop.de"}                                        // global: covers every search

{"domain": "good-source.de", "brand": "Sony"}                         // whitelist: trust omitted => "trusted"
{"domain": "good-source.de", "brand": "Sony", "trust": "normal"}      // whitelist: include only, no confidence boost

Whitelist only: trust is optional and defaults to trusted here (unlike create, where the default is normal). Ensure raises the covering term's or the rule's trust to the requested rung if it is lower. The blacklist request body has no trust field; sending one is 422.

Outcomes

Rule beforeRequestEffectoutcome
missingglobal or scopedrule createdcreated
globalanythingno scope change (already covers everything)already_global
scopedscope already covered by an existing termno scope changealready_covered
scopednew scopeterm appendedscope_added
scopedglobalall terms deleted, returned in removed_scopespromoted_to_global

An inactive rule is reactivated by any ensure call (reactivated: true). A 409 scope_term_limit_exceeded can therefore still have reactivated the rule; re-read the rule after a 409.

Responses
  • 200 Successful Response, schema: DomainBlacklistEnsureResponse / DomainWhitelistEnsureResponse
  • {
      ...single-rule detail fields (see GET /{list}/{item_id})...,
      "outcome": "scope_added",     // "created" | "scope_added" | "already_covered" | "already_global" | "promoted_to_global"
      "reactivated": false,         // true if the rule was inactive and this call switched it back on
      "trust_raised": false,        // whitelist only: true if this call raised a term's or the rule's trust
      "removed_scopes": []          // only on "promoted_to_global": the scope terms that were deleted
    }
  • 404 category_not_found
  • 409 scope_term_limit_exceeded (100 terms per rule); domain_blacklist_limit_exceeded / domain_whitelist_limit_exceeded on create
  • 422 invalid_scope_term; Validation Error
POST /api/v1/users/search-settings/{list}/{item_id}/scopes Add scope term

Add one scope term to a rule. At least one of category_id / brand is required. Adding a term to a global rule scopes it down (explicit narrowing).

Request Body

{
  "category_id": "uuid",     // optional
  "brand": "Bosch",          // optional; normalized (trim, collapse whitespace, lowercase, max 128 chars)
  "trust": "authoritative"   // whitelist only, optional: overrides the rule's trust when this term matches
}
Responses
  • 201 Successful Response, schema: RuleScopeTermPublic (whitelist) / BlacklistRuleScopeTermPublic (blacklist, without the two trust fields)
  • {
      "id": "uuid",
      "category_id": "uuid",
      "category_name": "Power Tools",
      "brand": "bosch",                 // normalized identity
      "brand_display": "Bosch",         // as entered
      "trust": null,                    // whitelist only: this term's own override, null = inherits the rule's trust
      "effective_trust": "trusted",     // whitelist only
      "attached_at": "2026-01-01T00:00:00Z"
    }
  • 404 domain_blacklist_item_not_found / domain_whitelist_item_not_found; category_not_found
  • 409 scope_term_duplicate; scope_term_limit_exceeded (100 terms per rule)
  • 422 invalid_scope_term (empty term, or trust on a blacklist term); invalid_brand
GET /api/v1/users/search-settings/{list}/{item_id}/scopes List scope terms

Paginated list of the rule's scope terms, newest first.

Parameters

skip Optional integer. Default: 0.

limit Optional integer. Default: 100, max: 1000.

Responses
  • 200 Successful Response: {"items": [...scope terms...], "pagination": {...}}
  • 404 domain_blacklist_item_not_found / domain_whitelist_item_not_found
DELETE /api/v1/users/search-settings/{list}/{item_id}/scopes/{term_id} Remove scope term

Remove one scope term. A rule's last term cannot be deleted (that would silently turn a scoped rule into a global one); use ensure without a scope to promote the rule to global explicitly, or delete the rule.

Responses
  • 204 Successful Response (no content)
  • 404 scope_term_not_found; domain_blacklist_item_not_found / domain_whitelist_item_not_found
  • 409 scope_term_last_undeletable (exception: the last term of a whitelist rule flagged globally_active may be deleted; the flag is cleared)
SHORTCUTS /api/v1/users/search-settings/{list}/{item_id}/categories | /brands Single-part scope terms

Convenience routes for scope terms that name only a category or only a brand. They map onto the term routes above and return the same error codes. A two-part term (category AND brand) can only be added via POST .../scopes.

POST .../categories/{category_id} - add the term (category, -). 201 {"detail": "attached"}; 409 scope_term_duplicate.

DELETE .../categories/{category_id} - remove only the term (category, -). 204; 404 scope_term_not_found if the category is referenced only inside a two-part term; 409 scope_term_last_undeletable.

GET .../categories?skip=&limit= - distinct categories across all of the rule's terms: {"items": [{"category_id", "category_name", "attached_at"}], "pagination": {...}}.

POST .../brands/{brand} - add the term (-, brand). The path segment is URL-encoded and normalized server-side. 201 {"detail": "attached"}; 409 scope_term_duplicate; 422 invalid_brand (empty or longer than 128 characters).

DELETE .../brands/{brand} - remove only the term (-, brand). Same errors as the category variant.

GET .../brands?skip=&limit= - distinct brands across all of the rule's terms: {"items": [{"brand", "brand_display", "attached_at"}], "pagination": {...}}.

GET /api/v1/users/search-settings/brands Brand autocomplete

Distinct brands the current user has attached to any rule on either list, in display form, for autocomplete inputs.

Parameters

search Optional string. Case-insensitive substring filter.

limit Optional integer. Default: 50, range 1-200.

Responses
  • 200 Successful Response, schema: BrandSuggestResponse
  • {"items": ["Bosch", "Bosch Professional", "Sony"]}
  • 422 Validation Error

Example Request & Response

This example demonstrates a standard request to the /api/v1/search endpoint and the corresponding structured JSON response.

GET /api/v1/search REQUEST
GET /api/v1/search?product_name=iPhone%2012%20Pro&brand=Apple&category=3fa85f64-5717-4562-b3fc-2c963f66afa6

Headers:
x_api_key: sp_user_c64e2c071c4135d5.iKTBaTZFW...

# optional: steer sources for this request only
#   &blacklist_domains=["ebay.com"]
#   &whitelist_domains=[{"domain":"apple.com","trust":"authoritative"}]
#   &whitelist_exclusive=true
200 OK RESPONSE
{
  "specs": [
    {
      "id": "3fa85f64-...",
      "name": "weight",
      "value": [
        {
          "value": 189.0,
          "unit": "gram",
          "sources": ["https://www.apple.com/iphone-12-pro/specs/", "https://www.gsmarena.com/apple_iphone_12_pro-10508.php"],
          "confidence": 1.0
        }
      ],
      "unit": "gram",
      "instance": "float"
    },

    {
      "id": "4fa85f64-...",
      "name": "dimensions",
      "value": [
        {
          "value": [7.4, 71.5, 146.7],
          "unit": "mm",
          "sources": ["https://www.gsmarena.com/apple_iphone_12_pro-10508.php"],
          "confidence": 0.95
        }
      ],
      "unit": "mm",
      "instance": "float"
    },

    {
      "id": "5fa85f64-...",
      "name": "display_size",
      "value": [
        {
          "value": 6.1,
          "unit": "inch",
          "sources": ["https://www.apple.com/iphone-12-pro/specs/"],
          "confidence": 1.0
        }
      ],
      "unit": "inch",
      "instance": "float"
    },

    {
      "id": "6fa85f64-...",
      "name": "color",
      "value": [
        {
          "value": ["silver", "graphite", "gold", "pacific blue"],
          "unit": null,
          "sources": ["https://www.apple.com/iphone-12-pro/specs/"],
          "confidence": 0.9
        }
      ],
      "unit": "",
      "instance": "str"
    },

    {
      "id": "7fa85f64-...",
      "name": "battery_capacity",
      "value": [
        {
          "value": 2815,
          "unit": "mAh",
          "sources": ["https://www.gsmarena.com/apple_iphone_12_pro-10508.php"],
          "confidence": 0.9
        }
      ],
      "unit": "mAh",
      "instance": "int"
    },

    {
      "id": "8fa85f64-...",
      "name": "ram",
      "value": [
        {
          "value": 6,
          "unit": "GB",
          "sources": ["https://www.gsmarena.com/apple_iphone_12_pro-10508.php"],
          "confidence": 0.9
        }
      ],
      "unit": "GB",
      "instance": "int"
    },

    {
      "id": "9fa85f64-...",
      "name": "storage",
      "value": [
        {
          "value": [128, 256, 512],
          "unit": "GB",
          "sources": ["https://www.apple.com/iphone-12-pro/specs/"],
          "confidence": 0.95
        }
      ],
      "unit": "GB",
      "instance": "int"
    }
  ],

  "status": "success",

  "not_found": [],

  "errors": [],

  "image_links": null

}

Ready to get structured data?

Get API Key