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.
Install
Vaquill-AI/vaquill-sdks, js/ on GitHub.
Quickstart
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 })
apiKeydefaults toVAQUILL_API_KEY. Keys that do not start withvq_key_are rejected.baseURLdefaults toVAQUILL_BASE_URL, thenhttps://api.vaquill.ai.timeoutis per attempt, in milliseconds (default 60000).maxRetriesdefaults to 2.fetchinjects a custom implementation, for a proxy or a test double.maxCreditsis the spend guard, described below.client.creditsSpentshows the running total.loggeris where log lines go: any subset ofdebug,info,warnanderror, defaulting toconsole.logLevelis the minimum level, defaultwarn.debuglogs each request and response,infoadds retries, andwarnreports 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.
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 theIdempotency-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
PassmaxCredits 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.
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-mswins overRetry-After(seconds or date), which wins over the body’sretryAfterSeconds. 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 marksretryable: 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. PassidempotencyKey: "..."to choose your own. - With
idempotencyKey: falsea charged call is not retried after a timeout, connection error or 5xx, because it could charge twice. A 429rate_limitedis 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.
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.optionstakes the usual request options pluschunkSize. 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 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.
See Alerts for the payload and delivery behavior.
