> ## Documentation Index
> Fetch the complete documentation index at: https://docs.quivr.thevibecompany.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Plugin protocol

> The routes a plugin serves, their fields, errors and fixtures, generated from the schemas.

The HTTP routes a plugin serves, with every field of their JSON bodies, the error envelope and the fixture files `quivr plugin test` reads. A plugin serves JSON over HTTP; every route is under `/v0`, the major version of the Plugin API. The schemas in `contracts/plugins/v0/` are the source of truth; the SDKs and `quivr plugin test` implement them.

## Discovery and health

### `GET /v0/discovery`

manifest\_digest is sha256 over the exact bytes of the quivr-plugin.yaml the plugin was built from; the engine compares it with the pinned manifest.

**Response body (200)** (`discovery.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `plugin_api` | string | yes | Plugin API version the plugin implements. |
| `plugin` | object | yes | — |
| `plugin.id` | `PluginId` (shared Manifest schema) | yes | — |
| `plugin.version` | `Version` (shared Manifest schema) | yes | — |
| `manifest_digest` | string | yes | — |
| `contributions` | array of `normalizer`, `subscription`, `connector`, `ingestion`, `retrieval` | yes | — |

### `GET /v0/health`

Returns 200 with this body when the plugin can accept invocations; otherwise 503 with the error envelope.

**Response body (200)** (`health.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `status` | `"ok"` | yes | — |

## Normalizer

### `POST /v0/contributions/normalizer`

Immutable invocation context plus a reference to the input Blob; bodies are never inline.

**Request body** (`normalizer-request.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `invocation_id` | string | yes | Unique per attempt; correlate logs with it. 1 to 128 characters. |
| `idempotency_key` | string | yes | Opaque, stable across retries of the same logical invocation. The same key must yield the same logical output. 1 to 256 characters. |
| `contribution` | `"normalizer"` | yes | — |
| `organization_id` | string | yes | — |
| `corpus_id` | string | yes | — |
| `record_id` | string | yes | — |
| `record_version_id` | string | yes | — |
| `source` | `SourceIdentity` (shared Manifest schema) | yes | Source namespace and Record Key of the Record. |
| `input` | object | yes | — |
| `input.blob_id` | string | yes | — |
| `input.media_type` | string | yes | At most 255 characters. |
| `input.size_bytes` | integer | yes | — |
| `input.sha256` | string | yes | — |
| `input.reference` | object | yes | — |
| `extensions` | `Extensions` (shared Manifest schema) | no | Extensions submitted with the Record Version. |
| `provenance` | `Provenance` (shared Manifest schema) | no | Provenance submitted with the Record Version. |
| `configuration` | object | yes | Installer configuration, already validated against the manifest configuration schema. |

**Response body (200)** (`normalizer-response.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `manifest` | `ManifestContent` (shared Manifest schema) | yes | — |
| `extensions` | `Extensions` (shared Manifest schema) | no | Record Version extensions, only in namespaces the plugin declares. |
| `language` | string | no | BCP 47 language tag of the normalized content. |
| `warnings` | array of object | no | At most 32 items. |
| `warnings[].code` | string | yes | At most 64 characters. |
| `warnings[].message` | string | yes | 1 to 1024 characters. |

## Alert rule (`subscription`)

### `POST /v0/contributions/subscription`

Since Plugin API 0.2. One Record Version's text Parts and metadata (source identity, acceptance time, provenance, extensions) and a batch of distinct evaluations to decide. The core deduplicates Subscriptions that share the same expression and configuration into one evaluation and never sends more evaluations than the manifest's max\_batch\_size.

**Request body** (`subscription-request.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `invocation_id` | string | yes | Unique per attempt; correlate logs with it. 1 to 128 characters. |
| `idempotency_key` | string | yes | Opaque, stable across retries of the same logical invocation. The same key must yield the same decisions and evidence. 1 to 256 characters. |
| `contribution` | `"subscription"` | yes | — |
| `organization_id` | string | yes | — |
| `record` | object | yes | The evaluated Record Version. |
| `record.corpus_id` | string | yes | — |
| `record.record_id` | string | yes | — |
| `record.record_version_id` | string | yes | — |
| `record.enriched` | boolean | yes | True when the Version has embedding coverage in the active generation. A rule that needs enrichment answers not\_ready while it is false; the core evaluates again after enrichment. |
| `record.parts` | array of object | yes | The canonical text Parts of the Version, in Manifest order. Blob Parts are not sent. Keys are unique. At most 256 items. |
| `record.parts[].key` | string | yes | — |
| `record.parts[].role` | string | yes | — |
| `record.parts[].text` | string | yes | — |
| `record.parts[].vector` | reserved | no | Reserved: a document that sets it is rejected. |
| `record.source` | object | no | Where the Record comes from: its Source Namespace and Record Key, and the Source Position of the accepted revision when the producer sent one. Always sent by the core since Plugin API 0.2. |
| `record.source.namespace` | string | yes | — |
| `record.source.record_key` | string | yes | — |
| `record.source.position` | string | no | Opaque source ordering value (for example a feed timestamp or a mail change key). |
| `record.accepted_at` | string | no | When Quivr accepted the revision this Version publishes (RFC 3339, UTC). Always sent by the core since Plugin API 0.2. |
| `record.provenance` | object | no | Who produced the Version. Always sent by the core since Plugin API 0.2. |
| `record.provenance.origin` | `client`, `connector` | yes | connector for a revision a Connector Instance acquired, client for one an API client submitted. A client cannot claim connector. |
| `record.provenance.producer` | string | no | The producer the client declared, or the Connector Instance id. |
| `record.provenance.producer_version` | string | no | — |
| `record.provenance.connector` | object | no | Present when origin is connector. |
| `record.provenance.normalization` | object | no | Present when an external normalizer produced the published Manifest. |
| `record.extensions` | `Extensions` (shared Manifest schema) | no | The Version's structured metadata by namespace (for example author, section or categories when the source provides them). Absent when the Version has none. |
| `evaluations` | array of object | yes | Distinct evaluations to decide, each answered by exactly one decision with the same id. 1 to 256 items. |
| `evaluations[].id` | string | yes | Opaque, unique within the request. 1 to 128 characters. |
| `evaluations[].expression` | object | yes | The pinned Saved Query Version expression, valid against the manifest's expression\_schema. |
| `evaluations[].configuration` | object | yes | The pinned Subscription evaluator configuration, valid against the manifest's configuration\_schema. |
| `evaluations[].subscriptions` | array of object | yes | The Subscription Versions this evaluation stands for. Informational (logs, tracing): a decision must depend only on the record, the expression and the configurations. |
| `evaluations[].subscriptions[].subscription_id` | string | yes | — |
| `evaluations[].subscriptions[].subscription_version_id` | string | yes | — |
| `evaluations[].subscriptions[].saved_query_id` | string | yes | — |
| `evaluations[].subscriptions[].saved_query_version_id` | string | yes | — |
| `evaluations[].subscriptions[].owner` | string | no | The Subscription Owner (an opaque end-user reference of the client application); absent for a global Subscription. Informational like the ids. 1 to 128 characters. |
| `evaluations[].query_vector` | reserved | no | Reserved: a document that sets it is rejected. |
| `configuration` | object | yes | The validated plugin (installer) configuration, as for every Contribution. |

**Response body (200)** (`subscription-response.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `decisions` | array of object | yes | At most 256 items. |
| `decisions[].id` | string | yes | The id of the evaluation this decision answers. 1 to 128 characters. |
| `decisions[].decision` | `match`, `no_match`, `not_ready` | yes | match: the Record Version satisfies the expression. no\_match: it does not. not\_ready: it cannot be decided yet (for example before enrichment); the core evaluates again on a later trigger. A plugin that cannot decide at all answers with the error envelope instead. |
| `decisions[].evidence` | object | no | Required for match, optional otherwise. |
| `decisions[].evidence.explanation` | string | yes | Human explanation of the decision, at most 4096 Unicode code points. 1 to 4096 characters. |
| `decisions[].evidence.part_keys` | array of string | no | Keys of the request Parts that support the decision; each must exist in the request. At most 100 items. |
| `decisions[].evidence.details` | object | no | Plugin-defined structured evidence; at most 16 KiB once serialized as JSON. |

## Connector

### `POST /v0/contributions/connector/check_credential`

Since Plugin API 0.3. Check that the source accepts the credential, without fetching items. Used for Connector Health.

**Request body** (`connector-check-credential-request.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `invocation_id` | string | yes | Unique per attempt; correlate logs with it. 1 to 128 characters. |
| `contribution` | `"connector"` | yes | — |
| `organization_id` | string | yes | — |
| `configuration` | object | yes | The plugin configuration the installer supplies, valid against configuration.schema. |
| `connector` | any | yes | — |
| `credential` | any | yes | — |
| `now` | any | yes | — |

**Response body (200)** (`connector-check-credential-response.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `status` | `"ok"` | yes | — |
| `expires_at` | string | no | When the credential stops working, when the source says so; drives the credential\_expiring health state. |

### `POST /v0/contributions/connector/describe_attachment`

Since Plugin API 0.4. The core asks for the exact size and SHA-256 of one attachment of an item it has not accepted yet, before it issues an upload grant. The plugin reads the bytes from the source, at most attachments.max\_bytes, or answers a skip.

**Request body** (`connector-describe-attachment-request.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `invocation_id` | string | yes | Unique per attempt; correlate logs with it. 1 to 128 characters. |
| `contribution` | `"connector"` | yes | — |
| `organization_id` | string | yes | — |
| `configuration` | object | yes | The plugin configuration the installer supplies, valid against configuration.schema. |
| `connector` | any | yes | — |
| `credential` | object or null | yes | The decrypted Deposited Credential, or null for a kind without one. Never log, echo or persist it. |
| `now` | string | yes | The core's clock for this invocation (RFC 3339, UTC). |
| `item` | object | yes | The item the attachment belongs to, as the plugin returned it. |
| `item.record_key` | string | yes | 1 to 1024 characters. |
| `item.revision` | string | no | 1 to 256 characters. |
| `item.extensions` | `Extensions` (shared Manifest schema) | no | The item's current extensions. |
| `attachment` | `Attachment` (shared Manifest schema) | yes | The attachment descriptor, as the plugin returned it. |

**Response body (200)** (`connector-describe-attachment-response.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `size_bytes` | integer | no | Exact size, at most attachments.max\_bytes. |
| `sha256` | string | no | Lowercase hex SHA-256 of the exact bytes. |
| `skip` | string | no | Why the attachment is left out, for example too\_large when the bytes exceed attachments.max\_bytes. At most 64 characters. |
| `item_extensions` | `Extensions` (shared Manifest schema) | no | Replaces the item's extensions, for example to record the skipped attachment; only in namespaces the plugin declares. |

### `POST /v0/contributions/connector/fetch`

Since Plugin API 0.3. Fetch one page of new or changed source items after the checkpoint. The plugin is stateless between invocations: the core persists the checkpoint and advances it only after the page's items are durably accepted.

**Request body** (`connector-fetch-request.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `invocation_id` | string | yes | Unique per attempt; correlate logs with it. 1 to 128 characters. |
| `contribution` | `"connector"` | yes | — |
| `organization_id` | string | yes | — |
| `configuration` | object | yes | The plugin configuration the installer supplies, valid against configuration.schema. |
| `connector` | object | yes | The Connector Instance the core invokes the plugin for. |
| `connector.instance_id` | string | yes | 1 to 128 characters. |
| `connector.kind` | string | yes | — |
| `connector.corpus_id` | string | no | The Corpus the instance writes to, since Plugin API 0.3.1. Relation targets bind to it: a target Source Identity is this corpus\_id and source\_namespace with the target's Record Key. |
| `connector.source_namespace` | string | no | The Source Namespace of the instance's Records, since Plugin API 0.3.1. 1 to 200 characters. |
| `connector.webhook_url` | string | no | Since Plugin API 0.5, for a kind that declares the push mode on a deployment with a public URL: the public address where the source delivers to this instance. The core relays each delivery to receive, so a plugin registers this address with the source and never needs to be reachable itself. At most 2048 characters. |
| `connector.config` | object | yes | The Connector Instance configuration, already valid against the kind's config\_schema. |
| `credential` | object or null | yes | The decrypted Deposited Credential, valid against the kind's credential\_schema; null for a kind without one, or for an instance without one when the kind's credential\_required is false. Sent only for this invocation: never log, echo or persist it. |
| `checkpoint` | any | yes | The opaque Acquisition Checkpoint the plugin returned last (any JSON value); null on the first run of a Connector Instance. |
| `now` | string | yes | The core's clock for this invocation (RFC 3339, UTC). Use it instead of the local clock so runs are reproducible. |
| `page_in_run` | integer | yes | 0 for the first page of a run, then counts up while the plugin answers more: true. |
| `reads_today` | integer | yes | Source resources read during the current UTC day, including earlier pages of this run, for sources that bill or rate-limit per resource. |

**Response body (200)** (`connector-fetch-response.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `items` | array of object | yes | At most the declared max\_items. At most 1000 items. |
| `items[].record_key` | string | yes | Stable source identity within the Source Namespace. 1 to 1024 characters. |
| `items[].revision` | string | no | Identifies the item content; absent derives it from the content. Set it when the item has attachments, so an unchanged item is recognised before anything is downloaded. 1 to 256 characters. |
| `items[].source_position` | string | no | Optional monotonic Source Position. 1 to 256 characters. |
| `items[].content` | `TextContent` (shared Manifest schema) or `ManifestContent` (shared Manifest schema) | no | Text or a Manifest. A connector Manifest carries no Blob Parts: binary Parts are attachments. |
| `items[].extensions` | `Extensions` (shared Manifest schema) | no | Record Version extensions, only in namespaces the plugin declares. |
| `items[].withdraw` | boolean | no | true asks for a Tombstone instead of a new version, for example a post deleted at the source. |
| `items[].attachments` | array of object | no | At most 64 items. |
| `items[].attachments[].key` | string | yes | 1 to 256 characters. |
| `items[].attachments[].parent_key` | string | no | 1 to 256 characters. |
| `items[].attachments[].role` | string | yes | 1 to 64 characters. |
| `items[].attachments[].media_type` | string | yes | At most 255 characters. |
| `items[].attachments[].size_bytes` | integer | no | Announced size, when the source provides it; the exact size when sha256 is set. |
| `items[].attachments[].sha256` | string | no | Lowercase hex SHA-256 of the exact bytes, when the plugin already knows them; the core then skips describe\_attachment. |
| `items[].attachments[].extensions` | `Extensions` (shared Manifest schema) | no | — |
| `items[].attachments[].ref` | string | yes | Opaque handle the plugin understands, given back when the core asks for the bytes. 1 to 1024 characters. |
| `checkpoint` | any | yes | The opaque checkpoint that resumes after this page (any JSON value, at most the declared limits.max\_checkpoint\_bytes serialized, 64 KiB by default). Return the request's checkpoint unchanged when nothing moved. |
| `more` | boolean | yes | true asks for another page in the same run; the checkpoint must then have moved. |
| `reads` | integer | no | Source resources this page read; feeds the per-UTC-day usage counters. Default `0`. |
| `diagnostics` | object | no | Kind-defined diagnostics exposed as Connector Health diagnostics (at most 16 KiB serialized); the latest page's value replaces the previous one. |
| `notice` | string | no | A condition to report on a run that completed normally, such as a spend cap; it ends the run. At most 64 characters. |
| `not_due` | boolean | no | true: the source asked not to be polled yet (for example an RSS ttl). The run is skipped: no items, more false and the request's checkpoint unchanged. |
| `push` | object | no | Since Plugin API 0.5, for a push kind: the plugin's view of the push channel it sets up at the source (a registered webhook and its subscriptions). active: deliveries are expected. pending: not set up yet; code may say why. failed: the source refused or broke the setup, and polling carries the collection alone; error\_class access shows as the access\_error Connector Health state. The core keeps the latest report. |
| `push.state` | `active`, `pending`, `failed` | yes | — |
| `push.error_class` | `access`, `transient`, `source` | no | Required when state is failed, absent otherwise. |
| `push.code` | string | no | Required when state is failed; an optional reason when pending; absent when active. At most 64 characters. |
| `push.poll_interval_seconds` | integer | no | Only with state active: while deliveries arrive, the core runs pull at most this often (the greater of this and the instance's interval), as a safety net. It returns to the instance's interval as soon as push degrades. 60 to 86400. |

### `POST /v0/contributions/connector/receive`

Since Plugin API 0.5, for a kind that declares the push mode. The core relays one request a source sent to the instance's public webhook route: the plugin verifies it with the credential (for example a signature over the raw body), answers any challenge, and returns the items it carries. The core ingests the items, then returns the plugin's answer to the source.

**Request body** (`connector-receive-request.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `invocation_id` | string | yes | Unique per delivery; correlate logs with it. 1 to 128 characters. |
| `contribution` | `"connector"` | yes | — |
| `organization_id` | string | yes | — |
| `configuration` | object | yes | The plugin configuration the installer supplies, valid against configuration.schema. |
| `connector` | object | yes | The Connector Instance the delivery is addressed to. |
| `connector.instance_id` | string | yes | 1 to 128 characters. |
| `connector.kind` | string | yes | — |
| `connector.corpus_id` | string | yes | The Corpus the instance writes to; Relation targets bind to it. |
| `connector.source_namespace` | string | yes | 1 to 200 characters. |
| `connector.config` | object | yes | The Connector Instance configuration, already valid against the kind's config\_schema. |
| `credential` | object or null | yes | The decrypted Deposited Credential, for verifying the delivery. Sent only for this invocation: never log, echo or persist it. |
| `checkpoint` | any | yes | The current Acquisition Checkpoint, read-only: a receive answer carries no checkpoint, because pull runs own it. Null before the first run. |
| `now` | string | yes | The core's clock (RFC 3339, UTC). |
| `reads_today` | integer | yes | Source resources read during the current UTC day, by pull runs and deliveries. |
| `request` | object | yes | The relayed request, bounded by the core: a body of at most 1 MiB, at most 64 header names. Hop-by-hop headers and Cookie are not relayed. |
| `request.method` | `GET`, `POST` | yes | — |
| `request.query` | string | yes | The raw query string, without the leading ?. At most 8192 characters. |
| `request.headers` | map of array of string | yes | Header values by lowercase name, in arrival order. At most 64 entries. |
| `request.body_base64` | string | yes | The exact body bytes, standard base64 (signatures cover the raw bytes); empty for no body. At most 1398104 characters. |

**Response body (200)** (`connector-receive-response.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `verdict` | `accepted`, `refused` | yes | — |
| `response` | object | yes | What the core answers the source: a 2xx status for accepted, a 4xx status for refused. A challenge (such as a CRC check) is an accepted delivery with no items whose body is the challenge answer. |
| `response.status` | integer | yes | 200 to 499. |
| `response.content_type` | string | no | At most 256 characters. |
| `response.body` | string | no | Text body returned to the source. At most 65536 characters. |
| `items` | array of `Item` (shared Manifest schema) | no | Items the delivery carries, at most the declared max\_items. Only with verdict accepted; attachments are not supported in a delivery. At most 1000 items. |
| `reads` | integer | no | Source resources the delivery counts as read; feeds the per-UTC-day usage counters. Default `0`. |

### `POST /v0/contributions/connector/upload_attachment`

Since Plugin API 0.4. The plugin uploads the bytes it described to the grant, then answers. The core verifies the stored bytes before it accepts the item. Bytes that changed at the source since describe\_attachment are a source error attachment\_changed; the core describes the attachment again once.

**Request body** (`connector-upload-attachment-request.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `invocation_id` | string | yes | Unique per attempt; correlate logs with it. 1 to 128 characters. |
| `contribution` | `"connector"` | yes | — |
| `organization_id` | string | yes | — |
| `configuration` | object | yes | The plugin configuration the installer supplies, valid against configuration.schema. |
| `connector` | any | yes | — |
| `credential` | object or null | yes | The decrypted Deposited Credential, or null for a kind without one. Never log, echo or persist it. |
| `now` | string | yes | The core's clock for this invocation (RFC 3339, UTC). |
| `item` | object | yes | The item the attachment belongs to, as the plugin returned it. |
| `item.record_key` | string | yes | 1 to 1024 characters. |
| `item.revision` | string | no | 1 to 256 characters. |
| `item.extensions` | `Extensions` (shared Manifest schema) | no | The item's current extensions. |
| `attachment` | `Attachment` (shared Manifest schema) | yes | The attachment descriptor, as the plugin returned it. |
| `grant` | object | yes | One presigned PUT for one storage object. Storage refuses any other length, checksum or media type. Send exactly these headers, never log the URL or the headers, and do not reuse the grant. |
| `grant.url` | string | yes | 1 to 8192 characters. |
| `grant.method` | `"PUT"` | yes | — |
| `grant.headers` | map of string | yes | Headers the PUT must carry exactly. |
| `grant.size_bytes` | integer | yes | — |
| `grant.sha256` | string | yes | — |
| `grant.media_type` | string | yes | 1 to 255 characters. |
| `grant.expires_at` | string | yes | — |

**Response body (200)** (`connector-upload-attachment-response.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `status` | `"uploaded"` | yes | — |

## Ingestion

### `POST /v0/contributions/ingestion/embed_query`

Since Plugin API 0.6. Encode one query into one of the plugin's spaces, so a query vector always comes from the model that produced the document vectors.

**Request body** (`ingestion-embed-query-request.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `invocation_id` | string | yes | Unique per attempt; correlate logs with it. 1 to 128 characters. |
| `contribution` | `"ingestion"` | yes | — |
| `organization_id` | string | yes | — |
| `configuration` | object | yes | The validated plugin (installer) configuration. |
| `space` | `SpaceId` (shared Manifest schema) | yes | — |
| `query` | object | yes | — |
| `query.modality` | `text` | yes | One of the space's query\_modalities. Plugin API 0.6 knows only text. |
| `query.text` | string | yes | The query, trimmed, with line breaks normalized to LF. 1 to 32768 characters. |

**Response body (200)** (`ingestion-embed-query-response.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `vector` | array of number | yes | One vector, exactly as many numbers as the space's dimensions. Values must be finite as 32-bit floats; under the cosine metric the vector must not be all zeros. 1 to 4096 items. |

### `POST /v0/contributions/ingestion/segment_and_embed`

Since Plugin API 0.6. One Record Version's text Parts and the enabled vector spaces to embed each segment in.

**Request body** (`ingestion-segment-and-embed-request.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `invocation_id` | string | yes | Unique per attempt; correlate logs with it. 1 to 128 characters. |
| `idempotency_key` | string | yes | Opaque, stable across retries of the same logical invocation. The same key must yield the same segments, vectors, lexical text and provenance. 1 to 256 characters. |
| `contribution` | `"ingestion"` | yes | — |
| `organization_id` | string | yes | — |
| `configuration` | object | yes | The validated plugin (installer) configuration. |
| `version` | object | yes | The Record Version being ingested. Informational: the answer must depend only on the Parts, the language, the spaces and the configuration. |
| `version.corpus_id` | string | yes | — |
| `version.record_id` | string | yes | — |
| `version.record_version_id` | string | yes | — |
| `language` | string | no | BCP 47 language hint, when the core knows one. |
| `parts` | array of object | yes | The Version's canonical text Parts, in Manifest order, with unique keys. Blob Parts are not sent. Segment offsets address these texts. 1 to 256 items. |
| `parts[].key` | string | yes | — |
| `parts[].role` | string | yes | — |
| `parts[].text` | string | yes | — |
| `spaces` | array of `SpaceId` (shared Manifest schema) | yes | The declared spaces to embed, among those this deployment enables. Every segment carries exactly one vector for each of them. Empty (since Plugin API 0.8) asks for the segments only, without vectors: the core segments a Version first and embeds it later, and the segments must then be the same. 0 to 8 items. |

**Response body (200)** (`ingestion-segment-and-embed-response.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `segments` | array of object | yes | 1 to 1024 items. |
| `segments[].part_key` | string | yes | Key of the request Part the segment is cut from. |
| `segments[].start` | integer | yes | First Unicode code point of the segment in the Part text. |
| `segments[].end` | integer | yes | Code point after the last one. start = end is an empty segment, allowed only when the request has a Part with the role title: the segment then stands for the title alone. |
| `segments[].vectors` | map of array of number | yes | One vector per requested space, by space id; empty when the request asks for no space (since Plugin API 0.8). 0 to 8 entries. |
| `segments[].lexical_text` | string | no | Optional keyword-search text for the segment (for example lower-cased, folded or stemmed words). The core indexes it in a separate keyword field; excerpts always come from the source text. At most 16384 code points, no NUL. |
| `segments[].provenance` | object | no | Optional details of how the segment was made (token counts, the embedded text's template), at most 4 KiB once serialized. Stored with the segmentation, never interpreted. |

## Retrieval

### `POST /v0/contributions/retrieval/search`

Since Plugin API 0.7. One round of one search: the query, the scope, the profile, the spaces available, and every candidate the core served in earlier rounds. The plugin answers candidate requests or the final ranking.

**Request body** (`retrieval-search-request.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `invocation_id` | string | yes | Unique per round; correlate logs with it. 1 to 128 characters. |
| `contribution` | `"retrieval"` | yes | — |
| `organization_id` | string | yes | — |
| `configuration` | object | yes | The validated plugin (installer) configuration. |
| `profile` | `ProfileName` (shared Manifest schema) | yes | One of the manifest's profiles. |
| `round` | integer | yes | 1 for the first call of a search; the last round the manifest allows must answer a ranking. 1 to 3. |
| `query` | object | yes | — |
| `query.text` | string | yes | 1 to 8192 characters. |
| `query.mode` | `lexical`, `semantic`, `hybrid` | yes | The mode the client asked for; a hint the plugin may follow. |
| `limit` | integer | yes | Most hits the ranking may hold. 1 to 50. |
| `scope` | object | yes | — |
| `scope.corpus_ids` | array of string | yes | 1 to 16 items. |
| `scope.source_namespaces` | array of string | no | When present, every candidate comes from one of these Source Namespaces. 1 to 50 items. |
| `spaces` | array of object | yes | The vector spaces near\_vector and hybrid may name, the served one first. At most 16 items. |
| `spaces[].id` | string | yes | The space id near\_vector and hybrid name (vector\_space\_id in the HTTP API). 1 to 200 characters. |
| `spaces[].owner` | object | yes | — |
| `spaces[].owner.kind` | `engine`, `plugin` | yes | — |
| `spaces[].owner.plugin_id` | `PluginId` (shared Manifest schema) | no | — |
| `spaces[].owner.plugin_version` | `Version` (shared Manifest schema) | no | — |
| `spaces[].model` | string | yes | — |
| `spaces[].dimensions` | integer | yes | — |
| `spaces[].metric` | `cosine`, `dot`, `l2` | yes | — |
| `spaces[].indexes` | array of string | yes | — |
| `spaces[].query_modalities` | array of string | yes | — |
| `spaces[].role` | `served`, `evaluation` | yes | served answers search by default; evaluation is indexed and compared. |
| `spaces[].coverage` | object | yes | — |
| `spaces[].coverage.segments` | integer | yes | Current segments holding a vector in this space, over every requested Corpus. |
| `spaces[].coverage.total` | integer | yes | Current segments of the requested Corpora. |
| `served` | array of object | yes | Every candidate request of earlier rounds with the candidates the core served for it, in order. Empty in round 1. At most 24 items. |
| `served[].round` | integer | yes | 1 to 3. |
| `served[].request_index` | integer | yes | 0 to 7. |
| `served[].request` | object | yes | One candidate request. bm25 ranks by keywords on a field; near\_vector ranks by one space's vectors (the core encodes query\_text with the space's owner, or takes vector as given); hybrid fuses both in one index query. Candidates are Record segments the caller may read, deduplicated by segment. |
| `served[].request.primitive` | `bm25`, `near_vector`, `hybrid` | yes | — |
| `served[].request.query_text` | string | no | The text to rank by: the search query or one the plugin rewrote. 1 to 8192 characters. |
| `served[].request.vector` | array of number | no | near\_vector only: a query vector with the space's dimensions, instead of query\_text. 1 to 4096 items. |
| `served[].request.space` | string | no | near\_vector and hybrid: one id of the request's spaces. 1 to 200 characters. |
| `served[].request.field` | `source`, `lexical` | no | bm25 and hybrid: source ranks the title and body as written; lexical ranks the lexical text an ingestion plugin produced. Default `"source"`. |
| `served[].request.alpha` | number | no | hybrid: weight of the vector side (1 is vector only, 0 is keywords only). 0 to 1. Default `0.5`. |
| `served[].request.fusion` | `relative_score`, `ranked` | no | hybrid: how the index fuses the two sides. Default `"relative_score"`. |
| `served[].request.k` | integer | yes | Most candidates to serve; at most limits.max\_candidates. 1 to 100. |
| `served[].request.filter` | object | no | Narrows the search's scope for this request; it never widens it. |
| `served[].request.group_by` | `record` | no | record keeps the best candidate of each Record. |
| `served[].candidates` | array of object | yes | At most 100 items. |
| `served[].candidates[].segment_id` | string | yes | 1 to 200 characters. |
| `served[].candidates[].record_id` | string | yes | 1 to 200 characters. |
| `served[].candidates[].version_id` | string | yes | 1 to 200 characters. |
| `served[].candidates[].part_key` | string | yes | 1 to 200 characters. |
| `served[].candidates[].text` | string | yes | The segment's text, exactly as the engine stores it. |
| `served[].candidates[].start` | integer | yes | Offsets of the text in its Part, in Unicode code points. |
| `served[].candidates[].end` | integer | yes | — |
| `served[].candidates[].score` | number | yes | The index score for this request; higher is better. bm25: BM25F; hybrid: the fused score; near\_vector: 1 minus the distance. |

**Response body (200)** (`retrieval-search-response.schema.json`)

| Field | Type | Required | Description |
| - | - | - | - |
| `requests` | array of `CandidateRequest` (shared Manifest schema) | no | At most limits.max\_requests; refused in the last round. 1 to 8 items. |
| `ranking` | object | no | — |
| `ranking.hits` | array of object | yes | Best first, at most the request's limit, each segment once. At most 50 items. |
| `ranking.hits[].segment_id` | string | yes | A candidate the core served in this search. |
| `ranking.hits[].score` | number | yes | The plugin's score; hits are in rank order. |
| `ranking.hits[].explanation` | string | no | Why this hit ranks here, returned to the client with it. 1 to 1024 characters. |
| `usage` | object | no | — |
| `usage.paid_calls` | integer | no | Paid calls made for this round (for example a re-ranking API). |
| `usage.cost_cents` | number | no | What those calls cost; a search's total stays within the profile's max\_cost\_cents. |

## Errors

Body of every non-2xx plugin response. retryable is authoritative: true asks the engine to retry within the declared retry intent; false is terminal. Connector errors (since Plugin API 0.3) also carry error\_class, and optionally retry\_after\_seconds.

| Field | Type | Required | Description |
| - | - | - | - |
| `code` | string | yes | At most 64 characters. |
| `message` | string | yes | 1 to 1024 characters. |
| `retryable` | boolean | yes | — |
| `error_class` | `access`, `transient`, `source` | no | Connector error class: access (the source refuses the credential or the access; retryable false), transient (an outage or a rate limit; retryable true) or source (the source returned unusable data; retryable false). Required on connector errors: the core maps it to Connector Health. |
| `retry_after_seconds` | integer | no | The source accepts requests again after this delay, such as a rate-limit reset. It defers the next run and never shortens the configured interval. Only with error\_class. 1 to 86400. |

## Fixtures

Local test inputs that `quivr plugin dev` and `quivr plugin test` turn into requests. A plugin keeps its own in `fixtures/*.json`.

### Connector fixture

A local test input that tools turn into connector requests: quivr plugin test fetches pages from checkpoint, feeding each returned checkpoint back, checks the credential and, for a push kind, relays each receive case. A file is a connector fixture when it has a top-level connector property. Fixture credentials are test values; the Contract Runner checks they never appear in responses or plugin logs.

Schema: `connector-fixture.schema.json`.

| Field | Type | Required | Description |
| - | - | - | - |
| `description` | string | no | — |
| `connector` | object | yes | — |
| `connector.kind` | string | yes | — |
| `connector.config` | object | yes | — |
| `credential` | object or null | no | Validated against the kind's credential\_schema. Default `null`. |
| `configuration` | object | no | Plugin configuration; defaults to \{}. |
| `checkpoint` | any | no | Starting checkpoint; defaults to null (a first run). |
| `now` | string | no | Default `"2026-01-01T00:00:00Z"`. |
| `max_pages` | integer | no | Most pages fetched in one run, like the core's page bound. 1 to 50. Default `10`. |
| `expect` | object | no | — |
| `expect.pages` | array of object | no | Expected pages of the run, in order: the Record Keys of each page and optionally its more flag. |
| `expect.error` | object | no | The first fetch is expected to fail with this error class. |
| `expect.check_credential` | object | no | The expected credential check: status ok, or an error\_class (and optionally its code). |
| `receive` | array of object | no | Since Plugin API 0.5, for a push kind: deliveries the Contract Runner relays to receive, in order, with the fixture's config, credential and checkpoint. Signatures in headers are computed by the fixture author over the exact body. At most 32 items. |
| `receive[].description` | string | no | — |
| `receive[].request` | object | yes | — |
| `receive[].expect` | object | no | — |

### Ingestion fixture

A local test input for an ingestion plugin (since Plugin API 0.6): the text Parts of one Record Version, the queries to encode and what to expect. quivr plugin test turns it into segment\_and\_embed and embed\_query requests.

Schema: `ingestion-fixture.schema.json`.

| Field | Type | Required | Description |
| - | - | - | - |
| `description` | string | no | — |
| `ingestion` | object | yes | — |
| `ingestion.parts` | any | yes | — |
| `ingestion.language` | any | no | — |
| `ingestion.configuration` | object | no | Plugin configuration; default \{}. |
| `ingestion.spaces` | array of `SpaceId` (shared Manifest schema) | no | Spaces to request; default every declared space. |
| `ingestion.queries` | array of string | no | Queries to encode in every requested space; default the first 200 code points of the first non-empty Part. 1 to 16 items. |
| `ingestion.expect` | object | no | — |

### Normalizer fixture

A language-neutral local test input for a normalizer. Tools (quivr plugin dev, the Contract Runner, SDK test helpers) turn it into a normalizer request: they read input.path relative to the fixture file, compute its size and SHA-256, reference it through an absolute file:// URL, and fill the identity fields with deterministic development values.

Schema: `plugin-fixture.schema.json`.

| Field | Type | Required | Description |
| - | - | - | - |
| `description` | string | no | 1 to 1024 characters. |
| `input` | object | yes | — |
| `input.path` | string | yes | Input file, relative to the directory of the fixture file (an absolute path is used as is). Tools resolve symbolic links before building the file:// URL. |
| `input.media_type` | string | yes | At most 255 characters. |
| `configuration` | object | no | Plugin configuration for this invocation; validated against the manifest configuration schema. Defaults to an empty object. |
| `source` | `SourceIdentity` (shared Manifest schema) | no | Source identity to send. Defaults to the development Corpus, namespace dev and the input path as Record Key. |
| `extensions` | `Extensions` (shared Manifest schema) | no | — |
| `provenance` | `Provenance` (shared Manifest schema) | no | — |

### Retrieval fixture

A local test input for a retrieval plugin (since Plugin API 0.7): a query, the spaces available and a catalogue of candidates. quivr plugin test drives the rounds and serves candidates from the catalogue, offline.

Schema: `retrieval-fixture.schema.json`.

| Field | Type | Required | Description |
| - | - | - | - |
| `description` | string | no | — |
| `retrieval` | object | yes | — |
| `retrieval.query` | string | yes | 1 to 8192 characters. |
| `retrieval.mode` | `lexical`, `semantic`, `hybrid` | no | Default `"hybrid"`. |
| `retrieval.limit` | integer | no | 1 to 50. Default `10`. |
| `retrieval.configuration` | object | no | Plugin configuration; default \{}. |
| `retrieval.profiles` | array of `ProfileName` (shared Manifest schema) | no | Profiles to certify; default every declared profile. |
| `retrieval.spaces` | array of `space` (shared Manifest schema) | no | Ids of the spaces offered (served first); default one served space, fixture.served\@1. At most 16 items. |
| `retrieval.candidates` | array of object | yes | The segments the runner may serve. Unless order says otherwise, every primitive serves them ranked by how many query words they contain, ties in catalogue order. 1 to 200 items. |
| `retrieval.order` | object | no | The exact segment order a primitive serves, whatever its query. |
| `retrieval.expect` | object | no | — |

### Subscription fixture

A language-neutral local test input for a subscription Contribution, since Plugin API 0.2. Tools (quivr plugin dev, the Contract Runner, SDK test helpers) turn it into a subscription request: they number the evaluations e1, e2, ... in order, give each one development Subscription ids, derive the Record Version ids and the idempotency key from the SHA-256 of the fixture bytes, default the record metadata (source dev-namespace/dev-record-\<digest>, origin client, accepted\_at 2026-01-01T00:00:00Z) when the fixture omits it, and validate every expression and configuration against the manifest schemas. A fixture file is told apart from an invocation fixture by its top-level evaluations property.

Schema: `subscription-fixture.schema.json`.

| Field | Type | Required | Description |
| - | - | - | - |
| `description` | string | no | 1 to 1024 characters. |
| `configuration` | object | no | Plugin (installer) configuration; validated against the manifest configuration schema. Defaults to an empty object. |
| `record` | object | yes | — |
| `record.enriched` | boolean | no | Default `false`. |
| `record.parts` | any | yes | — |
| `record.source` | any | no | — |
| `record.accepted_at` | any | no | — |
| `record.provenance` | any | no | — |
| `record.extensions` | any | no | — |
| `evaluations` | array of object | yes | 1 to 256 items. |
| `evaluations[].description` | string | no | 1 to 1024 characters. |
| `evaluations[].expression` | object | yes | — |
| `evaluations[].configuration` | object | no | Subscription evaluator configuration. Defaults to an empty object. |
| `evaluations[].expect` | `match`, `no_match`, `not_ready` | no | Expected decision. The Contract Runner and quivr plugin dev report a different answer. |
