curl against https://api.brussle.com/v1.
Methods
Methods mirror the routes. TypeScript uses the same names in camelCase.
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.ptype-checks only afteranswer.type === "bool". - Filters are typed tuples:
[field, op, value],["And" | "Or", [...]]or["Not", filter]. An unknown operator is a type error.
And, Or and Not:
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 anApiError 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.
429or503withRetry-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. ARetry-Afterover 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 noRetry-Afterand says when it renews indetails.resets_at.- Network errors, timeouts,
500,502,503and504. Retried only when the call is safe to repeat: a read (everyGET,query, and a subscription’squery), awrite, 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 aRetry-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 theIdempotency-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_limitedandRetry-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.
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
concurrencywrites 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.
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 sendssubscription.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):
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.
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:
id (the webhook-id header), and for one subscription and document, drop an event whose data.sequence is lower than one you already handled.