Search documents

A search finds documents by what they are rather than by their identifier — their metadata definition, category, creation time, transaction status or the values of their metadata fields. This guide narrows the collection down with query parameters, searches on a metadata value, and applies a view somebody saved in the web app. It takes four calls.

Before you begin

Step 1: List documents with filters

The documents collection is filtered with query parameters:

GET /documents?metadataDefinition=d_employment&category=Contract
Authorization: Bearer YOUR_API_KEY
HTTP/2 200

{
    "total": 2,
    "pagination": {
        "limit": 20,
        "nextCursor": null,
        "offset": 0
    },
    "results": [
        {
            "id": "hL8vXe3RmQ7zBnT5wKdY",
            "title": "Employment Agreement (Robert Brown)",
            "category": "Contract",
            "metadataDefinition": "d_employment",
            ...
        },
        {
            "id": "wN5cRj8XmT2qKb7VdYgA",
            "title": "Employment Agreement (Jane Doe)",
            "category": "Contract",
            "metadataDefinition": "d_employment",
            ...
        }
    ]
}

A few things to notice:

  • Every filter you add narrows the result further — a document is returned only if it matches all of them. Repeat one parameter to widen it instead: ?category=Contract&category=Lease returns documents in either category.

  • Documents come back newest first, each one the same representation GET /documents/{id} returns.

  • total counts every match, not just this page. pagination describes the page you got.

Tip

query searches the document's title and its text content instead of a specific field — useful when your integration has a search box rather than a set of filters.

Long result sets are walked with limit and the opaque cursor rather than a growing offsetPage through collections covers both.

Step 2: Search on metadata values

The values of metadata-definition fields do not fit into query parameters, so the search endpoint takes the same filters in a request body and adds metadataFields:

POST /documents/search
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{
    "metadataDefinition": [
        "d_employment"
    ],
    "metadataFields": {
        "D_EMPLOYMENT_DEPARTMENT": "Finance"
    }
}
HTTP/2 200

{
    "total": 1,
    "results": [
        {
            "id": "hL8vXe3RmQ7zBnT5wKdY",
            "title": "Employment Agreement (Robert Brown)",
            "metadataFields": {
                "D_EMPLOYMENT_DEPARTMENT": "Finance"
            },
            ...
        }
    ],
    ...
}

Values are keyed by field identifier, exactly as when you store them. Selection, date and number fields are matched exactly; free-text fields are matched by full text.

Note

Both endpoints search your active documents. Pass trashed to search what you have thrown away instead.

Step 3: List your saved views

A view is a set of filters saved in the Circularo web app. Your own are listed on your account:

GET /me
Authorization: Bearer YOUR_API_KEY
HTTP/2 200

{
    "documentViews": [
        {
            "id": "contracts_to_send",
            "name": "Contracts to send this month"
        }
    ],
    ...
}

Each entry is an identifier and the name the person gave it. The filters behind it stay private to the interface — your integration applies a view rather than reading it.

Step 4: Apply the saved view

Pass the identifier as viewId:

GET /documents?viewId=contracts_to_send
Authorization: Bearer YOUR_API_KEY
HTTP/2 200

{
    "total": 2,
    "results": [
        {
            "id": "hL8vXe3RmQ7zBnT5wKdY",
            "title": "Employment Agreement (Robert Brown)",
            ...
        },
        {
            "id": "wN5cRj8XmT2qKb7VdYgA",
            "title": "Employment Agreement (Jane Doe)",
            ...
        }
    ],
    ...
}

The view's filters run on the server exactly as they do in the interface — here: contracts created this month whose transaction has not started yet. Because a view already carries a complete set of filters, it is used on its own: combining it with another filter is rejected with 400, while limit and offset work as usual.

Complete example

Pulling every match of one search, page by page, as a Node.js script. Error handling is minimal for brevity — in production, inspect the error envelope of non-2xx responses.

JavaScript
const BASE_URL = "https://sandbox.circularo.com/api/v1/public"; // your instance's base URL
const API_KEY = "YOUR_API_KEY";
const HEADERS = { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json" };

const filters = {
    metadataDefinition: [
        "d_employment"
    ],
    metadataFields: {
        D_EMPLOYMENT_DEPARTMENT: "Finance"
    }
};

const documents = [];
let cursor = null;
do {
    // a cursor is sent on its own; the filters ride along only on the first request
    const body = (cursor === null) ? { ...filters, limit: 50 } : { cursor: cursor };
    const res = await fetch(`${BASE_URL}/documents/search`, { method: "POST", headers: HEADERS, body: JSON.stringify(body) });
    if (!res.ok) throw new Error(`Search failed: ${res.status}`); // error handling kept minimal for brevity

    const page = await res.json();
    documents.push(...page.results);
    cursor = page.pagination.nextCursor;
} while (cursor !== null);

console.log(`Found ${documents.length} document(s)`);

Next steps