> 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

Upload documents, wait for processing, then search them from your application
using the GroundX SDKs or REST API. You can pass the search results to a language
model as context for its answers.

To have Claude, Codex, or another agent run these operations, see [Install GroundX Agent Harness](/documentation/agent-harness/install-ground-x-agent-harness).

This guide covers:

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

First, [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")

Store your API key in the `GROUNDX_API_KEY` environment variable. Keep it out of source code and chat.

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

## Step 2: Setting Up GroundX

Install the Python or TypeScript SDK:

**`Python`**

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

**`TypeScript`**

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

Initialize the client using your `GROUNDX_API_KEY` environment variable:

**`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,
});
```

For other languages, or without an SDK, use cURL or any HTTP client.

## Step 3: Creating a New Bucket

GroundX identifies text, graphics, and tables, then converts them into text a
language model can use. It adds context from elsewhere in the document to each
section. These contextualized sections are called semantic objects.

A bucket stores the semantic objects from your documents and lets you search
them with natural language queries. Create a bucket and save its unique ID for
uploads and searches. Bucket names do not have to be unique:

**`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

List the buckets in your GroundX account:

**`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"
#     }
#   ]
# }
```

Use the `bucketId` of the bucket you want to upload to or search.

## Step 5: Upload Content

The [Document Upload API](/reference/api-reference/documents/ingest-documents)
processes local files or documents available at a URL and stores their semantic
objects in a bucket.

To upload a local file:

**`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"}'
```

To ingest a document from a URL:

**`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 all arguments.

Both local and remote ingest return a response with this shape:

```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

Check processing status:

**`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"
```

Use the `processId` returned by ingest.

The response has this shape:

```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>"
  }
}
```

Poll while `status` is `queued`, `training`, or `processing`. Search after
`complete`; investigate `error` or `cancelled` before continuing.

## Step 7: Search Your Content

Search the processed documents:

**`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"}'
```

Use a `bucketId` to search one bucket, a `groupId` for a group of buckets, or a
`documentId` for one document. Set `query` to your search question.

The response has this shape:

```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>"
      }
    ]
  }
}
```

Find `groupId` and `bucketId` values in the [GroundX Dashboard](https://dashboard.groundx.ai) or through the list APIs.

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

Use `search.text` as context for your language model. If you build your own
context from individual results, use `search.results[n].suggestedText`.

Read `search.text` from the response:

**`Python`**

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

**`TypeScript`**

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

Combine your instructions and GroundX context, then submit them to OpenAI.
For a system prompt example and guidance, see [Prompting and Integration](/documentation/guides/prompting-and-integration).

**`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;
```

Set `openaiModel` to your model, `instruction` to your instructions, `llmText`
to the GroundX context, and `query` to your question. This is retrieval augmented
generation (RAG): the model answers using retrieved document content.

## 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                    |

For `401`, check the `X-API-Key` header and the key's resource access. For `400`,
check the request body and path/query parameters. The response usually describes
the validation 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. Split documents above the
recommended limits to keep searches focused. Batches of 20 leave room for
retries and reduce timeout risk 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

See [Prompting and Integration](/documentation/guides/prompting-and-integration) for guidance on producing consistent answers.