Document Management Workflow
List, filter, download and upload documents for an organization or a single entity using the SingleFile API.
How documents are exposed
| Endpoint | Returns |
|---|---|
GET /organizations/{organization_id}/documents | Paginated list of documents across every entity in the organization |
GET /entities/{entity_id}/documents | Paginated list for one entity |
GET /documents/{document_id} | One document's details, including document_url for download |
POST /entities/{entity_id}/documents/upload and POST /organizations/{organization_id}/documents/upload | Multipart upload of a PDF or DOCX |
List responses contain id, title, entity_name, entity_id, organization, jurisdiction, document_type, filing_type, certified, created_at and read_at. They do not contain a download URL; fetch the detail endpoint for that. document_type and filing_type are display labels such as Filing Document, Filing Evidence, Service Of Process or Formation, and filing_type can be empty.
All examples use the sandbox host, https://api.demo.singlefile.ai. Production is https://api.app.singlefile.io.
Search Documents by Criteria
The list endpoints accept these query parameters. Any other parameter is silently ignored, so a filter that "works" but returns the whole organization's documents was probably misspelled.
| Parameter | Behavior |
|---|---|
entity_id | Entity UUID |
jurisdiction | Exact, case-sensitive match on the jurisdiction name, for example Delaware (not delaware) |
created_after, created_before | ISO 8601 date or datetime |
ordering | Field name, prefixed with - for descending, for example -created_at |
page_number, page_size | Pagination; page_size defaults to 10 and is capped at 1000 |
There is no filter on filing_type or document_type; filter those client-side from the list results.
BASE_URL="https://api.demo.singlefile.ai/external-api/v1"
ACCESS_TOKEN="your_bearer_token_here"
ORG_ID="your_organization_uuid"
# Delaware documents created since 2024-01-01, newest first
curl -G "$BASE_URL/organizations/$ORG_ID/documents" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d "page_size=100" \
-d "ordering=-created_at" \
-d "jurisdiction=Delaware" \
-d "created_after=2024-01-01"import requests
BASE_URL = "https://api.demo.singlefile.ai/external-api/v1"
ACCESS_TOKEN = "your_bearer_token_here"
HEADERS = {"Authorization": f"Bearer {ACCESS_TOKEN}"}
def search_documents(organization_id, **filters):
"""Return every matching document, walking all pages."""
documents, page_number = [], 1
while True:
params = {"page_size": 100, "ordering": "-created_at", "page_number": page_number, **filters}
response = requests.get(f"{BASE_URL}/organizations/{organization_id}/documents", params=params, headers=HEADERS)
response.raise_for_status()
body = response.json()
documents.extend(body["data"])
if page_number >= body["total_pages"]:
return documents
page_number += 1
delaware_documents = search_documents(organization_id, jurisdiction="Delaware", created_after="2024-01-01")
formation_filings = [d for d in delaware_documents if d["filing_type"] == "Formation"]
print(f"Found {len(delaware_documents)} Delaware documents, {len(formation_filings)} from formations")const BASE_URL = "https://api.demo.singlefile.ai/external-api/v1";
const ACCESS_TOKEN = "your_bearer_token_here";
const HEADERS = { Authorization: `Bearer ${ACCESS_TOKEN}` };
async function searchDocuments(organizationId, filters = {}) {
const documents = [];
for (let pageNumber = 1; ; pageNumber++) {
const params = new URLSearchParams({ page_size: "100", ordering: "-created_at", page_number: String(pageNumber), ...filters });
const res = await fetch(`${BASE_URL}/organizations/${organizationId}/documents?${params}`, { headers: HEADERS });
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
const body = await res.json();
documents.push(...body.data);
if (pageNumber >= body.total_pages) return documents;
}
}
const delawareDocuments = await searchDocuments(organizationId, { jurisdiction: "Delaware", created_after: "2024-01-01" });
const formationFilings = delawareDocuments.filter((d) => d.filing_type === "Formation");
console.log(`Found ${delawareDocuments.length} Delaware documents, ${formationFilings.length} from formations`);package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
)
const (
baseURL = "https://api.demo.singlefile.ai/external-api/v1"
accessToken = "your_bearer_token_here"
)
type documentsPage struct {
Data []map[string]interface{} `json:"data"`
TotalPages int `json:"total_pages"`
}
func searchDocuments(orgID string, filters map[string]string) ([]map[string]interface{}, error) {
var all []map[string]interface{}
for page := 1; ; page++ {
params := url.Values{"page_size": {"100"}, "ordering": {"-created_at"}, "page_number": {fmt.Sprint(page)}}
for k, v := range filters {
params.Set(k, v)
}
req, _ := http.NewRequest("GET", fmt.Sprintf("%s/organizations/%s/documents?%s", baseURL, orgID, params.Encode()), nil)
req.Header.Set("Authorization", "Bearer "+accessToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
var body documentsPage
json.NewDecoder(resp.Body).Decode(&body)
resp.Body.Close()
all = append(all, body.Data...)
if page >= body.TotalPages {
return all, nil
}
}
}using System.Net.Http.Json;
using System.Text.Json;
using System.Web;
const string BaseUrl = "https://api.demo.singlefile.ai/external-api/v1";
const string AccessToken = "your_bearer_token_here";
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {AccessToken}");
async Task<List<JsonElement>> SearchDocuments(string organizationId, Dictionary<string, string>? filters = null)
{
var all = new List<JsonElement>();
for (var page = 1; ; page++)
{
var query = HttpUtility.ParseQueryString(string.Empty);
query["page_size"] = "100";
query["ordering"] = "-created_at";
query["page_number"] = page.ToString();
foreach (var (key, value) in filters ?? new()) query[key] = value;
var doc = await httpClient.GetFromJsonAsync<JsonDocument>($"{BaseUrl}/organizations/{organizationId}/documents?{query}");
all.AddRange(doc!.RootElement.GetProperty("data").EnumerateArray());
if (page >= doc.RootElement.GetProperty("total_pages").GetInt32()) return all;
}
}Download a Document
Downloading is two steps: fetch the document's details to get document_url, then request that URL with the same bearer token. The URL points at the SingleFile application host, not the API host, and an unauthenticated request is redirected to the sign-in page (an HTML response, not the file).
DOCUMENT_ID="document_uuid_from_a_list_response"
DOCUMENT_URL=$(curl -s "$BASE_URL/documents/$DOCUMENT_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.data.document_url')
curl -o "document.pdf" -H "Authorization: Bearer $ACCESS_TOKEN" "$DOCUMENT_URL"import os
import re
def download_document(document_id, download_dir="downloads"):
"""Fetch document details, then download the file with the bearer token."""
detail = requests.get(f"{BASE_URL}/documents/{document_id}", headers=HEADERS)
detail.raise_for_status()
doc = detail.json()["data"]
safe = lambda s: re.sub(r"[^A-Za-z0-9 _-]", "", s or "")
filename = f"{safe(doc['entity_name'])}_{safe(doc['document_type'])}_{doc['created_at'][:10]}.pdf"
os.makedirs(download_dir, exist_ok=True)
path = os.path.join(download_dir, filename)
with requests.get(doc["document_url"], headers=HEADERS, stream=True) as response:
response.raise_for_status()
with open(path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
return path
def bulk_download_documents(documents, download_dir="downloads"):
paths = [download_document(doc["id"], download_dir) for doc in documents]
print(f"Downloaded {len(paths)} documents to {download_dir}")
return paths
bulk_download_documents(formation_filings)import { writeFile, mkdir } from "fs/promises";
import { join } from "path";
async function downloadDocument(documentId, downloadDir = "downloads") {
const detail = await fetch(`${BASE_URL}/documents/${documentId}`, { headers: HEADERS });
if (!detail.ok) throw new Error(`Detail failed: ${detail.status}`);
const doc = (await detail.json()).data;
const safe = (s) => (s ?? "").replace(/[^A-Za-z0-9 _-]/g, "");
const filename = `${safe(doc.entity_name)}_${safe(doc.document_type)}_${doc.created_at.slice(0, 10)}.pdf`;
await mkdir(downloadDir, { recursive: true });
const file = await fetch(doc.document_url, { headers: HEADERS });
if (!file.ok) throw new Error(`Download failed: ${file.status}`);
const path = join(downloadDir, filename);
await writeFile(path, Buffer.from(await file.arrayBuffer()));
return path;
}
async function bulkDownloadDocuments(documents, downloadDir = "downloads") {
const paths = [];
for (const doc of documents) paths.push(await downloadDocument(doc.id, downloadDir));
console.log(`Downloaded ${paths.length} documents to ${downloadDir}`);
return paths;
}
await bulkDownloadDocuments(formationFilings);import (
"io"
"os"
"path/filepath"
)
func downloadDocument(documentID, downloadDir string) (string, error) {
req, _ := http.NewRequest("GET", fmt.Sprintf("%s/documents/%s", baseURL, documentID), nil)
req.Header.Set("Authorization", "Bearer "+accessToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var detail struct {
Data struct {
DocumentURL string `json:"document_url"`
EntityName string `json:"entity_name"`
CreatedAt string `json:"created_at"`
} `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&detail)
fileReq, _ := http.NewRequest("GET", detail.Data.DocumentURL, nil)
fileReq.Header.Set("Authorization", "Bearer "+accessToken) // required: the URL is behind the app login
fileResp, err := http.DefaultClient.Do(fileReq)
if err != nil {
return "", err
}
defer fileResp.Body.Close()
if fileResp.StatusCode != 200 {
return "", fmt.Errorf("download failed: %d", fileResp.StatusCode)
}
os.MkdirAll(downloadDir, 0755)
path := filepath.Join(downloadDir, documentID+".pdf")
out, _ := os.Create(path)
defer out.Close()
io.Copy(out, fileResp.Body)
return path, nil
}async Task<string> DownloadDocument(string documentId, string downloadDir = "downloads")
{
var detail = await httpClient.GetFromJsonAsync<JsonDocument>($"{BaseUrl}/documents/{documentId}");
var data = detail!.RootElement.GetProperty("data");
var documentUrl = data.GetProperty("document_url").GetString()!;
// httpClient already carries the Authorization header; the document URL requires it.
var bytes = await httpClient.GetByteArrayAsync(documentUrl);
Directory.CreateDirectory(downloadDir);
var path = Path.Combine(downloadDir, $"{documentId}.pdf");
await File.WriteAllBytesAsync(path, bytes);
return path;
}Tip: For large document sets, download in parallel, but stay under the API rate limit of 120 requests per minute. Each document costs two requests (detail plus file).
Upload a Document
Uploads are multipart/form-data with a file part and a required filing_type form field. Only PDF and DOCX files are accepted, up to 50 MB. Optional fields: title, description, document_type and jurisdiction. The response carries the new document's id.
ENTITY_ID="entity_uuid"
curl -X POST "$BASE_URL/entities/$ENTITY_ID/documents/upload" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-F "[email protected]" \
-F "filing_type=Formation" \
-F "title=Operating Agreement" \
-F "document_type=OPERATING_AGREEMENT"def upload_document(entity_id, path, filing_type, title=None, document_type=None):
with open(path, "rb") as f:
data = {"filing_type": filing_type}
if title:
data["title"] = title
if document_type:
data["document_type"] = document_type
response = requests.post(
f"{BASE_URL}/entities/{entity_id}/documents/upload",
headers=HEADERS,
files={"file": (os.path.basename(path), f, "application/pdf")},
data=data,
)
response.raise_for_status()
return response.json()["data"]["document_id"]
document_id = upload_document(entity_id, "operating-agreement.pdf", "Formation", title="Operating Agreement")import { readFile } from "fs/promises";
async function uploadDocument(entityId, path, filingType, title) {
const form = new FormData();
form.append("file", new Blob([await readFile(path)], { type: "application/pdf" }), "operating-agreement.pdf");
form.append("filing_type", filingType);
if (title) form.append("title", title);
const res = await fetch(`${BASE_URL}/entities/${entityId}/documents/upload`, { method: "POST", headers: HEADERS, body: form });
if (!res.ok) throw new Error(`Upload failed: ${res.status} ${await res.text()}`);
return (await res.json()).data.document_id;
}
const documentId = await uploadDocument(entityId, "operating-agreement.pdf", "Formation", "Operating Agreement");Successful upload response:
{
"success": true,
"data": { "document_id": "79f521ea-ca24-4b5f-89a6-4aad147ecfa3" }
}To attach an uploaded document to an order, reference it in the order payload as "document": {"id": "<document_id>"}. A document uploaded inline with an order (base64 content in the order payload) is stored hidden until the order is processed, so it will not appear in these lists or resolve on the detail endpoint until then.
Updated 7 days ago

