Skip to content

Reference

API conventions

Every endpoint in the Exofind HTTP API follows a shared set of conventions for routing, media types, authentication, state handling, and error reporting.

The reference pages that follow describe the API by subject. For one page per endpoint, listing every field of its request and every response it returns, see the REST API pages. Those pages are generated from the OpenAPI document, which the engine build writes.

Every endpoint is served under the path prefix /v1alpha1. The API version is v1alpha1. For what a change to the API can do within that version, see Compatibility.

The API contains the following endpoint groups:

  • /v1alpha1/admin/indexes: define, read, and delete indexes, and trigger the actions promote, commit, pull, and reindex.
  • /v1alpha1/admin/indexes/{name}/settings: search settings.
  • /v1alpha1/admin/keys: API keys.
  • /v1alpha1/admin/indexers: indexer assignments across nodes.
  • /v1alpha1/admin/registry: audit and repair of the index registry.
  • /v1alpha1/admin/reindexes: reindex job records, one per index, and the action cancel on one of them.
  • /v1alpha1/indexes/{name}/documents: index, read, and remove documents.
  • /v1alpha1/indexes/{name}/search: search an index.

The endpoint GET /q/health/ready reports whether a node is ready. It is outside the versioned API and requires no credentials.

An index name in a path parameter represents either the index itself (such as books), which references its active generation, or a specific generation by name (such as books@2).

An API version whose name ends in alpha can include breaking changes. The changelog records each breaking change.

Apart from changes recorded in the changelog, the API follows the rules in this section.

The following changes can occur without a change to the version prefix:

  • A new endpoint or a new HTTP method on an existing path.
  • A new optional request field. Omitted fields retain the behavior of the previous release.
  • A new response field.
  • A new error code, or a new value for a field that carries a fixed set of values.
  • A more specific status code for a condition that previously returned 500 Internal Server Error.

To maintain compatibility across releases within an API version, a client must:

  • Ignore unknown response fields.
  • Match on the code field of an error body instead of message, and handle unknown error codes.
  • Accept unknown values in fields that carry a fixed set of values.
  • Send only explicitly set fields. Omitted fields take their documented defaults.

The following do not change while the version prefix remains the same:

  • The path, HTTP method, and meaning of an endpoint.
  • The name, type, and meaning of a request field or response field.
  • The meaning of an error code. Error codes are never renamed or reused.
  • The permission an endpoint requires.
  • The status class of a condition. A condition that returns 409 Conflict does not change to 400 Bad Request.

Compatibility guarantees do not cover search result behavior. The following can change within an API version:

  • The order of search hits and the scores that produce that order.
  • The text of a highlight fragment.
  • The precision of a facet count.
  • The documents a search query matches when the change results from analysis, typo tolerance, or query interpretation.

Compatibility rules cover index definition fields and usages. They do not cover ranking and matching behavior.

Requests and responses use application/json.

The documents endpoints also accept and return application/x-ndjson (one document per line) for datasets too large to hold in memory. The NDJSON representation for read requests is registered with quality value qs=0.9, so a client that accepts both receives application/json.

Authentication credentials are sent in the Authorization header:

Authorization: Bearer <key>

A 401 Unauthorized response includes the WWW-Authenticate: Bearer header.

Authentication and authorization responses hide the deployment contents from unprivileged callers:

  • Absent, malformed, unknown, and lapsed credentials all return 401 Unauthorized.
  • An index on which a key has no permissions returns 404 Not Found (code index:not_found) rather than 403 Forbidden.
  • Index listings omit indexes on which the key has no permissions rather than refusing the listing.

Every operation in the OpenAPI document names what a caller has to be granted, in four extension fields:

FieldValue
x-required-permissionThe permission name, as it is stored in a key.
x-permission-scopeindex when the permission is checked against the index the path names, any-index when the caller needs it on at least one index, and deployment when it is not about one index.
x-permission-rolesThe roles that include the permission.
x-permission-anonymousWhether a node that sets an anonymous key answers the endpoint to requests that carry no credential.

Every operation carries all four, so a client or a code generator can read the permission from the document. The description of an operation closes with the same fact as a sentence, which is what a generated client carries as a doc comment.

For what each permission covers and how to grant one, see Authentication.

