Skip to content

index4.ai API Reference ​

Reference for the index4.ai REST API (v1): ingestion, retrieval and document endpoints.

  • Base URL: https://api.index4.ai/v1
  • Auth: Authorization: Bearer <token> for every endpoint. The token may be a login token, an account API key, or a project API key (prefix poma_proj_gr_). When you authenticate with account-level credentials, add the X-Project-ID header to select the target project (protected projects reject account credentials — use a project API key).
  • Interactive reference: api.index4.ai/docs

For higher-level walkthroughs, see Ingestion, Retrieval, RetrievalContext format, and Document management.

Endpoint summary ​

MethodPathPurpose
POST/ingestSubmit a file for ingestion into the project's namespace.
POST/ingestBucketBulk-ingest a whole S3/GCS bucket (or prefix) as one task.
POST/searchHybrid search across the namespace; returns prompt-ready context.
POST/searchInDocSame as /search but doc_filter is required.
GET/attributesList the project's typed attributes (name + type) and the name budget. Call before declaring a new attribute.
GET/docsList documents in the namespace (paged).
GET/docs/{docId}Get metadata for one document.
PATCH/docs/{docId}Set, update or remove a document's attributes.
DELETE/docs/{docId}Remove a document's vectors and storage.

Ingest jobs are tracked with GET /jobs/{job_id}/status (polling) or the streaming endpoint GET https://api.index4.ai/status/v1/jobs/{job_id}.


POST /ingest ​

Create an ingestion job. Either send the file as raw bytes (application/octet-stream, filename carried in Content-Disposition) or point at a public URL with X-Remote-URL and let the server fetch it. Multipart is not accepted.

Request-size limit: the HTTP body is capped at 50 MB — larger uploads are rejected with 413 before processing starts. For files above that, host them and pass X-Remote-URL so the server fetches the bytes instead of receiving them in the request body.

Request headers

HeaderRequiredNotes
AuthorizationyesBearer <token> — login token, account API key, or project API key.
X-Project-IDconditionalRequired only when authenticating with account-level credentials, to select the target project. Omit when using a project API key.
Content-Typeconditionalapplication/octet-stream for a raw-bytes upload. Not needed when using X-Remote-URL.
Content-Dispositionconditionalattachment; filename="<name>.<ext>". The extension drives parser selection. Not required when using X-Remote-URL.
X-Remote-URLnoA publicly accessible URL to fetch the file from instead of uploading bytes. When set, Content-Disposition and the request body are optional.
X-Unencrypted-StringsnoJSON object of plaintext key→value metadata for wildcard/glob filtering. Stored unencrypted (vendor-visible — do not put secrets/PII here; use an encrypted_text attribute in X-Attributes for sensitive values). Max 32 keys; keys [A-Za-z0-9_] (case-insensitive) ≤ 64 chars, values ≤ 1024 chars, ≤ 16384 chars total. e.g. {"project": "acme-merger", "dept": "legal"}. Filter at search time with unencrypted_strings_match.
X-Base-URLnoBase URL used to resolve relative image links in the input file.
X-CompletionnoURL (and optional headers) to send a completion webhook to when the job finishes.
X-AttributesnoTyped attributes — one JSON object holding any type. Before declaring a name, call GET /attributes and reuse an existing name and type where one fits. string/int/float/bool and their arrays are inferred; datetime and encrypted_text must be declared via X-Attribute-Schema (the reserved labels is typed []encrypted_text automatically). e.g. {"region":"emea","revision":12}. A name's type and the name itself are permanent — the first document that uses a name fixes both, and each project has a budget of 64 names. Names must match ^[a-z0-9_]{1,64}$. On ingest this header is the only carrier — ingest is octet-stream, with no multipart form path — so attributes are capped at 2048 chars per request. Declare datetime / encrypted_text with the companion X-Attribute-Schema header (same cap). Filter at search time with attribute_filters.
X-Attribute-SchemanoType declarations for entries in X-Attributes, shaped {"name": {"type": "…"}}. Required for datetime and encrypted_text, which cannot be inferred from a value. A declaration that conflicts with the type already in force for that name returns 400 attribute_type_conflict.

