Skip to main content
Both SDKs are thin. They are generated from the same OpenAPI document as the API reference, with no caching and no validation beyond types. They add a few things of their own: they retry failed requests that are safe to repeat, import large sets of documents in batches, page through the events feed, and verify webhook signatures. Any request the SDK makes, you can make with curl against https://api.brussle.com/v1.

Methods

Methods mirror the routes. TypeScript uses the same names in camelCase.
The rest of the API: Namespace names and document ids may contain /. The SDKs send it as %2F, as the API requires. An id or name that is empty, . or .. is refused before any request is sent (a ValueError in Python, a TypeError in TypeScript), because a URL would read it as a different path. A template is a namespace handle on a prefix ending in /*, such as db.namespace("acme/prod/*"), and its * is sent as it is.

Requests and responses are the API’s JSON

Request and response bodies use the API’s own field names (wait_for, top_k, rank_by) in both languages. Any JSON body in these docs, or one copied from the dashboard’s query builder, can be passed to the SDK unchanged. Options that are not part of a JSON body follow each language’s conventions, such as include_history=True in Python and { includeHistory: true } in TypeScript. The types are strict:
  • Answers and definitions are discriminated unions on type. In TypeScript, answer.p type-checks only after answer.type === "bool".
  • Filters are typed tuples: [field, op, value], ["And" | "Or", [...]] or ["Not", filter]. An unknown operator is a type error.
In Python, filters are tuples. mypy cannot check a nested tuple literal, so use And, Or and Not:
Python responses are plain dicts, typed with TypedDicts in brussle.types. Nothing is validated at runtime, so fields and enum values the API adds later never break your client.

Errors and retries

Every error response becomes an ApiError with the HTTP status, the API’s code (such as budget_exceeded or too_large), its message and details. An error response that is not the API’s JSON, such as a proxy’s error page, or a success whose body is not JSON, gets the code unknown, a generic message and, when the response has one, details.request_id. Network errors, and timeouts in Python, are raised as they are. Each call is retried at most twice by default. Set max_retries in Python or maxRetries in TypeScript on the client to change it.
  • 429 or 503 with Retry-After. Retried after waiting that long, for any call: the API refused it without acting on it, or it was a write, which is always safe to repeat. A Retry-After over 60 seconds (MAX_RETRY_AFTER_SECONDS) is not waited out, and the error is returned at once. A daily allowance that is used up sends no Retry-After and says when it renews in details.resets_at.
  • Network errors, timeouts, 500, 502, 503 and 504. Retried only when the call is safe to repeat: a read (every GET, query, and a subscription’s query), a write, which is idempotent by construction, a call that changes nothing when repeated (marking or unmarking a non-production prefix, updating a subscription or webhook endpoint, recovering an endpoint’s deliveries), or any call with an idempotency key. Creating an endpoint or subscription, rotating a secret, a test send and a redelivery are not retried without a key. The wait starts at 0.5 seconds and doubles on each retry up to 8 seconds, with random jitter, unless the response gives a Retry-After.
  • Anything else is returned at once, including every other 4xx, and a failed call that is not safe to repeat, such as creating a judgment without a key.

Idempotency keys

Every call that changes something takes an idempotency key, sent as the Idempotency-Key header: idempotency_key="..." in Python and { idempotencyKey: "..." } as the last argument in TypeScript. Use one key per logical request, and reuse it only for that request’s retries. A call with a key is retried like a write.
  • A retry gets the first response. For 24 hours after a call succeeds, a call with the same key and the same arguments gets that response back, verbatim, instead of running again: a retried create returns the version it made, a retried backfill confirm the job it started, a retried draw the documents it leased, and a retried rotation the secret it made. The response carries the header Idempotent-Replayed: true.
  • A failed call runs again. Only a successful response is kept, so retrying a call that failed runs it again.
  • One key, one request. A key belongs to one route and one set of arguments in your organization. The same key with different arguments is refused with idempotency_key_reused (422) while the first call’s response is kept.
  • A retry while the first call runs is refused with rate_limited and Retry-After: 1, without running. The SDKs wait and retry, and get the first call’s response once it finishes.
  • Keys are 1 to 255 visible ASCII characters; an empty or longer one is invalid_request.
Writes need no key to be retried safely: a retried append adds nothing while its values are still in the array, and a retried upsert or patch sets the same content again (see system behavior). A key only saves the second write when the first one landed.

Importing documents

ns.import_documents(documents) in Python and ns.importDocuments(documents) in TypeScript upsert a large set of documents. They read documents as they go, from any iterable (and in TypeScript any async iterable), so an import never has to fit in memory.
  • Batches. Each write holds up to 1,000 documents or 64 MB, the most the API takes.
  • Concurrency. Up to concurrency writes at once, 4 by default. Python runs them on a thread pool.
  • Retries. Each batch has its own idempotency key, reused on its retries, which follow the client’s rules above.
  • Repeated ids. No id is in two writes at once. A document whose id is still being written waits for that write, so when your input repeats an id, the later document is the one that stays.
  • Failures. A batch that still fails after its retries is recorded, and the import carries on. stop_on_failure=True (stopOnFailure: true) stops starting new batches after the first failure.
  • Progress. on_progress (onProgress) is called after each batch with the totals so far.
It returns a summary: documents and batches written, failures, each with the batch’s ids and its error, and stopped. A write commits all of its documents or none, and writing them again is safe, so once you have fixed the cause, write a failed batch’s ids again. See import existing data.

Webhooks and events

A subscription is a saved query: it sends subscription.entered when a document starts matching, and subscription.exited when it stops if you ask for it. Platform events such as job.completed go to the endpoints whose events patterns match them. Every event also goes to the events feed. See webhooks for how deliveries work.
rotate_secret / rotateSecret returns a new secret. The old one keeps signing beside it for previous_valid_for, 24 hours by default, so your receivers can switch over without missing an event.

The events feed

db.events.list(...) returns one page of the feed, oldest first, with a next_cursor that is present even on an empty page. db.events.iterate(...) follows next_cursor for you and yields each event until a page comes back empty. Both take cursor, limit, types (patterns such as job.*) and namespace_prefix (namespacePrefix). An event’s id also works as a cursor, so a poller can store the last id it handled and resume after it (see the events feed):
The feed keeps 30 days. An older cursor fails with invalid_request and details.reason cursor_expired: re-read the current state, for a subscription with its query, and start again without a cursor.

Verifying webhooks

verify_webhook(payload, headers, secret) in Python and verifyWebhook(payload, headers, secret) in TypeScript check a delivery’s signature (Standard Webhooks, HMAC-SHA256) and its webhook-timestamp, which must be within 5 minutes of your clock, and return the parsed event. They need nothing beyond the SDK in Python and WebCrypto in TypeScript, so they run on edge runtimes too. secret may also be a list of secrets. A request that fails a check raises WebhookVerificationError, whose reason is headers, timestamp or signature: answer it with a 400. An empty or malformed secret, or an empty list, raises an ordinary error instead (ValueError in Python, TypeError in TypeScript), before the request is checked: it never verifies anything.
Pass the raw body, exactly the bytes you received. A body parsed as JSON and serialized again can differ by a space or a key order, and its signature will not match.
The event is typed as WebhookEvent, a union discriminated on type, so checking type narrows data to that event’s fields. test is true on every event a test send sends. New event types may be added, so ignore the ones you don’t handle:
Deliveries are at least once and may arrive out of order: dedupe on the event’s id (the webhook-id header), and for one subscription and document, drop an event whose data.sequence is lower than one you already handled.