Skip to main content
A keyword alert fires when a new article matches a query such as "harbour strike" AND (ferry OR port) NOT football. Quivr records each catch as a Match with the words that matched, and sends it to your webhook. The first-party alerts plugin decides the matches. How alerts work explains the objects this page creates.

Prerequisites

  • The alerts plugin pinned. The local stack pins it; elsewhere see Pin a plugin.
  • QUIVR_API_URL and a QUIVR_API_KEY with monitoring:read and monitoring:write, plus content:write to add the test article below. The first request creates a Corpus, which needs corpora:write and a key not limited to some Corpora; with another key, use an existing Corpus id instead. The local stack’s make env key has all of these.
  • QUIVR_DESTINATION: the id of a webhook destination the operator configured for your Organization. Destinations live in Quivr’s configuration, never in API requests. The local stack’s make env sets one that delivers nowhere.

Write the query

  • Operators are written in capitals; and, or and not in lower case are ordinary words. NOT binds tighter than AND, which binds tighter than OR.
  • Case, accents and punctuation are ignored: greve finds “Grève”.
  • Words match whole, and there is no stemming: strike does not find “strikes”. List the forms you need, strike OR strikes.
  • Terms search the title and body Parts.
  • source:, origin: (client or connector), producer:, connector: and connector_kind: filter on metadata Quivr always has. Other names, such as author:, work once the operator maps them (below).

Turn it into an expression

Quivr stores a query as a JSON tree, which it validates when you save the alert, so a malformed query is refused at once. An unmapped metadata field is still valid syntax but never matches (see Filter on your own metadata). The plugin ships the parser. From a quivr checkout:
An application can also build the tree from a form: all, any and not map to “all of these words”, “any of these words” and “none of these words”.

Save the alert

These requests use a Corpus of their own:
The Saved Query holds the expression and the Corpora it watches:
The Subscription turns it on, with the alerts plugin as evaluator. Give it an owner when the alert belongs to one of your application’s users: Quivr echoes it on every Match and webhook, so you can route each alert, and GET /v0/subscriptions?owner=user-123 lists that user’s alerts.
The alert judges articles that become searchable from now on. Earlier articles are never judged; to see what a query would have caught before saving it, use POST /v0/subscription-previews.

Read what matched

Add an article that matches:
Within seconds a Match appears, with its evidence:
The explanation lists the terms that made the article match and the Parts where each was found. Terms under NOT are never listed. evidence.details has the same facts as JSON.

Receive the webhook

For each Match, Quivr sends a POST to the destination’s URL. The body only references what happened; fetch the Match with GET /v0/matches/{match_id} to read its evidence:
Answer with a 2xx status within 10 seconds to acknowledge it. Timeouts, connection failures, 408, 429 and 5xx answers are retried with growing delays, from 1 second up to 5 minutes, for 24 hours; any other answer stops the retries. These are defaults the operator can change, and a Retry-After header on a 429 or 503 sets the next delay. GET /v0/deliveries/{delivery_id} shows a delivery’s state and its attempts. When an article that matched is corrected or withdrawn, the same receiver gets a follow-up: match.corrected, match.no_longer_matches or match.withdrawn. Corrections and withdrawals says which ones store a new Match.

Verify the signature

Check the signature on the raw body before you parse it. Requests follow Standard Webhooks: the webhook-signature header holds v1, and a base64 HMAC-SHA256 of webhook-id, webhook-timestamp and the raw body, joined by dots. The key is the destination’s secret, whsec_ followed by base64. The operator sets it in Quivr’s configuration and gives it to you. The header can hold several signatures separated by spaces; accept the request when one of them matches. This Python function takes the headers as a dictionary with lower-case names. It accepts only v1 signatures, skips malformed ones, and refuses a request whose timestamp is more than five minutes old; every retry carries a fresh timestamp. It was run by hand against Quivr’s public test vector and against altered copies of it.
Answer 401 when it returns False. That answer is permanent, so Quivr stops retrying that webhook. With a wrong secret, every webhook is refused this way; the Matches stay listable through the API.

Deduplicate

Delivery is at least once: a webhook can arrive twice, for example when your answer is lost on the way back. A retry sends the same webhook-id, which is the body’s event_id, and the same body. Each event, follow-ups included, has its own id. Store the ids you have handled for at least the retry window, 24 hours by default, and answer 2xx without acting again when one comes back. Record an id only after you have acted on the event, so a crash before then gets the webhook again.
This receiver uses only the standard library. It verifies each webhook with the verify function above, acts on each event once, and acknowledges with 204. It was run by hand with signed test requests: a valid one, a repeat and a forged one. Quivr refuses loopback and private addresses by default, so put the receiver behind the public URL of the destination the operator configured. Keep the handled ids in your database rather than in memory, so they survive a restart, and give them a unique constraint if two copies can arrive at the same time.
A receiver that stays down past the retry window misses those webhooks. List Matches with GET /v0/matches?subscription_id=… to recover first matches and corrections that still matched. Corrections that no longer match and withdrawals create notices without new Matches: an application that already follows the change feed from a saved position, with a key that has changes:read, finds them there for as long as the operator keeps events, 7 days by default.

Change or stop an alert

Filter on your own metadata

Ask your operator to map the metadata fields your alerts use, such as author or category. A list field matches when one of its values does. A name the operator has not mapped never matches.

Timing

A keyword alert is decided as soon as the article becomes searchable, before its vectors are computed. Set {"wait_for_enrichment": true} in the Subscription’s evaluator.configuration to wait for them.

For operators

Moving existing alerts to a new version of the alert rule, and abandoning evaluations whose old rule cannot be restored, are operator tasks: see Upgrade an alert rule and Retire alert evaluations.

Next