Opaque vs. plaintext metadata. An encrypted_text attribute in X-Attributes is HMAC'd — the vector DB never sees the values — so it is safe for sensitive values, at the cost of exact-token matching. X-Unencrypted-Strings is stored in plaintext (vendor-visible) precisely so its values can be wildcard-matched (filter via unencrypted_strings_match) — never put secrets or PII there.

Legacy fields. X-Labels, X-Meta-Int-1 and X-Meta-Int-2 on ingest, and meta_tags_any, meta_tags_all, meta_int_1_gte / _lte and meta_int_2_gte / _lte on search, are still accepted and mapped onto typed attributes. Their removal will be announced in advance; see Migrating from labels / meta_int for the replacements. PrimeCut is unaffected by this retirement and continues to accept X-Labels.

Response (201 Created) — PublicJob

json
{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "created_at": "2026-04-30T10:00:00Z",
  "properties": {
    "file": { "filename": "manual.pdf", "size": 1048576 }
  }
}

Status codes

StatusMeaning
201Job created. Poll status with GET /jobs/{job_id}/status or stream via GET /status/v1/jobs/{job_id}.
400Bad request (no X-Remote-URL and missing Content-Disposition, unsupported MIME, or empty body).
401Missing/invalid Bearer token.
403Multipart submitted.
500Server error.

See Ingestion for the full lifecycle.


POST /ingestBucket ​

Preview

Bucket ingest is served by a separately enabled service and is not yet part of the published index4.ai OpenAPI spec. Request shape and limits below may change; check availability for your project before relying on it.

Bulk-ingest an entire S3 or GCS bucket (or a prefix inside one) as a single task — the server lists the objects and streams each file through the normal ingestion pipeline, so nothing passes through your HTTP request body (the 50 MB request limit does not apply here).

Same auth as the rest of the API: project API key, or account credentials + X-Project-ID.

Request body

json
{
  "source": {
    "type": "s3",                  // "s3" or "gcs"
    "bucket": "my-corpus",
    "prefix": "contracts/",        // optional
    "region": "eu-central-1",      // optional
    "endpoint": "…",               // optional (S3-compatible stores)
    "credentials": { "access_key_id": "…", "secret_access_key": "…" }
  },
  "filter_glob": "**/*.pdf",       // optional
  "dry_run": false,                // true = validate + sample listing, no task created
  "acknowledge_egress_cost": true
}

Credentials per source type: s3 needs access_key_id + secret_access_key; gcs needs service_account_json. An optional X-Completion header (pipe-separated webhook/email targets, max 1024 bytes) fires once when the task reaches a terminal state.

Response — 202 Accepted:

json
{
  "bucket_ingest_task_id": "…",
  "state": "created",
  "state_description": "…",
  "poll_url": "/index4ai/v1/ingestBucket/{id}",
  "polling_guidance": { "min_interval_seconds": …, "max_interval_seconds": … }
}

With dry_run: true you instead get a validation report: a sample of discovered objects and, for cross-region setups, a rough egress-cost estimate.

Task lifecycle & sub-operations (all under /ingestBucket/{id}):

OperationEffect
GET /{id}Task status. States: created → queued_org → listing → materialising → done / done_with_errors / failed; may pause as paused_credit_exhausted, paused_credential_error, paused_destination_credential_error, paused_egress_quota_exhausted, paused_user.
POST /{id}/pause · /{id}/resumePause / resume the task.
DELETE /{id}Cancel the task.
GET /{id}/failuresList per-file failures.
POST /{id}/retryFailedRe-queue the failed files.
PATCH /{id}/credentialsRotate source credentials (e.g. after paused_credential_error).

