> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.eyelevel.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.eyelevel.ai/_mcp/server.

# Direct SDK/API Quickstart

The primary function of GroundX is to make integrating data from complex human documents into LLM powered applications simple and performant. This is the direct SDK/API path: you call the GroundX SDKs or REST API from your own application code. By following this quick-start guide you will understand the core workflow of GroundX, and how you can use it to upload complex documents to be parsed, how those documents are stored, and how they can be queried for use in LLM powered applications.

If you'd rather have an agent do this for you — ingest, search, and debug GroundX from Claude, Codex, or another agent runtime instead of writing this code yourself — see [Install GroundX Agent Harness](/documentation/agent-harness/install-ground-x-agent-harness) instead. The underlying GroundX operations are the same either way.

This Guide Covers the following:

1. [How to get your GroundX API Key](#step-1-getting-your-api-key)
2. [Setting up GroundX](#step-2-download-groundx-sdk-optional)
3. [How to create a new bucket](#step-3-creating-a-new-bucket)
4. [How to use an existing bucket](#step-4-using-an-existing-bucket)
5. [How to upload content to a bucket](#step-5-upload-content)
6. [How to check the status of your upload](#step-6-check-the-status-of-your-upload)
7. [How to search for content](#step-7-search-your-content)
8. [How to use your search results to augment an LLM (i.e. RAG)](#step-8-using-search-results-to-augment-an-llm)

## Step 1: Getting Your API Key

Before you can use our APIs, you will need to [create an account](https://dashboard.groundx.ai/auth/register).

Log into the [GroundX Dashboard](https://dashboard.groundx.ai) and navigate to API Keys.

![Navigate to API Keys](file:assets/gs1.webp "Navigate to API Keys")

Copy your API Key and save it somewhere for use later in this tutorial.

![Copy Your API Key](file:assets/gs2.webp "Copy Your API Key")

## Step 2: Setting Up GroundX

Users that wish to use the Python or TypeScript SDKs can get started by downloading the relevant package via the following shell commands:

**`Python`**

```python title="Python"
pip install groundx
```

**`TypeScript`**

```typescript title="TypeScript"
npm i -s groundx
```

The GroundX client can be set up with the following code in either language. We recommend storing your API key in an environment variable (`GROUNDX_API_KEY`) rather than hardcoding it in source:

**`Python`**

```python title="Python"
import os
from groundx import GroundX

client = GroundX(
  api_key=os.environ["GROUNDX_API_KEY"],
)
```

**`TypeScript`**

```typescript title="TypeScript"
import { GroundXClient } from "groundx";

const client = new GroundXClient({
  apiKey: process.env.GROUNDX_API_KEY,
});
```

If you're using a language other than Python or TypeScript, or if you don't wish to install the Python or TypeScript SDKs, you can use cURL to communicate with the API via HTTP directly.

## Step 3: Creating a New Bucket

When you upload a document to GroundX:

* The document is passed through a vision model to identify key components
* Each of those components are passed through different pipelines, depending on if the component is textual, graphical, or tabular, in order to re-represent that data into an LLM friendly textual representation
* contextual information from other parts of the document are baked into that textual representation, creating a context rich and LLM friendly textual representation of each section of the document.

These re-represented components of the document are called "Semantic Objects", and can be thought of as objects which contain fully contextualized ideas within the document.

After these semantic objects are created, they're stored within a bucket.

Buckets are queryable containers which store semantic objects. Essentially, you upload a bunch of files to a bucket, the semantic objects derived from those documents are stored in said bucket, then you can search the bucket for semantic objects that are relevant to a natural language query.

Thus, to use GroundX, it's useful to first create a bucket. You can give a bucket a name, which does not necessarily have to be unique, and you'll get back a unique `bucket_id` which can be used to upload documents to and search from that bucket.

The following code creates a new bucket and gets back the bucket\_id which we'll use in future steps:

**`Python`**

```python title="Python"
# Upload documents to GroundX

response = client.buckets.create(
    name="your_bucket_name",
)

bucket_id = response.bucket.bucket_id
```

**`TypeScript`**

```typescript title="TypeScript"
const response = await client.buckets.create({
    name: "your_bucket_name"
});

const bucketId = response.bucket.bucketId;
```

**`cURL`**

```bash title="cURL"
# Upload hosted documents to GroundX

curl https://api.groundx.ai/api/v1/bucket \
     -X POST \
     -H "X-API-Key: GROUNDX_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
           "name": "your_bucket_name"
         }'
```

## Step 4: Using an Existing Bucket

You can list all existing buckets in your GroundX account via the following command:

**`Python`**

```python title="Python"
# printing a list of existing buckets

#here, the `list` function is not casting, but rather
#is calling the API which lists all buckets.
buckets = client.buckets.list()

#the response object can be cast to a dictionary for
#legibility
print(buckets.dict())
# The value of `buckets.dict()` will resemble the following:
# {
#   "buckets": [
#     {
#       "bucketId": 1,
#       "created": "2023-10-03T08:59:39Z",
#       "fileCount": 1,
#       "fileSize": "3.1GB",
#       "name": "name",
#       "updated": "2023-10-03T08:59:39Z"
#     }
#     ...
#   ]
# }
```

**`TypeScript`**

```typescript title="TypeScript"
// printing a list of existing buckets
const buckets = await client.buckets.list();

console.log(buckets);
// The value of `buckets` will resemble the following:
// {
//   "buckets": [
//     {
//       "bucketId": 1,
//       "created": "2023-10-03T08:59:39Z",
//       "fileCount": 1,
//       "fileSize": "3.1GB",
//       "name": "name",
//       "updated": "2023-10-03T08:59:39Z"
//     }
//     ...
//   ]
// }
```

**`cURL`**

```bash title="cURL"
# getting a list of all buckets
curl https://api.groundx.ai/api/v1/bucket \
     -H "X-API-Key: <apiKey>"

# The response will resemble the following:
# {
#   "buckets": [
#     {
#       "bucketId": 1,
#       "created": "2023-10-03T08:59:39Z",
#       "fileCount": 1,
#       "fileSize": "3.1GB",
#       "name": "name",
#       "updated": "2023-10-03T08:59:39Z"
#     }
#   ]
# }
```

This code can be used to retrieve the `bucketId` from an existing bucket.

## Step 5: Upload Content

To upload content to a bucket, the [Document Upload API](/reference/api-reference/documents/ingest-documents) can be used. This will allow you to upload complex documents in a variety of formats to a particular bucket. In the upload process, the documents will automatically be parsed and the final representation stored in the bucket will be a set of semantic objects.

There are two key ways a document can be uploaded; either by uploading a locally hosted document, or one which is publicly hosted behind some endpoint.

Uploading a locally hosted document can be done with the following code:

**`Python`**

```python title="Python"
# Upload documents to GroundX
from groundx import GroundX, Document

ingest = client.ingest(
  documents=[
    Document(
      bucket_id=bucket_id,
      file_name="my_file1.txt",
      file_path="/local/path/file1.txt",
      file_type="txt",
      search_data=dict(
        key = "value",
      ),
    )
  ]
)
```

**`TypeScript`**

```typescript title="TypeScript"
// Upload documents to GroundX

let ingest = await client.ingest(
  [
    {
      bucketId: bucketId,
      fileName: "my_file1.txt",
      filePath: "/local/path/file1.txt",
      fileType: "txt",
      searchData: {
        "key": "val",
      },
    }
  ]
);
```

**`cURL`**

```bash title="cURL"
# Upload a local document to GroundX

curl -X POST https://api.groundx.ai/api/v1/ingest/documents/local \
  -H "X-API-Key: <apiKey>" \
  -H "Content-Type: multipart/form-data" \
  -F blob=@my_file1.txt \
  -F 'metadata={"bucketId":123,"fileName":"my_file1.txt","fileType":"txt"}'
```

`/v1/ingest/documents/local` accepts one file per call, up to 8MB, and does not support
`callbackUrl`/`callbackData`. For larger files, multiple files in one batch, or callback
support, use the `client.ingest()` SDK method shown above (it uploads via a pre-signed
URL behind the scenes) rather than calling this endpoint directly.

To upload a second file, send another request the same way:

**`cURL`**

```bash title="cURL"
curl -X POST https://api.groundx.ai/api/v1/ingest/documents/local \
  -H "X-API-Key: <apiKey>" \
  -H "Content-Type: multipart/form-data" \
  -F blob=@my_file2.pdf \
  -F 'metadata={"bucketId":123,"fileName":"my_file2.pdf","fileType":"pdf"}'
```

Uploading remotely hosted documents can be done with the following code:

**`Python`**

```python title="Python"
# Upload documents to GroundX
from groundx import GroundX, Document

ingest = client.ingest(
  documents=[
    Document(
      bucket_id=bucket_id,
      file_name="my_file1.txt",
      file_path="https://my.source.url.com/file1.txt",
      file_type="txt",
      search_data=dict(
        key = "value",
      ),
    )
  ]
)
```

**`TypeScript`**

```typescript title="TypeScript"
// Upload documents to GroundX

ingest = await client.ingest(
  [
    {
      bucketId: bucketId,
      fileName: "my_file1.txt",
      filePath: "https://my.source.url.com/file1.txt",
      fileType: "txt",
      searchData: {
        "key": "val",
      },
    }
  ]
);
```

**`cURL`**

```bash title="cURL"
# Upload hosted documents to GroundX

curl -X POST https://api.groundx.ai/api/v1/ingest/documents/remote \
     -H "X-API-Key: <apiKey>" \
     -H "Content-Type: application/json" \
     -d '{
  "documents": [
    {
      "bucketId": 1234,
      "sourceUrl": "https://my.source.url.com/file1.txt",
      "fileName": "my_file1.txt",
      "fileType": "txt"
    }
  ]
}'

```

see the [Ingest API](/reference/api-reference/documents/ingest-documents) for more information on arguments.

If your request is successful, regardless of if the upload is from remote or local documents, you will receive a response that looks something like this:

```json
{
  "ingest": {
    "processId": "<unique_system_generated_id>",
    "status": "<enumerated_status>"
  }
}
```

The `processId` can be used to check the status of the upload.

## Step 6: Check the Status of Your Upload

The following request can be used to query the status of your upload:

**`Python`**

```python title="Python"
ingest = client.documents.get_processing_status_by_id(
  process_id=ingest.ingest.process_id
)
```

**`TypeScript`**

```typescript title="TypeScript"
ingest = await client.documents.getProcessingStatusById(
  ingest.ingest.processId,
);
```

**`cURL`**

```bash title="cURL"
curl https://api.groundx.ai/api/v1/ingest/:processId \
     -H "X-API-Key: GROUNDX_API_KEY"
```

Be sure to use `processId` from the previous step.

If your request is successful, you will receive a response that looks something like this:

```json
{
  "ingest": {
    "processId": "<unique_system_generated_id>",
    "progress": {
      "complete": {
        "documents": [
          {
            "documentId": "<unique_system_generated_id>",
            "fileName": "<given_file_name>",
            "fileSize": "<files_size_total>",
            "fileType": "<file_type>",
            "bucketId": <your_bucket_id>,
            "processId": "<unique_system_generated_id>",
            "sourceUrl": "<document_url>",
            "status": "<enumerated_status>"
          }
        ],
        "total": 1
      }
    },
    "status": "<enumerated_status>"
  }
}
```

The value of `status` will be one of the in-flight values `queued`, `training`, or `processing`, or one of the terminal values `complete`, `error`, or `cancelled`. This can be used to, for instance, wait for a document to be uploaded via incrementally polling.

## Step 7: Search Your Content

Make the following request to search your ingested content:

**`Python`**

```python title="Python"
search_response = client.search.content(
  id=bucket_id,
  query=query,
)
```

**`TypeScript`**

```typescript title="TypeScript"
const searchResponse = await client.search.content(
  bucketId,
  {
    query: query,
  }
);
```

**`cURL`**

```bash title="cURL"
curl https://api.groundx.ai/api/v1/search/:id \
     -X POST \
     -H "X-API-Key: GROUNDX_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{"query":"your query"}'
```

You can also use a `groupId` or a `documentId` in place of `bucketId` in your search query. These will allow you to search a group of buckets or a single document, respectively. Replace `query` with the query you want to use to search your content.

If your request is successful, will receive a response that looks something like this:

```json
{
  "search": {
    "count": <int_number_of_results>,
    "query": "<your_query>",
    "score": <float_highest_relevance_score_in_results>,
    "text": "<combined_text_of_search_results>",
    "nextToken": "<token_for_next_set_of_results>",
    "results":[
      {
        "documentId": "<unique_system_generated_id>",
        "score": <float_relevance_score_of_result>,
        "searchData": {
            <document_metadata>
        },
        "sourceUrl": "<source_document_url>",
        "suggestedText": "<rewritten_text_for_LLM_completions>",
        "text":  "<original_text_of_result>"
      }
    ]
  }
}
```

If you need to look up a `groupId` or `bucketId`, you can find them in the [GroundX Dashboard](https://dashboard.groundx.ai) or by querying for them using the APIs.

## Step 8: Using Search Results to Augment an LLM

After search has been completed, the `search.text` can be used to provide context to a language model. We strongly recommend you use `search.text` for your LLM completions. We provide `search.results` in case you want to create your own context from the search results. If you choose to do this, rather than use `search.text`, we strongly recommend you use `search.results[n].suggestedText` for your context.

First, the `search.text` can be unpacked from the response

**`Python`**

```python title="Python"
llm_text = search_response.search.text
```

**`TypeScript`**

```typescript title="TypeScript"
let llmText = searchResponse.search.text;
```

Once you have relevant text for your request, you will need to combine the text with instructions and submit them to OpenAI.

Here's an example of what a completion instruction could look like. For a full example system prompt and prompt-writing guidance, see [Prompting and Integration](/documentation/guides/prompting-and-integration).

Combine your completion instructions with your curated GroundX search results:

**`Python`**

```python title="Python"
import os
from openai import OpenAI
openai_client = OpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
)

completion = openai_client.chat.completions.create(
    model=openaiModel,
    messages=[
        {
            "role": "system",
            "content": f"""{instruction}
===
{llm_text}
==="""
        },
        {"role": "user", "content": query},
    ],
)

completion = completion.choices[0].message.content
```

**`TypeScript`**

```typescript title="TypeScript"
import OpenAI from "openai";

const openaiClient = new OpenAI({
    apiKey: process.env.OPENAI_API_KEY,
});

const completion = (await openaiClient.chat.completions.create({
    model: openaiModel,
    messages: [
        {
            role: 'system',
            content: `${instruction}
===
${llmText}
===`,
        },
        { role: 'user', content: query },
    ],
})).choices[0].message?.content;
```

Replace `openaiModel` with your preferred model, `instruction` with your completion instructions, `llmText` with your curated GroundX search results, and `query` with your query. This, effectively is an implementation of "Retrieval Augmented Generation" (RAG) where an augmented prompt consisting of a query and retrieved context about that query are passed to a language model for generation.

## Errors and Rate Limits

### HTTP status codes

All GroundX API endpoints use standard HTTP status codes. The two you're most likely to
encounter while integrating are:

| Code  | Meaning      | Common causes                                                                                         |
| ----- | ------------ | ----------------------------------------------------------------------------------------------------- |
| `400` | Bad Request  | Missing required field, invalid parameter type, invalid ID format, unsupported file type, invalid URL |
| `401` | Unauthorized | API key is missing, malformed, expired, or lacks permission to access the resource                    |

A `401` always means the `X-API-Key` header is wrong or the key doesn't have access to
the requested resource. A `400` means the request body or path/query parameters failed
server-side validation — the response body typically includes a description of the
problem.

### Ingest limits

| Constraint          | Recommended | Hard limit                           |
| ------------------- | ----------- | ------------------------------------ |
| File size           | 25 MB       | 25 MB (trial) / 50 MB (subscription) |
| Pages per document  | 200 pages   | 750 pages (PDF/PPTX/DOCX/HWP)        |
| Documents per batch | 20          | 50                                   |

Check file size and page count before uploading, and split documents that exceed the
recommended limits. Very long documents produce large numbers of chunks that dilute
search relevance, hence the 200-page recommendation. A batch size of 20 (rather than the
50 hard limit) leaves headroom for retries and avoids client-side or network timeouts on
slower connections.

### Search limits

| Constraint          | Recommended |
| ------------------- | ----------- |
| Concurrent searches | 3           |
| Searches per minute | 20          |

Queue or debounce search requests in your application to stay within these targets. If
you fan out a single user query to multiple buckets or groups, count each search call
separately toward the concurrency limit.

### Handling HTTP 429

If you receive a `429 Too Many Requests` response, back off and retry with exponential
delay: start at 1 second, double on each retry, cap at 60 seconds. Don't retry
immediately.

**`Python`**

```python title="Python"
import time

def search_with_backoff(client, bucket_id, query, max_retries=5):
    delay = 1
    for attempt in range(max_retries):
        try:
            return client.search.content(id=bucket_id, query=query)
        except Exception as e:
            if getattr(e, "status_code", None) == 429 and attempt < max_retries - 1:
                time.sleep(delay)
                delay = min(delay * 2, 60)
            else:
                raise
```

**`TypeScript`**

```typescript title="TypeScript"
async function searchWithBackoff(client, bucketId, query, maxRetries = 5) {
  let delay = 1000;
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await client.search.content(bucketId, { query });
    } catch (e) {
      if (e?.statusCode === 429 && attempt < maxRetries - 1) {
        await new Promise((resolve) => setTimeout(resolve, delay));
        delay = Math.min(delay * 2, 60000);
      } else {
        throw e;
      }
    }
  }
}
```

## Next Steps

This guide covered the most fundamental ideas in employing GroundX. We recommend checking out the [prompting and integration guide](/documentation/guides/prompting-and-integration) for tips on how you can achieve consistent and high quality responses in your use case.