Upload multiple documents at once via API

TL;DR

  • Messages API accept only one file per chat message.** The POST /api/v1/projects/{projectId}/conversations/{sessionId}/messages endpoint takes a single file field (not an array). Max size 100MB (Max batch size is 1GB); allowed types: pdf, docx, doc, odt, txt, jpg, jpeg, png, webp.
  • The OpenAI‑format Chat Completions API (/chat/completions) is JSON‑only—no file uploads there.
  • If you need to analyze many docs “at once,” the supported pattern is to pre‑load sources into the agent (multiple calls to /sources or a sitemap of many URLs), then chat normally.

Below are four practical approaches (with code) and when to use each.


What’s supported today (and why)

1) Single file per chat message

Agent Chat’s Messages endpoint accepts multipart/form‑data with one file field (not files[]). That’s why the UI only takes one file at a time. The contract explicitly documents a single file with 100MB limit and the allowed content types.

2) JSON‑only “OpenAI format” endpoint

POST /api/v1/projects/{projectId}/chat/completions uses a JSON body (messages, model, etc.) and does not take files. Use this when you don’t need to upload attachments.


Recommended workarounds (choose what fits your workflow)

A) Pre‑load documents into the Agent’s knowledge (best for reusable corpora)

Upload each document as a Source or point the agent to a sitemap that lists many doc URLs. Then chat without re‑attaching files.

  • Create source by file (repeat per doc): POST /api/v1/projects/{projectId}/sources with file in form‑data.
  • Create source by sitemap (ingests many URLs at once): POST /api/v1/projects/{projectId}/sources with sitemap_path. Optional: Trigger a refresh via PUT /api/v1/projects/{projectId}/sources/{sourceId}/instant-sync.

Tip (scanned PDFs/images): When pre‑loading via Sources, you can enable OCR using is_ocr_enabled=true. That toggle isn’t available on per‑message chat uploads.

You can verify ingestion with List Pages: GET /api/v1/projects/{projectId}/pages.


B) Attach files across multiple chat turns, then ask your question

If you must stay within Agent Chat, send a sequence of messages—each with one file—then ask a summarizing question (e.g., “Compare all documents uploaded above…”). Each upload call uses the same sessionId so the assistant can consider the whole set in that conversation. (See code below.)

Note: Per the API schema, each call still accepts one file. There is no files[] array today.


C) Combine documents into one binder (quick & simple)

Merge multiple PDFs into a single PDF under 100MB and upload that one file in a single message. Works well for small bundles. (For large corpora or frequent updates, prefer Approach A.)


D) Host content and use a sitemap (hands‑off batch ingest)

If your docs can be hosted (e.g., on a site/CDN), generate a sitemap.xml that lists each file URL and add it as a Source. This batches ingestion with one API call and lets you instant‑sync later.


Step‑by‑step guides & code

1) Pre‑load many docs as Sources (file upload)

cURL (repeat per file):

curl -X POST "https://app.customgpt.ai/api/v1/projects/123/sources" \
  -H "Authorization: Bearer $CUSTOMGPT_API_KEY" \
  -F "file=@/path/to/DocumentA.pdf" \
  -F "is_ocr_enabled=true" \
  -F "is_anonymized=false" \
  -F "file_data_retension=true"

Python (batch):

import requests, glob

API = "https://app.customgpt.ai/api/v1"
PROJECT_ID = 123
HEADERS = {"Authorization": f"Bearer {YOUR_API_KEY}"}

for path in glob.glob("/docs/batch/*.pdf"):
    files = {"file": open(path, "rb")}
    data = {"is_ocr_enabled": "true", "is_anonymized": "false", "file_data_retension": "true"}
    r = requests.post(f"{API}/projects/{PROJECT_ID}/sources", headers=HEADERS, files=files, data=data)
    r.raise_for_status()

(Optional) Sync a sitemap source right now:

# After creating a sitemap-based source, sync it:
curl -X PUT "https://app.customgpt.ai/api/v1/projects/123/sources/456/instant-sync" \
  -H "Authorization: Bearer $CUSTOMGPT_API_KEY"

Check pages indexed:

curl -H "Authorization: Bearer $CUSTOMGPT_API_KEY" \
  "https://app.customgpt.ai/api/v1/projects/123/pages?limit=50"

2) Pre‑load many docs via Sitemap (one call for many URLs)

Example sitemap.xml:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://cdn.example.com/policy/HR-Policy.pdf</loc></url>
  <url><loc>https://cdn.example.com/manuals/Onboarding-Guide.pdf</loc></url>
  <url><loc>https://docs.example.com/faq.html</loc></url>
</urlset>

Add sitemap as a Source:

curl -X POST "https://app.customgpt.ai/api/v1/projects/123/sources" \
  -H "Authorization: Bearer $CUSTOMGPT_API_KEY" \
  -F "sitemap_path=https://yourdomain.com/sitemap.xml"

3) Multiple files within a single conversation (separate messages)