Limits

LimitValue
Active tasks per account1 — a second submit returns 409 while one is running
Max size per file500 MB
Max total size per task500 GB
Files processed concurrently100 per account
Files per taskCapped during the phased rollout (target ceiling: 100,000). Listing beyond the cap ends the task as done_with_errors — everything discovered up to the cap is ingested.

Documents land in the project namespace and are searchable exactly like single-file ingests.


POST /search ​

Hybrid retrieval. Returns a prompt-ready RetrievalContext.

Request body — SearchRequest

FieldTypeRequiredDescription
querystringyesNatural-language query.
doc_filterstringnoOptional single-document id filter. Use /searchInDoc when this is required.
doc_idsarray of stringnoRestrict search to this set of document ids.
exclude_doc_idsarray of stringnoDoc ids to exclude from results (max 100). Useful in agent loops to avoid re-citing docs already shown.
attribute_filtersarray of {attr, op, value}noFilter on typed attributes. Operators depend on the attribute's type: eq ne in nin glob iglob (string); eq ne in nin lt lte gt gte (int); eq ne lt lte gt gte (float, datetime); eq ne (bool); contains contains_any eq in ([]string); contains contains_any any_lt any_gt ([]int, []float); contains any_lt any_gt ([]datetime); contains contains_any ([]bool); has_all_tokens has_any_tokens (encrypted_text, []encrypted_text). An operator the type does not offer returns 400 naming the ones it does. Clauses AND together. A filter naming an attribute the project has never used matches nothing — it is not ignored; list the names that exist with GET /attributes. encrypted_text matching is exact: same tokenization on both sides, no stemming, no partial words. Max 32 clauses.
unencrypted_strings_matchobject (string→string)noMatch on plaintext X-Unencrypted-Strings: a key→glob-pattern map. Case-insensitive Unix glob (* = any run, ? = one char; a pattern with no wildcard is an exact match). Multiple keys AND together.
return_assetsbooleannoReturn cited docs' figures (and tables, where available) in an assets field keyed by doc_id; images are base64 data URIs, referenced as [IMAGE: name] in the context.
formatstringnoprompt_ready (default) renders the XML+Markdown context block; json returns structured ranked hits instead.
min_relevancefloat 0..1noRelevance floor (default 0.3). Hits below it are dropped. Higher = stricter, lower = more permissive.
min_top_relevancefloat 0..1noFloor for the top hit. If the best hit scores below this, the entire result set comes back empty.
target_tokensinteger 100..500000noSoft token budget — the typical answer size (default 5000). Hits are admitted best-first up to this. The knob to size a response. Can never exceed max_tokens (clamped down if it would).
max_tokensinteger 100..500000noHard ceiling (default 15000). index4.ai expands past target_tokens toward it only for tight multi-document clusters. Set it below target_tokens and the soft target is clamped down to match — the hard ceiling always wins; it never inflates a response on its own.
expand_tightnessfloat 0..1noHow aggressively the engine expands context around each hit.
retrieval_tierstringnoPer-query retrieval tier — standard (fusion only) or advanced (adds a reranker). See Retrieval tiers.
premiumbooleannoLegacy flag — true is equivalent to retrieval_tier: "advanced". Prefer retrieval_tier.

Result count is bounded server-side by relevance and the token budget — there is no top_k parameter.

Response (200 OK) — RetrievalContext (default, format: "prompt_ready")

json
{
  "context": "<context><doc id=\"…\">…</doc></context>",
  "query": "How did operating margin change year over year?",
  "namespace": "project_docs_rag_4f2",
  "result_count": 4,
  "tokens_estimated": 5820,
  "results_dropped": 2,
  "detected_lang": "english",
  "mode": "advanced",
  "search_units": 1
}

