Documents carry more than a title. This guide finds out which structured fields a document can hold, then creates one that fills them and adds a couple of values of your own. It takes three calls.
Before you begin
-
An uploaded file. The document is built on a file identifier, as in Create a document from a file.
-
Custom fields need the
allow_documents_custom_fieldsright. Without it, a create call that carriescustomFieldsis rejected with403; the rest of the document is unaffected.
Two kinds of structured data
A document holds two separate sets of values, and they behave differently:
|
Metadata fields |
Custom fields |
|
|---|---|---|
|
Declared by |
a metadata definition set up for your organization |
your integration, on each document |
|
Keys |
the field identifiers the definition declares |
any key you choose |
|
Values |
typed by the definition: dates, selections, text |
text, with a type of your choosing |
|
Usable as a search filter |
yes |
no |
A metadata definition is the shape of a document: it lists the fields the document can hold, their data types and which of them are mandatory. Every document is created with exactly one definition, and its values go into metadataFields, keyed by field identifier. Definitions are configured for your organization by Circularo and cannot be created through the API, so an integration discovers what exists and works with it.
Custom fields are your own key/value pairs, stored alongside. Use them for data that belongs to your system rather than to Circularo — an identifier from your HR or CRM database, for example.
Step 1: Find the definitions you can use
Definitions are configured by your organization, so start by listing what is available:
GET /metadata-definitions
Authorization: Bearer YOUR_API_KEY
HTTP/2 200
{
"total": 2,
"pagination": null,
"results": [
{
"name": "d_default",
"description": null,
...
},
{
"name": "d_employment",
"description": "Employment agreements and their key terms",
...
}
]
}
The complete list comes back in one response: pagination is null, because this collection is never paged. Among the definitions is d_default — the one a document gets when you do not name any.
Each entry is a whole definition, including its field list; the block above is curated down to the identifiers. One call is therefore enough to learn everything about every definition available to you.
Step 2: Read the definition you will use
When the definition your integration works with is fixed — its identifier sitting in your configuration — read that one on its own:
GET /metadata-definitions/d_employment
Authorization: Bearer YOUR_API_KEY
HTTP/2 200
{
"description": "Employment agreements and their key terms",
"fields": [
{
"defaultValue": null,
"description": "Department the employee joins",
"isMultiselect": false,
"name": "D_EMPLOYMENT_DEPARTMENT",
"options": [
"Engineering",
"Finance",
"Operations"
],
"required": true,
"type": "list"
},
{
"defaultValue": null,
"description": "First working day",
"isMultiselect": false,
"name": "D_EMPLOYMENT_START_DATE",
"options": [],
"required": true,
"type": "date"
},
{
"defaultValue": null,
"description": "Anything the HR team should know",
"isMultiselect": false,
"name": "D_EMPLOYMENT_NOTE",
"options": [],
"required": false,
"type": "text"
}
],
"name": "d_employment"
}
A few things to notice:
-
nameis the key you use when you supply the field's value. -
typesays what a value looks like:D_EMPLOYMENT_START_DATEtakes a calendar date,D_EMPLOYMENT_NOTEfree text. -
optionslists the values a selection field accepts, andisMultiselectsays whether more than one of them may be chosen. -
requiredmarks the fields the definition expects every document to carry.
The document's title, identifier and author are not among the fields: they are properties of the document itself, not metadata you fill in.
Step 3: Create a document with values
Name the definition and supply the values, keyed by field identifier:
POST /documents
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"title": "Employment Agreement (Jane Doe)",
"mainFileId": "4b8e17d0c92a35f6e1478bc0d25a9e31",
"metadataDefinition": "d_employment",
"metadataFields": {
"D_EMPLOYMENT_DEPARTMENT": "Engineering",
"D_EMPLOYMENT_START_DATE": "2026-06-01",
"D_EMPLOYMENT_NOTE": "Remote contract, equipment shipped before the start date"
},
"customFields": [
{
"key": "hrisEmployeeId",
"value": "EMP-4821",
"displayName": "HRIS employee id"
}
]
}
HTTP/2 201
{
"id": "pXq4NcYBhK9tWdA2mFzR",
"title": "Employment Agreement (Jane Doe)",
"metadataDefinition": "d_employment",
"metadataFields": {
"D_EMPLOYMENT_DEPARTMENT": "Engineering",
"D_EMPLOYMENT_START_DATE": "2026-06-01",
"D_EMPLOYMENT_NOTE": "Remote contract, equipment shipped before the start date"
},
"customFields": [
{
"displayName": "HRIS employee id",
"key": "hrisEmployeeId",
"type": "string",
"value": "EMP-4821"
}
],
...
}
The response shows both sets of data as they were stored. The custom field gained a type, which defaults to string; displayName is the label shown next to the value. A custom field's value always comes back as text, whatever its type — a field typed number returns "42", not 42, so convert it on your side.
A key the definition does not declare is rejected and no document is created, so a typo in a field identifier cannot leave you with a document that is missing part of its metadata.
Reading the document back with GET /documents/{id} returns the same metadataFields and customFields. Filtering documents by their metadata values is covered in Search documents.
Complete example
Reading a definition and creating a document that fills it, as a Node.js script. Error handling is minimal for brevity — in production, inspect the error envelope of non-2xx responses.
const BASE_URL = "https://sandbox.circularo.com/api/v1/public"; // your instance's base URL
const API_KEY = "YOUR_API_KEY";
const AUTH_HEADER = { "Authorization": `Bearer ${API_KEY}` };
const mainFileId = "4b8e17d0c92a35f6e1478bc0d25a9e31"; // from an earlier upload
const definition = await (await fetch(`${BASE_URL}/metadata-definitions/d_employment`, { headers: AUTH_HEADER })).json();
// fail early when the definition no longer offers a field the integration fills
const declared = definition.fields.map((field) => field.name);
for (const key of ["D_EMPLOYMENT_DEPARTMENT", "D_EMPLOYMENT_START_DATE"]) {
if (!declared.includes(key)) throw new Error(`Field ${key} is not declared by ${definition.name}`);
}
const createRes = await fetch(`${BASE_URL}/documents`, {
method: "POST",
headers: { ...AUTH_HEADER, "Content-Type": "application/json" },
body: JSON.stringify({
title: "Employment Agreement (Jane Doe)",
mainFileId: mainFileId,
metadataDefinition: "d_employment",
metadataFields: {
D_EMPLOYMENT_DEPARTMENT: "Engineering",
D_EMPLOYMENT_START_DATE: "2026-06-01",
D_EMPLOYMENT_NOTE: "Remote contract, equipment shipped before the start date"
},
customFields: [
{
key: "hrisEmployeeId",
value: "EMP-4821",
displayName: "HRIS employee id"
}
]
})
});
if (!createRes.ok) throw new Error(`Create failed: ${createRes.status}`); // error handling kept minimal for brevity
console.log(`Document ${(await createRes.json()).id} created`);
Next steps
-
Send the document for signature or approval — Signing & Approvals.
-
Create documents with attachments and categories — Create a document from a file.