Skip to main content

Enigma API Client

The Enigma Python SDK, enigma-api-client, sends GraphQL queries to the Enigma GraphQL API and turns questions in plain English into those queries. This page is the SDK reference.

Prerequisites

  • An Enigma API key. Find yours in the Enigma Console under Settings > Account > API keys.
  • Python 3.11 or later.
  • To generate queries on your own machine with a local strategy (see Choose a Strategy): an Anthropic or OpenRouter API key, or AWS credentials with access to Claude on Amazon Bedrock.

The examples read your Enigma API key from the ENIGMA_API_KEY environment variable:

export ENIGMA_API_KEY="your-api-key"

Install

pip install enigma-api-client

Optional extras add dependencies for specific features:

Extra Installs Needed for
enigma-api-client[bedrock] botocore BedrockProvider, Claude on Amazon Bedrock as the large language model (LLM) of TemplateStrategy or GenerativeStrategy
enigma-api-client[anthropic] claude_agent_sdk AgentStrategy
enigma-api-client[all] botocore and claude_agent_sdk BedrockProvider and AgentStrategy

AnthropicProvider and OpenRouterProvider need no extra. The anthropic extra installs the Claude Agent SDK, which only AgentStrategy uses.

Quick Start

Ask a Question

import asyncio
import os

from enigma_api_client import Client, ClientConfig


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        response = await client.search(prompt="Tell me about Chipotle")
        if not response.is_success():
            raise RuntimeError(f"HTTP {response.status_code}: {response.errors}")
        brand = response.data["search"][0]
        print(brand["names"]["edges"][0]["node"]["name"])  # CHIPOTLE MEXICAN GRILL
        print(brand["__url"])  # the brand's profile in the Enigma Console


asyncio.run(main())

The SDK turns the question into a GraphQL query, runs it, and returns the result. By default, Enigma's hosted query service writes the query, so your Enigma API key is all you need. For the kinds of questions you can ask, see Generate Queries from Questions.

Send a GraphQL Query

import asyncio
import os

from enigma_api_client import Client, ClientConfig

QUERY = """
query Brand($name: String!) {
  search(searchInput: { name: $name, entityType: BRAND, conditions: { limit: 1 } }) {
    ... on Brand {
      names(first: 1) { edges { node { name } } }
      websites(first: 1) { edges { node { website } } }
    }
  }
}
"""


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        response = await client.search(query=QUERY, variables={"name": "Chipotle"})
        if not response.is_success():
            raise RuntimeError(f"HTTP {response.status_code}: {response.errors}")
        brand = response.data["search"][0]
        print(brand["names"]["edges"][0]["node"]["name"])  # CHIPOTLE MEXICAN GRILL
        print(brand["websites"]["edges"][0]["node"]["website"])  # https://www.chipotle.com


asyncio.run(main())

A name can match many entities, so conditions: { limit: 1 } keeps only the first. The GraphQL guide and the schema reference describe the queries you can write, and Write GraphQL by Hand lists the mistakes to avoid.

Later snippets show only what changes: usually a new config = ... line, sometimes with the imports and strategy it needs. To run a snippet, start from one of the preceding programs and put the snippet's lines in place of the program's config = ... line. If the snippet has no config = ... line, put its lines just before the program's config = ... line instead, and pass any strategy it builds, such as TemplateStrategy(...), to ClientConfig as query_strategy=.

Configure the Client

ClientConfig takes these fields:

Field Default Meaning
enigma_api_key Required Your Enigma API key
query_strategy ServiceStrategy The strategy that turns questions into queries (see Choose a Strategy)
tier AccessTier.REDISTRIBUTION Your key's access tier, which local strategies write queries for (see Access Tiers)
url https://api.enigma.com/graphql The GraphQL endpoint
auto_inject_id_and_typename True Adds id and __typename to the inline fragments and connection nodes a query selects (see The Shape of data)
auto_inject_page_info False Adds pageInfo { hasNextPage endCursor } to every connection a query selects that doesn't already select pageInfo (see Paginate)
customize_headers_callback None A function that takes the default request headers and returns headers to add or replace. A header returned with an empty value is removed. It applies to searches only (see the note after this table).

The client doesn't apply customize_headers_callback to its request for the API schema (see Schema Checks and Several Tiers) or to the hosted query service.

Client Lifecycle

  • Create one Client and reuse it for all your requests. It keeps connections open until you close it, either with async with Client(config) as client: or with await client.aclose(). Closing the client also closes its strategy.
  • All methods are asynchronous. In a Jupyter notebook, which already runs an event loop, replace asyncio.run(main()) with await main(). Start Jupyter from a shell where ENIGMA_API_KEY is set.
  • Use a client from one event loop. Don't share it across threads that each run their own loop.

Run Searches Concurrently

One client can run many searches at once:

import asyncio
import os

from enigma_api_client import Client, ClientConfig


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        prompts = ["Tell me about Chipotle", "Tell me about Sweetgreen", "Tell me about Shake Shack"]
        responses = await asyncio.gather(*(client.search(prompt=p) for p in prompts), return_exceptions=True)
        for prompt, response in zip(prompts, responses):
            if isinstance(response, Exception):
                print(prompt, "failed:", type(response).__name__)
            else:
                print(prompt, response.status_code)


asyncio.run(main())

return_exceptions=True keeps one failed search from hiding the others' results.

Your plan's rate limits cap requests per second, bursts, and requests per day. To cap how many calls run at once, use an asyncio.Semaphore. That keeps bursts down but doesn't enforce a rate.

Timeouts

The client waits up to 60 seconds for the API, and ServiceStrategy waits up to 30 seconds for the hosted query service. A timeout raises httpx.TimeoutException.

Read the Response

client.search() returns a SearchResponse:

Field Holds
status_code The HTTP status of the API's answer
data The GraphQL data object, or None
errors The GraphQL errors list, or None
extensions The GraphQL extensions object, or None
unavailable_data For a question: the parts Enigma can't answer (see Unavailable Data)
raw_body The response body as text
headers The HTTP response headers, looked up case-insensitively, such as Retry-After

response.is_success() is true when the API answers with a 2xx status and no errors, and neither of these applies:

  • HTTP 206. Some values the API computes in the background, such as some counts, weren't ready, so the API left them null. The rest of data is filled in, so you can still use a 206 answer. Before you treat a response as failed, check whether response.status_code is 206 and read response.errors.
  • A failed background task. The answer's extensions report a background task that failed, was canceled, or ran out of credits.

To have the API wait up to 25 seconds for background values before it answers, the client adds the max_background_task_wait and max_call_duration query parameters to the URL. If ClientConfig.url already sets either one to a non-blank value, the client sends the URL as written and adds neither.

