Skip to main content
The Python client needs Python 3.11 or later. It depends on httpx and pydantic. It has a sync client (Vaquill) and an async client (AsyncVaquill) with the same methods. For the options shared with the TypeScript client, see the SDKs overview.
Use the SDK on a server only. An API key (vq_key_...) must never ship to a browser.

Install

Source: Vaquill-AI/vaquill-sdks, python/ on GitHub.

Quickstart

Resolve a citation, then read the section text.
An unresolved citation is a normal 200 response, not an exception. Check result.resolved before you rely on it. See Citation quality. Search statutes.
Required arguments come first and may be positional. Optional ones are keyword-only snake_case and are sent to the API as camelCase. Every method also accepts timeout, max_retries, idempotency_key and headers as keyword arguments.

Namespaces

The SDK covers 36 operations. Each method is the operationId with the namespace noun removed, so search_statutes is statutes.search and get_session_law_body is session_laws.get_body.

Which calls cost credits

A call is charged when the API declares the Idempotency-Key header on its operation. That is 25 of the 36 operations. Free: statutes.coverage, boards.list, every watch operation except watches.change_diff (create, list, update, delete, test, changes, deliveries), credits.balance and pricing.get. Prices are not hard-coded in the SDK. client.pricing.get() is free and returns the live price list. Some charged calls return credits_consumed of 0 (for example a body that is not held), so read the figure from the response.

Credits and the spend guard

The guard checks what is already spent before a charged call, not the cost of the next call. credits_spent adds each response’s credits_consumed. The guard is exact. While max_credits is set, charged calls run one at a time, from the check to the moment their credits are counted, so concurrent threads or tasks cannot all pass the check and overshoot the cap. Without max_credits, calls keep full concurrency. Async is the same with AsyncVaquill and await.

Error handling

Every failure is a VaquillError. Status errors carry status_code, request_id and the parsed body.
ResponseParseError is raised when a 2xx response is not the JSON the endpoint promises (bad JSON or the wrong shape). It carries message, request_id and status_code. Other classes: APIConnectionError and APITimeoutError (no response arrived), AuthenticationError, PermissionDeniedError, NotFoundError, BadRequestError, UnprocessableEntityError, ConflictError, DailyCapExceededError (a RateLimitError; retrying cannot succeed until the cap resets), InternalServerError, SpendGuardExceeded and WebhookVerificationError. Idempotency failures are IdempotencyConflictError with IdempotencyKeyReusedError, IdempotencyInProgressError and IdempotencyReplayUnavailableError. Each status error also exposes code, retryable and retry_after_seconds, and the legacy detail and errors members. They are None when the server did not send them. The code is checked first, then the status, so an unknown code still gives the status class. The message of an UnprocessableEntityError (422) ends with the first validation reasons from the response’s errors list, so the rejected field is in str(err).

Retries and idempotency

  • Retried: 408, 429 and 5xx, plus 409 only for idempotency_in_progress. Connection errors and timeouts are retried when it is safe.
  • Backoff starts at 0.5 s, doubles to an 8 s cap and is scaled down by 0 to 25 percent jitter. retry-after-ms wins over Retry-After, which wins over the body’s retryAfterSeconds. A wait above 120 s is not retried. The default is 2 retries; max_retries=0 turns them off.
  • Never retried: insufficient_credits (402), daily_cap_exceeded, any other 4xx, and anything the server marks retryable: false.
  • Each charged call sends one generated Idempotency-Key, reused on every retry of that call. When the server honours the key, an ambiguous failure cannot charge twice. Pass idempotency_key="..." (16 to 255 visible ASCII characters) to supply your own.
  • With idempotency_keys=False on the client, or with_options(idempotency_keys=False), charged calls are not retried after a timeout, connection error or 5xx, because they could charge twice. A 429 is still retried, since it is refused before any work.
  • Free POSTs without a key (watches.create, watches.test) follow the same rule, so a retry cannot create a second watch.
  • Every top-level response has credits_consumed, request_id and idempotent_replayed. A replayed response was stored and nothing new was charged.

Pagination

iter_* helpers hide the offset, cursor and sinceId/beforeId styles behind one iterator.
On a charged endpoint every page spends credits. Iterating a whole result set can cost far more than one call, so always set max_pages or max_items.
Iterate the pager for items, call .pages() for whole pages and read .truncated after iterating. Charged helpers: statutes.iter_search, statutes.iter_divisions, statutes.iter_section_changes, session_laws.iter_search, session_laws.iter_list, session_laws.iter_changes, session_laws.iter_body. Free helpers: watches.iter_list, watches.iter_changes, boards.iter_list. Two guards stop an iteration that has no max_pages or max_items. truncated is True whenever a guard left rows unread, including a max_items that cut items off the last page. stuck is True when the server returned the offset or cursor it was just sent, or an empty page with hasMore: true. The iterator then stops, logs a warning on the vaquill logger and does not fetch again, so a misbehaving server cannot burn credits.

Batch helpers

Each helper wraps one batch operation, splits the input at the per-call cap and returns the members of every response as one flat list in input order.
  • The max_credits guard is checked before every chunk, so a guard reached part way raises SpendGuardExceeded and the remaining chunks are not sent.
  • Exact repeats in the input are sent once.
  • A caller’s idempotency_key is kept for the first chunk and suffixed with -<chunk number> for later ones.
  • An empty input sends nothing and returns [].
  • get_sections_all and get_batch_all drop ids with no match. Call the single-call method to read notFound.
All three are charged. Each exists on AsyncVaquill with await.

Argument checks

A path argument that is empty, . or .. raises ValueError before any request, because a URL parser would resolve it to another endpoint. extract_citations costs 0 credits when resolve=False.

Raw responses

client.raw_request(method, path, ...) reaches endpoints newer than this SDK. It returns an APIResponse, not plain JSON, so call .parse() for the body. The TypeScript rawRequest returns the parsed value directly.

Alerts and webhook verification

Create a law change alert. Watches are free.
Verify the signature on the raw request body, never on re-serialised JSON.
verify_webhook checks the Standard Webhooks headers (webhook-id, webhook-timestamp, webhook-signature) with a 300 second tolerance. It accepts several v1 signatures and a list of secrets, so you can rotate a secret without downtime. A secret that starts with whsec_ is base64 decoded; any other secret is used as its UTF-8 bytes. It raises WebhookVerificationError on any signature failure. A whsec_ secret must be valid base64. From the first release after 0.2.0, one that is not raises ValueError instead of being used as plain text (0.2.0 still uses the text). An empty or whitespace-only secret (or whsec_ with no key) raises ValueError in every version, so an unset environment variable cannot verify a forged event.
verify_webhook rejects the old X-Vaquill-Signature: sha256=<hex> header unless you pass allow_legacy=True. The TypeScript client behaves the same with allowLegacy: true. That header signs only the body, so it has no replay protection: a captured delivery verifies forever. Opt in only if you still receive legacy deliveries.
See Alerts for the payload and delivery behavior.

Debug logging and custom HTTP client

The client logs through the standard logging module under the vaquill logger, and logs idempotency keys by their first 8 characters only. There is no logging option on the client. Configure the logger as you would any other, for example logging.getLogger("vaquill").setLevel(logging.DEBUG). The API key never appears in a log line. A stuck pager and a deprecated endpoint each log a warning on this logger. Pass http_client= (an httpx.Client) to set a proxy or client certificate, or to inject a test double. The SDK does not close a client you pass.

Types

Responses are Pydantic v2 models in vaquill.types, generated from the API contract. Unknown fields are kept and unknown enum values never raise.
Last modified on October 29, 2026