AI
AI providers, models and the tasks that use them.
On this page
- List AI providers
- Add an AI provider
- Get an AI provider
- Change an AI provider
- Delete an AI provider
- Test unsaved provider settings
- Check a provider's connection
- List a provider's models
- Add several models at once
- List AI models
- Add an AI model
- Get an AI model
- Change an AI model
- Remove an AI model
- Try a model
- List AI tasks
- Get an AI task
- Change an AI task
- Run a task on existing records
List AI providers
GET/ai_providers
AI model providers.
curl "$NIFTY_URL/api/v1/ai_providers" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Query parameters
| Name | Description |
|---|---|
orderid | name | updated_at · optional | The field to sort by; ties are broken by id, so the order is stable. |
directionasc | desc · optional | asc (ascending) or desc (descending), for order. |
updated_sincedate-time · optional | Only records updated at or after this time (ISO 8601 with Z or a UTC offset), ordered by updated_at, id. |
cursorstring · optional | meta.next_cursor from the previous page. |
limitinteger · optional | Items per page, up to 100; a larger number is taken as 100. |
counttrue | false · optional | true adds meta.total_count: the size of the filtered set (with updated_since too), ignoring cursor and limit. Anything but true or false is a 400. |
Response
200 OK Errors: 400 401 429
{
"data": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Ollama",
"kind": "openai_compatible",
"base_url": "http://localhost:11434/v1",
"api_key_set": true,
"last_succeeded_at": "2026-10-10T09:30:00Z",
"last_failed_at": "2026-10-10T09:30:00Z",
"last_error": "string",
"last_error_reason": "string",
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
],
"meta": {
"next_cursor": "eyJrIjpbIjIwMjYtMTAtMDlUMDg6MzA6MDBaIl19",
"limit": 1,
"total_count": 1
}
}
Add an AI provider
POST/ai_providers
A name another provider has, ignoring case, is a 422. On update, moving base_url to another origin
(scheme, host or port) while a key is stored needs the key sent again in the same request (422 on
api_key otherwise).
curl -X POST "$NIFTY_URL/api/v1/ai_providers" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ai_provider":{"name":"Ollama","kind":"openai_compatible","base_url":"http://localhost:11434/v1"}}'
Headers
| Name | Description |
|---|---|
Idempotency-Keystring · optional | Makes a POST safe to retry for 24 hours. Needs a bearer token (400 under session auth). |
Request body
| Name | Description |
|---|---|
ai_providerobject | |
ai_provider.namestring · optional | Spaces are squished. Unique ignoring case. |
ai_provider.kindAIProviderKind · optional | openai_compatible covers OpenAI, Ollama, LM Studio, llama.cpp and vLLM. mock needs no network and isn't available in production. |
ai_provider.base_urlstring or null · optional | Required unless mock. http(s), including the version path (e.g. http://localhost:11434/v1); no user, query or fragment. A trailing / is dropped. |
ai_provider.api_keystring or null · optional | Write-only, stored encrypted. No spaces or line breaks inside. Required for anthropic; local servers need none. Null or blank clears it. Must be sent again when base_url moves to another origin. |
Response
201 Created Errors: 400 401 403 409 415 422 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Ollama",
"kind": "openai_compatible",
"base_url": "http://localhost:11434/v1",
"api_key_set": true,
"last_succeeded_at": "2026-10-10T09:30:00Z",
"last_failed_at": "2026-10-10T09:30:00Z",
"last_error": "string",
"last_error_reason": "string",
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Get an AI provider
GET/ai_providers/{id}
curl "$NIFTY_URL/api/v1/ai_providers/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Response
200 OK Errors: 401 404 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Ollama",
"kind": "openai_compatible",
"base_url": "http://localhost:11434/v1",
"api_key_set": true,
"last_succeeded_at": "2026-10-10T09:30:00Z",
"last_failed_at": "2026-10-10T09:30:00Z",
"last_error": "string",
"last_error_reason": "string",
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Change an AI provider
PATCH/ai_providers/{id}
Also PUT /ai_providers/{id}, the same.
Only the fields sent change. Omit api_key to keep it; null or "" clears it.
curl -X PATCH "$NIFTY_URL/api/v1/ai_providers/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ai_provider":{"name":"Ollama","kind":"openai_compatible","base_url":"http://localhost:11434/v1"}}'
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Request body
| Name | Description |
|---|---|
ai_providerobject | |
ai_provider.namestring · optional | Spaces are squished. Unique ignoring case. |
ai_provider.kindAIProviderKind · optional | openai_compatible covers OpenAI, Ollama, LM Studio, llama.cpp and vLLM. mock needs no network and isn't available in production. |
ai_provider.base_urlstring or null · optional | Required unless mock. http(s), including the version path (e.g. http://localhost:11434/v1); no user, query or fragment. A trailing / is dropped. |
ai_provider.api_keystring or null · optional | Write-only, stored encrypted. No spaces or line breaks inside. Required for anthropic; local servers need none. Null or blank clears it. Must be sent again when base_url moves to another origin. |
Response
200 OK Errors: 400 401 403 404 415 422 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Ollama",
"kind": "openai_compatible",
"base_url": "http://localhost:11434/v1",
"api_key_set": true,
"last_succeeded_at": "2026-10-10T09:30:00Z",
"last_failed_at": "2026-10-10T09:30:00Z",
"last_error": "string",
"last_error_reason": "string",
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Delete an AI provider
DELETE/ai_providers/{id}
Delete a provider and its models.
Permanent. Writes an ai_provider deletion record and an ai_model one per model. Deleting the default model's provider leaves no default.
curl -X DELETE "$NIFTY_URL/api/v1/ai_providers/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Response
204 Deleted Errors: 401 403 404 429
Test unsaved provider settings
POST/ai_providers/test
Test a provider's typed settings.
Lists the models with unsaved settings (Test connection in a provider form) and answers with only how many
there are. Saves and records nothing, and ignores Idempotency-Key. GETs <base_url>/models (Anthropic:
?limit=1000), following no redirects, reading at most 5 MB, waiting up to 90 seconds.
Editing a saved provider (ai_provider_id) with a stored key, a blank or absent api_key uses the stored key
only when base_url has the stored URL's origin (scheme, host and port); otherwise it's 422 key_required and
no request is made, so a stored key never goes to another server. remove_api_key: true tests with no key,
and kind: mock never sends one. Limited to 20 a minute per IP address, shared with check and available
models.
curl -X POST "$NIFTY_URL/api/v1/ai_providers/test" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"kind":"openai_compatible","name":"Ollama","base_url":"http://localhost:11434/v1"}'
Request body
| Name | Description |
|---|---|
ai_provider_idULID · optional | The saved provider being edited, for its stored key. An unknown id is a 404. |
namestring · optional | Only names the provider in a failure's message; not validated. |
kindAIProviderKind | openai_compatible covers OpenAI, Ollama, LM Studio, llama.cpp and vLLM. mock needs no network and isn't available in production. |
base_urlstring or null · optional | |
api_keystring or null · optional | |
remove_api_keyboolean · optional | Test without the stored key. |
Response
200 Connected. Errors: 400 401 403 404 415 422 429
{
"data": {
"model_count": 1
}
}
Check a provider's connection
POST/ai_providers/{ai_provider_id}/check
Lists the models the provider offers. Records the outcome on the provider (last_succeeded_at, or
last_failed_at and last_error), which gives it a new updated_at. Takes no body; ignores
Idempotency-Key. Waits up to 90 seconds for a reply. Limited to 20 a minute per IP address, shared with
POST /ai_providers/test and GET /ai_providers/{id}/available_models.
curl -X POST "$NIFTY_URL/api/v1/ai_providers/01j9zq3k8m5x2v7c4n6b0t1r9e/check" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
ai_provider_idULID | A record's ID. |
Response
200 OK Errors: 400 401 403 404 415 422 429
{
"data": {
"models": [
"string"
]
}
}
List a provider's models
GET/ai_providers/{ai_provider_id}/available_models
The models a provider lists.
Lists the models the provider offers, with its saved settings, as check does, but records nothing: the
provider's last_* and updated_at don't change. Waits up to 90 seconds for a reply. Limited to 20 a minute
per IP address, shared with check and POST /ai_providers/test.
curl "$NIFTY_URL/api/v1/ai_providers/01j9zq3k8m5x2v7c4n6b0t1r9e/available_models" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
ai_provider_idULID | A record's ID. |
Response
200 OK Errors: 401 403 404 422 429
{
"data": {
"models": [
"string"
]
}
}
Add several models at once
POST/ai_providers/{ai_provider_id}/model_additions
Add several models to a provider at once.
Adds models in one transaction. identifiers are picked from the provider's list
(GET /ai_providers/{id}/available_models): any already added (even by a race) are skipped, not errors.
identifier is one typed by name and is validated: if it's invalid or already added, nothing is added.
With default_if_none (off by default) and no default model, the first model added becomes the default.
Every model added gets an updated_at; no other record changes. Honours Idempotency-Key.
curl -X POST "$NIFTY_URL/api/v1/ai_providers/01j9zq3k8m5x2v7c4n6b0t1r9e/model_additions" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"identifiers":["llama3.2","qwen3:8b"],"default_if_none":true}'
Path parameters
| Name | Description |
|---|---|
ai_provider_idULID | A record's ID. |
Request body
| Name | Description |
|---|---|
identifiersstring[] or null · optional | Picked identifiers. Not an array of strings, over 100, or any over 255 characters or with a NUL: 400. |
identifierstring or null · optional | One typed by name. |
default_if_noneboolean · optional |
Response
201 Added. added is empty when every identifier was already added. Errors: 400 401 403 404 409 415 422
{
"data": {
"added": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"ai_provider_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"identifier": "llama3.2",
"name": "Llama 3.2",
"is_default": true,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
],
"default_model_id": "01j9zq3k8m5x2v7c4n6b0t1r9e"
}
}
List AI models
GET/ai_models
The models the owner uses.
curl "$NIFTY_URL/api/v1/ai_models" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Query parameters
| Name | Description |
|---|---|
ai_provider_idULID · optional | Only this provider's models. |
orderid | name | updated_at · optional | The field to sort by; ties are broken by id, so the order is stable. |
directionasc | desc · optional | asc (ascending) or desc (descending), for order. |
updated_sincedate-time · optional | Only records updated at or after this time (ISO 8601 with Z or a UTC offset), ordered by updated_at, id. |
cursorstring · optional | meta.next_cursor from the previous page. |
limitinteger · optional | Items per page, up to 100; a larger number is taken as 100. |
counttrue | false · optional | true adds meta.total_count: the size of the filtered set (with updated_since too), ignoring cursor and limit. Anything but true or false is a 400. |
Response
200 OK Errors: 400 401 429
{
"data": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"ai_provider_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"identifier": "llama3.2",
"name": "Llama 3.2",
"is_default": true,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
],
"meta": {
"next_cursor": "eyJrIjpbIjIwMjYtMTAtMDlUMDg6MzA6MDBaIl19",
"limit": 1,
"total_count": 1
}
}
Add an AI model
POST/ai_models
Add a model to a provider.
identifier is what the provider calls the model (from its check, or typed in); unique per provider.
is_default: true makes it the default, clearing the old one.
curl -X POST "$NIFTY_URL/api/v1/ai_models" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ai_model":{"ai_provider_id":"01j9zq3a9b8c7d6e5f4g3h2j1k","identifier":"llama3.2","name":"Llama 3.2","is_default":true}}'
Headers
| Name | Description |
|---|---|
Idempotency-Keystring · optional | Makes a POST safe to retry for 24 hours. Needs a bearer token (400 under session auth). |
Request body
| Name | Description |
|---|---|
ai_modelobject | |
ai_model.ai_provider_idULID · optional | Create only. An unknown id is a 422 on ai_provider_id. |
ai_model.identifierstring · optional | What the provider calls the model. Unique per provider. |
ai_model.namestring · optional | The owner's label. Spaces are squished; blank is the identifier. |
ai_model.is_defaultboolean · optional |
Response
201 Created Errors: 400 401 403 409 415 422 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"ai_provider_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"identifier": "llama3.2",
"name": "Llama 3.2",
"is_default": true,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Get an AI model
GET/ai_models/{id}
curl "$NIFTY_URL/api/v1/ai_models/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Response
200 OK Errors: 401 404 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"ai_provider_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"identifier": "llama3.2",
"name": "Llama 3.2",
"is_default": true,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Change an AI model
PATCH/ai_models/{id}
Also PUT /ai_models/{id}, the same.
Rename a model or change the default.
Only the fields sent change; ai_provider_id is ignored (a model stays with its provider).
is_default: true makes this the default and gives the old default a new updated_at; false on the
default leaves none, which turns AI features off.
curl -X PATCH "$NIFTY_URL/api/v1/ai_models/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ai_model":{"ai_provider_id":"01j9zq3a9b8c7d6e5f4g3h2j1k","identifier":"llama3.2","name":"Llama 3.2","is_default":true}}'
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Request body
| Name | Description |
|---|---|
ai_modelobject | |
ai_model.ai_provider_idULID · optional | Create only. An unknown id is a 422 on ai_provider_id. |
ai_model.identifierstring · optional | What the provider calls the model. Unique per provider. |
ai_model.namestring · optional | The owner's label. Spaces are squished; blank is the identifier. |
ai_model.is_defaultboolean · optional |
Response
200 OK Errors: 400 401 403 404 415 422 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"ai_provider_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"identifier": "llama3.2",
"name": "Llama 3.2",
"is_default": true,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Remove an AI model
DELETE/ai_models/{id}
Permanent. Writes an ai_model deletion record. Removing the default leaves none, which turns AI features off.
curl -X DELETE "$NIFTY_URL/api/v1/ai_models/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Response
204 Deleted Errors: 401 403 404 429
Try a model
POST/ai_models/{ai_model_id}/trial
Try a model with a test prompt.
Sends prompt to the model and returns its reply and how long it took. Nothing is stored or logged,
and Idempotency-Key is ignored. Records the outcome on the provider, as a check does. Waits up to 90
seconds for a reply. Limited to 20 a minute per IP address.
curl -X POST "$NIFTY_URL/api/v1/ai_models/01j9zq3k8m5x2v7c4n6b0t1r9e/trial" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"prompt":"Say hello in five words."}'
Path parameters
| Name | Description |
|---|---|
ai_model_idULID | A record's ID. |
Request body
| Name | Description |
|---|---|
promptstring |
Response
200 The reply. Errors: 400 401 403 404 415 422 429
{
"data": {
"reply": "string",
"duration_ms": 1
}
}
List AI tasks
GET/ai_tasks
The built-in AI tasks and their settings.
Every task the app has (e.g. note_titles), in a fixed order. Not paginated, and tasks are never
deleted, so there's no updated_since. A task is off until turned on, and uses the default model
(GET /ai_models, is_default) unless ai_model_id picks another.
curl "$NIFTY_URL/api/v1/ai_tasks" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Response
200 OK Errors: 401 429
{
"data": [
{
"key": "note_titles",
"name": "Note titles",
"description": "Suggests a title for a note without one.",
"enabled": true,
"ai_model_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"reasoning": "none",
"max_output_tokens": 16,
"default_max_output_tokens": 1,
"backfill_pending": 1,
"updated_at": "2026-10-10T09:30:00Z"
}
]
}
Get an AI task
GET/ai_tasks/{key}
curl "$NIFTY_URL/api/v1/ai_tasks/string" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
keystring | The task's key, e.g. note_titles. |
Response
200 OK Errors: 401 404 429
{
"data": {
"key": "note_titles",
"name": "Note titles",
"description": "Suggests a title for a note without one.",
"enabled": true,
"ai_model_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"reasoning": "none",
"max_output_tokens": 16,
"default_max_output_tokens": 1,
"backfill_pending": 1,
"updated_at": "2026-10-10T09:30:00Z"
}
}
Change an AI task
PATCH/ai_tasks/{key}
Also PUT /ai_tasks/{key}, the same.
Turn a task on or off, choose its model, or set its request options.
Only the fields sent change: enabled, ai_model_id, reasoning or max_output_tokens alone leaves the
others as they were. A non-boolean enabled, an unknown ai_model_id, an unknown reasoning, or a
max_output_tokens that isn't a whole number from 16 to 32,768 is a 422.
curl -X PATCH "$NIFTY_URL/api/v1/ai_tasks/string" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ai_task":{"enabled":true,"ai_model_id":null,"reasoning":"low"}}'
Path parameters
| Name | Description |
|---|---|
keystring | The task's key, e.g. note_titles. |
Request body
| Name | Description |
|---|---|
ai_taskobject | |
ai_task.enabledboolean · optional | |
ai_task.ai_model_idULID or null · optional | The model to use; null for the default. An unknown id is a 422 on ai_model_id. |
ai_task.reasoningAITaskReasoning · optional | How hard a reasoning model thinks before replying; none answers at once. Sent to OpenAI-compatible
providers as reasoning_effort; null (the default) sends nothing, leaving it to the model. Anthropic and
Mock models ignore it (the value is kept). Anything else is a 422.
|
ai_task.max_output_tokensinteger or null · optional | Null for the task's default. Not a whole number in range (including non-finite numbers) is a 422. |
Response
200 OK Errors: 400 401 403 404 415 422 429
{
"data": {
"key": "note_titles",
"name": "Note titles",
"description": "Suggests a title for a note without one.",
"enabled": true,
"ai_model_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"reasoning": "none",
"max_output_tokens": 16,
"default_max_output_tokens": 1,
"backfill_pending": 1,
"updated_at": "2026-10-10T09:30:00Z"
}
}
Run a task on existing records
POST/ai_tasks/{ai_task_key}/backfill
Queues the task for the records it would do (backfill_pending), e.g. a title for each untitled note,
and returns how many. The work happens in the background, one model call at a time. Running it again is
safe. 404 for a task without a backfill; 422 (details.base) when the task is off or has no model.
Limited to 5 a minute per IP address.
curl -X POST "$NIFTY_URL/api/v1/ai_tasks/string/backfill" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
ai_task_keystring |
Headers
| Name | Description |
|---|---|
Idempotency-Keystring · optional | Makes a POST safe to retry for 24 hours. Needs a bearer token (400 under session auth). |
Response
202 Queued. Errors: 400 401 403 404 409 415 422 429
{
"data": {
"queued": 0
}
}