Common Patterns
Patterns and best practices for working with the Findable Partner API, including pagination, document uploads, and error handling.
Pagination
List endpoints use cursor-based pagination. Pass a next_token query parameter to fetch the next page. The response includes a nextToken field — when it is null, you have reached the last page. Note the casing difference: the query parameter is snake_case (next_token) while the response field is camelCase (nextToken).
How it works
- Make the initial request without
next_token - Check the
nextTokenfield in the response - If not
null, pass it asnext_tokenin the next request - Repeat until
nextTokenisnull
Example: Iterate all documents
const API_KEY = 'YOUR_API_KEY'; // pragma: allowlist secret
const BASE_URL = 'https://api.findable.ai';
async function getAllDocuments(ownerId: string, buildingId: string) {
const documents = [];
let nextToken: string | null = null;
do {
const params = new URLSearchParams({ limit: '10' });
if (nextToken) params.set('next_token', nextToken);
const res = await fetch(
`${BASE_URL}/building_owners/${ownerId}/buildings/${buildingId}/documents?${params}`,
{ headers: { 'x-api-key': API_KEY } },
);
const data = await res.json();
documents.push(...data.documents);
nextToken = data.nextToken;
} while (nextToken);
return documents;
}Document Upload Workflow
Uploading a document is a two-step process: first request a pre-signed URL from the API, then PUT the file directly to that URL.
Step 1: Request upload URL
Send a PUT request with the filename in the body. The API returns a documentId and a pre-signed uploadUrl.
curl -X PUT \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filename": "report.pdf"}' \
"https://api.findable.ai/building_owners/cust-456/buildings/bld-789/documents"Step 2: Upload the file
PUT the raw file content to the returned URL. Set the Content-Type header to match the file type.
# Set Content-Type to match your file (e.g. application/pdf, image/png, etc.)
curl -X PUT \
-H "Content-Type: application/pdf" \
--data-binary @report.pdf \
"UPLOAD_URL_FROM_STEP_1"Step 3: Check processing status
After uploading, the document is processed asynchronously. Poll the status endpoint to check when processing is complete.
curl -H "x-api-key: YOUR_API_KEY" \
"https://api.findable.ai/building_owners/cust-456/buildings/bld-789/documents/doc-123/status"Full TypeScript example (Node.js)
import { readFile } from 'node:fs/promises';
const API_KEY = 'YOUR_API_KEY'; // pragma: allowlist secret
async function uploadDocument(ownerId: string, buildingId: string, filePath: string) {
const filename = filePath.split('/').pop()!;
const fileBuffer = await readFile(filePath);
// Step 1: Get upload URL
const res = await fetch(
`https://api.findable.ai/building_owners/${ownerId}/buildings/${buildingId}/documents`,
{
method: 'PUT',
headers: { 'x-api-key': API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ filename }),
},
);
if (!res.ok) {
let message = res.statusText;
try { message = (await res.json()).message ?? message; } catch {} // non-JSON body
throw new Error(`Failed to request upload URL: ${res.status} ${message}`);
}
const { documentId, uploadUrl } = await res.json();
// Step 2: Upload file — set Content-Type to match your file
const ext = filename.split('.').pop()?.toLowerCase();
const contentType = ext === 'pdf' ? 'application/pdf'
: ext === 'png' ? 'image/png'
: ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg'
: 'application/octet-stream';
await fetch(uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': contentType },
body: fileBuffer,
});
// Step 3: Poll status (timeout after 5 minutes)
const maxAttempts = 60;
let attempts = 0;
let status;
while (true) {
const statusRes = await fetch(
`https://api.findable.ai/building_owners/${ownerId}/buildings/${buildingId}/documents/${documentId}/status`,
{ headers: { 'x-api-key': API_KEY } },
);
status = await statusRes.json();
if (status.categorised) break;
if (status.text_extraction_error || status.invalid_file) {
throw new Error(`Processing failed for document ${documentId}`);
}
if (++attempts >= maxAttempts) throw new Error('Processing timed out');
await new Promise((r) => setTimeout(r, 5000));
}
return { documentId, status };
}Error Handling
Error responses follow a consistent format with an HTTP status code and a JSON body containing a message field.
Error response format
{
"message": "Building owner with ID cust-999 not found"
}| Status | Description |
|---|---|
400 | Bad Request — Invalid parameters or request body |
401 | Unauthorised — Missing or invalid API key |
403 | Forbidden — API key does not have access to this resource |
404 | Not Found — The requested resource does not exist |
409 | Conflict — The resource is in a conflicting state (e.g. active processing) |
429 | Too Many Requests — Rate limit exceeded. Back off and retry after the delay indicated in the Retry-After header. |
500 | Internal Server Error — An unexpected error occurred |
Example: Error handling in TypeScript
async function apiRequest(url: string) {
const res = await fetch(url, {
headers: { 'x-api-key': 'YOUR_API_KEY' }, // pragma: allowlist secret
});
if (!res.ok) {
let message = res.statusText;
try { message = (await res.json()).message ?? message; } catch {} // non-JSON body
throw new Error(`API error ${res.status}: ${message}`);
}
return res.json();
}