System
The server's version, updates, licence and the words for values (vocabulary).
On this page
- Get the version and update status
- Change update and error report settings
- Report an error in the web UI
- Send a test crash report
- Check for an update now
- Get the licence
- Get Nifty Plus
- Search Unsplash
- Get email forwarding
- Collect forwarded emails now
- Dismiss forwarding notices
- Get the Send to Nifty API
- Collect what's been sent now
- Dismiss Send to Nifty API notices
- Get the vocabulary
- Get open source licences
- Identify a Nifty install
Get the version and update status
GET/system
This server's version, update channel and update status.
One resource; there's no collection, updated_since or deletion record. update.status is worked out on each read
against the running version: off when this build can't check (update.can_check false: managed, no
release signing key, or a development build), or when daily checks are off (update_checks false) and there's
no result; else available while the last result is newer than version, up_to_date when it isn't,
unknown before any result. So with daily checks off, a Check now's result still shows. A failed check keeps
the last result and sets update.error. A newer release the licence doesn't cover (it came out after the
licence's updates_until, or refuses its key) is not_covered instead of available, worked out on each read,
so adding a licence that covers it makes it available without a new check. Without a licence (the trial, or
read-only) every release is offered.
curl "$NIFTY_URL/api/v1/system" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Response
200 OK Errors: 401 429
{
"data": {
"version": "1.2.0",
"channel": "stable",
"update_checks": true,
"update_checks_managed": true,
"update_checks_managed_by": "desktop",
"error_reports": true,
"error_reports_forced": true,
"plain_http": true,
"update": {
"status": "up_to_date",
"not_covered_reason": "updates_ended",
"latest_released_on": "2027-12-03",
"can_check": true,
"latest_version": "1.3.0",
"notes_url": "https://example.com",
"checked_at": "2026-10-10T09:30:00Z",
"dismissed": true,
"error": "Nifty couldn’t reach download.thenifty.app."
}
}
}
Change update and error report settings
PATCH/system
Change the update channel, turn daily checks or error reports on or off, or dismiss an update.
Only the fields sent change. A channel other than stable or beta, an update_checks that isn't a boolean
(or any update_checks while it's managed), an error_reports that isn't a boolean (or any error_reports on a
beta, error_reports_forced), or a dismissed_version that isn't a version like 1.2.3 or
1.2.3-beta.1 (or null) is a 422. Changing channel clears the last result (status unknown); changing it
or turning checks on queues a check.
curl -X PATCH "$NIFTY_URL/api/v1/system" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"system":{"channel":"stable","update_checks":true}}'
Request body
| Name | Description |
|---|---|
systemobject | |
system.channelUpdateChannel · optional | stable (the default) gets stable releases; beta gets betas too, and stable releases newer than them. |
system.update_checksboolean · optional | Check daily. |
system.error_reportsboolean · optional | Send error reports. Can't change on a beta (error_reports_forced). |
system.dismissed_versionstring or null · optional | The update the owner has seen; update.dismissed while it's at least latest_version. Null clears it. |
Response
200 OK Errors: 400 401 403 415 422 429
{
"data": {
"version": "1.2.0",
"channel": "stable",
"update_checks": true,
"update_checks_managed": true,
"update_checks_managed_by": "desktop",
"error_reports": true,
"error_reports_forced": true,
"plain_http": true,
"update": {
"status": "up_to_date",
"not_covered_reason": "updates_ended",
"latest_released_on": "2027-12-03",
"can_check": true,
"latest_version": "1.3.0",
"notes_url": "https://example.com",
"checked_at": "2026-10-10T09:30:00Z",
"dismissed": true,
"error": "Nifty couldn’t reach download.thenifty.app."
}
}
}
Report an error in the web UI
POST/error_reports
The web UI's uncaught errors, sent on through this server so the error report service never sees the browser's
address. A 202 whether or not it's sent: nothing is kept or sent while error_reports is off (see
GET /system), nor ever from a development build. The server adds the version, platform, time and
fingerprint, scrubs the message and file paths again (emails, addresses, quoted text, numbers and ids go), and
sends at most one report a fingerprint an hour and 50 a day. A 422 when a field is missing or over its limit,
or the body is over 16 KB. Limited to 30 a minute per IP address.
curl -X POST "$NIFTY_URL/api/v1/error_reports" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"kind":"error","type":"TypeError","message":"Cannot read properties of undefined (reading '\''id'\'')","frames":[{"function":"NoteView","file":"/assets/notes-AB12.js","line":1,"column":4021}],"browser":"chrome"}'
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 |
|---|---|
kinderror | panic | error for a caught error (a route's error page), panic for an uncaught one. |
typestring | The error's name. |
messagestring | Scrubbed by the client; scrubbed again here. |
framesErrorReportFrame[] | The stack, innermost first, as the bundle's files (mapped to source by the error report service). |
browserchrome | safari | firefox | edge | other · optional |
Response
202 Accepted (sent later, or dropped while reporting is off). Errors: 400 401 403 409 415 422 429
Send a test crash report
POST/error_reports/test
A beta's Send test crash report: the server panics with a TestCrash and recovers as it does from any request's
panic, so a real report (its frames included) goes through the send job to the error report service. Takes no
body. A 202 whether or not it's sent (never from a development build); a 404 unless the version is a beta.
Test reports skip the hourly duplicate check but count towards the 50 a day. Limited with POST /error_reports.
curl -X POST "$NIFTY_URL/api/v1/error_reports/test" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Response
202 Accepted (sent later). Errors: 401 403 404 415 429
Check for an update now
POST/system/check_for_updates
Fetches the channel's signed manifest and answers with the result (a failure is update.error, still a 200).
Works while daily checks are off; a 422 (details.base) when they're managed or update.can_check is false.
Takes no body; ignores Idempotency-Key. Waits up to 30 seconds. Limited to 10 a minute per IP address.
curl -X POST "$NIFTY_URL/api/v1/system/check_for_updates" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Response
200 OK Errors: 401 403 415 422 429
{
"data": {
"version": "1.2.0",
"channel": "stable",
"update_checks": true,
"update_checks_managed": true,
"update_checks_managed_by": "desktop",
"error_reports": true,
"error_reports_forced": true,
"plain_http": true,
"update": {
"status": "up_to_date",
"not_covered_reason": "updates_ended",
"latest_released_on": "2027-12-03",
"can_check": true,
"latest_version": "1.3.0",
"notes_url": "https://example.com",
"checked_at": "2026-10-10T09:30:00Z",
"dismissed": true,
"error": "Nifty couldn’t reach download.thenifty.app."
}
}
}
Get the licence
GET/licence
The licence, or the free trial, and whether Nifty is read-only.
One resource; there's no collection, updated_since or deletion record. state is licensed when the stored
key is valid, not revoked and covers this version (released on or before its updates_until); else trial
for 14 days from first launch; else read_only. A stored key that doesn't apply keeps its details, with
problem saying why.
curl "$NIFTY_URL/api/v1/licence" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Response
200 OK Errors: 401 429
{
"data": {
"state": "licensed",
"problem": "revoked",
"licensee": {
"name": "Ada Lovelace",
"email": "[email protected]"
},
"key_id": "3F9A-C27B-0E15",
"order_reference": "LS-482913",
"issued_on": "2026-10-10",
"updates_until": "2027-10-10",
"trial_ends_at": "2026-10-10T09:30:00Z",
"trial_days_left": 0,
"released_on": "2026-10-10",
"onboarded": true,
"updated_at": "2026-10-10T09:30:00Z"
}
}
Get Nifty Plus
GET/plus
Whether this install is connected to Nifty Plus, the subscription's status, and whether Plus features can be used now (docs/plus.md).
While connected, a read checks the subscription again when the last check (checked_at) is more than an
hour old. If the account service can't be reached, the last known status stays and error is unreachable.
curl "$NIFTY_URL/api/v1/plus" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Response
200 OK Errors: 401 429
{
"data": {
"state": "not_connected",
"available": true,
"unavailable_reason": "not_connected",
"install_name": "Nifty on Ada's MacBook",
"account_url": "https://account.thenifty.app/connect",
"account": {
"email": "[email protected]",
"status": "active",
"plan": "monthly",
"period_end": "2026-10-10T09:30:00Z",
"cancel_at_period_end": true
},
"error": "unreachable",
"checked_at": "2026-10-10T09:30:00Z"
}
}
Search Unsplash
GET/unsplash/photos
This endpoint is in beta, so it can change in any release (Versions).
A Nifty Plus feature: landscape photos on Unsplash for q, 24 a page, through Nifty Plus's Unsplash service.
Show urls straight from Unsplash, as its guidelines ask, with the photographer's credit. Choosing one is
PUT /projects/{id}/cover_photo/unsplash. The service limits each install to 30 searches a minute and 200 an
hour (429 rate_limited with Retry-After).
curl "$NIFTY_URL/api/v1/unsplash/photos?q=string" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Query parameters
| Name | Description |
|---|---|
qstring | Blank or longer is 400. |
pageinteger · optional |
Response
200 OK Errors: 400 401 403 429 502
{
"data": [
{
"id": "string",
"alt": "string",
"width": 1,
"height": 1,
"color": "string",
"blur_hash": "string",
"thumb_url": "https://example.com",
"preview_url": "https://example.com",
"credit": {
"name": "string",
"profile_url": "https://example.com",
"photo_url": "https://example.com"
}
}
],
"meta": {
"page": 1,
"total_pages": 1
}
}
Get email forwarding
GET/email_forwarding
A Nifty Plus feature (docs/plus.md#forwarding-emails): the address emails can be forwarded to, who may send to
it, and what collecting found. Emails forwarded there wait on Nifty Plus, sealed for this install, for up to 7
days; Nifty collects them every 2 minutes (POST /email_forwarding/check collects now), and each becomes an
unsorted document as POST /email_imports makes, with forwarded_at set.
While Plus is available, a read asks the forwarding service, making the address the first time. Otherwise, or
when the service can't be reached (error: unreachable), it's as last known.
Needs a token with full access (or a session): a read token gets 403 read_only_token, as the address lets
anyone put documents into Nifty.
curl "$NIFTY_URL/api/v1/email_forwarding" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Response
200 OK Errors: 401 403 429
{
"data": {
"available": true,
"unavailable_reason": "not_connected",
"address": "[email protected]",
"senders": {
"anyone": true,
"addresses": [
"[email protected]"
]
},
"waiting": 0,
"error": "unreachable",
"checked_at": "2026-10-10T09:30:00Z",
"collected_at": "2026-10-10T09:30:00Z",
"lost": 0,
"unreadable": 0
}
}
Collect forwarded emails now
POST/email_forwarding/check
Collects the emails waiting now (and anything sent through the Send to Nifty API), rather than at the next
2-minute check. Works after the subscription ends,
for emails that arrived before; not while read-only (403). 409 conflict when Plus isn't connected.
curl -X POST "$NIFTY_URL/api/v1/email_forwarding/check" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Response
200 Collected; collected_at and waiting say what's changed. Errors: 401 403 409 429 500 502
{
"data": {
"available": true,
"unavailable_reason": "not_connected",
"address": "[email protected]",
"senders": {
"anyone": true,
"addresses": [
"[email protected]"
]
},
"waiting": 0,
"error": "unreachable",
"checked_at": "2026-10-10T09:30:00Z",
"collected_at": "2026-10-10T09:30:00Z",
"lost": 0,
"unreadable": 0
}
}
Dismiss forwarding notices
DELETE/email_forwarding/notices
Sets lost and unreadable back to 0, once the owner has seen them.
curl -X DELETE "$NIFTY_URL/api/v1/email_forwarding/notices" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Response
200 OK Errors: 401 403 429
{
"data": {
"available": true,
"unavailable_reason": "not_connected",
"address": "[email protected]",
"senders": {
"anyone": true,
"addresses": [
"[email protected]"
]
},
"waiting": 0,
"error": "unreachable",
"checked_at": "2026-10-10T09:30:00Z",
"collected_at": "2026-10-10T09:30:00Z",
"lost": 0,
"unreadable": 0
}
}
Get the Send to Nifty API
GET/send_api
A Nifty Plus feature (docs/plus.md#send-to-nifty-api): with an API token, other apps and services can send
tasks, notes, events, documents and emails to url. They wait on Nifty Plus, sealed for this install, for up to
7 days; Nifty collects them every 2 minutes (POST /send_api/check collects now) and adds each under the ID the
sender was given, with received_via_api set.
While Plus is available, a read asks the service. Otherwise, or when it can't be reached (error: unreachable), it's as last known. Needs a token with full access (or a session): a read token gets 403 read_only_token.
curl "$NIFTY_URL/api/v1/send_api" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Response
200 OK Errors: 401 403 429
{
"data": {
"available": true,
"unavailable_reason": "not_connected",
"url": "https://api.nifty.fyi",
"tokens": [
{
"id": "string",
"name": "Zapier",
"ending": "string",
"created_at": "2026-10-10T09:30:00Z",
"last_used_at": "2026-10-10T09:30:00Z"
}
],
"waiting": 0,
"error": "unreachable",
"checked_at": "2026-10-10T09:30:00Z",
"collected_at": "2026-10-10T09:30:00Z",
"skipped": 0,
"lost": 0,
"expired": 0
}
}
Collect what's been sent now
POST/send_api/check
Collects what's waiting now (forwarded emails too), up to 50 items, rather than at the next 2-minute check.
Works after the subscription ends, for what arrived before; not while read-only (403). 409 conflict when
Plus isn't connected.
curl -X POST "$NIFTY_URL/api/v1/send_api/check" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Response
200 Collected; collected_at and waiting say what's changed. Errors: 401 403 409 429 500 502
{
"data": {
"available": true,
"unavailable_reason": "not_connected",
"url": "https://api.nifty.fyi",
"tokens": [
{
"id": "string",
"name": "Zapier",
"ending": "string",
"created_at": "2026-10-10T09:30:00Z",
"last_used_at": "2026-10-10T09:30:00Z"
}
],
"waiting": 0,
"error": "unreachable",
"checked_at": "2026-10-10T09:30:00Z",
"collected_at": "2026-10-10T09:30:00Z",
"skipped": 0,
"lost": 0,
"expired": 0
}
}
Dismiss Send to Nifty API notices
DELETE/send_api/notices
Sets skipped, lost and expired back to 0, once the owner has seen them.
curl -X DELETE "$NIFTY_URL/api/v1/send_api/notices" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Response
200 OK Errors: 401 403 429
{
"data": {
"available": true,
"unavailable_reason": "not_connected",
"url": "https://api.nifty.fyi",
"tokens": [
{
"id": "string",
"name": "Zapier",
"ending": "string",
"created_at": "2026-10-10T09:30:00Z",
"last_used_at": "2026-10-10T09:30:00Z"
}
],
"waiting": 0,
"error": "unreachable",
"checked_at": "2026-10-10T09:30:00Z",
"collected_at": "2026-10-10T09:30:00Z",
"skipped": 0,
"lost": 0,
"expired": 0
}
}
Get the vocabulary
GET/vocabulary
Every fixed list a client shows, with labels.
Types, statuses, labels, colours, icons, appearance and sound choices, and upload limits, so a client needn't hard-code them. Every list is in display order. Not paginated; it changes only with a server release.
curl "$NIFTY_URL/api/v1/vocabulary" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Response
200 OK Errors: 401 429
{
"data": {
"relationships": [
{
"key": "spouse",
"label": "direct report",
"group": "family"
}
],
"person_links": [
{
"key": "spouse",
"label": "parent",
"group": "family",
"inverse_key": "spouse",
"inverse_label": "child",
"symmetric": true,
"synonyms": [
"mother",
"father",
"mum",
"mom",
"dad"
]
}
],
"key_date_kinds": [
{
"key": "birthday",
"label": "Work anniversary",
"icon": "cake"
}
],
"social_platforms": [
{
"key": "website",
"label": "LinkedIn"
}
],
"relationship_groups": [
{
"key": "family",
"label": "Family",
"icon": "house"
}
],
"contact_labels": {
"email_addresses": {
"suggestions": [
"Personal",
"Work",
"Other"
],
"default": "string"
},
"phone_numbers": {
"suggestions": [
"Personal",
"Work",
"Other"
],
"default": "string"
},
"social_profiles": {
"suggestions": [
"Personal",
"Work",
"Other"
],
"default": "string"
}
},
"project_statuses": [
{
"key": "planned",
"label": "On hold",
"icon": "circle-pause",
"tone": "neutral",
"open": true
}
],
"task_count_kinds": [
{
"key": "open",
"label": "Won’t do",
"icon": "circle-slash"
}
],
"colors": [
{
"key": "teal",
"label": "Teal"
}
],
"picker_icons": [
"folder"
],
"accents": [
{
"key": "iris",
"label": "Iris"
}
],
"cover_colors": [
{
"key": "dusk",
"label": "Dusk",
"kind": "gradient"
}
],
"themes": [
{
"key": "system",
"label": "Match device"
}
],
"code_themes": [
{
"key": "match",
"label": "Match theme"
}
],
"text_sizes": [
{
"key": "small",
"label": "Medium"
}
],
"mood_groups": [
{
"key": "tense",
"label": "Tense"
}
],
"moods": [
{
"key": "anxious",
"label": "Anxious",
"emoji": "😟",
"group": "tense"
}
],
"event_types": [
{
"key": "string",
"label": "string",
"icon": "string"
}
],
"note_kinds": [
{
"key": "meeting",
"label": "Meeting",
"icon": "users"
}
],
"insight_kinds": [
{
"key": "you_owe",
"label": "You owe",
"icon": "send"
}
],
"sounds": {
"task_completed": [
{
"key": "pop_in",
"label": "Tap trio"
}
],
"created": [
{
"key": "blip_blip",
"label": "Blip blip"
}
],
"deleted": [
{
"key": "knock_away",
"label": "Paper away"
}
],
"calendar_alert": [
{
"key": "arpeggio",
"label": "Music box"
}
]
},
"ai_provider_kinds": [
{
"key": "openai_compatible",
"label": "OpenAI-compatible"
}
],
"uploads": {
"avatar": {
"content_types": [
"image/jpeg",
"image/png"
],
"max_bytes": 10485760
},
"note_image": {
"content_types": [
"image/jpeg",
"image/png"
],
"max_bytes": 10485760
},
"document": {
"content_types": [
"image/jpeg",
"image/png"
],
"max_bytes": 10485760
},
"email": {
"content_types": [
"image/jpeg",
"image/png"
],
"max_bytes": 10485760
}
},
"calendar_sync_frequencies": [
{
"key": "30_minutes",
"label": "30 minutes",
"minutes": 30
}
],
"meeting_providers": [
{
"key": "google_meet",
"label": "Google Meet"
}
],
"calendar_sync_errors": [
{
"key": "not_found",
"label": "the server refused the address"
}
],
"document_link_kinds": [
{
"key": "person",
"label": "Person"
}
],
"calendar_sources": [
{
"key": "google",
"label": "Google Calendar",
"url": "https://calendar.google.com/"
}
],
"api_token_scopes": [
{
"key": "full",
"label": "Read only"
}
],
"backup_reasons": [
{
"key": "before_update",
"label": "Before update"
}
],
"webhook_events": [
{
"key": "task.created",
"label": "Task completed"
}
],
"webhook_outcomes": [
{
"key": "pending",
"label": "Succeeded"
}
],
"limits": {
"tag_name": 50,
"folder_name": 100,
"document_name": 200,
"task_title": 200,
"task_list_name": 100,
"project_name": 100,
"project_text": 100,
"project_url": 2048,
"project_member_role": 100,
"position_title": 100,
"company_name": 100,
"calendar_feed_name": 100,
"calendar_address": 2048,
"webhook_name": 100,
"webhook_url": 2048
}
}
}
Get open source licences
GET/licences
The open source software in this build, with its licences.
The notices distributed with Nifty, grouped by where the software is used. Each package names its licence
text by licence_id in licences, so a shared text appears once. Not paginated; it changes only with a
server release.
curl "$NIFTY_URL/api/v1/licences" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Response
200 OK Errors: 401 429
{
"data": {
"groups": [
{
"key": "web",
"label": "string",
"packages": [
{
"name": "string",
"version": "string",
"licence": "string",
"url": "string",
"licence_id": "string"
}
]
}
],
"licences": [
{
"id": "string",
"text": "string"
}
]
}
}
Identify a Nifty install
GET/.well-known/nifty
Unauthenticated. Reveals nothing about the owner.
curl "$NIFTY_URL/api/v1/.well-known/nifty" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Response
200 OK
{
"app": "nifty",
"version": "1.2.3",
"min_shell_version": "0.0.0",
"api_versions": [
{
"version": "v1",
"base_url": "https://example.com"
}
]
}