Working with Filters

Filter Endpoints Overview

The Moonlit API provides five GET endpoints that return the available filter values you can use in search requests: /v1.1/search/filters/documenttypes -- Returns document type codes and labels (case_law, legislation, parliamentary, etc.). These map directly to the documentTypes parameter on search endpoints. /v1.1/search/filters/jurisdictions_portals -- Returns jurisdictions (countries) and their nested portals (sub-collections like NL_case_law, EU_legislation). Use jurisdiction codes with the jurisdictions parameter, or portal codes with the portals parameter for narrower scoping. /v1.1/search/filters/trees -- Returns a hierarchical tree of legal domains (constitutional_law > fundamental_rights, civil_law > contract_law > tort_law, etc.). Use codes with the fieldsOfLaw parameter. /v1.1/search/filters/sources -- Returns courts and legislative bodies, optionally filtered by jurisdiction. Use names with the sources parameter. /v1.1/search/semantic_portals -- Returns which portals support semantic and hybrid search (not all portals have embeddings). Check this before offering semantic search options for a specific portal. All filter endpoints require only the Ocp-Apim-Subscription-Key header. No request body is needed.

Hierarchical Filters

Jurisdictions and fields of law are hierarchical. Understanding the hierarchy is important for building intuitive filter UIs. Jurisdictions have portals nested under them. When a user selects a parent jurisdiction (e.g., "Netherlands"), the API searches across all Dutch portals (case law, legislation, parliamentary documents). Alternatively, passing a specific portal code (e.g., "NL_case_law") restricts the search to only Dutch case law. Fields of law form a tree structure. Selecting a parent field (e.g., "civil_law") includes all children (contract_law, tort_law, property_law). Selecting a child field searches only that specific domain. You have two options for your UI: - Simple: Show only top-level items (jurisdictions, not portals; top-level fields, not children). This is easier to implement and works well for most use cases. - Advanced: Show expandable hierarchies that let power users drill down. This gives more control but requires more UI complexity.

// Search across all Dutch sources
{
  "query": "bestuursrecht",
  "jurisdictions": ["Netherlands"]
}

// Search only Dutch case law (narrower)
{
  "query": "bestuursrecht",
  "portals": ["NL_case_law"]
}

// Search across all civil law fields
{
  "query": "aansprakelijkheid",
  "fieldsOfLaw": ["civil_law"]
}

// Search only tort law (child of civil law)
{
  "query": "aansprakelijkheid",
  "fieldsOfLaw": ["tort_law"]
}

Building Dynamic Filter UIs

A common pattern is to fetch all filter values when the page loads, then let users compose their filters before searching. Here is the recommended approach: 1. On page load, fetch all filter endpoints in parallel. These requests are fast (< 100ms) and the responses are highly cacheable. 2. Render filter options as checkboxes, dropdowns, or tree views depending on your UI design. Show the label (human-readable name) and use the code (machine-readable identifier) in API requests. 3. When the user toggles a filter, immediately re-run the search with the updated filter parameters. If facets are enabled, also update the filter counts. 4. Cache filter responses for at least 24 hours -- these values change infrequently (only when Moonlit adds new jurisdictions, courts, or document types). Use a stale-while-revalidate pattern for the best user experience. 5. Use the semantic_portals endpoint to conditionally show a "Semantic search" or "AI-powered search" toggle only when the selected portals support it. Keyword search is available for all portals; semantic search is available only for portals listed in the semantic_portals response.

// Fetch all filter options in parallel on page load
const [docTypes, jurisdictions, fields, sources, semanticPortals] =
  await Promise.all([
    fetch("/v1.1/search/filters/documenttypes", { headers }).then(r => r.json()),
    fetch("/v1.1/search/filters/jurisdictions_portals", { headers }).then(r => r.json()),
    fetch("/v1.1/search/filters/trees", { headers }).then(r => r.json()),
    fetch("/v1.1/search/filters/sources", { headers }).then(r => r.json()),
    fetch("/v1.1/search/semantic_portals", { headers }).then(r => r.json()),
  ]);

// Check if a portal supports semantic search
// semanticPortals is an array of { name, value }
function supportsSemanticSearch(portalCode) {
  return semanticPortals.some(p => p.value === portalCode);
}

Combining Multiple Filters

All filter parameters in a search request are combined with AND logic. Passing multiple values within a single filter parameter uses OR logic. Example: jurisdictions: ["Netherlands", "European Union"] AND documentTypes: ["case_law"] means "case law from either the Netherlands OR the EU." This translates to: (jurisdiction = Netherlands OR jurisdiction = European Union) AND (documentType = case_law) Filters can be combined freely: - jurisdictions + documentTypes: Only specific document types from specific countries - jurisdictions + sources: Only specific courts within a jurisdiction - fieldsOfLaw + from_date: Only recent documents in a specific legal domain - All of the above at once: A fully scoped, precise query Important: if you pass both jurisdictions and portals, the portals parameter takes precedence. The jurisdictions parameter is ignored for any jurisdiction that has a portal specified. We recommend using one or the other, not both.

{
  "query": "privacy GDPR",
  "jurisdictions": ["Netherlands", "European Union"],
  "documentTypes": ["case_law"],
  "fieldsOfLaw": ["data_protection"],
  "sources": ["Hoge Raad", "CJEU"],
  "from_date": "2020-01-01",
  "sort_type": 1,
  "num_results": 25,
  "facets": true
}

Source Filtering by Jurisdiction

The /v1.1/search/filters/sources endpoint accepts optional jurisdictions (plural, comma-separated string) and portals (comma-separated string) query parameters to return only courts and legislative bodies for specific countries or portals. This is useful for building cascading filter UIs where the user first selects a jurisdiction, then sees only the relevant courts. Without the jurisdictions parameter, the endpoint returns all sources across all jurisdictions. This can be a long list (hundreds of courts), so filtering by jurisdiction is recommended for most UIs. Source names in the response are the exact strings you should pass in the sources parameter of search requests. They are case-sensitive. Common source names by jurisdiction: - EU: CJEU, General Court, Court of Auditors - NL: Hoge Raad, Raad van State, Centrale Raad van Beroep, Gerechtshof Amsterdam, Rechtbank Den Haag - DE: BGH, BVerfG, BAG, BFH, BSG, BVerwG - BE: Cour de cassation, Conseil d'Etat, Cour constitutionnelle

# Get sources for the Netherlands only
curl "https://api.moonlit.ai/v1.1/search/filters/sources?jurisdictions=Netherlands" \
  -H "Ocp-Apim-Subscription-Key: YOUR_API_KEY"

# Get sources for Germany
curl "https://api.moonlit.ai/v1.1/search/filters/sources?jurisdictions=Germany" \
  -H "Ocp-Apim-Subscription-Key: YOUR_API_KEY"

# Get all sources (all jurisdictions)
curl "https://api.moonlit.ai/v1.1/search/filters/sources" \
  -H "Ocp-Apim-Subscription-Key: YOUR_API_KEY"