Prerequisites
- Python 3.12+ (
python3 --version), a Quivr checkout and its CLI (go build -o .scratch/quivr ./cmd/quivr). Add.scratchto yourPATH. - Read How plugins work for the engine/plugin boundary and Write a connector for source kinds and stable record identities.
- For signature routes, the provider’s signature format and signing secret. The operator deposits that secret as the instance’s credential; you receive it decrypted in the handler. The offline sample below needs no provider account.
Steps
Generate a push source
From the checkout:CERTIFIED. The generated fixture covers an accepted record and a refused blank record. The offline sample in plugins/push-source follows the same pattern; its CI report is uploaded as push-source-contract-reports.
Declare and handle a route
Declaremodes: [pull, push] on your source kind; API routes require push mode, and a push kind must also support pull. Declare the requests it accepts under api.routes. Its method and relative path select a named handler; request_schema validates JSON before the handler runs. The generated manifest contains this route under contributions.connector.kinds.events.api.routes:
@plugin.connector_route("events", "publish") with a typed ReceiveRequest. request.body holds parsed JSON; request.request preserves the caller’s path, query and raw body. Return a ConnectorReceiveResponse with accepted items or a refusal; the generated handler shows the complete mapping. Go implements Receiver.Receive, selects req.Route and decodes req.Body. For a route such as events/{category}, read the actual relative path, such as events/news, from Python’s request.request.path or Go’s req.Request.Path.
Return a stable Record Key for each document and a new revision when its source content changes. Accepted POST items enter normal ingestion, and Quivr returns 202 with Receipts, identifiers used to follow processing. The handler’s accepted status does not replace that response. Read the plugin contract for wire fields and the Python kit or Go kit for handler types.
For certification, a fixture’s receive case sets route: "publish" and request.path: "records", with source JSON in request.body. The runner checks plugin handling; engine authentication and admission are separate checks.
Choose authentication
Your compatibility range must admit that Plugin API version. Token routes and Quivr-key routes accept only their declared credential type. Quivr strips bearer credentials and cookies before relaying requests; they never reach your handler.
Verify provider signatures
Declaresignature: {header, timestamp_header?, window_seconds} on a signature route. The window is 1–86400 seconds. If the provider signs a Unix-seconds timestamp, name its header: Quivr rejects timestamps outside the window, including too far in the future. Without a signed timestamp, replay protection lasts only for the reservation window.
Your handler verifies the provider’s signature over the exact raw bytes before accepting items. On the wire these are request.body_base64. In Python, decode request.request.body_base64 with base64.b64decode; Go exposes req.Request.Body(). Re-encoding parsed JSON can change the bytes and invalidate the signature. If you declare timestamp_header, verify that timestamp as part of the signed message too. Quivr checks its age, not its authenticity.
For example, the first-party X plugin compares sha256= plus the base64 HMAC-SHA256 of the raw body against x-twitter-webhooks-signature, using the deposited consumer secret and a constant-time comparison. It refuses a mismatch with 401 and no items. See its handler and offline checks.
Answer provider challenges
A challenge asks your plugin to prove it can receive at this address. Declare a GET signature route and return its response synchronously, without ingestion items. GET skips the POST signature-header, freshness and replay guards, but still passes JSON/schema, IP and rate admission. An empty GET body is validated asnull, so omit request_schema or allow null on that route.
For example, not run: X’s route declarations, under a kind’s api.routes:
GET /v0/connectors/<instance_id>/api/receive?crc_token=<challenge>. The plugin signs the query’s challenge with the consumer secret and returns 200 application/json containing {"response_token":"sha256=<base64 HMAC>"}. Missing crc_token returns 400. The GET needs no POST signature header, and its Idempotency-Key never retrieves a cached answer. Test the reply offline with the linked X checks; registering the address with X needs provider credentials.
This Python excerpt implements the same signature and challenge format. In your plugin module, declare the two routes above on events and a credential schema with a required consumer_secret string; the operator deposits that credential. Reuse your module’s plugin object. The POST branch accepts an empty delivery to show verification; add your record mapping after the signature check.
Push checks
An IP allowlist restricts sender addresses; a rate bucket limits how quickly an instance admits requests. An idempotency key names one delivery: bearer routes (quivr_key and instance_token) may reuse its cached answer, while signed POSTs reserve fingerprints to refuse replay. The operator configures these policies in Receive and secure pushes.
Quivr runs freshness and replay before schema validation, then IP and rate admission; only your plugin verifies the provider’s cryptographic signature.
- Quivr recognizes a supplied bearer, checks a recognized API key’s push permission and bounds the request.
- It loads the enabled instance, checks API key Organization and Corpus scope, resolves the route and checks its authentication, including an instance token.
- Signed POSTs require one signature header, check any timestamp’s age and reserve signature and delivery-key fingerprints to refuse replays. GET challenges skip these guards.
- Quivr checks delivery-key metadata, parses JSON and validates the route schema, then checks the IP allowlist.
- A completed bearer cache entry skips the rate limit and plugin. Otherwise, a bearer request with a delivery key claims its cache entry; all uncached requests pass the rate bucket.
- Quivr opens the source credential and calls the plugin. The plugin verifies a signed POST over raw bytes, maps a bearer POST to items, or answers a GET challenge without items.
- Quivr validates and ingests accepted POST items, records acceptance, then caches bearer answers or keeps successful signed reservations. Failed signed processing releases its reservation when cleanup succeeds.
- Quivr commits the attempt’s audit and counters before replying with Receipts, a challenge answer or a refusal. Cached answers follow this audit step too.
receive path, with the same protections and audit. The X plugin supports this alias; its old address is scheduled for removal in engine 1.0.0. Other legacy webhooks keep their plugin-owned authentication.
Check it worked
Certify the generated handler, then pin it or register and activate it. The local stack already runs the sample inplugins/push-source; the offline push example exercises that sample. To exercise your generated plugin, run quivr plugin dev --port 9940 incoming-records from the checkout, replace the sample’s pin with its absolute manifest path and an endpoint reachable by Quivr, then restart Quivr. Follow the same instance and push steps with your declared kind and route. Add provider-specific fixtures for valid signatures, invalid signatures and challenges before deploying a signed handler.
Clean up
The generatedincoming-records directory is local to your checkout. Remove it when you no longer need the example. Revoke issued tokens and disable any deployed example instance.
Next
- Receive and secure pushes for token lifecycle, limits, retries and audit.
- Write a connector for scheduled collection and attachments.
- Plugin protocol for the exact request and response fields.