The Shape of data

  • search returns a list of entities. __typename says what each one is: Brand, LegalEntity, OperatingLocation, Person, or Address.
    • A Brand is a business as its customers know it, such as Chipotle.
    • A LegalEntity is a company as a government registers it, such as Chipotle Mexican Grill, Inc.
    • An OperatingLocation is one place where a brand does business, such as a store.
    • Understanding Enigma Data describes them in full.
  • Related data comes as connections: {"edges": [{"node": {...}}, ...]}. Each connection returns one page (see Paginate).
  • A question that names several businesses, people, or street addresses can get several searches, under the keys search, search2, and search3 of data (see Generate Queries from Questions).
  • A question that counts the businesses of a category, overall or in a place, as in "How many coffee shops are in Denver?", gets aggregate, an object of counts such as locationsCount and brandsCount.
  • Where a count of a brand's locations appears depends on the strategy that writes the query (see Choose a Strategy):
    • From a template (ServiceStrategy's default, or TemplateStrategy): as locationsCount on the brand in search, with one search per brand (search, search2, and so on).
    • From GenerativeStrategy, or ServiceStrategy(query_strategy="generative"): as locationsCount in aggregate, with one aggregate per brand (aggregate, aggregate2, and so on).
  • GenerativeStrategy, ServiceStrategy(query_strategy="generative"), and AgentStrategy have an LLM write the whole query, including its keys. With these strategies, read every key of data.

The client adds fields to what you asked for:

  • id and __typename on the inline fragments (... on Brand { ... }) and connection nodes the query selects. The client skips:
    • An inline fragment that carries a directive, such as @include or @skip.
    • A connection node when the node or its edges carries a directive.
    • An entity selected only through a named fragment at the top of search, as in search(...) { ...BrandFields }.
  • __url:
    • On each brand, legal entity, and operating location that has an id and a __typename: a link to its profile in the Enigma Console.
    • On a person: a link to the brand the person appears under, or to Enigma's website when the person appears under none.

To get __url for a selection the client skips, select id and __typename in it yourself.

To send your query exactly as written, set auto_inject_id_and_typename=False. A query that selects a connection node with no id, such as the MediaContent nodes of isMentionedInMediaContents, fails unless you do.

With auto_inject_id_and_typename=False, a brand, legal entity, or operating location gets __url only when your query selects its id and __typename, and a person only when your query selects its __typename.

The GraphQL query in the quick start returns:

{
  "search": [
    {
      "id": "825ce859-b742-49cb-9787-b9b32f674f25",
      "__typename": "Brand",
      "names": {"edges": [{"node": {"id": "000000d0-0003-cd02-0000-0a8000000000", "__typename": "BrandName", "name": "CHIPOTLE MEXICAN GRILL"}}]},
      "websites": {"edges": [{"node": {"id": "0000002a-0010-60fa-0000-0a8000000000", "__typename": "Website", "website": "https://www.chipotle.com"}}]},
      "__url": "https://console.enigma.com/profile/brand/825ce859-b742-49cb-9787-b9b32f674f25"
    }
  ]
}

Generate Queries from Questions

A question passed as client.search(prompt=...) must name what it is about. Every strategy answers these kinds of questions:

Kind of question Example
A business's profile, by name or website "What does Sweetgreen do?", "Enrich company data for hubspot.com"
A business's locations in a city or state "Find Costco locations in Spokane"
One location of a business, by street address "Is the Costco at 12020 N Newport Hwy, Spokane, WA still open?"
What is at a street address "What is at 350 Fifth Ave, New York, NY?"
A person and the businesses tied to them "What businesses is Jane Doe associated with?"
A company's officers, owners, and related companies "Who runs Airbnb?", "Who owns Shake Shack?"
A legal entity's registrations, taxpayer identification numbers (TINs), and bankruptcies "Is Zoom Video Communications Inc registered in Delaware?"
Sanctions and watchlist screening "Screen Sweetgreen for sanctions"
How many businesses of a category are in a place, or how many locations a brand has "How many pharmacies are in Montana?", "How many Wendy's restaurants are there in Ohio?"
Government records of a business, person, or address "Show business licenses held by Walgreens", "Show SAM.gov registrations for Lockheed Martin" (see Get Government Records)
Up to three businesses or people at once "Compare Starbucks and Dunkin"
  • TINs and watchlist entries are left out for AccessTier.REDISTRIBUTION and listed in unavailable_data, so a screening question returns the business without them (see Access Tiers).
  • A question about a street address alone needs address lookups, which are on by default when the API supports them for your tier (see Look Up an Address).
  • A count by category needs a business type the SDK can map to an industry code. The default and TemplateStrategy map a fixed list of common types by name, such as pharmacies, dentists, and coffee shops, and refuse any other. GenerativeStrategy, AgentStrategy, and ServiceStrategy(query_strategy="generative") let the LLM pick the code, so check the code in the query.

The strategies differ in speed, in what they need, and in how they handle a question outside this table (see Choose a Strategy).

A question that names several businesses, people, or street addresses can get several searches, all run in one query:

  • Each business or street address the question names gets a search. A person named with their company ("Jane Doe at Sweetgreen") or a single street address given for a business ("the Starbucks at 1 Main St") shares that business's search.
  • Each person the question names with no company gets a search.

A refused question raises EnigmaClientException from client.search, and no query runs (see Generation Errors). Refused questions include these:

  • A question that needs more than three searches or counts, such as one naming more than three businesses. A generated query runs at most three; the government-record searches the SDK adds don't count (see Get Government Records).
  • A question in which any one of its searches would be refused on its own. The whole question is refused, not only that search.
  • A request to list businesses by category ("List coffee shops in Denver").

The limit of three applies to searches and counts, not to names. In a template query (from ServiceStrategy's default or TemplateStrategy), a lookup of one location of a business, or of an address, gets one search for each entry in the question's longest list. That list can be of businesses, people, streets, cities, or states, and in an address lookup also of units or ZIP codes (see How Template Queries Read Places). So "the Starbucks at 1 Main St in Boston and 2 Elm St in Cambridge" gets two searches. A question about "the Starbucks at 1 Main St, 2 Elm St, 3 Oak St, and 4 Pine St in Boston" names one business but needs four searches, so it is refused.

When a question asks for data Enigma doesn't serve, such as lawsuits, that part is left out and listed in unavailable_data.

Choose a Strategy

A strategy turns each question into a GraphQL query. The default runs on Enigma's servers. The other three are local strategies: they run on your machine and call an LLM with your own account.

Strategy How it writes the query What you need Speed
ServiceStrategy (default) On Enigma's hosted query service, an LLM picks one of Enigma's tested query templates and fills it in Your Enigma API key Fast
TemplateStrategy An LLM picks one of Enigma's tested query templates and fills in the names and places An LLM API key Fast
GenerativeStrategy An LLM writes the whole query; the SDK checks it against the API schema and repairs it An LLM API key Slower
AgentStrategy A Claude agent on Amazon Bedrock reads Enigma's query reference and writes the query AWS credentials with Bedrock access, and enigma-api-client[anthropic] Slowest, often tens of seconds

When to use each:

  • Start with the default. Its queries come from tested templates, so the fields a kind of question returns are predictable. A question no template fits gets the closest one, usually the business's profile, which may answer it only in part.
  • When the default's answer misses part of what you asked, such as a state, and unavailable_data doesn't list it, ServiceStrategy(query_strategy="generative", enigma_api_key=...) has the hosted service write the whole query instead of picking a template. For example, it can narrow a person's businesses to one state ("businesses registered to Jane Doe in Pennsylvania"), where a template lists that person's businesses in every state. Review the queries it writes.
  • To generate queries with your own LLM account, use TemplateStrategy in place of the default and GenerativeStrategy in place of the hosted generative option. Set tier in ClientConfig to your key's tier (see Access Tiers).
  • When the other strategies still leave something out and you can wait tens of seconds, try AgentStrategy. The agent reads Enigma's GraphQL reference and its examples as it works.
  • For card transactions, your tier must be AccessTier.EXTERNAL (see Access Tiers). Use any strategy except AgentStrategy.
  • For a pipeline that asks the same kind of question many times, generate the query once, review it, and run it with your own variables (see the section See and Reuse a Generated Query).

To use a strategy other than the default, pass it to ClientConfig:

from enigma_api_client.strategies import TemplateStrategy

config = ClientConfig(
    enigma_api_key=os.environ["ENIGMA_API_KEY"],
    query_strategy=TemplateStrategy(),
)

ServiceStrategy

ServiceStrategy sends your question to Enigma's hosted query service, authenticated with your Enigma API key, and returns the query the service writes. client.search then runs it. The service reads your access tier from the key.

The service picks a template by default, as TemplateStrategy does. To have it write the whole query instead, create the strategy yourself with query_strategy="generative". A ServiceStrategy you create doesn't take the key from ClientConfig: pass enigma_api_key=, or it reads ENIGMA_API_KEY.

from enigma_api_client.strategies import ServiceStrategy

strategy = ServiceStrategy(query_strategy="generative", enigma_api_key=os.environ["ENIGMA_API_KEY"])
config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"], query_strategy=strategy)

TemplateStrategy

An LLM reads the question, picks one of Enigma's query templates, and extracts the names, places, and other values the template needs. The SDK then builds the GraphQL from the template, so the query's fields always come from tested templates.

The templates cover:

  • Brand profiles
  • Brand lookups by website
  • A brand's locations
  • A location's profile
  • Person searches
  • Corporate officers
  • Legal entity verification
  • Contacts
  • Watchlist screening
  • Risk assessments
  • Counts by category and place
  • Counts of a brand's locations
  • Address lookups, when turned on (see Turn Address Lookups On or Off)
  • Card transactions, when your tier is AccessTier.EXTERNAL (see Access Tiers)

A brand, location, legal entity, officer, or person lookup returns a core set of fields plus only the data the question asks for: industry, website, phone, officers, bankruptcies, watchlist entries, TINs, registration details, locations, affiliates, alternate names, address, reviews, or headcount. Contact, watchlist, and risk lookups return the same fields whatever the question asks, apart from headcount and government records.

  • A compliance question adds whichever of officers, TINs, watchlist entries, bankruptcies, and registration details the lookup can return (a location's profile can return only registration details). A compliance question is one that mentions KYB, KYC, AML, compliance, due diligence, a background check, or vetting, or that asks to vet or screen the entity. A question that only asks to screen an entity or to run due diligence on it usually gets the watchlist or risk lookup instead.
  • A question asking for everything about an entity adds all of this data that its lookup can return, except headcount. A question gets headcount only by asking for it, as in "how many employees".
  • With several businesses or people, the data the question asks for is added to each one's search.

TemplateStrategy refuses a category listing with UNSUPPORTED_DATA_REQUESTED. It refuses with INVALID_QUERY a question that isn't about a business, location, person, or address. With AccessTier.REDISTRIBUTION, it usually refuses a request for card transaction data the same way. Any other question that fits no template gets the closest one, usually the business's profile.

Argument Default Meaning
anthropic_api_key ANTHROPIC_API_KEY The Anthropic API key, when llm_provider is not set
model claude-haiku-4-5-20251001 The Anthropic model, when llm_provider is not set
llm_provider None Another LLM provider (see LLM Providers)
shape_cache On, unless ENIGMA_SHAPE_CACHE_ENABLED turns it off Whether to answer a question that matches an earlier one's pattern without an LLM call (see Query Shape Cache)
shape_cache_max_shapes 8192 The most query shapes the cache keeps
address_search ENIGMA_ADDRESS_SEARCH, else "auto" Whether the strategy writes address lookups (see Look Up an Address)

How Template Queries Read Places

This section applies to template queries: those from ServiceStrategy's default and from TemplateStrategy. In this section, a place is a city, a state, or, in an address lookup, a unit or a ZIP code.

Which lookups use places:

  • A lookup of one location of a business, or of an address, searches by street, city, and state, and an address lookup also by unit and ZIP code. The next two lists say how a question's lists of these values pair.
  • A brand's locations, counts of a brand's locations, and counts by category are filtered by city and state. A count by category over several places is one count, not one per place. Each search or count matches every city it is given in every state it is given, so "coffee shops in Springfield, IL and Columbus, OH" also counts those in Springfield, OH.
  • A legal entity lookup isn't filtered by place. It lists each business's registrations in the states the question gives that business, under registrationsInState in each of its registeredEntities. The rules in the next list decide which states each business gets.
  • Brand profiles, person searches, and officer, contact, watchlist, risk, and card transaction lookups don't filter by place.

How places are matched to businesses, people, and streets:

  • A lookup of one location of a business ("the Starbucks at 1 Main St"), or of an address, pairs places by position. The first city or state in a list goes with the first business or street, the second with the second, and so on. In an address lookup, units and ZIP codes pair the same way. Each position gets its own search, so a list of four streets gets four searches.
  • Elsewhere, every place applies to each business or person. "Starbucks locations in Boston and Dunkin locations in Chicago" gets one search per brand, each covering both cities.
  • To give each one its own place, say "respectively" ("Starbucks and Dunkin locations in Boston and Chicago, respectively"), or ask about them separately.
  • When the question gives a place for some of them and none for another, each keeps its own. "Starbucks in Boston, Dunkin, and Peet's in Chicago" searches Starbucks in Boston and Peet's in Chicago, and doesn't filter Dunkin by city.
  • The rules apply to a question's cities and to its states separately, so in one question each business can keep its own city while every state applies to each business.

How pairing by position works:

  • A single business or person pairs with every street and every place in a list, and a single street pairs with every place. So "the Starbucks at 1 Main St in Boston and Cambridge" gets two searches, and so does "the Starbucks at 1 Main St and 2 Elm St".
  • Several businesses, people, or streets pair by position with a list of cities, states, units, or ZIP codes.
  • A list of places can be longer than the list of businesses, people, or streets it pairs with. When the shorter list is of streets that all go with one business or person, that business or person is also searched in each extra place, without a street. In "the Starbucks at 1 Main St and 2 Elm St in Boston, Cambridge, and Somerville", Starbucks goes with both streets and is also searched in Somerville. Otherwise, the extra place has nothing to pair with, and the question is refused.
  • Outside a count question, a street that pairs with no business or person gets an address lookup of its own (see Look Up an Address). While address lookups are off, such a question is refused.

GenerativeStrategy

One LLM call writes the whole query. It handles questions the templates don't cover. GenerativeStrategy takes the same arguments as TemplateStrategy.

  • The LLM works from an excerpt of Enigma's GraphQL reference, example query patterns, and rules. The rules tell it to write the question's values into the query text.
  • The SDK checks the query against the API schema and the rules the API enforces at execution.
  • The SDK fixes what it can without a model call. Otherwise, it makes one more LLM call to repair the query.
  • The LLM can still choose fields that don't answer the question, so review the queries it writes before you rely on them.

GenerativeStrategy raises UNSUPPORTED_DATA_REQUESTED instead of returning a query in these cases:

  • Any search in the query is one of these:
    • A person search by surname alone, when the question uses the word "surname", "last name", or "family name".
    • An employer identification number (EIN) written with its dash, such as 12-3456789, searched as a name.
    • A number of five or more digits from the question searched as a name, when the question names an identifier, such as a filing number or license number, unless the question introduces the number as a business's name, as in "the store called 90210".
    • A wildcard name pattern, such as Star%.
    • A street address searched as a name, when the question uses wording such as "tenants", "occupants", "located at", "registered at", "operating out of", or "at this address".
  • The query is a category count the question didn't ask for. That is, all of these hold:
    • The query only counts businesses: every top-level field in it is an aggregate count.
    • Each count writes its search input into the query text, not into variables.
    • No count names a business, website, person, phone number, id, or TIN.
    • At least one count is by category.
    • The question doesn't ask for a count.

AgentStrategy

A Claude agent, built with the Claude Agent SDK, reads Enigma's GraphQL reference, examples, and notes on the schema's fields, and writes the query. The SDK checks the agent's answer against the API schema and applies the same checks as GenerativeStrategy, fixing what it can without a model call. AgentStrategy suits complex questions best and is the slowest strategy.

pip install "enigma-api-client[anthropic]"
from enigma_api_client.strategies import AgentStrategy

config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"], query_strategy=AgentStrategy())

The agent runs on Amazon Bedrock only:

  • Credentials come from the standard AWS credential chain.
  • The identity needs bedrock:InvokeModel and bedrock:InvokeModelWithResponseStream on the inference profile and on the foundation model in every region the profile routes to.
  • The default model is the US inference profile us.anthropic.claude-haiku-4-5-20251001-v1:0. Pass model= to change it.
  • The region comes from region=, then AWS_REGION, then AWS_DEFAULT_REGION, and is us-east-1 when none is set. Keep the region inside the profile's geography.
  • max_turns (default 8) limits each call's agent turns, and max_budget_usd (default 0.25) limits each call's model spend in US dollars. A call that reaches either limit raises INVALID_QUERY.

The agent runs the Claude Code CLI bundled with claude_agent_sdk as a subprocess:

  • Its nonessential network traffic is turned off.
  • Its only tool reads files inside the query reference bundled with the SDK. It has no shell, file writes, web access, or file attachments.
  • Every environment variable it inherits is blanked, except basic process settings (such as PATH, HOME, locale, and temporary directories), TLS and proxy settings, and the AWS credential chain.
  • It keeps no session transcript on disk.

The agent writes values into the query itself, so the query it returns has no variables.

When its answer still fails the schema check after the SDK's fixes, AgentStrategy raises UnrepairedQueryError.

See and Reuse a Generated Query

strategy.generate_query(prompt) returns the query without running it:

import asyncio
import os

from enigma_api_client import Client, ClientConfig
from enigma_api_client.strategies import ServiceStrategy


async def main():
    key = os.environ["ENIGMA_API_KEY"]
    strategy = ServiceStrategy(enigma_api_key=key)
    async with Client(ClientConfig(enigma_api_key=key, query_strategy=strategy)) as client:
        generated = await strategy.generate_query("Tell me about Chipotle")
        print(generated.query)
        print(generated.variables)  # {'searchInput': {'name': 'Chipotle', 'entityType': 'BRAND', 'conditions': {'limit': 1}}}

        variables = {"searchInput": {**generated.variables["searchInput"], "name": "Sweetgreen"}}
        response = await client.search(query=generated.query, variables=variables)
        print(response.data["search"][0]["names"]["edges"][0]["node"]["name"])  # SWEETGREEN


asyncio.run(main())
  • Check a query before you reuse it with other variables. Any strategy can write some values from the question directly into the query text, where your variables don't replace them:
    • When a question names a state, a legal entity lookup writes that state into the query.
    • A query that selects government records writes the searched name, person's name, and address into its archive searches.
    • GenerativeStrategy is told to write every value into the query text, and AgentStrategy always does.
  • A template query numbers its variables like the keys of data: searchInput, searchInput2, and searchInput3. Other variables follow the same pattern, such as locConditions2.
  • Call generate_query inside the async with block. A local strategy checks its query against the API schema, which the Client fetches with your key.
  • Called directly, a local strategy's generate_query writes for AccessTier.REDISTRIBUTION unless you pass your tier, as in generate_query(prompt, tier=AccessTier.EXTERNAL). client.search passes ClientConfig.tier for you. ServiceStrategy always writes for your key's tier.

generate_query returns a GenerateQueryResponse:

Field Holds
query, variables The GraphQL query and its variables
unavailable_data The parts of the question Enigma can't answer
deterministic_repair The kinds of fixes, comma separated, when the SDK fixed the query without a model call
cache_rule "shape" when the query shape cache answered without an LLM call; other values report what the cache learned from the call
records For a question asking for government records, from a local strategy: whether the SDK added record fields (see the list after this table)

records takes these values:

  • selected: the SDK added record fields.
  • no_schema: the SDK had no API schema yet (see Records and the API Schema).
  • no_entity: the query has no entity search to add them to, such as a count.
  • already_selected: the query already selects records, or already uses a response key the SDK would add, such as archiveRecords.
  • invalid: the query doesn't parse, or adding record fields would break a rule the query passes.

Access Tiers

The fields you can read depend on your API key's access tier:

  • AccessTier.REDISTRIBUTION is the narrowest tier.
  • AccessTier.EXTERNAL adds fields such as TINs, watchlist entries, and card transaction data.

The hosted service reads the tier from your key. A local strategy writes queries for the tier in ClientConfig.tier, which defaults to AccessTier.REDISTRIBUTION. Set it to your key's tier:

from enigma_api_client import AccessTier
from enigma_api_client.strategies import TemplateStrategy

config = ClientConfig(
    enigma_api_key=os.environ["ENIGMA_API_KEY"],
    query_strategy=TemplateStrategy(),
    tier=AccessTier.EXTERNAL,
)

If you keep the default tier, a local strategy writes queries that leave out fields a broader key could read. If you don't know your key's tier, ask Enigma support.

  • A local strategy removes from the query each field the API doesn't serve your tier, and lists it in unavailable_data with the reason contractual. enigma_api_client.restricted_fields_for(tier) returns those fields.
  • For AccessTier.REDISTRIBUTION, once the SDK has the API schema (see Records and the API Schema), a local strategy also leaves out the email addresses and phone numbers of the people in a company's roles (emailAddresses and phoneNumbers under a role), because the API answers a redistribution key with placeholder values for them. It lists a field it removed in unavailable_data, as Role.emailAddresses or Role.phoneNumbers with the reason contractual, only when the question asks for it and the query had selected it, as in "What is the email of Sweetgreen's CFO?" answered by the contacts lookup. A location's own phone numbers are real and stay.
  • To filter data before you pass it on to others, use enigma_api_client.gate_blocked_fields(tier). It returns those fields plus, for AccessTier.REDISTRIBUTION, fields the API serves but a redistribution contract forbids. A local strategy doesn't remove those extra fields, and the API can return them to your own queries too.

Schema Checks and Several Tiers

  • The SDK fetches the API schema for the tier of the first Client you create in a process, with that client's key, and checks generated queries against it. Set tier before you create your first Client. In a notebook, restart the kernel after you change it.
  • A later Client with a different tier gets no schema. On that client, AgentStrategy raises PROVIDER_ERROR. TemplateStrategy and GenerativeStrategy still answer, with these differences:
    • Generated queries aren't checked against the schema.
    • Government records are left out.
    • Address lookups are off unless you turn them on with address_search=True or ENIGMA_ADDRESS_SEARCH=on (see Turn Address Lookups On or Off).
    • For AccessTier.REDISTRIBUTION, role email addresses and phone numbers stay in the query, so the API answers them with placeholder values (see Access Tiers).

To serve several tiers in one process, register a schema introspector with validation.set_schema_introspector before you create the first Client. An introspector is a function that takes an AccessTier and returns that tier's schema.

The SDK calls it either on your event loop's thread, blocking every search on that loop until it returns, or from a background thread with no context variables (contextvars) set. So the introspector must:

  • Be synchronous and thread-safe.
  • Return in bounded time.
  • Not read context variables.
  • Return None when a fetch fails, as the following example does, so a failed fetch doesn't raise from client.search.
import os

from enigma_api_client import AccessTier, validation

KEYS = {
    AccessTier.EXTERNAL: os.environ["ENIGMA_EXTERNAL_KEY"],
    AccessTier.REDISTRIBUTION: os.environ["ENIGMA_API_KEY"],
}


def introspector(tier):
    key = KEYS.get(tier)
    if key is None:
        return None
    try:
        return validation.introspect_schema("https://api.enigma.com/graphql", key)
    except Exception:
        return None


validation.set_schema_introspector(introspector)  # before you create the first Client

Each Client then gets the schema for its own tier.

The first time a tier's schema is used after it is five minutes old, the SDK fetches it again in the background. Pass refresh_seconds= to set_schema_introspector to change that interval, or refresh_seconds=None to turn the refresh off.

LLM Providers

TemplateStrategy and GenerativeStrategy call Anthropic's API by default, with anthropic_api_key= or the ANTHROPIC_API_KEY environment variable; their model= argument applies only to that default. Pass llm_provider= to use another provider:

from enigma_api_client.strategies import (
    AnthropicProvider, BedrockProvider, GenerativeStrategy, OpenRouterProvider, TemplateStrategy,
)

# Anthropic's API (ANTHROPIC_API_KEY)
TemplateStrategy(llm_provider=AnthropicProvider(model="claude-haiku-4-5-20251001"))

# OpenRouter (OPENROUTER_API_KEY). provider_routing is sent as OpenRouter's "provider"
# object and reasoning as its "reasoning" object; omit either to keep OpenRouter's default.
GenerativeStrategy(llm_provider=OpenRouterProvider(
    model="anthropic/claude-haiku-4.5",
    provider_routing={"order": ["anthropic"], "allow_fallbacks": False},
    reasoning={"enabled": False},
))

# Claude on Amazon Bedrock (pip install "enigma-api-client[bedrock]"), signed with your AWS credentials.
# The default model is a US inference profile; pass model= outside US regions.
TemplateStrategy(llm_provider=BedrockProvider(region="us-east-1"))

Write Your Own LLM Provider

To use another LLM, implement the LlmProvider protocol. Import LlmProvider, Completion, and ProviderHTTPError from enigma_api_client.strategies.

  • Implement async def complete(system, user, max_tokens) -> Completion.
  • Return Completion(text, truncated), where truncated is true when the model hit its output limit.
  • Pass the call's token counts to Completion as input_tokens=, output_tokens=, cache_read_tokens=, and cache_write_tokens=, so that Token Usage includes them. Leave cache reads and writes out of input_tokens; they have their own fields.
  • If your provider reports which host served the call, pass it as served_by=.
  • If your provider reports what it billed for the call, pass the amount in USD as a float, cost_usd=.
  • Raise these exceptions for failures:
Raise Reported as
ProviderHTTPError(status_code, message) with 408, 429, or 5xx TRANSIENT_PROVIDER_ERROR
ProviderHTTPError with any other HTTP status PROVIDER_ERROR
ProviderHTTPError with a value that isn't an HTTP status TRANSIENT_PROVIDER_ERROR
ValueError, for a missing key or bad configuration GENERATE_QUERY_CALLBACK_NOT_CONFIGURED
RuntimeError, for any other provider failure TRANSIENT_PROVIDER_ERROR

Any other exception propagates unchanged.

Query Shape Cache

TemplateStrategy and GenerativeStrategy keep an in-memory cache of query shapes learned from successful generations:

  • A shape is a generated query with its names and places left as blanks.
  • Once a shape has been confirmed across several different entities, a matching question is answered from the cache without an LLM call.
  • The cache is on by default and is shared by every caller of the same strategy instance at the same tier.
  • To turn it off, pass shape_cache=False, or set ENIGMA_SHAPE_CACHE_ENABLED=0 before you create the strategy.

Token Usage

After client.search(prompt=...) or generate_query returns or raises, enigma_api_client.strategies.GENERATION_USAGE.get() returns that call's LLM token counts as an LlmUsage:

Field Holds
completions The number of LLM completions
input_tokens, cache_read_tokens, cache_write_tokens, output_tokens The token counts
unmetered_completions The number of completions whose provider left out the input or the output token count, so the sums miss their tokens
served_by The host each completion named as having served it, in call order, for providers that report one, such as OpenRouterProvider
cost_usd The amount in USD billed for the completions whose provider reported one, such as OpenRouterProvider; completions from other providers add nothing
  • Read it in the same task that awaited the call. asyncio.gather runs each call in a task of its own, so read the usage inside the coroutine you pass to it.
  • ServiceStrategy reports zero usage, and AgentStrategy reports its run's token totals and no cost_usd.

Write Your Own Strategy

To write your own strategy:

  1. Subclass QueryStrategy.
  2. Implement generate_query and aclose.
  3. At the start of generate_query, set GENERATION_USAGE to a fresh LlmUsage(), so Token Usage reports this call, not an earlier one.

This strategy treats the whole question as a brand name:

import asyncio
import os

from enigma_api_client import DEFAULT_TIER, Client, ClientConfig, QueryStrategy
from enigma_api_client.strategies import GENERATION_USAGE, LlmUsage
from enigma_api_client.strategies.base import GenerateQueryResponse

QUERY = """
query Brand($name: String!) {
  search(searchInput: { name: $name, entityType: BRAND, conditions: { limit: 1 } }) {
    ... on Brand { names(first: 1) { edges { node { name } } } }
  }
}
"""


class BrandNameStrategy(QueryStrategy):
    async def generate_query(self, prompt, *, tier=DEFAULT_TIER, prior_attempt=None):
        GENERATION_USAGE.set(LlmUsage())
        return GenerateQueryResponse(QUERY, {"name": prompt})

    async def aclose(self):
        pass


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"], query_strategy=BrandNameStrategy())
    async with Client(config) as client:
        response = await client.search(prompt="Chipotle")
        print(response.data["search"][0]["names"]["edges"][0]["node"]["name"])  # CHIPOTLE MEXICAN GRILL


asyncio.run(main())

Handle Errors

Errors reach you in these ways:

  • When the SDK can't generate a query, client.search(prompt=...) raises EnigmaClientException (see Generation Errors).
  • An error status from the API doesn't raise. It comes back in the response, so check response.is_success().
  • A network failure reaching the API or the hosted query service raises an httpx exception (see API and Network Errors).
  • An Enigma API key or custom header value that an HTTP header can't carry as written, such as one with a newline or a trailing space, raises ValueError (see API and Network Errors).
import asyncio
import os

from enigma_api_client import Client, ClientConfig, EnigmaClientException


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        try:
            response = await client.search(prompt="List coffee shops in Denver")
        except EnigmaClientException as exc:
            print(exc.error_code, exc)
            return
        if not response.is_success():
            print("HTTP", response.status_code, response.errors)


asyncio.run(main())

The example prints the error message, which is useful while you develop. In production, log the exception type and error_code instead, because the message can quote values from the query.

Generation Errors

When the SDK can't generate a query, client.search(prompt=...) raises EnigmaClientException, whose error_code is an EnigmaClientErrorCode.

With ServiceStrategy, the default, the code is always INVALID_QUERY, including when the hosted service fails with HTTP 401, 429, or 5xx. To tell these failures apart, read the exception:

  • When the service answers with an error status, exc.status_code holds it and exc.headers holds its response headers. The message reads Enigma API returned HTTP <status>: followed by the start of its answer.
  • When the service answers with a GraphQL error, the message starts Enigma API GraphQL error:.
  • Retry when the status is 429 or 5xx. When exc.headers.get("Retry-After") gives a number of seconds, wait that long first.

For the preceding example, the message begins Enigma API returned HTTP 400 and explains that listing businesses by category is not supported.

Local strategies use these codes:

Code Meaning
INVALID_QUERY The question or the generated query is unusable
UNSUPPORTED_DATA_REQUESTED Enigma doesn't serve what the question asks for, or the question needs a query the SDK refuses (see Generate Queries from Questions and GenerativeStrategy)
GENERATE_QUERY_CALLBACK_NOT_CONFIGURED An LLM API key is missing, or the provider is misconfigured
TRANSIENT_PROVIDER_ERROR The LLM provider timed out, rate limited the call, or failed with a 5xx error; retry with backoff
PROVIDER_ERROR Any other LLM provider failure, such as HTTP 401 or 403. Retrying won't help.

AgentStrategy also raises PROVIDER_ERROR when the SDK has no API schema for your tier (see Schema Checks and Several Tiers). In that case, a retry helps only if the schema fetch failure was temporary.

For a provider failure, exc.status_code holds the provider's HTTP status when it reported one.

The SDK never raises RESTRICTED_DATA_REQUESTED or INVALID_VARIABLES; they exist for applications that enforce their own policies.

enigma_api_client exports two subclasses of EnigmaClientException, both with the code INVALID_QUERY:

  • TruncatedOutputError: the LLM hit its output limit. It happens again with the same model and question, so don't retry with the same model.
  • UnrepairedQueryError: GenerativeStrategy or AgentStrategy wrote a query that failed the schema check even after repair. It carries .draft, .errors, and .tier, and you can pass it to another GenerativeStrategy as generate_query(prompt, prior_attempt=exc) to start from that draft. Its message quotes values from the generated query, so log the exception type and error_code rather than str(exc).

Unavailable Data

When a question asks for data Enigma doesn't serve, the rest of the question is still answered, and response.unavailable_data lists what was left out, one entry per item:

  • field: what was left out.
  • reason: contractual when your tier isn't served the data, and out_of_scope otherwise, for example when Enigma doesn't have it.
  • message: why.

"What is Chipotle's annual revenue?" with a redistribution key returns the brand and this entry:

[{"field": "revenue", "reason": "contractual",
  "message": "Revenue and other card-derived financials are not served due to contractual restrictions."}]

"Tell me about Chipotle, including any lawsuits against it" lists lawsuits as out_of_scope.

API and Network Errors

  • An error status from the API doesn't raise. Check response.is_success() and response.status_code, and see Response status codes. A 402 means a billing problem, such as running out of credits.
  • A 429 means you went over a rate limit. Wait at least the number of seconds in its Retry-After header, response.headers.get("Retry-After"), before you retry. Stop after a few retries, because a 429 can also mean you have used your daily limit.
  • A response body that isn't a JSON object, such as a gateway's error page, comes back with data set to None, an error in errors, and the body in raw_body, so is_success() is false.
  • An API key or header value an HTTP header can't carry as written, such as one with a newline or a trailing space, raises ValueError naming the header, before the API request is sent.
  • Network failures, from the API or the hosted query service, raise httpx exceptions such as httpx.TimeoutException and httpx.ConnectError.

Paginate

Each connection returns one page. To read every page, select pageInfo { hasNextPage endCursor } on the connection and send the same query again with after: set to the last endCursor:

import asyncio
import os

from enigma_api_client import Client, ClientConfig

QUERY = """
query Locations($name: String!, $cursor: String) {
  search(searchInput: { name: $name, entityType: BRAND, conditions: { limit: 1 } }) {
    ... on Brand {
      operatingLocations(first: 100, after: $cursor) {
        edges { node { addresses(first: 1) { edges { node { fullAddress } } } } }
        pageInfo { hasNextPage endCursor }
      }
    }
  }
}
"""


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        cursor, addresses = None, []
        while True:
            response = await client.search(query=QUERY, variables={"name": "Sweetgreen", "cursor": cursor})
            if not response.is_success():
                raise RuntimeError(f"HTTP {response.status_code}: {response.errors}")
            locations = response.data["search"][0]["operatingLocations"]
            for edge in locations["edges"]:
                for address in edge["node"]["addresses"]["edges"]:
                    addresses.append(address["node"]["fullAddress"])
            if not locations["pageInfo"]["hasNextPage"]:
                break
            cursor = locations["pageInfo"]["endCursor"]
        print(len(addresses), "addresses")


asyncio.run(main())
  • Start with cursor set to None.
  • Use hasNextPage to decide whether more pages exist. A full page doesn't mean there are more.
  • Keep the connection's first and conditions (with its filter and orderBy) the same on every page, because a cursor is only valid for the arguments it came from.
  • ClientConfig(auto_inject_page_info=True) adds pageInfo { hasNextPage endCursor } to every connection that doesn't already select pageInfo, in every query the client sends, including generated ones.
  • Generated queries already select page info on:
    • A brand's affiliated brands (Brand.affiliatedBrands) and a legal entity's parent and subsidiary legal entities (LegalEntity.legalEntities). GenerativeStrategy and AgentStrategy add this page info only once they have the API schema for your tier.
    • The government record pages, which a local strategy adds only once it has that schema (see Records and the API Schema).
    • Brand.operatingLocations, in the template that lists a brand's locations.
    • registrationsInState, in the legal entity lookup, when it lists registrations in several states. The registrations of those states share one page.
  • To read more pages of a generated query:
    1. Get the query with generate_query.
    2. Add after: $cursor to the connection.
    3. Declare $cursor: String with the query's other variables, as the preceding example does. If the query declares no variables yet, add ($cursor: String) after its name, or begin it with query ($cursor: String) { when it has no name.
  • roles doesn't paginate (see Rules the API Enforces at Execution).

Look Up an Address

A question about a street address with a house number and no business name searches addresses:

import asyncio
import os

from enigma_api_client import Client, ClientConfig


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        response = await client.search(prompt="What is at 1600 Pennsylvania Ave NW, Washington DC?")
        if not response.is_success():
            raise RuntimeError(f"HTTP {response.status_code}: {response.errors}")
        for address in response.data["search"]:
            locations = address["operatingLocations"]["edges"]
            print(f'{address["fullAddress"]}: {len(locations)} operating locations listed')


asyncio.run(main())

The query is an entityType: ADDRESS search that returns up to 10 addresses: the building and units in it. When the question names a unit, that unit usually comes first. Each address carries:

  • Its address fields, such as fullAddress, city, state, and zip.
  • The first page of the operating locations (with brand names), legal entities, and registrations at the address.

No strategy selects the Address counts brandViaLegalEntityCount, brandViaPersonLegalEntityCount, and legalEntityViaRegistrationCount, because they take too long to compute. Leave them out of your own queries too.

An address question that asks how many, other than a headcount, gets an unavailable_data entry with field set to address counts and reason set to out_of_scope, because the query doesn't count what is at the address. When the question also asks for government records and the SDK adds record fields, each address gets recordCount (see Records on Each Entity), and the response has no address counts entry.

How questions are read:

  • A business name with an address ("the Starbucks at 123 Main St") is looked up as that business's location instead.
  • A ZIP code alone, a street with no house number, or a business type on a street ("the barbershop on Elm Street") is refused.
  • Before a local strategy returns the query, the SDK cleans the address the LLM extracted:
    • It moves a suite, floor, or apartment into street2 in US Postal Service (USPS) short form (STE 1A, FL 8).
    • It drops a unit designator with no number.
    • It removes surrounding prose and a trailing country.
    • It converts state names to postal codes.

Turn Address Lookups On or Off

Local strategies take an address_search= argument: True, False, or "auto".

  • Without the argument, a strategy reads the ENIGMA_ADDRESS_SEARCH environment variable (on, off, or auto).
  • When neither is set, it uses auto.
  • auto turns address lookups on when the API schema for your tier supports them. Each strategy instance decides at its first question for each access tier and keeps that answer. If the schema can't be fetched at that moment, address lookups stay off for that instance and tier.
  • With address lookups off, a local strategy handles a question that gives only an address in one of two ways:
    • Usually, it refuses the question, raising UNSUPPORTED_DATA_REQUESTED or INVALID_QUERY.
    • Sometimes its LLM takes the address as a business name, and the strategy can return a query that searches for that name. GenerativeStrategy and AgentStrategy refuse such a query only when the question uses wording such as "tenants" or "registered at" (see GenerativeStrategy).
  • While the API schema the SDK has cached for your tier shows no support for address lookups, a local strategy refuses an address search, whatever address_search says. It raises UNSUPPORTED_DATA_REQUESTED, and the message ends (bare address lookup). Other address refusals, such as those with address lookups off, can end the same way, so the ending alone doesn't tell you why the question was refused.
  • While no schema for your tier is cached yet, TemplateStrategy and GenerativeStrategy with address lookups turned on (address_search=True or ENIGMA_ADDRESS_SEARCH=on) still write address lookups.

Get Government Records

To get an entity's government records, name the entity and ask for its records, as in "Show business licenses held by Walgreens".

The records come from Enigma's Gov Archive, which holds rows of government and public datasets, such as state Uniform Commercial Code (UCC) filings, business licenses, and city food inspections:

  • Each row is a record.
  • Enigma links most records to the businesses, locations, people, and addresses they name.
  • Each dataset carries tags, such as ucc_lien, that name the types of records it holds.

You can ask for these record types:

  • UCC and lien filings
  • Business and occupational licenses, including liquor, contractor, tobacco and cannabis, insurance, money transmitter, firearms, and childcare licenses
  • Permits and sales tax permits
  • Inspections and health grades
  • Violations and enforcement actions
  • Government contracts, Paycheck Protection Program (PPP) loans, and SAM.gov registrations, including Unique Entity IDs (UEI) and CAGE codes
  • Form 5500 benefit plans, H-1B and labor certifications, workers' compensation, and Worker Adjustment and Retraining Notification (WARN) layoff notices
  • Securities and Exchange Commission (SEC) filings, 13F holdings, and Form 990 filings
  • Property records, Environmental Protection Agency (EPA) records, and US Department of Transportation (USDOT) motor carrier records
  • Campaign finance and lobbying
  • Medicare and Medicaid providers, and minority-, women-, and veteran-owned business certifications

How a question is read:

  • A question that asks for "public records", "government records", or the "gov archive" in general gets records of every type, unless it also names one of the preceding types.
  • Words in the names a question searches by don't count, so a question about a business named "Permit Pros" asks for no permits.

The response holds records in two places:

  • On each entity search returns (see Records on Each Entity).
  • In archive searches: searches of the Gov Archive that the SDK adds to the query next to search, answered under keys that start with archiveRecords or archiveBrandRecords (see Records from Archive Searches).

This example prints both:

import asyncio
import os

from enigma_api_client import Client, ClientConfig


async def main():
    config = ClientConfig(enigma_api_key=os.environ["ENIGMA_API_KEY"])
    async with Client(config) as client:
        response = await client.search(prompt="Show business licenses held by Walgreens")
        if not response.is_success():
            raise RuntimeError(f"HTTP {response.status_code}: {response.errors}")
        for entity in response.data["search"]:
            print(entity["__typename"], "linked records:", entity.get("recordCount"))
        for key, items in response.data.items():
            if key.startswith(("archiveRecords", "archiveBrandRecords")):
                for item in items or []:
                    if item["__typename"] == "Record":
                        print(key, item["sourceTitle"], item["data"])


asyncio.run(main())

Records on Each Entity

Each entity the search returns gains recordCount. Whether it also gets a records page depends on the question and on the API:

  • For a question that names record types, the SDK adds the page and matchingRecordCount only when the API can filter an entity's linked records quickly, which the SDK tells from the API schema. Otherwise, the entities get neither, and those records come from the archive searches alone.
  • A question asking for records in general gets the records page either way.

Read the page with entity.get("records").

Field Where Holds
recordCount Every returned entity The number of records linked to the entity, over every dataset
recordSourceCounts, recordTagCounts A returned address The record count per dataset title and per tag
recordSources A returned legal entity The first three of its registeredEntities, with page info. Each carries recordSourceCounts and recordTagCounts.
matchingRecordCount An entity that gets a records page, for a question that names record types The number of its linked records of those types
records An entity that gets a records page The linked records, with pageInfo { hasNextPage endCursor }: the first ten when the search is limited to one entity (conditions: { limit: 1 }), otherwise the first three. For a question that names record types, only records of those types.

Legal entities the query selects under a returned brand or location also get:

  • recordCount.
  • Their first three records, when the returned entities get a records page.

Records from Archive Searches

An archive search searches the Gov Archive by the same name or person (first and last name) as the search it sits beside, and by that search's address when the address has a street line. It takes the whole address: street line, unit, city, state, and ZIP code.

Which archive searches a query gets:

  • A search by name, by a person's first and last name, or by a street address gets archiveRecords, with two exceptions:
    • A street address alone, with no name or person's first and last name, gets one only in an entityType: ADDRESS search.
    • In a PERSON search, name doesn't count; only the person's first and last name do.
  • A name search with entityType: BRAND, entityType: OPERATING_LOCATION, or no entityType gets two archive searches:
    • archiveRecords searches legal entities.
    • archiveBrandRecords searches brands, which finds records that name a brand or trade name.
    • Neither one searches by a person's first and last name, even when the search has them.
    • The two can return the same record.
  • A search with no name, person, or street address, such as one by website alone, gets no archive search.
  • When several searches in the query get archive searches, the keys of the second and later ones end in a number, as in archiveRecords2. The number counts only the searches that get archive searches, so it need not match the number of the search key beside it.
  • An archive search identical to an earlier one is sent once.

What each archive search returns:

  • First, the entities it matched, with only __typename (and, for a person, __url).
  • Then up to 50 records: the items whose __typename is Record. It returns a record when the record's data names the searched name or address, whether or not the record is linked to an entity.
  • For a question that names record types, each record carries one of their tags. A question for records in general gets records with any tag.
  • One page, with no cursor to read more.

Check what you get:

  • The archive matches names loosely, so a record can belong to another business or person with a similar name. Unless the searched address has a street line, the archive search gets no city or state, so that namesake can be anywhere. Check the name in the record's data.
  • A dataset can carry a record type's tag without belonging to it, so check sourceTitle.
  • Records are not sorted by date.
  • The archive searches miss many records of these types: UCC and lien filings, contractor licenses, PPP loans, government contracts, SAM.gov registrations, workers' compensation, lobbying, and Form 990 filings. When the entities get no records page, a question for these types often returns few records or none.

To leave the archive searches out of queries a local strategy writes, set ENIGMA_ARCHIVE_SEARCH=off.

For the example question, the query gains recordCount on each entity it returns, and archive searches next to search. The archiveRecords search, abbreviated:

archiveRecords: search(searchInput: { name: "Walgreens", entityType: LEGAL_ENTITY, dataLayer: UNGROUNDED,
                                      conditions: { limit: 50, filter: { IN: ["tags", ["abc_license", "active_occupational_licenses", ...]] } } }) {
  __typename
  ... on Record { sourceTitle sourceUrl tags firstObservedDate lastObservedDate data }
}

dataLayer: UNGROUNDED makes the search read the archive's records directly, whether or not they are linked to an entity.

What Each Record Holds

Each record, on an entity or in an archive search, carries:

  • sourceTitle: the dataset's title.
  • sourceUrl: a link to the dataset, not to the row. It can be null.
  • tags: the dataset's tags, such as ucc_lien or abc_license.
  • firstObservedDate, lastObservedDate: when Enigma first and last saw the row. These are usually not filing dates.
  • data: the row itself, as column names and values. The filing dates are here.

Read More of an Entity's Records

When an entity's records page has pageInfo.hasNextPage set to true, read the next page:

  1. Search that entity alone by its id: searchInput: { id: "<its id>", entityType: <its type>, conditions: { limit: 1 } }. Set entityType to BRAND, LEGAL_ENTITY, OPERATING_LOCATION, PERSON, or ADDRESS to match its __typename.
  2. Select records with the same first and conditions as before, plus after: "<endCursor>". The cursor is only valid for those arguments.

To get a query to adapt, call generate_query.

Record Types and Their Phrases

The installed package lists the record types a question can ask for, with the phrases that ask for each and the tags that select it:

import json
from importlib.resources import files

path = files("enigma_api_client") / "plugins/enigma-api/skills/enigma-graphql/references/record_topics.json"
for topic in json.loads(path.read_text())["record_topics"]["topics"]:
    print(topic["id"], topic["phrases"][:3])

Records and Unavailable Data

  • An unavailable_data entry for something the selected records answer in full, such as "liquor licenses", is removed.
  • When the query selects record counts but none of the records the question asks for, the SDK keeps each entry those records would answer in full and gives it the message "This query does not return these records." This happens, for example, on a search by website alone when the entities get no records page.
  • When the query selects the records, an entry for something they answer only in part, such as "tax liens" or "lawsuits and violations", gets the message "The returned records cover this only in part."

Records and the API Schema

A local strategy adds records only once it has the API schema for your tier.

TemplateStrategy fetches the schema as follows:

  • With address_search set to auto, the default, it fetches the schema before it answers its first question, because it needs the schema to decide on address lookups.
  • If you set address lookups yourself (address_search=True or False, or ENIGMA_ADDRESS_SEARCH set to on or off), it doesn't wait for the schema; it fetches it in the background. Until the schema arrives, its answers can come without records, and generate_query reports records as no_schema.
  • After its first fetch fails, it starts another background fetch only when it finishes generating a query, so it starts none while no questions come in. These fetches start at most once every five minutes, or once every refresh_seconds seconds when you pass a number as refresh_seconds to set_schema_introspector. Until a fetch succeeds, its answers lack records.

GenerativeStrategy and AgentStrategy fetch the schema while they answer their first question, even when you set address lookups yourself. After a failed fetch, they try again on their next question.

  • GenerativeStrategy fetches it before it writes its first query when address_search is auto, and otherwise when it checks that query. Until a fetch succeeds, its answers lack records.
  • AgentStrategy fetches it before its agent starts. Until a fetch succeeds, it raises PROVIDER_ERROR.

Environment Variables

Variable Used by Meaning
ENIGMA_API_KEY ServiceStrategy The key of a ServiceStrategy you create without enigma_api_key= (the default strategy uses the ClientConfig key)
ANTHROPIC_API_KEY AnthropicProvider Anthropic API key
OPENROUTER_API_KEY OpenRouterProvider OpenRouter API key
AWS_REGION, AWS_DEFAULT_REGION BedrockProvider, AgentStrategy Region when region= is not passed: AWS_REGION, else AWS_DEFAULT_REGION, else us-east-1
ENIGMA_SHAPE_CACHE_ENABLED TemplateStrategy, GenerativeStrategy 0, false, no, or off turns off the query shape cache. Read when the strategy is created.
ENIGMA_ADDRESS_SEARCH TemplateStrategy, GenerativeStrategy, AgentStrategy on, off, or auto (see Turn Address Lookups On or Off). 1, true, and yes also mean on; 0, false, and no mean off. Read when the strategy is created.
ENIGMA_ARCHIVE_SEARCH TemplateStrategy, GenerativeStrategy, AgentStrategy off, 0, false, or no leaves the archive searches out of record queries (see Get Government Records). Read on each call.

Logging

The SDK logs through Python's logging module, on loggers named enigma_api_client.*:

  • Lines at INFO and above are fixed messages with counts, statuses, names of the query rules the SDK checks, exception class names, schema field names, and file paths. They never contain your questions or the LLM's text.
  • DEBUG lines can contain your questions, the LLM's output, and AgentStrategy's transcript. Don't turn on DEBUG for these loggers where your logs must not hold that data.

What Leaves Your Machine

  • Searches: the query and its variables go to the Enigma API with your Enigma API key.
  • ServiceStrategy: your question goes to Enigma's hosted query service with your Enigma API key. The service writes the query with an LLM. It has its own URL, which ClientConfig.url doesn't change.
  • Local strategies: your question and Enigma's query instructions go to the LLM provider you configure (Amazon Bedrock for AgentStrategy). When the SDK asks the LLM to repair a query, it also sends the draft query and its errors. Search results never go to the LLM.
  • Schema checks: the SDK fetches the API schema from the Enigma API with the first Client's key, or with the keys used by the introspector you register (see Schema Checks and Several Tiers).

The SDK sends nothing else.

Write GraphQL by Hand

The Enigma schema has hundreds of fields, several of them named differently from what you might guess. A query can also pass schema validation and still be rejected when it runs. This section lists what most often goes wrong.

Common Mistakes

Field you might guess Actual field
status (of a location) operatingStatus
url website
description industryDesc
title jobTitle
averageRating reviewScoreAvg
  • A field that doesn't exist fails the whole request with HTTP 400 and Cannot query field.
  • search returns a list. Select through inline fragments, as in search(...) { ... on Brand { ... } }; it has no edges.
  • A category in name matches business names. name: "coffee shop" returns businesses with names like "Coffee Shop". To count a category, use aggregate.
  • A company's officers are under its registrations. LegalEntity.persons is almost always empty for a company. Follow registeredEntities, then registrations, then roles, then legalEntities.
  • Connections have no totalCount. Count with an aggregate query, or with count(field: ...) on an entity.
  • Website content sits under each website. Select websiteContents inside websites: websites(first: 1) { edges { node { websiteContents(first: 1) { edges { node { httpStatusCode } } } } } }.
  • A brand search for a local franchise returns the whole chain. Searching BRAND for a single franchised hotel returns the parent brand with all its locations. Search OPERATING_LOCATION with an address to find one location.

Rules the API Enforces at Execution

These queries pass schema validation and are rejected when they run. GenerativeStrategy and AgentStrategy check these rules before they return a query, and the templates follow them.

Rule Error
search needs name, website, person, phoneNumber, id, or tin, even when it has a filter. An address alone works only in an entityType: ADDRESS search with address.street1 (a street line with a house number) or address.id. Search must include name, website, person, or phone_number
searchInput.prompt can't be combined with a selection set. To have the API answer a question in plain English, send the question in searchInput.prompt with no selection set (see API search). 'prompt' cannot be combined with a selection set
roles takes first and conditions only, although the schema lists last, after, and before. Fetch roles as one page. LegalEntity.roles does not currently support last, after or before (the error names the parent type)
A PERSON search finds people by person: { firstName, lastName }, id, or tin, not by name. Person search must include an identifier or first, middle or last name
A filter's AND, OR, and NOT take a list of clauses, and NOT takes exactly one. Rejected
A filter clause has exactly one operator. Combine several clauses in one AND list. Rejected
IS_NULL and IS_NOT_NULL test a column. To test for related records on a connection path such as registrations, use HAS or NOT: [{ HAS: [...] }]. Rejected
An aggregate filter on brands.industries.industryCode needs { EQ: ["brands.industries.industryType", "naics_2022_code"] } in the same AND. Joins aren't allowed for filter

Upgrade Notes

Each section lists what changed after the version it names. Read every section that names your current version or a later one.

Upgrading from 0.12.1

For AccessTier.REDISTRIBUTION, a local strategy no longer selects emailAddresses or phoneNumbers under a role (see Access Tiers), so code that reads them from a generated query's results raises KeyError. The API answers a redistribution key with placeholder values for both. A generated query that selects nothing else is refused with UNSUPPORTED_DATA_REQUESTED. On every tier, TemplateStrategy's contacts lookup now also returns the address and phone number of the location whose roles it lists. With Client's default strategy, this reaches you when the hosted service adopts this release.

Upgrading from 0.11.0

With Client's default strategy, the hosted service builds the query. The TemplateStrategy and GenerativeStrategy changes in this section reach you when the service adopts this release, whatever SDK version you run.

  • TemplateStrategy answers a question that names several businesses, people, or street addresses with several searches in the same query, under search, search2, and search3 (see Generate Queries from Questions). Before, it answered one of them or refused the question.
  • GenerativeStrategy likewise searches each brand a question names, under search, search2, and search3. Before, it usually searched only the first brand, and otherwise keyed the searches with other aliases, such as a and b.
  • GenerativeStrategy counts each brand a how-many question names, under aggregate, aggregate2, and so on. Before, it counted only the first brand.
  • Code that reads only data["search"] gets the first search. Read the other keys for the rest.
  • A generated query with more than three top-level fields (searches or counts) is refused. The government-record searches the SDK adds beside them don't count.
    • A local strategy raises UNSUPPORTED_DATA_REQUESTED.
    • ServiceStrategy raises INVALID_QUERY once the hosted service adopts this release.
  • GenerativeStrategy and AgentStrategy refuse a query when any one of its searches is a kind they refuse (see GenerativeStrategy). Before, they refused it only when every top-level field in it was such a search.
  • When the SDK repairs a GenerativeStrategy or AgentStrategy query that repeats a top-level field with different arguments, it keys the second one search2 (or aggregate2). Before, it keyed it search_2 (or aggregate_2).
  • Templates return a brand's affiliated brands 50 to a page, up from 10. GenerativeStrategy asks its LLM for the same page size.
  • A template legal entity lookup lists a business's registrations in each state the question gives that business (see How Template Queries Read Places). Before, it listed none when the question gave several states.
  • In a lookup of one location of a business ("the Starbucks at 1 Main St") or of an address, TemplateStrategy searches each street, city, and state the question lists, and, in an address lookup, each unit and ZIP code. Before, it sent the list in a single field, dropped it, or refused the question. A question about a brand's locations in several cities still gets one search covering them all.
  • How Template Queries Read Places describes how the places a question lists apply to several businesses or people, and when a list of places is refused.

Upgrading from 0.10.3

  • Address lookups no longer select the five Address count fields (associatedBrandCount, operatingLocationOperatesAtCount, legalEntityReceivesMailAtCount, registrationRecordedCount, and associatedPersonCount).
    • An address result no longer carries those keys, so code that reads address["associatedBrandCount"] raises KeyError. Read the operating locations, legal entities, and registrations each address lists instead.
    • With Client's default strategy, the hosted service builds the query, so this change reaches you when the service adopts the release, whatever SDK version you run.
  • Two kinds of schema now count as supporting address lookups, both for address_search="auto" and for the address-search refusal described in Turn Address Lookups On or Off:
    • One whose Address type carries associatedBrandCount, as before.
    • One that defines an Address type and the Node interface. This kind is new.
  • When a question asks how many, other than a headcount, and the query is an address lookup that selects no count, unavailable_data has an address counts entry (out_of_scope).

Upgrading from 0.10.2

  • SearchResponse.headers holds the HTTP response headers, so you can read Retry-After on a 429. It takes no part in == or repr. dataclasses.asdict(response) and dataclasses.astuple(response) now include it as an httpx.Headers, which json.dumps can't serialize.
  • A response body that isn't a JSON object, such as a gateway's error page, no longer raises ValueError (or AttributeError for JSON that isn't an object). client.search returns it with data set to None, an error in errors, and the body in raw_body, so is_success() is false. ServiceStrategy raises EnigmaClientException with INVALID_QUERY for one, and introspect_schema raises its "no schema data" RuntimeError.
  • A query written with no space after an opening brace, such as node{name}, is sent correctly. The client used to join the id and __typename it adds to the first field, and the API rejected the query.
  • EnigmaClientException.status_code holds the HTTP status of the failed LLM provider or hosted-service call behind an error, when there was one, so retry logic needs no message parsing.
  • For the hosted service, EnigmaClientException.headers holds its response headers.
  • When the SDK maps a provider's exception to EnigmaClientException, that exception is its __cause__.
  • The error code for each HTTP status is unchanged.
  • An API key or header value an HTTP header can't carry as written, such as one with a newline or a trailing space, raises ValueError naming the header, before the API request is sent. It replaces two errors, so the handlers that caught them no longer catch this case:
    • The httpx error that could quote the value, which except httpx.HTTPError caught.
    • For a character outside ASCII, the UnicodeEncodeError, which except UnicodeEncodeError caught.
  • introspect_schema, which sent an API key outside ASCII encoded as UTF-8, now refuses it the same way.
  • A custom LlmProvider can raise ProviderHTTPError, now exported from enigma_api_client.strategies, with the HTTP status of a failure. 408, 429, and 5xx are reported as TRANSIENT_PROVIDER_ERROR, any other HTTP status as PROVIDER_ERROR, and a value that is not an HTTP status as TRANSIENT_PROVIDER_ERROR.

Upgrading from 0.10.1

  • Client.search asks the API to wait up to 25 seconds for values it computes in the background, such as some counts, before it answers. The API answers a request with no background work at once. A ClientConfig.url that sets max_background_task_wait or max_call_duration to a non-blank value keeps its own wait.
  • SearchResponse.is_success() is false for an HTTP 206 answer, whose background values are still null. It is also false for an answer that reports a background task that failed, was canceled, or ran out of credits.
  • TemplateStrategy's brand watchlist template selects each watchlist entry's id in place of internalWatchlistEntryId, which the API is retiring. The two identifiers differ, so ids you stored from internalWatchlistEntryId won't match.

Upgrading from 0.10.0

  • An unavailable_data entry for something the selected government records answer only in part, such as "tax liens", gets the message "The returned records cover this only in part." when the query selects those records.
  • The phrases "work permit", "export permit", "licensing agreement", and "drivers' license" no longer ask for government records.

Upgrading from 0.9.14

  • With TemplateStrategy, GenerativeStrategy, or AgentStrategy, a question that asks for an entity's government or public records returns a query that selects them. Each returned entity carries the fields listed under Records on Each Entity. Beside a search by a name, person, or street address, the response gains archiveRecords. Beside a name search with entityType: BRAND, entityType: OPERATING_LOCATION, or no entityType, it also gains archiveBrandRecords. A question that asks for no records returns the same query as in 0.9.14.
  • unavailable_data leaves out an entry the selected records answer in full, such as "liquor licenses" or "violations".
  • When the SDK adds record counts but none of the records the question asks for, each entry those records would answer in full stays in unavailable_data and carries the message "This query does not return these records."
  • When the SDK adds no record fields, a violations entry says that violation records are served as the Gov Archive records of a business, location, person, or address.

Upgrading from 0.9.13

  • The query reference AgentStrategy reads no longer lists internalId, which the API is retiring on every type.

Upgrading from 0.9.12

  • Generated queries select pageInfo { hasNextPage endCursor } on Brand.affiliatedBrands and LegalEntity.legalEntities, so a corporate family longer than one page can be paged with after:.

Upgrading from 0.9.11

  • TemplateStrategy lookups return a core set of fields plus only the data the question asks for (see TemplateStrategy). A plain "verify" question no longer returns TINs, watchlist entries, bankruptcies, or officers; ask for them, or ask a compliance question.
  • TemplateStrategy's legal entity lookup searches by name only, and reads a state in the question as the registrations to list in that state.
  • TemplateStrategy can refuse a question about several businesses or people at once with UNSUPPORTED_DATA_REQUESTED. From 0.12.0 it answers up to three at once (see Upgrading from 0.11.0).
  • With TemplateStrategy, a question with no usable name, website, or person name raises UNSUPPORTED_DATA_REQUESTED instead of being sent as an empty search.
  • TemplateStrategy's brand profiles select operatingLocations in place of the hqAddress alias, so the response key changes, and no longer select isMarketables.
  • TemplateStrategy answers an officers question by searching legal entities when the name has more than one word, ends in Inc, Incorporated, Co, Company, LLC, LP, LLP, LLLP, or PLLC, and comes with no website. search then returns a LegalEntity whose officers are under registeredEntities, registrations, and roles. Other names, including names ending in Corp, Corporation, or Ltd, still search brands.

Upgrading from 0.9.10

  • restricted_fields_for and gate_blocked_fields also list placeClasses, siteTypes, and tiers for the EXTERNAL and REDISTRIBUTION tiers, and legalEntitiesAppearsOn and legalEntitiesIsFlaggedBy for REDISTRIBUTION; the API does not serve them to those tiers.
  • TemplateStrategy answers a question about how many businesses of a category are in a place with a count instead of refusing it.

Upgrading from 0.9.9

  • restricted_fields_for and gate_blocked_fields no longer list headcounts for the EXTERNAL and REDISTRIBUTION tiers; the API serves it to every tier.
  • A headcount question on a brand or legal entity now selects each legal entity's headcount band instead of declaring headcount in unavailable_data.
  • For a headcount question whose query is not a company or legal entity lookup, TemplateStrategy and GenerativeStrategy still declare employees (out_of_scope). TemplateStrategy's entry has a new message.

Upgrading from 0.9.8

  • The SDK also calls a schema introspector, the function registered with enigma_api_client.validation.set_schema_introspector (including the one Client registers), from a background thread with no context variables set. It does so:
    • When a strategy needs a tier's schema and none is cached yet.
    • On the first use of a cached schema that is more than five minutes old.
  • The introspector must be thread-safe, return in bounded time, and not rely on context variables.
  • set_schema_introspector(introspector, refresh_seconds=None) turns off the refresh of cached schemas. A missing schema is still fetched in the background. The introspector Client registers uses the five-minute interval.
  • A local strategy refuses an address search, whatever address_search says, while the cached schema for the access tier shows no support for address lookups. The refusal raises UNSUPPORTED_DATA_REQUESTED with bare address lookup in its message. From 0.9.9 through 0.10.3, such a schema is one that lacks Address.associatedBrandCount. For 0.11.0 and later, see Upgrading from 0.10.3.
  • While no schema for the access tier is cached yet, TemplateStrategy and GenerativeStrategy with address lookups turned on (address_search=True or ENIGMA_ADDRESS_SEARCH=on) still write address lookups.
  • In addition to the restricted_fields_for fields, TemplateStrategy output and answers from the query shape cache drop any field your tier's schema doesn't define, and declare each in unavailable_data as contractual.

Upgrading from 0.9.7

  • restricted_fields_for and gate_blocked_fields list tins, watchlistEntries, isFlaggedByWatchlistEntries, and appearsOnWatchlistEntries for the REDISTRIBUTION tier; the API no longer serves them to it.
  • GenerativeStrategy, TemplateStrategy, and AgentStrategy drop from the query they return each field the tier is not served, declare it in unavailable_data (contractual, with field set to the field's schema name), and add restricted-field to deterministic_repair. A query left with nothing else, or whose filter, orderBy, or field argument path still names such a field, raises UNSUPPORTED_DATA_REQUESTED.
  • ClientConfig.tier (default AccessTier.REDISTRIBUTION) is the tier a local strategy generates for and validates against. Set it to your key's tier to keep TIN and watchlist selections with an external key. ServiceStrategy ignores it.
  • Schema validation is process-wide and follows the first Client's tier. A later Client at another tier validates without the schema, and its AgentStrategy raises PROVIDER_ERROR.
  • Client.search passes tier= to generate_query when ClientConfig.tier is not the default, so a custom QueryStrategy must accept a tier keyword argument.

Upgrading from 0.9.1

  • __url links point at the Enigma Console's profile pages: https://console.enigma.com/profile/brand/<id>, /profile/legal-entity/<id>, and /profile/operating-location/<id>. Old links still redirect. A person's __url links to the brand it appears under, or to Enigma's website when it appears under none, instead of a person profile.

Upgrading from 0.9.0

  • TemplateStrategy counts a brand's locations ("How many Starbucks are in Texas?") with a brand search, so the count moves from data.aggregate.locationsCount to data.search[0].locationsCount. brandsCount is no longer returned, and search is empty when no brand matches. The count covers only that brand's open locations in the state and city the question names; before, it counted every open location, whatever the brand.

Upgrading from 0.8.x

  • AgentStrategy runs on Amazon Bedrock only and takes the keyword arguments model, region, max_turns, and max_budget_usd; anthropic_api_key is removed. The calling identity needs bedrock:InvokeModel and bedrock:InvokeModelWithResponseStream on the model.
  • AgentStrategy raises instead of returning an empty query:
    • UnrepairedQueryError when its answer fails schema validation or does not parse.
    • UNSUPPORTED_DATA_REQUESTED from the refusal checks GenerativeStrategy applies.
    • INVALID_QUERY for:
      • An empty prompt or answer (the message starts Query is not applicable to the Enigma API).
      • A run that ends without a query.
      • An answer that doesn't match the agent's answer format.
      • A mutation or subscription.
      • An answer whose variables don't fit their declared types.
      • An answer nested too deeply.
      • An answer the SDK couldn't check.
      • An answer over 10,000 characters.
    • TRANSIENT_PROVIDER_ERROR or PROVIDER_ERROR when the model call or the Claude Code CLI the agent runs fails.
  • AgentStrategy records its token usage in GENERATION_USAGE.
  • The anthropic and all extras pin an exact claude_agent_sdk version (currently 0.2.121).
  • AgentStrategy writes every value into the query text and returns {} as its variables. When the tier has no schema, it refuses the call with PROVIDER_ERROR before the agent runs. It takes address_search= like GenerativeStrategy.

Upgrading from 0.7.x or Earlier

  • Custom LlmProvider: complete() now returns enigma_api_client.strategies.Completion(text, truncated, input_tokens=..., cache_read_tokens=..., cache_write_tokens=..., output_tokens=...) instead of str.
  • Custom QueryStrategy: generate_query(prompt, *, tier=DEFAULT_TIER, prior_attempt=None) gained prior_attempt. Implementations must set GENERATION_USAGE to a fresh LlmUsage() on entry.
  • GenerativeStrategy raises TruncatedOutputError and UnrepairedQueryError. Both subclass EnigmaClientException with code INVALID_QUERY, so existing handlers still catch them.
  • LLM provider transport failures now raise EnigmaClientException: TRANSIENT_PROVIDER_ERROR for timeouts and connection errors, GENERATE_QUERY_CALLBACK_NOT_CONFIGURED for configuration faults. They used to surface as raw httpx exceptions.
  • restricted_fields_for and gate_blocked_fields no longer list isMentionedInMediaContents, and now list headcounts, corporateFamilies, and customs for the EXTERNAL and REDISTRIBUTION tiers.
  • Address lookups are new and default to auto (see Look Up an Address).
  • With address lookups off (for example, ENIGMA_ADDRESS_SEARCH=off), TemplateStrategy usually refuses a question that gives only an address, raising UNSUPPORTED_DATA_REQUESTED or INVALID_QUERY (see Turn Address Lookups On or Off). Before 0.8.0, it could send such a question as a nameless operating location search, which the API rejected with HTTP 400.
  • GenerativeStrategy now refuses some queries its LLM writes with UNSUPPORTED_DATA_REQUESTED instead of serving them: a person search by surname alone, an EIN or filing number placed in name, a wildcard name pattern, a street address placed in name, and a category count written for a question that asks for a list.
  • AgentStrategy logs its transcript at DEBUG on enigma_api_client.strategies.agent, not at INFO on the root logger.
  • TemplateStrategy refuses a blank prompt with INVALID_QUERY before any LLM call.

Metadata

Release files for enigma-api-client 0.12.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distribution (wheel)

Table of built distributions (wheels) for enigma-api-client 0.12.2
File Interpreter ABI Platform Actions
enigma_api_client-0.12.2-py3-none-any.whl Python 3 none any Details for enigma_api_client-0.12.2-py3-none-any.whl

Release files / enigma_api_client-0.12.2-py3-none-any.whl

Download URL enigma_api_client-0.12.2-py3-none-any.whl
Size 343.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fe10999061ad9aa61fa050f1ad4bd79bc300ef8ad60146ea0c6f4a34e2b11bff
BLAKE2b-256 checksum
How to use checksums
a7a38274e52ca60a3444234f368348928c52ddd78252c6f307a08f8d78d71358
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.13

Security for enigma-api-client 0.12.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page