Most write endpoints operate as assertions of desired state:

  • PUT /v1alpha1/admin/indexes/{name} sends an index definition in full, replacing any previous definition. Repeating the request produces the same outcome.
  • Indexing a document provides the complete document with its primary key. Repeating the request replaces any existing document under that key.
  • Removing a document is a statement of desired state. Requesting the deletion of an unindexed key produces a success response.
  • Requests that describe modifications rather than desired state require existing resources. For example, POST /v1alpha1/indexes/{name}/documents/actions/update describes changes to an existing document and is refused if the document does not exist.

Because desired-state writes are idempotent, a request that times out can be sent again without inspecting the target state first.

An operation that is not a create, read, update or delete on a resource is spelled with an actions path segment followed by an imperative verb:

  • On a single resource: /{id}/actions/{verb}
  • On a collection: /actions/{verb}

Every action endpoint uses the POST HTTP method.

The actions segment separates the names of operations from the names of resources so that the two never collide.

On document endpoints, the route /v1alpha1/indexes/{name}/documents/{key} matches any text primary key. Without the marker segment, the path /v1alpha1/indexes/{name}/documents/update is ambiguous: it could name the batch update operation or a document whose primary key is update.

Where no path parameter sits beside the action, the segment reserves the namespace so that a sub-resource added later cannot clash with the name of an existing action.

A segment under actions/ is an operation on the resource named by the preceding path. Every other segment names a resource.

To determine whether an endpoint is an action or a resource, apply this test:

  • If the response is a representation that a client can name and fetch again, the endpoint is a resource.
  • If the endpoint produces a one-shot effect or computes something about another request, the endpoint is an action.

Worked examples:

  • /v1alpha1/indexes/{name}/search is a resource. It answers with search results.
  • /v1alpha1/indexes/{name}/suggest is a resource.
  • /v1alpha1/indexes/{name}/facets/{field}/values is a resource. It is a sub-collection of an index.
  • /v1alpha1/admin/registry/audit is a resource. It reports the state of the registry.
  • /v1alpha1/indexes/{name}/search/actions/explain is an action. It computes an explanation of a search request rather than answering with a resource.
  • /v1alpha1/admin/indexes/{name}/actions/commit is an action. It produces an effect and answers with no resource of its own.

Every action endpoint follows these rules:

  • The method is always POST. An action is never served on GET, and a response to an action is never cached.
  • An action that acts on one resource sits on that resource (/{id}/actions/{verb}). An action that acts on multiple resources sits on the collection (/actions/{verb}).
  • The verb is one lowercase word, imperative, and singular, such as commit, promote, rotate, or cancel.
  • An action on a collection states in its own reference entry what the response contains when the action succeeds for some targets and fails for others. For example, the batch update endpoint uses the missing=skip query parameter and the missing array in its response.
  • An action states in its own reference entry whether a repeat is safe. This is an exception to the rule in Requests as desired state. A desired-state write can always be repeated, but an action cannot be assumed to be repeatable. For example, POST /v1alpha1/indexes/{name}/documents/actions/update describes a modification rather than desired state; a client that times out must read the document before sending the request again.
  • The name of an action does not change while the version prefix stays the same, following the rules in What holds for the life of a version.
  • No resource, sub-resource, or collection is named actions.
  • The path /v1alpha1/indexes/{name}/documents/actions is not answered as an action, because an action always carries a verb after the segment. A document whose primary key is actions remains reachable at that path.
  • Every action names the permission it requires in the four OpenAPI extension fields described in The permission an endpoint requires, and reports its own error codes. An action is an independent permission surface, not an extension of the permission on the resource it sits under.

A primary key field is either text or a whole number (int32 or int64).

The API writes every primary key as a JSON string in responses, regardless of the field type:

  • The next field in document listings (GET /v1alpha1/indexes/{name}/documents).
  • The missing array in batch update responses (POST /v1alpha1/indexes/{name}/documents/actions/update?missing=skip).
  • The key argument in error responses, such as document:not_found and document:key_conflicting.

For example, a whole-number key 42 or 42.0 is returned as "42".

Outside a request body, the API reads primary keys as text. This applies to the {key} path parameter (such as in DELETE or PATCH /v1alpha1/indexes/{name}/documents/{key}) and the after query parameter.

Inside a request body, the API requires the JSON type declared by the key field:

  • The primary key field of an indexed document.
  • The primary key field of a batch change.
  • The keys array of POST /v1alpha1/indexes/{name}/documents/actions/delete.

For a whole-number key field, request bodies accept 42 and reject "42".