context is the prompt-ready block (see RetrievalContext format for the grammar) and the field you'll use 99% of the time; the siblings are metadata — result_count / results_dropped (how many hits made it / were dropped for budget), tokens_estimated (rendered size), detected_lang, mode (retrieval tier used), and search_units (billing). When return_assets: true and a cited doc has figures/tables, an assets object (keyed by doc_id) is also present; images are base64 data URIs referenced as [IMAGE: name] inside context.

With format: "json" the response is a SearchResponse instead: a results array of per-doc hits (doc_id, content, title, canonical_url, pages, score, scores.{ann,bm25,rrf,reranker}, …) plus the same query / namespace / result_count / detected_lang / results_dropped metadata — no rendered context.

Status codes

StatusMeaning
200OK.
400Validation error or upstream 400.
401Missing/invalid token, or upstream 401.
403Upstream 403.
404doc_filter references an unknown doc.
502Other upstream / proxy error.
503Cannot reach the retrieval backend.

POST /searchInDoc ​

Same shape as /search, but doc_filter is required and must be non-empty. Use this when you want server-side enforcement that retrieval stays inside one document.

Request body — SearchInDocRequest

FieldTypeRequiredDescription
querystringyesNatural-language query.
doc_filterstringyesDocument id to restrict search to.
return_page_imagesbooleannoDeprecated / not available — no-op today (page screenshots will be served via a dedicated endpoint, not inline).

It also accepts every optional field from /search — doc_ids, exclude_doc_ids, attribute_filters, unencrypted_strings_match, return_assets, format, min_relevance, min_top_relevance, target_tokens, max_tokens, expand_tightness, retrieval_tier, and premium — with the same semantics.

Same as /search: result count is bounded by relevance and the token budget; there is no top_k.

Response (200 OK) — RetrievalContext (identical to /search).

Status codes

StatusMeaning
200OK.
400Missing query, missing/empty doc_filter, or upstream 400.
401Missing/invalid token.
403Upstream 403.
404Doc not found in this project's namespace.
502Upstream / proxy error.
503Retrieval backend unreachable.

GET /attributes ​

List the typed attributes already in use in the project: each name with its type, plus the project's name budget. No request body, no query parameters. Same auth as every other route (add X-Project-ID with account-level credentials).

