REST API
Neatbase provides a REST API that lets you read, write, and manage notes and fields in your notebooks programmatically. Use it to build integrations, automate workflows, sync with external services, or power dashboards from your Neatbase data.
The API uses a simple JSON format with field names as keys — no internal IDs to deal with. Changes made via the API sync to the Neatbase app automatically, and changes made in the app are available via the API.
Getting Started
Prerequisites
- A Neatbase Pro subscription
Enable API Access
- Open your notebook in Neatbase
- Open the ⋯ (More) menu in the toolbar
- Select API...
- Choose Enable API
Neatbase will generate an API key and a documentation URL for your notebook.
- API Key — A 64-character token used to authenticate all API requests. Keep it secret.
- Docs URL — A personalized documentation page showing your notebook's actual field names, types, and curl examples.
Enabling the API changes the notebook's encryption. To answer API requests, the server needs to read your data — so the encryption key is stored on the server for as long as the API is on. See Security before enabling it on a notebook holding sensitive data.
Disable API Access
To revoke API access, open the API panel from the ⋯ (More) menu and choose Disable API. The API key is immediately invalidated — all subsequent requests will fail with a 401 error.
Authentication
All API requests require a Bearer token in the Authorization header:
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://cloud.neatbase.com/api/v1/notes
Replace YOUR_API_KEY with the key shown in your notebook's API panel.
| Scenario | Response |
|---|---|
| Missing or invalid API key | 401 Unauthorized |
| API was disabled by the notebook owner | 401 Unauthorized |
| The notebook is no longer shared | 410 Gone |
Base URL
All endpoints are relative to:
https://cloud.neatbase.com/api/v1
Endpoints
Get Schema
Retrieve your notebook's field definitions — useful for understanding the structure before reading or writing notes.
GET /v1/schema
Example:
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://cloud.neatbase.com/api/v1/schema
Response:
{
"notebook_name": "Contacts",
"fields": [
{
"id": "550E8400-E29B-41D4-A716-446655440000",
"name": "Name",
"type": "shortText",
"order": 0
},
{
"id": "6BA7B810-9DAD-11D1-80B4-00C04FD430C8",
"name": "Email",
"type": "email",
"order": 1
},
{
"id": "7C9E6679-7425-40DE-944B-E07FC1F90AE7",
"name": "Amount",
"type": "currency",
"order": 2,
"config": {
"currencyCode": "USD"
}
},
{
"id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
"name": "Priority",
"type": "tags",
"order": 3,
"config": {
"availableTags": "Low,Medium,High"
}
},
{
"id": "B2C3D4E5-F6A7-8901-BCDE-F12345678901",
"name": "Total",
"type": "formula",
"order": 4,
"config": {
"formula": "{Amount} * {Quantity}"
},
"read_only": true
}
]
}
Fields marked read_only: true (formula, image) can be read but not written. Button fields read and write differently from each other — see Button fields.
List Fields
Retrieve all field definitions for the notebook.
GET /v1/fields
Example:
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://cloud.neatbase.com/api/v1/fields
Response:
{
"fields": [
{
"id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
"name": "Name",
"type": "shortText",
"order": 0
},
{
"id": "B2C3D4E5-F6A7-8901-BCDE-F12345678901",
"name": "Amount",
"type": "currency",
"order": 1,
"config": {
"currencyCode": "USD"
}
}
]
}
Get Field
Retrieve a single field definition by ID.
GET /v1/fields/{id}
Example:
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://cloud.neatbase.com/api/v1/fields/A1B2C3D4-E5F6-7890-ABCD-EF1234567890
Returns 404 if the field doesn't exist.
List Notes
Retrieve notes with pagination, sorting, and filtering.
GET /v1/notes
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
integer | 1 |
Page number (minimum 1) |
per_page |
integer | 100 |
Items per page (1–100) |
sort |
string | created_at |
Sort by created_at, updated_at, or a field name |
order |
string | desc |
Sort direction: asc or desc |
filter |
string | — | Filter in the format FieldName:operator:value |
filter[] |
string[] | — | Multiple filters (ANDed together) |
Basic Example:
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://cloud.neatbase.com/api/v1/notes?page=1&per_page=50"
Response:
{
"notes": [
{
"id": "550E8400-E29B-41D4-A716-446655440000",
"app_url": "neatbase://note/00000000-0000-0000-0000-000000000000/550E8400-E29B-41D4-A716-446655440000",
"delete_protected": false,
"avatar_url": null,
"fields": {
"Name": "Jane Doe",
"Email": "jane@example.com",
"Amount": 42.50,
"Priority": ["High"],
"Total": null
},
"created_at": "2026-04-05 12:00:00",
"updated_at": "2026-04-05 14:30:00"
}
],
"total": 42,
"page": 1,
"per_page": 50,
"total_pages": 1
}
The updated_at field reflects when the note was last edited (not the sync timestamp). Deleted notes are excluded.
Sorting
Sort by any field name, created_at, or updated_at:
# Oldest notes first
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://cloud.neatbase.com/api/v1/notes?sort=created_at&order=asc"
# Sort by a field value
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://cloud.neatbase.com/api/v1/notes?sort=Amount&order=desc"
Notes with null values for the sort field appear last regardless of sort direction.
Filtering
Filter notes by field values using the filter parameter. The format is FieldName:operator:value.
# Single filter
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://cloud.neatbase.com/api/v1/notes?filter=Name:contains:Jane"
# Multiple filters (ANDed)
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://cloud.neatbase.com/api/v1/notes?filter[]=Amount:gte:100&filter[]=Priority:contains:High"
# Combined with sorting
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://cloud.neatbase.com/api/v1/notes?filter=Amount:gt:0&sort=Amount&order=desc"
Filter Operators:
| Operator | Applies To | Needs Value | Description |
|---|---|---|---|
eq |
text, number, date, toggle | Yes | Equals (case-insensitive for text) |
neq |
text, number, date | Yes | Not equals |
contains |
text, tags | Yes | Substring match (text) or has tag (tags) |
gt |
number, date | Yes | Greater than |
lt |
number, date | Yes | Less than |
gte |
number, date | Yes | Greater than or equal |
lte |
number, date | Yes | Less than or equal |
is_empty |
all | No | Field is null or empty |
is_not_empty |
all | No | Field has a value |
Type compatibility — which operators a field accepts depends on the category it falls into:
| Category | Field types |
|---|---|
| text | shortText, longText, email, phone, url, reference, select, barcode, qrCode, color |
| number | number, currency, rating, duration, percent, formula |
| date | date |
| toggle | toggle |
| tags | tags |
Toggle filtering: Use eq:true or eq:false (the value is a string in the URL).
Tags filtering: contains checks if the tag array includes the specified tag (case-insensitive). For example, Priority:contains:High matches notes where the Priority tags include "High".
An invalid filter returns 400 and names the problem:
{
"error": "Unknown filter operator: \"like\". Supported: eq, neq, contains, gt, lt, gte, lte, is_empty, is_not_empty",
"code": "invalid_request"
}
Get Note
Retrieve a single note by its ID.
GET /v1/notes/{id}
Example:
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://cloud.neatbase.com/api/v1/notes/550E8400-E29B-41D4-A716-446655440000
Response:
{
"id": "550E8400-E29B-41D4-A716-446655440000",
"app_url": "neatbase://note/00000000-0000-0000-0000-000000000000/550E8400-E29B-41D4-A716-446655440000",
"delete_protected": false,
"avatar_url": null,
"fields": {
"Name": "Jane Doe",
"Email": "jane@example.com",
"Amount": 42.50,
"Priority": ["High"]
},
"created_at": "2026-04-05 12:00:00",
"updated_at": "2026-04-05 14:30:00"
}
Returns 404 if the note doesn't exist or has been deleted.
app_url — Deep link into the Neatbase app
Every note response includes an app_url field with a neatbase://... deep link. Opening it on a device that has the Neatbase app installed (and is signed in to a participant device for this shared notebook) opens the note directly. Useful for linking back to a note from a chat message, a ticket, or your own tooling. The link works for any participant, on any of their devices.
delete_protected — Prevent deletion in the Neatbase app
Every note response also includes a top-level delete_protected boolean. When true, the Neatbase app disables note deletion for this note on every participant's device. Useful for notes that shouldn't be deleted by accident — a summary row a dashboard depends on, or a record another system references. Deleting the whole notebook still removes it.
Set or clear it with POST (on create) or PUT (on update) — see the next two sections. The flag is API-only: there is no in-app UI to toggle it. Editing the note's field values, locking via API, duplicating, opening in detail view, etc. all still work — only the delete entry points are gated.
This only hides the delete controls in the app — it is not an access control. Anyone holding your API key can still DELETE /v1/notes/{id} directly, or clear the flag and then delete.
avatar_url — The note's avatar image
Every note response includes a top-level avatar_url: a link to the note's avatar image, or null when no image has been uploaded. Fetching it requires the same Authorization header as any other request.
null doesn't mean the app shows nothing. When a notebook has Show note avatars turned on, notes without an uploaded image fall back to the site icon for their email address or first link, and then to the note's initials. Those are rendered on the device, so there's nothing for the API to link to. See Notebooks for the full behavior.
Avatars are set and cleared with their own endpoints — see Set Note Avatar below — not through the note body.
Create Note
Create a new note with field values.
POST /v1/notes
Request Body:
{
"fields": {
"Name": "John Smith",
"Email": "john@example.com",
"Amount": 99.00,
"Priority": ["Medium", "High"]
},
"delete_protected": false
}
Example:
curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"fields": {"Name": "John Smith", "Email": "john@example.com", "Amount": 99.00}}' \
https://cloud.neatbase.com/api/v1/notes
Response (201 Created):
{
"id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
"app_url": "neatbase://note/00000000-0000-0000-0000-000000000000/A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
"delete_protected": false,
"avatar_url": null,
"fields": {
"Name": "John Smith",
"Email": "john@example.com",
"Amount": 99.00,
"Priority": ["Medium", "High"]
},
"created_at": "2026-04-05 15:30:00",
"updated_at": "2026-04-05 15:30:00"
}
Behavior:
- All fields are optional. Omitted fields are stored as
null. - Unknown field names return
400 Bad Request. - Read-only fields (
formula,image) are silently ignored. Button fields require an object write (see Field Types table) — booleans, strings, and other shapes return 400. - The note ID is generated server-side.
- All field values are validated before writing. If any validation fails, nothing is saved.
delete_protectedis optional and defaults tofalse. Set it totrueto prevent the note from being deleted in the Neatbase app (see the "delete_protected" section above).
Update Note
Update specific fields on an existing note. This is a partial update — only the fields you include are changed; all other fields are preserved.
PUT /v1/notes/{id}
Request Body:
{
"fields": {
"Amount": 149.00,
"Priority": ["High"]
},
"delete_protected": true
}
fields is optional — a request body with only delete_protected is valid (and leaves all field values untouched). Likewise, omitting delete_protected preserves the existing value.
Example:
curl -X PUT \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"fields": {"Amount": 149.00}}' \
https://cloud.neatbase.com/api/v1/notes/550E8400-E29B-41D4-A716-446655440000
# Lock a note from in-app deletion without changing any field values
curl -X PUT \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"delete_protected": true}' \
https://cloud.neatbase.com/api/v1/notes/550E8400-E29B-41D4-A716-446655440000
# Unlock it again
curl -X PUT \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"delete_protected": false}' \
https://cloud.neatbase.com/api/v1/notes/550E8400-E29B-41D4-A716-446655440000
Response (200 OK):
The complete updated note is returned (same format as Get Note).
Returns 404 if the note doesn't exist.
Delete Note
Soft-delete a note. The note is marked as deleted and will no longer appear in list or get endpoints.
DELETE /v1/notes/{id}
Example:
curl -X DELETE \
-H "Authorization: Bearer YOUR_API_KEY" \
https://cloud.neatbase.com/api/v1/notes/550E8400-E29B-41D4-A716-446655440000
Response (200 OK):
{
"id": "550E8400-E29B-41D4-A716-446655440000",
"deleted": true
}
Returns 404 if the note doesn't exist or has already been deleted.
Set Note Avatar
Upload or replace a note's avatar — the circular image shown beside the note's title in the app.
POST /v1/notes/{id}/avatar
Send the image as multipart/form-data in a field named avatar. Accepts JPEG, PNG, GIF, and WebP, up to 10 MB. The type is detected from the file's contents, not its name.
Very high-resolution images are rejected even when they're under the size limit — an avatar is displayed at a small size, so resize before uploading if you hit that. Animated images aren't supported; send a still frame.
Example:
curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "avatar=@/path/to/photo.jpg" \
https://cloud.neatbase.com/api/v1/notes/550E8400-E29B-41D4-A716-446655440000/avatar
Response (200 OK):
{
"id": "550E8400-E29B-41D4-A716-446655440000",
"avatar_url": "https://cloud.neatbase.com/api/v1/images/share_AbC123_9f8e7d6c5b4a3210.jpg"
}
The image is cropped to a square from the center and resized to 256×256 — the same treatment the app applies — so send a square image if the framing matters. The result reaches every participant's device on their next sync.
Uploading replaces any existing avatar. Returns 400 if the field is missing, the file isn't a supported image, or it exceeds 10 MB.
Delete Note Avatar
Remove a note's avatar.
DELETE /v1/notes/{id}/avatar
Example:
curl -X DELETE \
-H "Authorization: Bearer YOUR_API_KEY" \
https://cloud.neatbase.com/api/v1/notes/550E8400-E29B-41D4-A716-446655440000/avatar
Response (200 OK):
{
"id": "550E8400-E29B-41D4-A716-446655440000",
"avatar_url": null
}
The removal propagates to every participant. The app then falls back to the note's site icon or initials, as described under avatar_url in Get Note.
Avatars only appear when the notebook has Show note avatars enabled in its settings — uploading one while that's off stores the image without displaying it.
Field Management
Manage your notebook's field schema programmatically — add, rename, reorder, or remove fields without opening the app.
Important: A field's type cannot be changed after creation. To change the type, delete the field and create a new one.
Create Field
POST /v1/fields
Request Body:
{
"name": "Amount",
"type": "currency",
"order": 3,
"config": {
"currencyCode": "USD"
}
}
| Parameter | Required | Description |
|---|---|---|
name |
Yes | Field name (max 40 characters, must be unique within the notebook) |
type |
Yes | One of the valid field types (see table below) |
order |
No | Position in the field list (defaults to end) |
showInList |
No | Whether the field's value shows as a subtitle line under the note title in the app's note list. Defaults to true. |
config |
Depends | Required for types that need configuration |
Types that require config:
| Type | Config Key | Example |
|---|---|---|
currency |
currencyCode |
{"currencyCode": "USD"} |
tags |
availableTags |
{"availableTags": "Low,Medium,High"} |
select |
availableOptions |
{"availableOptions": "Option A,Option B,Option C"} |
formula |
formula |
{"formula": "{Price} * {Qty}"} |
reference |
notebookIds |
{"notebookIds": "NOTEBOOK-UUID,OTHER-UUID"} (optional; comma-separated. The legacy single notebookId is still read and written for compatibility) |
reference |
notebookShareIds |
App-managed — comma-separated share IDs, index-aligned with notebookIds, written when a target notebook is shared so other participants can resolve it. If you change notebookIds, omit this key and let the app rebuild it; the server drops it whenever its slot count doesn't match notebookIds, since a misaligned echo would make targets resolve to the wrong notebook. |
reference, url |
allowMultiple |
{"allowMultiple": "true"} (optional — makes the field hold a LIST of values; see Multi-value fields) |
Example:
curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Amount", "type": "currency", "config": {"currencyCode": "USD"}}' \
https://cloud.neatbase.com/api/v1/fields
Response (201 Created):
{
"id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
"name": "Amount",
"type": "currency",
"order": 3,
"config": {
"currencyCode": "USD"
}
}
Valid types for creation: shortText, longText, email, phone, url, number, currency, rating, formula, date, toggle, tags, select, reference, checklist, image, comments, color, duration, percent, barcode, qrCode, button
Layout types (sectionDivider, sectionHeading, sectionDescription) and secretText cannot be created via the API.
Update Field
Update a field's name, order, or config. This is a partial update — only the properties you include are changed.
PUT /v1/fields/{id}
Request Body (all optional, at least one required):
{
"name": "New Name",
"order": 5,
"config": {
"currencyCode": "EUR"
}
}
Example:
curl -X PUT \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Price", "order": 2}' \
https://cloud.neatbase.com/api/v1/fields/FIELD-UUID
Response (200 OK):
showInList is accepted here too. Omitting it preserves the field's current value, so a partial update that only changes the name or config can't flip the field's visibility by accident.
The complete updated field is returned (same format as Create Field).
Notes:
- Including
typein the request body returns400 Bad Request— type is immutable. - Field names must be unique across all fields in the notebook.
- Returns
404if the field doesn't exist.
Rename Tags
Rename one or more tags on a tags field — updating the field's tag list and rewriting the tag on every note that carries it.
PUT /v1/fields/{id}
Use rename_tags instead of rewriting config.availableTags. A config rewrite can't tell a rename apart from a delete-and-add, so the old tag would stay behind on your notes; rename_tags states the intent, and the server migrates everything in one step.
Request Body:
{
"rename_tags": {
"Old Name": "New Name"
}
}
Example:
curl -X PUT \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"rename_tags": {"Urgent": "Critical"}}' \
https://cloud.neatbase.com/api/v1/fields/FIELD-UUID
Response (200 OK): the updated field, plus migrated_values — how many notes had the tag rewritten:
{
"id": "FIELD-UUID",
"name": "Tags",
"type": "tags",
"order": 3,
"config": { "availableTags": "Critical,Blocked" },
"migrated_values": 7
}
Notes:
- Only valid on
tagsfields. - Old names are matched against the configured tags case-insensitively; tag colors are preserved.
- New names can't contain
,or:, and can't collide with another existing tag (renaming onto an existing tag — a merge — isn't supported). - Can't be combined with
configin the same request.nameandorderare fine. - Safe to retry: re-sending a rename that already applied is a no-op.
- Devices pick up the change on their next sync, like any other API edit.
Rename Select Options
The same operation exists for select fields as rename_options — it updates the field's option list and rewrites the option on every note that holds it, with the same rules and the same migrated_values response. The one difference: a select option may contain : (only , is reserved).
curl -X PUT \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"rename_options": {"In Progress": "Doing"}}' \
https://cloud.neatbase.com/api/v1/fields/FIELD-UUID
Delete Field
Soft-delete a field and all its values across every note in the notebook.
DELETE /v1/fields/{id}
Example:
curl -X DELETE \
-H "Authorization: Bearer YOUR_API_KEY" \
https://cloud.neatbase.com/api/v1/fields/FIELD-UUID
Response (200 OK):
{
"id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
"deleted": true
}
Returns 404 if the field doesn't exist or has already been deleted.
Warning: Deleting a field permanently removes all data stored in that field across all notes. This cannot be undone via the API.
Field Types
The API uses the same field types as the Neatbase app. Each type has a specific JSON representation:
| Type | JSON Type | Read | Write | Notes |
|---|---|---|---|---|
shortText |
string | Yes | Yes | Single-line text |
longText |
string | Yes | Yes | Multi-line text |
email |
string | Yes | Yes | Email address |
phone |
string | Yes | Yes | Phone number |
url |
string, or array when multiple |
Yes | Yes | Web URL. See Multi-value fields when the field allows several. |
number |
number | Yes | Yes | Numeric value |
currency |
number | Yes | Yes | Monetary amount (see schema for currency code) |
rating |
integer | Yes | Yes | Integer from 1 to 5 |
date |
string | Yes | Yes | Format depends on field config. Date-only fields: YYYY-MM-DD. Fields with "Include time" enabled: ISO 8601 datetime in UTC (e.g., 2026-05-17T15:00:00Z). See "Date Fields" under Validation Rules below for accepted write formats. |
toggle |
boolean | Yes | Yes | true or false |
tags |
array | Yes | Yes | Array of strings, e.g. ["Tag1", "Tag2"] |
select |
string | Yes | Yes | One of the field's configured options (or null/"" to clear). Strict validation. |
checklist |
array | Yes | Yes | Array of objects: [{"id": "...", "text": "...", "checked": true}] |
comments |
array | Yes | Yes | Array of {id, date, author, content} objects. Supports {"append": "text"} and {"delete": "id"} shortcuts. |
reference |
string, or array when multiple |
Yes | Yes | UUID of a note in one of the referenced notebooks (or null/"" to clear). Must be UUID-shaped, else 400. Existence is not verified — see Reference Fields under Validation Rules. |
duration |
number | Yes | Yes | Total seconds (e.g., 5400 = 1h 30m) |
percent |
number | Yes | Yes | Number from 0 to 100 |
color |
string | Yes | Yes | Hex color (e.g., #FF5733) |
barcode |
string | Yes | Yes | Barcode string |
qrCode |
string | Yes | Yes | QR code string |
image |
string | Yes | No | Returns an image URL (read-only) |
formula |
null | Yes | No | Always null — computed on device (read-only) |
button |
string (read) / object (write) | Yes | Yes | A resolved URL when read, an object when written — see Button fields |
secretText |
— | No | No | Excluded from API entirely |
sectionDivider |
— | No | No | Layout element, excluded from API |
sectionHeading |
— | No | No | Layout element, excluded from API |
sectionDescription |
— | No | No | Layout element, excluded from API |
Button fields
Button fields behave differently on read and write, so they're worth their own note.
Reading returns the resolved URL as a string — the per-note override if one is set, otherwise the field's default template — with {NOTE_ID} and {NOTEBOOK_ID} filled in.
You get null instead when any of these is true:
- the button is disabled on that note,
- the template is empty, or
- the template is exactly
#.
That last one is a deliberate placeholder: set a field's default URL to # and every note's button stays inert until you give it a real one. It wins over the enabled flag.
{NOTEBOOK_ID}resolves to the share ID — the short identifier in your notebook's documentation URL (/d/{shareId}) and share links — not the notebook UUID you'd see inside the app. The app substitutes its own local UUID there, so the same template resolves differently depending on which side fills it in.
Writing takes an object. Both keys are optional, and a partial write merges with whatever is already set:
{
"fields": {
"Open Invoice": { "url": "https://billing.example.com/{NOTE_ID}", "enabled": true }
}
}
Sending null for the field clears both the URL override and the enabled flag, returning the note to the field's default.
To change the default URL for every note at once, update the field itself with PUT /v1/fields/{id} — that's a field setting, not a per-note value.
Validation Rules
When creating or updating notes, field values are validated based on their type. Invalid values return a 400 Bad Request error.
Text Fields
Types: shortText, longText, email, phone, url, barcode, qrCode, color
"Name": "Jane Doe"
"Name": null
Must be a string or null. Any other type (number, boolean, array) is rejected.
Multi-value fields
Types: reference, url
Reference and URL fields can be switched, per field, to hold a list of values instead of one. The field's schema entry then carries "multiple": true:
{ "name": "Links", "type": "url", "multiple": true }
Check that flag rather than guessing from a note's data — the shape follows the field's configuration, not what happens to be stored:
- Reading, a
multiplefield is always an array, even when it holds zero or one value. Fields without the flag still return a plain string ornull, exactly as before. - Writing, a
multiplefield accepts either shape — a bare string for one value, or an array for several — so existing code keeps working when the flag gets turned on. - Writing an array to a field that is NOT
multiplereturns400. The first element isn't silently kept.
"Links": ["https://example.com/pricing", "https://example.com/docs"]
"Links": "https://example.com/pricing"
"Links": null
Send null or [] to clear the field. Array elements must be strings (URL) or UUID-shaped strings (Reference). Duplicate Reference UUIDs in one array are collapsed to the first occurrence.
To turn the flag on for a field, set allowMultiple in its config:
curl -X PUT https://cloud.neatbase.com/api/v1/fields/FIELD-UUID \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"config": {"allowMultiple": "true"}}'
Note:
filter=andsort=operate on the stored value as text, soeqwill not match a single value inside a multi-value field. Usecontainsthere.
Reference Fields
Types: reference
"Client": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890"
"Client": ["A1B2C3D4-E5F6-7890-ABCD-EF1234567890", "B1B2C3D4-E5F6-7890-ABCD-EF1234567890"]
"Client": null
Must be a UUID-shaped string, or null/"" to clear. Anything else returns 400. Lowercase UUIDs are accepted and normalized to uppercase. A field with allowMultiple accepts an array of UUIDs — see Multi-value fields.
Existence is not verified. A reference points at a note in a different notebook — which is a different share, or one that was never uploaded to the server at all. The API has no access to that notebook's notes, so it cannot confirm the UUID resolves to anything. A syntactically valid UUID that points nowhere is stored as-is and displays in the app as "Unknown note".
To get a valid UUID, call GET /v1/notes against the referenced notebook's own API and use the id of the note you want to link to. That notebook needs its API enabled separately.
A Reference field can target more than one notebook (notebookIds in its config). The stored value is just a note UUID either way — the app resolves it against whichever of the field's target notebooks contains it, and, failing that, against any notebook on the device: note IDs are globally unique, so a valid link renders even if the target list is out of date.
Cleared references read back as null, not "".
Number Fields
Types: number, currency, duration, percent
"Amount": 42.50
"Amount": -10
"Amount": null
Must be a numeric value (integer or float) or null.
Rating
"Stars": 4
"Stars": null
Must be an integer from 1 to 5, or null. Floats like 3.5 are rejected — use whole numbers only.
Date
Date fields come in two flavors depending on whether the field has the "Include time" option enabled in the app's field editor.
Date-only fields (default):
"Due Date": "2026-04-05"
"Due Date": null
Must be a string in YYYY-MM-DD format, or null. Read responses return the same YYYY-MM-DD shape.
Fields with "Include time" enabled:
"Meeting": "2026-04-05T15:00:00Z"
"Meeting": "2026-04-05T15:00:00-07:00"
"Meeting": "2026-04-05 15:00:00"
"Meeting": "2026-04-05"
"Meeting": null
Accepts ISO 8601 datetimes (with Z for UTC, an explicit offset like -07:00, or fractional seconds), the legacy yyyy-MM-dd HH:mm:ss format (UTC assumed if no zone), or a bare YYYY-MM-DD (stored at midnight UTC). Read responses always return ISO 8601 UTC: "2026-04-05T15:00:00Z".
You can tell which flavor a field uses by checking config.includesTime === "true" in the field schema (GET /v1/fields).
Toggle
"Active": true
"Active": false
"Active": null
Must be a JSON boolean (true or false) or null. String values like "true" are rejected.
Tags
"Categories": ["Design", "Marketing"]
"Categories": []
"Categories": null
Must be an array of strings, an empty array, or null. Each element must be a string. Objects and non-string elements are rejected.
Select
"Status": "Active"
"Status": null
Must be a string matching one of the field's configured availableOptions, or null/"" to clear. Submitting an unknown value returns 400 with the list of valid options.
Checklist
"Tasks": [
{"text": "Design mockup", "checked": true},
{"text": "Review copy", "checked": false}
]
"Tasks": null
Must be an array of checklist items or null. Each item should have text (string) and checked (boolean) keys.
Comments
"Activity Log": [
{
"id": "A1B2C3D4-...",
"date": "2026-04-05 12:00:00.000",
"author": "Alice",
"content": "Called the customer"
}
]
Three accepted shapes:
- Full array — replace the entire list. Each entry needs
content(max 2000 chars). Missingid/date/authorare filled in server-side (UUID, now,"API"). - Append shortcut —
{"append": "text"}or{"append": "text", "author": "Bob"}. Adds a single entry without reading the existing list. - Delete shortcut —
{"delete": "<comment-id>"}. Removes a single entry by id; no-op if the id doesn't match.
Sending null or [] clears all entries. Date format is yyyy-MM-dd HH:mm:ss.SSS UTC.
Duration
"Prep Time": 5400
"Prep Time": null
Must be a numeric value representing total seconds, or null. For example, 5400 = 1 hour 30 minutes.
Percent
"Progress": 75
"Progress": null
Must be a numeric value from 0 to 100, or null.
Color
"Brand Color": "#FF5733"
"Brand Color": null
Must be a hex color string (e.g., #FF5733), or null.
Barcode / QR Code
"Barcode": "012345678905"
"QR Code": "https://example.com"
Must be a string or null. Any text value is accepted.
Pagination
List endpoints return paginated results:
GET /v1/notes?page=2&per_page=25
| Parameter | Default | Range | Description |
|---|---|---|---|
page |
1 | 1+ | Page number |
per_page |
100 | 1–100 | Results per page |
Response metadata:
{
"notes": [...],
"total": 150,
"page": 2,
"per_page": 25,
"total_pages": 6
}
Security
The API key is a password. Anyone holding it can read, change, and delete every note in that notebook. Keep it in an environment variable or a secrets manager — not in client-side code, a public repository, or a URL.
Enabling the API turns off end-to-end encryption for that notebook. Normally Neatbase encrypts a shared notebook on your device and the server only ever stores unreadable ciphertext. The API can't work that way — the server has to read your data to answer a request — so turning the API on stores the encryption key on the server. Turning it off deletes that key and restores end-to-end encryption. Notebooks without the API enabled are unaffected.
To rotate a key, disable the API and enable it again. That invalidates the old key immediately and issues a new one; anything still using the old key starts getting 401.
There is no read-only key. One key, full access. If you need a lower-privilege integration, put your own service in front of the API rather than handing the key out.
Rate Limits
API requests are rate-limited per API key:
| Operation | Limit |
|---|---|
| Read (GET requests) | 240 per hour |
| Write (POST, PUT, DELETE) | 120 per hour |
When a limit is exceeded, the API returns:
{
"error": "Rate limit exceeded. Try again later.",
"code": "rate_limit"
}
HTTP Status: 429 Too Many Requests
Each limit uses a fixed one-hour window that starts with your first request. When the window closes, the counter resets.
Error Handling
All errors follow a consistent format:
{
"error": "Human-readable error message",
"code": "machine_readable_code"
}
Error Reference
| HTTP Status | Code | Description |
|---|---|---|
400 |
invalid_request |
Malformed request body or missing fields object |
400 |
invalid_request |
Field validation failed (message includes field name and reason) |
400 |
invalid_field |
Unknown field name (not in the notebook's schema) |
401 |
unauthorized |
Missing or invalid API key |
404 |
not_found |
Note or resource not found |
410 |
gone |
The notebook is no longer shared, so its API is gone |
429 |
rate_limit |
Rate limit exceeded |
500 |
server_error |
Internal error (e.g., database transaction failed) |
Images
Image fields are read-only in the API. When a note has an image field with data, the API returns a URL:
{
"fields": {
"Photo": "https://cloud.neatbase.com/api/v1/images/share_abc123_def456.jpg"
}
}
To download the image, make a GET request to the URL with your API key:
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://cloud.neatbase.com/api/v1/images/share_abc123_def456.jpg" \
-o photo.jpg
Images that haven't been set return null.
Image fields cannot be written via the API. Use the Neatbase app to add or update images.
Sync Behavior
The Neatbase app syncs with the server automatically while it's open. This means:
- API → App: Notes and fields created or changed through the API show up in the app within about a minute, or immediately when the app comes back to the foreground.
- App → API: Notes and fields created or modified in the app will be available via the API after the next sync cycle.
- Deletions: Soft-deleted notes and fields (via API or app) are removed from both sides after sync.
The sync is bidirectional and automatic — no additional setup required.
Examples
List All Notes
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://cloud.neatbase.com/api/v1/notes
Sort and Filter Notes
# Get high-value notes, sorted by amount
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://cloud.neatbase.com/api/v1/notes?filter=Amount:gte:1000&sort=Amount&order=desc"
# Find notes with a specific tag
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://cloud.neatbase.com/api/v1/notes?filter=Priority:contains:High"
# Notes with upcoming due dates, sorted by most recently edited
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://cloud.neatbase.com/api/v1/notes?filter=Due%20Date:gte:2026-03-29&sort=updated_at&order=desc"
Create a Note
curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"fields": {
"Name": "Acme Corp",
"Email": "hello@acme.com",
"Amount": 5000,
"Priority": ["High"],
"Active": true
}
}' \
https://cloud.neatbase.com/api/v1/notes
Update Specific Fields
Only include the fields you want to change:
curl -X PUT \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"fields": {"Amount": 7500, "Priority": ["High", "Urgent"]}}' \
https://cloud.neatbase.com/api/v1/notes/YOUR_NOTE_ID
Delete a Note
curl -X DELETE \
-H "Authorization: Bearer YOUR_API_KEY" \
https://cloud.neatbase.com/api/v1/notes/YOUR_NOTE_ID
Paginate Through All Notes
# Page 1
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://cloud.neatbase.com/api/v1/notes?page=1&per_page=50"
# Page 2
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://cloud.neatbase.com/api/v1/notes?page=2&per_page=50"
Python Example
import requests
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://cloud.neatbase.com/api/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
# List notes
response = requests.get(f"{BASE_URL}/notes", headers=HEADERS)
notes = response.json()["notes"]
for note in notes:
print(note["fields"]["Name"], "-", note["fields"].get("Email"))
# Create a note
new_note = requests.post(
f"{BASE_URL}/notes",
headers={**HEADERS, "Content-Type": "application/json"},
json={"fields": {"Name": "New Contact", "Email": "new@example.com"}}
)
print("Created:", new_note.json()["id"])
JavaScript Example
const API_KEY = "YOUR_API_KEY";
const BASE_URL = "https://cloud.neatbase.com/api/v1";
// List notes
const response = await fetch(`${BASE_URL}/notes`, {
headers: { "Authorization": `Bearer ${API_KEY}` }
});
const { notes } = await response.json();
// Create a note
const created = await fetch(`${BASE_URL}/notes`, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
fields: { "Name": "New Contact", "Email": "new@example.com" }
})
});
console.log("Created:", (await created.json()).id);