For client applications, these rules mean:

  • You can pass a key returned in any response directly into a {key} path parameter or the after query parameter without conversion.
  • Two keys are identical when their string representations match.
  • When sending a key from a response (such as missing or next) back in a request body, you must convert the key to its declared JSON type. For a whole-number key field, convert the string to a number before passing it in the keys array or document body.

GET requests for an index definition or search settings return the current version in an ETag header.

Clients can supply this version in the If-Match header on subsequent PUT and PATCH requests. If the resource changes on the server before the request executes, the update is refused instead of overwriting the intermediate change.

Clients can also supply the version in the If-None-Match header on a later GET of the same resource. While the stored version is one the header names, the response is 304 Not Modified with the version in the ETag header and no body, so a client that polls for changes transfers the resource only when it has changed.

Conditional requests follow these rules:

  • The header holds one or more entity tags, separated by commas, such as If-Match: "9f2c1a0b3d4e5f60", "1a2b3c4d5e6f7089". The header is satisfied while the stored version equals one of them.
  • If-Match compares versions exactly. A weak tag, written W/"9f2c1a0b3d4e5f60", matches no version.
  • If-None-Match compares versions weakly, as RFC 9110 states for a read. A weak tag is read the same as a strong one.
  • If-Match: * asks only that the resource exists, whatever version it is at. If-None-Match: * names every version, so a GET that carries it returns 304 Not Modified whenever the resource exists.
  • An If-Match header that the stored version does not satisfy returns 412 Precondition Failed.
  • An If-None-Match header that names the stored version returns 304 Not Modified. One that does not returns the resource as an unconditional GET would.
  • An If-Match or If-None-Match header sent for a resource that does not exist returns 404 Not Found. This covers *, which asks for a resource that exists rather than for one to be created.
  • Search settings that have never been configured return 404 Not Found rather than an empty object, ensuring the ETag always represents an explicit stored version.

Clients can send any request to any node in a deployment without tracking index writer assignments.

Read requests are served locally by the node that receives them, using the data that the node has pulled. When a read request includes a freshness token, the receiving node answers from the state named in the token or a later state. The node waits to reach that state before answering and does not forward the request. See Freshness.

Write requests that must run on an index writer are forwarded automatically when received by another node:

  • The request is forwarded with its original HTTP method, headers, and body, including the caller credential. The destination node evaluates permissions directly; forwarding grants no additional privileges.
  • A write naming an index that currently has no assigned writer causes the receiving node to claim writer leadership if it participates in indexing.
  • Forwarded requests carry the header X-Exofind-Forwarded: true. A request that arrives with this header at a node that cannot serve it is rejected rather than forwarded a second time.
  • Connecting to the index writer has a timeout of 10 seconds. Streaming request bodies are not restricted by this connection timeout.

The API returns the following HTTP status codes:

StatusMeaning
200 OKThe request was served and the response carries a body.
201 CreatedA resource was created: an index, a generation, the first search settings of an index, or a key. A PUT that replaces a resource returns 200 OK instead.
202 AcceptedA job was started and runs asynchronously. The Location header names the status endpoint of the job.
204 No ContentA resource was removed: an index, its search settings, a key, or a document named by key in the path.
304 Not ModifiedA GET carried an If-None-Match header naming the stored version. The response carries the ETag header and no body.
400 Bad RequestThe request is invalid and must change before it can be served.
401 UnauthorizedThe request carries no credential accepted by this node.
403 ForbiddenThe credential is known, but the key lacks permission for the action.
404 Not FoundThe index, generation, key, reindex job, or search settings do not exist, the caller key lacks permissions on the index, or no endpoint answers the path.
405 Method Not AllowedThe path is not answered for the HTTP method of the request.
406 Not AcceptableThe Accept header names no media type the endpoint answers in.
409 ConflictThe request is well formed, but the current state of the deployment prevents execution.
412 Precondition FailedThe version specified in If-Match does not match the current stored version.
413 Content Too LargeThe request body is larger than the node accepts. The response closes the connection.
415 Unsupported Media TypeThe Content-Type header names a media type the endpoint does not read.
500 Internal Server ErrorAn unmapped engine failure occurred, or the node failed in a way no error code names (node:error). The node logs the error code and root cause.
502 Bad GatewayThe request was forwarded to the index writer and the writer did not respond.
503 Service UnavailableThe node cannot serve the request at this time; retrying the same request is expected to work. A response that includes a Retry-After header specifies the number of seconds to wait before retrying.