Call this before declaring a new attribute. A name and its type are permanent once the first document uses them, and every name counts against max_names. Agents and integrations should read this list first, reuse an existing name and type where one fits (don't invent doc_year when year already exists), and only introduce a new name when nothing fits. It is also the way to learn which names — and so which attribute_filters operators — exist before searching.

bash
curl -sS "https://api.index4.ai/v1/attributes" -H "authorization: Bearer $INDEX4AI_API_KEY"

Response (200 OK)

json
{
  "attributes": [
    { "name": "region", "type": "string" },
    { "name": "revision", "type": "int" },
    { "name": "published_at", "type": "datetime" },
    { "name": "case_notes", "type": "encrypted_text" }
  ],
  "max_names": 64,
  "note": "…"
}
  • type is one of string, int, float, bool, datetime, encrypted_text, or an array form ([]string, []int, []float, []bool, []datetime, []encrypted_text).
  • max_names is the per-project cap on distinct names (64). Names already listed are spent; clearing a value does not free its name.
  • note is human-readable guidance; don't parse it.
  • An empty attributes array means the project has no typed attributes yet.

Status codes

StatusMeaning
200OK.
401Missing/invalid token.
403The key has no access to this project
410The project was purged (namespace_purged).
503The attribute schema is temporarily unreadable. Retry with backoff — never treat a 503 as "no attributes exist", or you will declare duplicates of names that are already there.

GET /docs ​

List documents in the project's namespace. Results are paged.

Query parameters

NameTypeDescription
limitintegerMax documents to return (default 100, max 500; larger values are clamped). Omit both limit and cursor to get all documents of a namespace with up to 100 documents; larger namespaces return the first 100 plus has_more / next_cursor.
cursorstringOpaque cursor from a previous response's next_cursor. Do not construct it manually; a malformed cursor returns 400.

Response (200 OK) — ListDocsResponse

json
{
  "namespace": "project_docs_rag_4f2",
  "total_documents": 3,
  "documents": [ DocInfo, … ],
  "has_more": false,
  "next_cursor": null,
  "degraded": false
}
FieldTypeDescription
namespacestringProject namespace identifier.
total_documentsintegerFull count of documents in the namespace (never the page size).
documentsarrayDocInfo entries (see below).
has_morebooleantrue when more documents exist beyond this page; pass next_cursor to continue.
next_cursorstring or nullOpaque cursor for the next page; absent or null when has_more is false.
degradedbooleantrue when at least one document in this page failed a transient fetch and was skipped.
noticestring or nullSet only on truncated responses where neither limit nor cursor was supplied; says how to page through the rest.

Status codes

StatusMeaning
200OK.
401Missing/invalid token.
403Upstream 403.
404Upstream 404.
502 / 503Upstream issues.

GET /docs/{docId} ​

Fetch metadata for one document.

Path parameters

NameTypeDescription
docIdstringThe exact doc_id returned by /docs.

Response (200 OK) — DocInfo

FieldTypeDescription
doc_idstringStable identifier within the namespace.
titlestringDetected title (filename fallback).
languagestringBCP-47 language code.
filenamestringOriginal ingest filename.
canonical_urlstringCitation URL stamped at ingest by Core (e.g. the resolved target of a short link / redirect when ingesting from a URL). Empty when not applicable.
pagesintegerSource page count.
chunkset_countintegerChunksets produced.
chunk_countintegerChunks produced.
image_countintegerDetected figures/images.
table_countintegerDetected tables.
ingested_atstring (RFC 3339)Job completion time.
source_job_idstringjob_id of the originating /ingest.
bm25_statestringState of the document's BM25 (lexical) index.
attributesobjectTyped attributes exactly as supplied at ingest. Present only when the document has any.
attribute_schemaobjectType of each attribute of this document as {name: {"type": t}}. Empty means the types are unknown, not that nothing is encrypted.

Status codes

StatusMeaning
200OK.
400Empty docId.
401Missing/invalid token.
403Upstream 403.
404Doc not found in this namespace.
502 / 503Upstream issues.

PATCH /docs/{docId} ​

Set, update or remove typed attributes on an existing document without re-ingesting it.

Request body

FieldTypeRequiredDescription
attributesobjectyesNames to set or update. A name with a value is written; a name whose value is null is removed; names not present are untouched. Removing a value does not free the name — names stay permanent for the project and keep counting against its budget.
attribute_schemaobjectnoTypes, as {name: {"type": t}}, for names this document is the first in the project to use. Required for datetime and encrypted_text. Every name declared here must also appear in attributes. Declaring a name that already has a type returns 400 attribute_type_conflict.
json
{
  "attributes": { "region": "apac", "reviewed_at": "2026-09-21T07:30:00Z", "obsolete_key": null },
  "attribute_schema": { "reviewed_at": { "type": "datetime" } }
}

Response (200 OK) — the document id with its resulting attributes and attribute_schema.


DELETE /docs/{docId} ​

Remove a document's vectors and storage from the namespace. Project, key, and other documents are unaffected.

Path parameters

NameTypeDescription
docIdstringDocument to delete.

Response (200 OK) — DeleteDocResponse

FieldTypeDescription
doc_idstringEchoes the deleted docId.
vectors_deletedintegerVector count removed from the namespace.
storage_deletedbooleanWhether the document's stored content and assets were removed. (Page images are never stored regardless of storage_deleted; nothing there was ever written.)

Status codes

StatusMeaning
200OK.
400Empty docId.
401Missing/invalid token.
403Upstream 403.
404Doc not found in this namespace.
502 / 503Upstream issues.

Schemas (canonical shapes) ​

These are the same definitions used by the OpenAPI v3 spec. All are JSON.

SearchRequest ​

jsonc
{
  "query": "string (required)",
  "doc_filter": "string",         // restrict to one doc
  "doc_ids": ["string"],          // restrict to a set of docs
  "exclude_doc_ids": ["string"],  // max 100
  "attribute_filters": [{ "attr": "region", "op": "eq", "value": "emea" }], // typed attributes
  "unencrypted_strings_match": { "path": "legal/contracts/*" }, // glob on X-Unencrypted-Strings
  "return_assets": false,
  "return_page_images": false,    // deprecated, no-op
  "format": "prompt_ready",       // or "json" for structured hits
  "min_relevance": 0.3,           // relevance floor, 0..1 (default 0.3)
  "min_top_relevance": 0.0,       // floor for the top hit; below → empty set
  "target_tokens": 5000,          // soft budget / answer size (default 5000, 100..500000; ≤ max_tokens)
  "max_tokens": 15000,            // hard ceiling (default 15000, 100..500000)
  "expand_tightness": 0.0,        // context-expansion aggressiveness, 0..1
  "retrieval_tier": "standard",   // or "advanced" (reranker)
  "premium": false                // legacy: true == retrieval_tier "advanced"
}

Every field except query is optional. min_relevance tunes precision (higher = stricter); target_tokens sizes the answer; max_tokens is a ceiling that only matters for tight multi-document clusters (set it below target_tokens and the soft target is clamped down to match — the hard ceiling always wins). attribute_filters and unencrypted_strings_match filter against metadata set at ingest (all combined via AND). retrieval_tier picks the reranking tier — see Retrieval tiers.

SearchInDocRequest ​

Same fields as SearchRequest, except both query and doc_filter are required and doc_filter must be non-empty.

RetrievalContext ​

jsonc
{
  "context": "string (XML+Markdown — see RetrievalContext format page)",
  "query": "string",
  "namespace": "string",
  "result_count": 0,        // hits included in context
  "tokens_estimated": 0,    // rendered size of context
  "results_dropped": 0,     // hits dropped for the token budget
  "detected_lang": "string",// BM25 query language; null on low confidence
  "mode": "string",         // retrieval tier actually used
  "search_units": 0,        // billing units
  "assets": {               // present only when return_assets=true and assets exist
    "<doc_id>": { "images": [], "tables": [] }
  }
}

format: "json" instead returns a SearchResponse: { results: [{ doc_id, content, title, canonical_url, pages, chunk_indices, score, scores: { ann, bm25, rrf, reranker } }], query, namespace, result_count, detected_lang, results_dropped }.

DocInfo ​

jsonc
{
  "doc_id": "string (required)",
  "title": "string",
  "language": "string",
  "filename": "string",
  "canonical_url": "string",   // citation URL, set by Core at ingest
  "pages": 0,
  "chunkset_count": 0,
  "chunk_count": 0,
  "image_count": 0,
  "table_count": 0,
  "ingested_at": "RFC 3339 timestamp",
  "source_job_id": "uuid",
  "bm25_state": "string",
  "attributes": { "region": "emea" },                 // present only when set
  "attribute_schema": { "region": { "type": "string" } }
}

ListDocsResponse ​

jsonc
{
  "namespace": "string",
  "total_documents": 0,
  "documents": [ /* DocInfo */ ],
  "has_more": false,
  "next_cursor": "string | null",
  "degraded": false
}

DeleteDocResponse ​

jsonc
{
  "doc_id": "string",
  "vectors_deleted": 0,
  "storage_deleted": true
}

PublicJob (for /ingest) ​

jsonc
{
  "job_id": "uuid",
  "created_at": "RFC 3339 timestamp",
  "status": { "job_id": "…", "status": "pending|processing|indexing|done|failed|blocked", "code": 200 },
  "properties": { "file": { "filename": "string", "size": 0 }, "base_url": "string" }
}

See also ​