Flow:

  1. Create a conversation (POST /conversations).
  2. For each file: POST /conversations/{sessionId}/messages with one file and a small “doc X of N” note.
  3. After all uploads, send a final message without a file that asks your actual question.

cURL:

# 1) Create conversation
curl -X POST "https://app.customgpt.ai/api/v1/projects/123/conversations" \
  -H "Authorization: Bearer $CUSTOMGPT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Multi-doc analysis"}'

# Suppose the response contains "session_id": "...UUID..."

# 2) Message with the first file (100MB max, one at a time)
curl -X POST "https://app.customgpt.ai/api/v1/projects/123/conversations/SESSION_UUID/messages?stream=false" \
  -H "Authorization: Bearer $CUSTOMGPT_API_KEY" \
  -F "prompt=Attaching Document A (1/3). Please ingest." \
  -F "file=@/docs/DocA.pdf"

# 3) Repeat for DocB, DocC, then ask:
curl -X POST "https://app.customgpt.ai/api/v1/projects/123/conversations/SESSION_UUID/messages?stream=false" \
  -H "Authorization: Bearer $CUSTOMGPT_API_KEY" \
  -F "prompt=Now compare all uploaded docs and summarize key differences."

(One file field per message, 100MB each; allowed types shown in the spec.)

Note: If you prefer the OpenAI‑style endpoint (/chat/completions), that API is JSON‑only and won’t accept attachments.


4) Combine into a single “binder” PDF (fastest quick fix)

If your documents are small, merge them locally into one PDF (under 100MB) and upload that single file in your message. This avoids multiple calls—but you lose per‑document citations.


Citations & confirming which docs were used

When a response includes citations, you can retrieve metadata for a specific citation via: GET /api/v1/projects/{projectId}/citations/{citationId}. This returns Open Graph style details about the cited source.


Limits & usage

To check your account’s limits (projects, storage credits, queries), call: GET /api/v1/limits/usage.


FAQs

Q: Can I pass files[] or multiple file fields in one message? A: Not today—the schema defines a single file in the multipart request. Use multiple messages (Approach B) or pre‑load Sources (Approach A).

Q: Can I upload files with the OpenAI‑format endpoint? A: No. That endpoint accepts JSON only (messages, model, etc.).

Q: Best practice for large or frequently updated corpora? A: Host your docs and add a sitemap as a Source, then use instant‑sync on a schedule (Approach D).

Q: What about scanned PDFs or images? A: Prefer Sources with is_ocr_enabled=true to improve text extraction. Chat attachments don’t expose that toggle.


Complete examples

Python — batch upload as Sources, then chat

import requests

API = "https://app.customgpt.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {YOUR_API_KEY}"}
PROJECT_ID = 123

# 1) Upload multiple files as Sources
for path in ["/docs/A.pdf", "/docs/B.pdf", "/docs/C.pdf"]:
    with open(path, "rb") as f:
        r = requests.post(
            f"{API}/projects/{PROJECT_ID}/sources",
            headers=HEADERS,
            files={"file": f},
            data={"is_ocr_enabled": "true"}
        )
        r.raise_for_status()

# 2) Start a conversation
conv = requests.post(
    f"{API}/projects/{PROJECT_ID}/conversations",
    headers=HEADERS,
    json={"name": "Summarize A+B+C"}
).json()
session_id = conv["data"]["session_id"]

# 3) Ask your question (no files needed—they're pre‑loaded)
msg = requests.post(
    f"{API}/projects/{PROJECT_ID}/conversations/{session_id}/messages",
    headers=HEADERS,
    data={"prompt": "Summarize A, B, and C and highlight conflicts."}
).json()

print(msg["data"]["assistant_response"])

Node.js — multi‑turn “one attachment per message”

import axios from "axios";
import FormData from "form-data";
import fs from "fs";

const API = "https://app.customgpt.ai/api/v1";
const TOKEN = process.env.CUSTOMGPT_API_KEY;
const PROJECT_ID = 123;

const auth = { headers: { Authorization: `Bearer ${TOKEN}` } };

async function createConversation(name) {
  const { data } = await axios.post(
    `${API}/projects/${PROJECT_ID}/conversations`,
    { name },
    auth
  );
  return data.data.session_id;
}

async function sendMessage(sessionId, prompt, filePath) {
  const form = new FormData();
  form.append("prompt", prompt);
  if (filePath) form.append("file", fs.createReadStream(filePath));
  const { data } = await axios.post(
    `${API}/projects/${PROJECT_ID}/conversations/${sessionId}/messages?stream=false`,
    form,
    { headers: { ...form.getHeaders(), ...auth.headers } }
  );
  return data;
}

(async () => {
  const sessionId = await createConversation("Compare docs A/B/C");
  await sendMessage(sessionId, "Attaching A (1/3).", "./A.pdf");
  await sendMessage(sessionId, "Attaching B (2/3).", "./B.pdf");
  await sendMessage(sessionId, "Attaching C (3/3).", "./C.pdf");
  const final = await sendMessage(
    sessionId,
    "Now compare A, B, and C. Summarize key differences and cite sources."
  );
  console.log(final.data.assistant_response);
})();