The status codes follow a consistent operational division:

  • 400 means the request itself must be modified. Changing deployment state will not make the request succeed.
  • 409 means the request is valid, but the deployment state must change or an active task must complete.
  • 412 means the client worked from an outdated version. The client must re-read the resource, rebuild the change against the new version, and resend.
  • 502 and 503 mean the client can send the exact same request again.

Status 400 Bad Request covers:

  • Request body schema and validation failures, including a property no endpoint has (request:property_unknown) and a value that does not fit the property it is written at (request:value_invalid). A property the endpoint does not have is refused; the server never drops one and serves the rest of the request.
  • Queries requesting missing or invalid index features, such as unknown fields, fields used in ways not configured in the definition, document lookups by key on indexes without a primary key, cursors used with a different sort order than the query that created them, or stored field requests on indexes that do not retain document source copies.
  • Unreadable or malformed request payloads.

Status 409 Conflict covers:

  • No node is available to write the index (indexer:unavailable).
  • The index cannot be modified on the target node because the node lost the writer role during execution (index:readonly) or is currently synchronizing (index:out_of_date).
  • An index definition conflicts with documents already stored in the generation (index:definition:incompatible).
  • An index or definition requires an engine version newer than the node (index:definition:unrepresentable, index:unsupported).
  • The index currently has no live generation (index:no_live_generation).
  • A conflicting reindex job is running or the target generation is locked by an existing reindex (reindex:in_progress, reindex:target_busy).
  • A valid change to the index registry, API keys, or search settings failed to persist. The stored state remains unchanged.
  • A registry endpoint was called on a node configured with local storage rather than shared storage (index:registry:audit_unavailable).

Status 503 Service Unavailable covers:

  • Indexer leadership assignments could not be read from shared storage (indexer:leadership_unreadable).
  • The request raced an index being closed to free local resources on the node (index:closed). Retrying the request reopens the index.
  • The node did not reach the state named in a freshness token within EXOFIND_SEARCH_FRESHNESS_WAIT (search:freshness:unavailable). The Retry-After header specifies the number of seconds to wait before retrying.

For per-endpoint status code tables, see Admin API, Documents API, and Search API. For error code prefixes and vocabulary, see Errors.

Every failed request returns a JSON response matching the following structure:

{
"code": "validation",
"message": "Request contains 2 errors",
"errors": [
{
"code": "index:field:primary_key:multiple_unsupported",
"message": "Field `id` is marked as a primary key and multiple, primary keys can not have multiple values",
"path": "id",
"arguments": { "name": "id" }
}
]
}

The error response fields are:

  • code: Machine-readable code describing the overall failure.
  • message: Human-readable summary for logging. Clients match on code, not message.
  • errors: List of specific issues encountered. For validation failures on multiple fields, all errors are included in this array.
  • path: Where in the request the issue was found, as a field path such as fields.title.sortable. Omitted when the error applies to the entire request.
  • arguments: Key-value map of string arguments used to render the message, allowing clients to format localized messages.

For validation failures, the top-level code is validation. When only one validation error occurs, its message is used as the top-level message; when multiple errors occur, the top-level message is Request contains N errors.

For errors other than validation failures, the errors array contains a single entry whose code matches the top-level code.

A request refused before it reaches an endpoint returns the same body. This covers a body that is not JSON, a path no endpoint answers, a method or media type an endpoint does not accept, and a body larger than the node accepts. Their codes are listed under the request:* prefix in Errors.

Every path uses the same form, wherever in the request the problem sits. A path is built from these parts:

Part of a pathFormExample
A nameThe name, joined to what holds it with .fields.title.sortable
One element of a list[n], counted from zerodocuments[1].name
A key of a free-form map, such as metadataThe key in brackets and double quotes, with \ and " escapedmetadata["build.sha"]

A path starts at the root of the request body. A newline-delimited body carries one object per line and no wrapper to name, so a path in it starts with the element, as [1].name.

A field inside an object field is named by its own dotted path, so a path reaches it the same way a query does. You can paste the path of an error into a request that takes a field.

Error codes use colon-separated namespaces (such as index:field:name_invalid). Error codes are stable across API versions and are never renamed or reused. For the complete error code list, see Errors.

Exofind is built by Level Four AB and is available under the Apache License 2.0.