// rag guide · 8 min

Ingesting & querying

By the end of this page you have a store holding your documents and an assistant answering questions from them with citations you can click. Five calls, every one of them shown in full, plus the four things that catch people the first time they do this.

Step 1. Create a vector store

The store is the container your files live in. You get back an id like vs_... that you will use everywhere after this. Pick the embedding model now; you can change it later without re-uploading.

curl https://api.fightclub.pro/v1/vector_stores \
  -H "Authorization: Bearer $FC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"acme-handbook","embedding_model":"text-embedding-3-small"}'
# => { "id": "vs_abc123", "status": "active", ... }

Step 2. Upload a file

Upload is separate from attaching. First you push the bytes to /v1/files with purpose=attachments, and you get back a file_... id. This is a multipart upload, not JSON.

curl https://api.fightclub.pro/v1/files \
  -H "Authorization: Bearer $FC_API_KEY" \
  -F purpose=attachments \
  -F file=@handbook.pdf
# => { "id": "file_xyz789", "mime_type": "application/pdf", ... }

Gotcha: purpose must be attachments. OpenAI muscle-memory says assistants and that returns a 400 here. The allowed values are attachments, batch and vision.

Step 3. Attach the file to the store

Attaching links an uploaded file to a store and kicks off ingest. The body is just the file id. This call returns immediately with status: "pending"; the actual work happens in the background.

curl https://api.fightclub.pro/v1/vector_stores/vs_abc123/files \
  -H "Authorization: Bearer $FC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"file_id":"file_xyz789"}'
# => { "id": "vsf_...", "status": "pending", ... }

Step 4. Wait for ingest (it is asynchronous)

This is the step people miss. Attaching does not parse and embed inline; it queues the work. Behind that pending status we parse the file to text, split it into chunks, embed each chunk and index them. For a small file that is seconds; for a big PDF it is longer. If you query before it finishes, the store has nothing yet and you get an empty result, not an error.

Poll the file status until it reads completed (or failed, with a reason in last_error). In production you can subscribe to the vector_store.file.completed webhook instead of polling.

# Poll until status is completed (or failed)
curl https://api.fightclub.pro/v1/vector_stores/vs_abc123/files/file_xyz789 \
  -H "Authorization: Bearer $FC_API_KEY"
# => { "status": "in_progress", "chunk_count": 0, ... }
# ... a few seconds later ...
# => { "status": "completed", "chunk_count": 42, ... }

The states are pending, then in_progress, then completed, failed or cancelled. Only failed and cancelled are retryable; a stuck ingest can be retried from the dashboard or the retry endpoint.

Step 5. Query with file_search

Retrieval runs as a tool called file_search, used inside an Assistants run. If you have not used Assistants before, the shape is: an assistant (the configured bot) runs over a thread (the conversation) and produces messages. You attach the store to the run, the model decides to search, we retrieve and feed the chunks back, and the model answers.

The minimal path is four calls: create an assistant once, create a thread, add the user question as a message, then create a run with the store attached and poll it to completion.

# (once) an assistant that is allowed to search files
ASST=$(curl -s https://api.fightclub.pro/v1/assistants \
  -H "Authorization: Bearer $FC_API_KEY" -H "Content-Type: application/json" \
  -d '{"model":"fc:openai/gpt-4o-mini","name":"handbook-bot",
       "instructions":"Answer only from retrieved files. Cite them.",
       "tools":[{"type":"file_search"}]}' | jq -r .id)

# a thread + the question
TH=$(curl -s https://api.fightclub.pro/v1/threads -H "Authorization: Bearer $FC_API_KEY" -d '{}' | jq -r .id)
curl -s https://api.fightclub.pro/v1/threads/$TH/messages \
  -H "Authorization: Bearer $FC_API_KEY" -H "Content-Type: application/json" \
  -d '{"role":"user","content":"What is the enterprise refund window?"}'

# the run, with the store attached to file_search
curl https://api.fightclub.pro/v1/threads/$TH/runs \
  -H "Authorization: Bearer $FC_API_KEY" -H "Content-Type: application/json" \
  -d '{"assistant_id":"'$ASST'",
       "tools":[{"type":"file_search","file_search":{"vector_store_ids":["vs_abc123"]}}]}'
# poll GET /threads/$TH/runs/$RUN until status is completed, then read the messages

Reading the answer and its citations

When the run completes, the newest assistant message holds the answer text. The tool step that ran underneath carries the retrieved passages: each citation has the chunk text, the file id, the chunk index and page, and a similarity score. Use the answer for display and the citations to show sources or to debug why the model said what it said.

If the answer is wrong or empty, read the citations before you touch the prompt. An empty result almost always means ingest had not finished, the store id was wrong, or nothing in the corpus is related to the question. A confident wrong answer splits on the similarity scores: low scores across the board mean the right chunk was never retrieved, which is a chunking or embedding-model problem, and high scores mean the right chunk was retrieved and the model talked over it, which is an instructions problem. Rewriting the system prompt to fix the first case wastes a day.

The query log on the dashboard keeps the question text, the top-K scores, the returned file ids and the latency for every search, so you can do this diagnosis on a complaint from last week rather than only on one you can reproduce.

Model references: fc: and match:

Models are named two ways. fc:<provider>/<model> pins an exact model, like fc:openai/gpt-4o-mini. match:<provider>/<family> resolves to the current best in a family, like match:anthropic/sonnet>=4, and tells you what it picked via response headers. Use fc: when you want a fixed, reproducible model; use match: when you want to ride upgrades automatically.

For the embedding model on a store, use the bare name (text-embedding-3-small). For chat/assistant models in a run, use the fc: or match: form.

The rest of the surface

Past these calls the API mirrors OpenAI: list stores and files, PATCH or DELETE a store, attach up to 500 files in one call with file_batches, cancel or retry a stuck ingest, read the query log and stats, and migrate the embedding model with a 7-day rollback window. The full list with request and response shapes is in the API reference.

Two flags layer on without changing any of the above: graphrag_enabled: true (see Graphs & GraphRAG) and encryption: "managed" or "byok" (see the next page).