(One file per call; 100MB; allowed types as documented.)


Summary

  • No: multiple attachments in a single Agent Chat message.
  • Yes: multiple documents per conversation (send several messages), or pre‑load them as Sources (file or sitemap) for the best UX and reusability.

If you run into ingestion or limits issues, use /limits/usage to check your quota and /pages to confirm what’s indexed.

Loop and upload files

Python — loop upload as Sources (recommended for “analyze these files anytime”)

import requests, glob

API = "https://app.customgpt.ai/api/v1"
PROJECT_ID = 123
HEADERS = {"Authorization": f"Bearer {YOUR_API_KEY}"}

for path in glob.glob("/docs/batch/*.*"):
    with open(path, "rb") as f:
        r = requests.post(
            f"{API}/projects/{PROJECT_ID}/sources",
            headers=HEADERS,
            files={"file": f},
            data={
                "is_ocr_enabled": "true",      # optional (helpful for scans)
                "is_anonymized": "false",
                "file_data_retension": "true"
            },
            timeout=60
        )
        r.raise_for_status()
print("Uploaded all sources.")

(Endpoint and fields per spec.)

Verify ingestion:

curl -H "Authorization: Bearer $CUSTOMGPT_API_KEY" \
  "https://app.customgpt.ai/api/v1/projects/123/pages?limit=50"

This lists the pages (documents) indexed for the agent.


Python — loop upload one file per message in a conversation

import requests, os

API = "https://app.customgpt.ai/api/v1"
PROJECT_ID = 123
HEADERS = {"Authorization": f"Bearer {YOUR_API_KEY}"}

# 1) Create a conversation
conv = requests.post(
    f"{API}/projects/{PROJECT_ID}/conversations",
    headers=HEADERS,
    json={"name": "Multi-file comparison"}
).json()
session_id = conv["data"]["session_id"]

# 2) Attach files across separate messages
files_to_send = ["/docs/A.pdf", "/docs/B.pdf", "/docs/C.pdf"]
for i, path in enumerate(files_to_send, start=1):
    with open(path, "rb") as f:
        form = {
            "prompt": f"Attaching {os.path.basename(path)} ({i}/{len(files_to_send)}). Please ingest."
        }
        r = requests.post(
            f"{API}/projects/{PROJECT_ID}/conversations/{session_id}/messages?stream=false",
            headers=HEADERS,
            files={"file": f},
            data=form,
            timeout=120
        )
        r.raise_for_status()

# 3) Ask a final question (no file)
final = requests.post(
    f"{API}/projects/{PROJECT_ID}/conversations/{session_id}/messages?stream=false",
    headers=HEADERS,
    data={"prompt": "Now compare all uploaded docs and summarize key differences with citations."},
    timeout=120
).json()

print(final["data"]["assistant_response"])
import axios from "axios";
import FormData from "form-data";
import fs from "fs";

const API = "https://app.customgpt.ai/api/v1";
const TOKEN = process.env.CUSTOMGPT_API_KEY;
const PROJECT_ID = 123;
const AUTH = { headers: { Authorization: `Bearer ${TOKEN}` } };

async function createConversation(name) {
  const { data } = await axios.post(
    `${API}/projects/${PROJECT_ID}/conversations`,
    { name },
    AUTH
  );
  return data.data.session_id;
}

async function sendMessage(sessionId, prompt, filePath) {
  const form = new FormData();
  form.append("prompt", prompt);
  if (filePath) form.append("file", fs.createReadStream(filePath));

  const { data } = await axios.post(
    `${API}/projects/${PROJECT_ID}/conversations/${sessionId}/messages?stream=false`,
    form,
    { headers: { ...form.getHeaders(), ...AUTH.headers } }
  );
  return data;
}

(async () => {
  const s = await createConversation("Compare docs A/B/C");
  await sendMessage(s, "Attaching A (1/3).", "./A.pdf");
  await sendMessage(s, "Attaching B (2/3).", "./B.pdf");
  await sendMessage(s, "Attaching C (3/3).", "./C.pdf");
  const final = await sendMessage(s, "Now compare A, B, C and cite sources.");
  console.log(final.data.assistant_response);
})();

(Per-message file upload; JSON‑only endpoint /chat/completions is not used here because it does not accept files.)


Practical tips & limits

  • Error 429 handling: if you push too fast, you may receive 429 (resource exhausted). Add simple backoff/retry in your loop. (429 is documented on conversation endpoints.)

  • When to use which pattern:

    • Prefer Sources for corpora you’ll reuse; easier to manage, can OCR scans, and you can verify via Pages later.
    • Prefer conversation attachments when you only need the files for a one‑off chat session.
  • Not for files: /chat/completions (OpenAI‑style) is JSON body only.

  • Reference Postman collection: Endpoints above are also present in the official Postman collection if you want to test by hand.

If you want, I can wrap this into a small CLI script that takes a folder path and does the looped uploads for either Sources or Conversation messages.