Skip to main content
The TypeScript client needs Node 20 or later. It is built on standard fetch, has no runtime dependencies, and is tested on Node 20, 22 and 24. For the options shared with the Python client, see the SDKs overview.
Use the SDK on a server only. An API key in browser code is readable by anyone, and the client warns when it sees a browser.

Install

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

Quickstart

Resolve a citation, then read the section text.
An unresolved citation is a normal 200 response, not an error. Check result.resolved before you rely on it. See Citation quality. Path parameters are positional arguments. Query and body fields keep the camelCase wire names.

Client options

new Vaquill({ apiKey, baseURL, timeout, maxRetries, defaultHeaders, fetch, maxCredits, idempotencyKeys, logger, logLevel })
  • apiKey defaults to VAQUILL_API_KEY. Keys that do not start with vq_key_ are rejected.
  • baseURL defaults to VAQUILL_BASE_URL, then https://api.vaquill.ai.
  • timeout is per attempt, in milliseconds (default 60000). maxRetries defaults to 2.
  • fetch injects a custom implementation, for a proxy or a test double.
  • maxCredits is the spend guard, described below. client.creditsSpent shows the running total.
  • logger is where log lines go: any subset of debug, info, warn and error, defaulting to console. logLevel is the minimum level, default warn. debug logs each request and response, info adds retries, and warn reports deprecated endpoints once per method and path. The API key is always redacted.
  • client.withOptions({ ... }) returns a derived client that shares the key and the spend counter.
  • Every method takes { timeout, maxRetries, headers, signal, idempotencyKey } as its last argument.
The key is held in a private field. It does not appear in JSON.stringify, util.inspect, String(client) or any error.

Namespaces

Each method maps to one operationId, so search_statutes is statutes.search and get_section_body is statutes.getSectionBody.

Which calls cost credits

A method is charged when the API declares the Idempotency-Key header on its operation. That is 25 operations: statutes.search, getSection, getSectionBody, getSectionNeighbors, getSections, count, listDivisions, getSectionChanges, getSectionEnactments, getSectionRulemaking, getPartRulemaking, getSectionCitedBy, getSectionDefinitions, getSectionCrossState, resolve, resolveBatch, extractCitations; sessionLaws.search, list, get, getBody, getVersions, getChanges, getBatch; and watches.getChangeDiff. The *All iterators call these and spend per page. Free: statutes.coverage, watches.list, create, update, delete, test, listChanges, listDeliveries, boards.list, credits.balance and pricing.get. Watches other than the change diff are free. client.pricing.get() returns the live price list.

Spend guard

Pass maxCredits to the constructor. Once responses on this client have reported that many credits, charged calls throw SpendGuardExceededError before sending anything. It checks what is already spent, not the cost of the next call. The guard is exact. While maxCredits is set, charged calls on the client and its derived clients run one at a time, from the check to the moment their credits are counted, so concurrent calls cannot all pass the check and overshoot the cap. Leave maxCredits unset to keep full concurrency.

Error handling

VaquillError is the base. APIConnectionError (with APITimeoutError and APIUserAbortError) means no response arrived. ResponseParseError is thrown when a success response is not valid JSON. It carries status, requestId and bodyPreview (the first 200 characters of the body). APIStatusError carries status, requestId, code, retryable, retryAfterSeconds, body, and the legacy detail and errors.
Subclasses of APIStatusError: BadRequestError, AuthenticationError, InsufficientCreditsError, PermissionDeniedError, NotFoundError, ConflictError, UnprocessableEntityError, RateLimitError (and DailyCapExceededError), InternalServerError, and IdempotencyConflictError with IdempotencyKeyReusedError, IdempotencyInProgressError and IdempotencyReplayUnavailableError. The code is checked first, then the status, so an unknown code still gives the status class. Fields the server did not send come back as null. The message of an UnprocessableEntityError (422) ends with the first validation reasons from the response’s errors list, so the rejected field is in err.message.

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 is 0.5 s doubling to an 8 s cap, scaled down by 0 to 25 percent jitter. retry-after-ms wins over Retry-After (seconds or date), which wins over the body’s retryAfterSeconds. A wait above 120 s is not retried.
  • Never retried: insufficient_credits (402), daily_cap_exceeded, any 4xx other than the above, and anything the server marks retryable: false.
  • Each charged call gets one generated Idempotency-Key, reused on every retry of that call. When the server honours the key, a retry cannot charge twice. Pass idempotencyKey: "..." to choose your own.
  • With idempotencyKey: false a charged call is not retried after a timeout, connection error or 5xx, because it could charge twice. A 429 rate_limited 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.

Pagination

The *All methods return an async iterable of items, with .pages() for whole pages and .toArray(). They hide offset with nextOffset, cursor with nextCursor, and sinceId / beforeId paging.
Charged endpoints spend credits on every page. Set maxPages, maxItems or a client maxCredits. Without one, an iterator runs until the data ends.

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 array in input order.
  • options takes the usual request options plus chunkSize. A value above the cap is clamped to it, so the helper never sends a request the server would refuse.
  • The spend guard is checked before each chunk, and exact repeats in the input are sent once.
  • An empty input sends nothing.
  • All three are charged.

Raw responses

.withResponse() returns the parsed data, the raw response and a meta object with status, headers, requestId, creditsConsumed, idempotentReplayed, rateLimit and retries.
client.rawRequest(method, path, { query, body, charged }) reaches endpoints newer than this SDK and returns the parsed value. The Python raw_request returns an APIResponse instead, so it needs .parse().

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 and unwrap check the Standard Webhooks headers (webhook-id, webhook-timestamp, webhook-signature) with a 300 second tolerance, accept several v1 signatures (rotation) and a list of secrets, and compare in constant time. A secret that starts with whsec_ is base64 decoded; any other secret is used as its UTF-8 bytes. From the first release after 0.2.0, a whsec_ secret that is not valid base64 throws WebhookVerificationError (0.2.0 still uses the text as the key). Copy a whsec_ secret exactly: it must be valid base64. The TypeScript client falls back to the UTF-8 bytes of a secret it cannot decode, whereas the Python, Go, Java and Kotlin clients reject one. An empty or blank secret (or a bare whsec_) throws WebhookVerificationError, so an unset environment variable cannot verify a forged event. They throw WebhookVerificationError on any failure.
{ allowLegacy: true } also accepts the old X-Vaquill-Signature: sha256=<hex> header when the Standard Webhooks headers are absent. That header signs only the body, so it has no replay protection: a captured delivery verifies forever. It is off by default, as in Python (allow_legacy).
See Alerts for the payload and delivery behavior.
Last modified on October 29, 2026