Calendar
Events from your calendar feeds and your own events on My Nifty Calendar.
On this page
- Get a range of the calendar
- Set an imported event's type
- Get the coming calendar alerts
- Hide a calendar event
- Undo hiding a calendar event
- Ignore a calendar event
- Stop ignoring a calendar event
- Get My Nifty Calendar's settings
- Change My Nifty Calendar's settings
- List your own events
- Add an event
- Get an event
- Change an event
- Delete an event
- Restore an event
- List calendar feeds
- Add a calendar feed
- Sync every calendar feed now
- Get a calendar feed
- Change a calendar feed
- Remove a calendar feed
- Sync a calendar feed now
- List event type rules
- Add an event type rule
- Count the events a rule would type
- Get an event type rule
- Change or move an event type rule
- Delete an event type rule
Get a range of the calendar
GET/calendar
Everything dated from from to to (inclusive, in the owner's time zone): the calendar feeds' events,
expanded from their repeat rules, the owner's own events (calendar_entries, with entry_id set), and Nifty's own dates (tasks due, project deadlines and starts, key dates).
A view, not a record: there's nothing to page or sync, so ask again when things change. Expired tasks are
archived first.
events: each occurrence touching the range (a timed event that started beforefrombut is still on, a multi-day all-day event), by start, all-day ones first on a day; then title, thenkey.tasks: live tasks,openorcompleted, whosedeadline_onis in the range (an overdue open task only on its own date), bydeadline_on, then title.project_dates: live projects that areplanned,activeoron_hold, with adeadlineorstart_datein the range, by date, then name.key_dates: live people's key dates falling in the range (29 February on 1 March in other years), by date, then person name, then kind.people: the people the events' attendees are, by name.
curl "$NIFTY_URL/api/v1/calendar?from=2026-10-10&to=2026-10-10" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Query parameters
| Name | Description |
|---|---|
event_typesstring · optional | Comma-separated event-type vocabulary keys, or none for events with no effective type. Matches any supplied type; omitted means all. Empty or unknown keys are a 400. Cannot be combined with updated_since on calendar_entries (use unfiltered sync to see events leaving a type). Filters events only, not tasks or other dated records. |
fromdate | The first day, YYYY-MM-DD. |
todate | The last day, YYYY-MM-DD; on or after from, and at most 42 days in all. Otherwise 400. |
Response
200 OK Errors: 400 401 429
{
"data": {
"from": "2026-10-10",
"to": "2026-10-10",
"today": "2026-10-10",
"events": [
{
"type_key": "string",
"received_via_api": {
"at": "2026-10-10T09:30:00Z",
"source": "Zapier"
},
"key": "string",
"link_key": "string",
"feed_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"entry_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"title": "Team stand-up",
"all_day": true,
"starts_at": "2026-10-10T09:30:00Z",
"ends_at": "2026-10-10T09:30:00Z",
"start_on": "2026-10-10",
"end_on": "2026-10-10",
"location": "Room 2",
"description": "Weekly check-in.",
"url": "string",
"meeting": {
"provider": "google_meet",
"url": "https://example.com"
},
"attendee_count": 0,
"attendee_names": [
"string"
],
"attendees": [
{
"name": "Maya Okafor",
"email": "[email protected]",
"organizer": true,
"person_id": "01j9zq3k8m5x2v7c4n6b0t1r9e"
}
],
"rrule": "FREQ=WEEKLY;BYDAY=MO,WE",
"repeats": true,
"series_key": "string",
"ignored": "event"
}
],
"tasks": [
{
"planned_start_on": "2026-10-10",
"delays": 0,
"deadline_moves": 0,
"delegated_at": "2026-10-10T09:30:00Z",
"now_since": "2026-10-10T09:30:00Z",
"received_via_api": {
"at": "2026-10-10T09:30:00Z",
"source": "Zapier"
},
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"title": "Book the plumber",
"status": "open",
"start_on": "2026-10-10",
"deadline_on": "2026-10-10",
"expires_on": "2026-10-10",
"completed_at": "2026-10-10T09:30:00Z",
"archived_at": "2026-10-10T09:30:00Z",
"task_list_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"project_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"assignee_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"position": 1,
"description_html": "<p>Ask about the boiler too.</p>",
"description_text": "Ask about the boiler too.",
"has_documents": true,
"document_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"note_ids": [
"01j9zq3k8m5x2v7c4n6b0t1r9e"
],
"expired": true,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
],
"project_dates": [
{
"date": "2026-10-10",
"kind": "deadline",
"project": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Kitchen renovation",
"status": "planned",
"status_changed_at": "2026-10-10T09:30:00Z",
"start_date": "2026-10-10",
"deadline": "2026-10-10",
"external_reference": "JIRA-123",
"external_url": "https://example.com",
"description_html": "<p>New units and worktops.</p>",
"description_text": "New units and worktops.",
"cover": {
"kind": "color",
"color": "dusk",
"photo": {
"url": "https://example.com",
"preview_url": "https://example.com",
"thumb_url": "https://example.com",
"width": 1,
"height": 1,
"credit": {
"name": "string",
"profile_url": "https://example.com",
"photo_url": "https://example.com"
}
},
"position": 0
},
"members": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"person_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"role": "string"
}
],
"pinned": true,
"pinned_at": "2026-10-10T09:30:00Z",
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
],
"key_dates": [
{
"date": "2026-10-10",
"years": 1,
"key_date": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"kind": "birthday",
"label": "Graduation",
"day": 1,
"month": 1,
"year": 1,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
},
"person": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Maya Okafor",
"avatar": {
"url": "https://example.com",
"thumb_url": "https://example.com",
"large_url": "https://example.com"
}
}
}
],
"people": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Maya Okafor",
"aliases": [
"Mum"
],
"is_me": true,
"time_zone": "America/New_York",
"avatar": {
"url": "https://example.com",
"thumb_url": "https://example.com",
"large_url": "https://example.com"
},
"key_dates": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"kind": "birthday",
"label": "Graduation",
"day": 1,
"month": 1,
"year": 1,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
],
"email_addresses": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"address": "[email protected]",
"label": "Work",
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
],
"phone_numbers": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"number": "+44 20 7946 0958",
"label": "Mobile",
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
],
"social_profiles": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"url": "https://example.com",
"platform": "website",
"platform_detected": true,
"label": "Blog",
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
],
"positions": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"title": "Head of Design",
"company_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
],
"connections": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"person_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"kind": "spouse",
"note": "Met at university"
}
],
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
]
}
}
Set an imported event's type
PUT/calendar/event_type
Applies to every occurrence with this event's UID in this feed. The source calendar is unchanged. Null clears the type. Initial assignment happens once on import. Annotations survive refresh, but are removed when the UID leaves a successfully refreshed feed or the feed is deleted.
curl -X PUT "$NIFTY_URL/api/v1/calendar/event_type" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"event_type":{"key":"string","feed_id":"01j9zq3k8m5x2v7c4n6b0t1r9e","date":"2026-10-10","type_key":"string"}}'
Request body
| Name | Description |
|---|---|
event_typeobject | |
event_type.keystring | An occurrence key from GET /calendar. |
event_type.feed_idULID | A record's ID. |
event_type.datedate | |
event_type.type_keyEventTypeKey | A key from event_types, or null for No type. On creation, omission assigns a type once, from the owner's rules (/calendar_type_rules) and then deterministic ones; while it has no type, a rule added or changed later may give it one. Sending it, even null, is the owner's choice, which no rule changes. On update, omission preserves the saved type. |
Response
204 Saved Errors: 400 401 403 404 415 422 429
Get the coming calendar alerts
GET/calendar/alerts
The alerts due from 2 minutes ago to 12 hours ahead, for the desktop app to show: one per timed occurrence in a
feed with alert_minutes set, due that many minutes before it starts. All-day events never alert. By
alert_at, then the event's key. An occurrence's key and alert_at together name an alert: an event moved
to a new time alerts again.
curl "$NIFTY_URL/api/v1/calendar/alerts" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Response
200 OK Errors: 401 429
{
"data": [
{
"alert_at": "2026-10-10T09:30:00Z",
"minutes": 0,
"event": {
"type_key": "string",
"received_via_api": {
"at": "2026-10-10T09:30:00Z",
"source": "Zapier"
},
"key": "string",
"link_key": "string",
"feed_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"entry_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"title": "Team stand-up",
"all_day": true,
"starts_at": "2026-10-10T09:30:00Z",
"ends_at": "2026-10-10T09:30:00Z",
"start_on": "2026-10-10",
"end_on": "2026-10-10",
"location": "Room 2",
"description": "Weekly check-in.",
"url": "string",
"meeting": {
"provider": "google_meet",
"url": "https://example.com"
},
"attendee_count": 0,
"attendee_names": [
"string"
],
"attendees": [
{
"name": "Maya Okafor",
"email": "[email protected]",
"organizer": true,
"person_id": "01j9zq3k8m5x2v7c4n6b0t1r9e"
}
],
"rrule": "FREQ=WEEKLY;BYDAY=MO,WE",
"repeats": true,
"series_key": "string",
"ignored": "event"
}
}
]
}
Hide a calendar event
POST/calendar/hidden_events
Hides one occurrence of a feed's event, by its key, everywhere: GET /calendar, the alerts, a person's
meetings and the tools leave it out from then on, across syncs. Hiding a hidden occurrence changes nothing.
Only DELETE /calendar/hidden_events/{key} within 10 seconds brings it back. An unknown feed, or a key that
isn't one of its occurrences on date, is a 422. A hide goes when its event leaves the feed.
curl -X POST "$NIFTY_URL/api/v1/calendar/hidden_events" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"hidden_event":{"key":"q1w2e3r4t5y6u7i8o9p0","feed_id":"01j9zq3k8m5x2v7c4n6b0t1r9e","date":"2026-10-09"}}'
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 |
|---|---|
hidden_eventobject | |
hidden_event.keystring | The occurrence's key (CalendarEvent.key). |
hidden_event.feed_idULID | The occurrence's feed (CalendarEvent.feed_id): removing that feed drops the hide. |
hidden_event.datedate | The day the occurrence starts, in the owner's time zone (its start_on, or starts_at's date there). |
Response
204 Hidden Errors: 400 401 403 409 415 422 429
Undo hiding a calendar event
DELETE/calendar/hidden_events/{key}
Within 10 seconds of hiding it, shows the occurrence again. Once they've passed, or for an occurrence that isn't hidden, it's a 404: a hide is otherwise for good.
curl -X DELETE "$NIFTY_URL/api/v1/calendar/hidden_events/string" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
keystring |
Response
204 Shown again Errors: 401 403 404 429
Ignore a calendar event
POST/calendar/ignored_events
Ignores one occurrence of a feed's event, by its key, or (repeats) every occurrence of its event. GET /calendar still gives it, with ignored set; the alerts, a person's meetings and the tools leave it out, across
syncs. Ignoring an ignored occurrence changes nothing. DELETE /calendar/ignored_events/{key} undoes it at any
time. An unknown feed, or a key that isn't one of its occurrences on date, is a 422. An ignore goes when its
event leaves the feed.
curl -X POST "$NIFTY_URL/api/v1/calendar/ignored_events" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ignored_event":{"key":"q1w2e3r4t5y6u7i8o9p0","feed_id":"01j9zq3k8m5x2v7c4n6b0t1r9e","date":"2026-10-09","repeats":false}}'
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 |
|---|---|
ignored_eventobject | |
ignored_event.keystring | The occurrence's key (CalendarEvent.key). |
ignored_event.feed_idULID | The occurrence's feed (CalendarEvent.feed_id): removing that feed drops the ignore. |
ignored_event.datedate | The day the occurrence starts, in the owner's time zone (its start_on, or starts_at's date there). |
ignored_event.repeatsboolean · optional | Every occurrence of its event, not only this one. For an event that doesn't repeat, only this one (ignored is event). |
Response
204 Ignored Errors: 400 401 403 409 415 422 429
Stop ignoring a calendar event
DELETE/calendar/ignored_events/{key}
Stops ignoring the occurrence: both its own ignore and its event's (repeats), so it's no longer ignored. The
occurrence is found as for an ignore; one that isn't ignored, or isn't on date, is a 404.
curl -X DELETE "$NIFTY_URL/api/v1/calendar/ignored_events/string?feed_id=01j9zq3k8m5x2v7c4n6b0t1r9e&date=2026-10-10" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
keystring |
Query parameters
| Name | Description |
|---|---|
feed_idULID | The occurrence's feed. |
datedate | The day the occurrence starts, in the owner's time zone. |
Response
204 No longer ignored Errors: 400 401 403 404 429
Get My Nifty Calendar's settings
GET/calendar/nifty
The built-in calendar that holds the owner's own events (/calendar_entries). It always exists.
curl "$NIFTY_URL/api/v1/calendar/nifty" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Response
200 OK Errors: 401 429
{
"data": {
"color": "fuchsia",
"hidden_from_today": true,
"updated_at": "2026-10-10T09:30:00Z"
}
}
Change My Nifty Calendar's settings
PATCH/calendar/nifty
Only the fields sent change.
curl -X PATCH "$NIFTY_URL/api/v1/calendar/nifty" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"nifty_calendar":{"color":"fuchsia","hidden_from_today":false}}'
Request body
| Name | Description |
|---|---|
nifty_calendarobject | |
nifty_calendar.colorstring · optional | A key from the vocabulary's colors. |
nifty_calendar.hidden_from_todayboolean · optional | Leave its events off Today, This week and Next week (Calendar still shows them). |
Response
200 OK Errors: 400 401 403 415 422 429
{
"data": {
"color": "fuchsia",
"hidden_from_today": true,
"updated_at": "2026-10-10T09:30:00Z"
}
}
List your own events
GET/calendar_entries
The owner's own events on My Nifty Calendar, by day. For a range of the calendar, in order, use
GET /calendar; this listing is for sync.
curl "$NIFTY_URL/api/v1/calendar_entries" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Query parameters
| Name | Description |
|---|---|
event_typesstring · optional | Comma-separated event-type vocabulary keys, or none for events with no effective type. Matches any supplied type; omitted means all. Empty or unknown keys are a 400. Cannot be combined with updated_since on calendar_entries (use unfiltered sync to see events leaving a type). Filters events only, not tasks or other dated records. |
orderdate | id | updated_at · optional | date sorts by day, with id as the tie-breaker. |
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": [
{
"type_key": "string",
"received_via_api": {
"at": "2026-10-10T09:30:00Z",
"source": "Zapier"
},
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"title": "Dentist",
"date": "2026-10-10",
"all_day": true,
"start_time": "09:30",
"end_time": "10:30",
"time_zone": "Europe/London",
"starts_at": "2026-10-10T09:30:00Z",
"ends_at": "2026-10-10T09:30:00Z",
"location": "string",
"notes": "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 event
POST/calendar_entries
Adds an event to My Nifty Calendar. Needs title and date, and for a timed event start_time and end_time.
curl -X POST "$NIFTY_URL/api/v1/calendar_entries" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"calendar_entry":{"title":"Car insurance expires","date":"2026-11-14","all_day":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 |
|---|---|
calendar_entryobject | |
calendar_entry.type_keyEventTypeKey · optional | A key from event_types, or null for No type. On creation, omission assigns a type once, from the owner's rules (/calendar_type_rules) and then deterministic ones; while it has no type, a rule added or changed later may give it one. Sending it, even null, is the owner's choice, which no rule changes. On update, omission preserves the saved type. |
calendar_entry.titlestring · optional | Required on create; spaces are squished; blank is a 422. |
calendar_entry.datedate · optional | Required on create. The day, in time_zone for a timed event. |
calendar_entry.all_dayboolean · optional | False on create unless sent. True clears start_time, end_time and time_zone. |
calendar_entry.start_timestring or null · optional | HH:MM, 24-hour, in time_zone. Required for a timed event. |
calendar_entry.end_timestring or null · optional | HH:MM, after start_time on the same day. Required for a timed event. |
calendar_entry.time_zonestring or null · optional | An IANA zone (Europe/London); null or unsent on a timed event takes the owner's. |
calendar_entry.locationstring or null · optional | Plain text; blank is null. |
calendar_entry.notesstring or null · optional | Plain text; blank is null. |
Response
201 Created Errors: 400 401 403 409 415 422 429
{
"data": {
"type_key": "string",
"received_via_api": {
"at": "2026-10-10T09:30:00Z",
"source": "Zapier"
},
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"title": "Dentist",
"date": "2026-10-10",
"all_day": true,
"start_time": "09:30",
"end_time": "10:30",
"time_zone": "Europe/London",
"starts_at": "2026-10-10T09:30:00Z",
"ends_at": "2026-10-10T09:30:00Z",
"location": "string",
"notes": "string",
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Get an event
GET/calendar_entries/{id}
A deleted event is a 404.
curl "$NIFTY_URL/api/v1/calendar_entries/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Response
200 OK Errors: 401 404 429
{
"data": {
"type_key": "string",
"received_via_api": {
"at": "2026-10-10T09:30:00Z",
"source": "Zapier"
},
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"title": "Dentist",
"date": "2026-10-10",
"all_day": true,
"start_time": "09:30",
"end_time": "10:30",
"time_zone": "Europe/London",
"starts_at": "2026-10-10T09:30:00Z",
"ends_at": "2026-10-10T09:30:00Z",
"location": "string",
"notes": "string",
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Change an event
PATCH/calendar_entries/{id}
Only the fields sent change; the result must still be a whole event (a timed one keeps its times). Setting
all_day true clears the times and zone; setting it false needs start_time and end_time.
curl -X PATCH "$NIFTY_URL/api/v1/calendar_entries/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"calendar_entry":{"title":"Car insurance expires","date":"2026-11-14","all_day":true}}'
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Request body
| Name | Description |
|---|---|
calendar_entryobject | |
calendar_entry.type_keyEventTypeKey · optional | A key from event_types, or null for No type. On creation, omission assigns a type once, from the owner's rules (/calendar_type_rules) and then deterministic ones; while it has no type, a rule added or changed later may give it one. Sending it, even null, is the owner's choice, which no rule changes. On update, omission preserves the saved type. |
calendar_entry.titlestring · optional | Required on create; spaces are squished; blank is a 422. |
calendar_entry.datedate · optional | Required on create. The day, in time_zone for a timed event. |
calendar_entry.all_dayboolean · optional | False on create unless sent. True clears start_time, end_time and time_zone. |
calendar_entry.start_timestring or null · optional | HH:MM, 24-hour, in time_zone. Required for a timed event. |
calendar_entry.end_timestring or null · optional | HH:MM, after start_time on the same day. Required for a timed event. |
calendar_entry.time_zonestring or null · optional | An IANA zone (Europe/London); null or unsent on a timed event takes the owner's. |
calendar_entry.locationstring or null · optional | Plain text; blank is null. |
calendar_entry.notesstring or null · optional | Plain text; blank is null. |
Response
200 OK Errors: 400 401 403 404 415 422 429
{
"data": {
"type_key": "string",
"received_via_api": {
"at": "2026-10-10T09:30:00Z",
"source": "Zapier"
},
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"title": "Dentist",
"date": "2026-10-10",
"all_day": true,
"start_time": "09:30",
"end_time": "10:30",
"time_zone": "Europe/London",
"starts_at": "2026-10-10T09:30:00Z",
"ends_at": "2026-10-10T09:30:00Z",
"location": "string",
"notes": "string",
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Delete an event
DELETE/calendar_entries/{id}
Restorable for 10 seconds (POST /calendar_entries/{id}/restore), then permanent. Writes a calendar_entry deletion record at once. Deleting again is a 404.
curl -X DELETE "$NIFTY_URL/api/v1/calendar_entries/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Response
204 Deleted Errors: 401 403 404 429
Restore an event
POST/calendar_entries/{id}/restore
Undo deleting an event.
Within 10 seconds of DELETE /calendar_entries/{id}: brings the event back, removes its deletion record and
gives it a new updated_at. No body. Restoring an event that isn't deleted changes nothing and returns it, so
a retry is safe. Once the 10 seconds have passed, or for an unknown id, it's a 404.
curl -X POST "$NIFTY_URL/api/v1/calendar_entries/01j9zq3k8m5x2v7c4n6b0t1r9e/restore" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Headers
| Name | Description |
|---|---|
Idempotency-Keystring · optional | Makes a POST safe to retry for 24 hours. Needs a bearer token (400 under session auth). |
Response
200 Restored. The event, as GET /calendar_entries/{id} returns it. Errors: 400 401 403 404 409 415 429
{
"data": {
"type_key": "string",
"received_via_api": {
"at": "2026-10-10T09:30:00Z",
"source": "Zapier"
},
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"title": "Dentist",
"date": "2026-10-10",
"all_day": true,
"start_time": "09:30",
"end_time": "10:30",
"time_zone": "Europe/London",
"starts_at": "2026-10-10T09:30:00Z",
"ends_at": "2026-10-10T09:30:00Z",
"location": "string",
"notes": "string",
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
List calendar feeds
GET/calendar_feeds
The iCal addresses whose events the calendar shows. The address itself is never returned.
curl "$NIFTY_URL/api/v1/calendar_feeds" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Query parameters
| Name | Description |
|---|---|
ordername | id | updated_at · optional | name sorts ignoring case and accents, with id as the tie-breaker. |
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": "Work",
"color": "teal",
"address_host": "calendar.google.com",
"source": "google",
"sync_every": "30_minutes",
"alert_minutes": 0,
"hidden_from_today": true,
"last_synced_at": "2026-10-10T09:30:00Z",
"last_attempt_at": "2026-10-10T09:30:00Z",
"sync_error": {
"code": "not_found",
"http_status": 100,
"failures": 1
},
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
],
"meta": {
"next_cursor": "eyJrIjpbIjIwMjYtMTAtMDlUMDg6MzA6MDBaIl19",
"limit": 1,
"total_count": 1
}
}
Add a calendar feed
POST/calendar_feeds
Fetches the address first (waiting up to 30 seconds), then saves the feed with its events. An address that
can't be read is a 422 on address saying why ("Address couldn't be read because the server says there's no
calendar there (404)"), and nothing is saved. A blank name takes the calendar's own (X-WR-CALNAME), else
the address's host.
curl -X POST "$NIFTY_URL/api/v1/calendar_feeds" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"calendar_feed":{"name":"Work","address":"https://calendar.example.com/feeds/work.ics","color":"blue","sync_every":"hourly","alert_minutes":5,"hidden_from_today":false}}'
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 |
|---|---|
calendar_feedobject | |
calendar_feed.namestring or null · optional | Spaces are squished. Unique ignoring case. Blank on create takes the calendar's own name, else the address's host; blank on update is a 422. |
calendar_feed.addressstring · optional | Required on create. An https://, http:// or webcal:// iCal address (webcal is fetched over https). Write-only: stored encrypted, never returned; only its host is (address_host). |
calendar_feed.colorstring · optional | A key from the vocabulary's colors. Required on create. |
calendar_feed.sync_everyCalendarSyncFrequency · optional | How often the feed is fetched; the words are the vocabulary's calendar_sync_frequencies. |
calendar_feed.alert_minutesinteger or null · optional | Minutes before an event starts that the desktop app alerts (0: as it starts); null turns alerts off. Off on create unless sent. |
calendar_feed.hidden_from_todayboolean · optional | Leave its events off Today, This week and Next week (Calendar still shows them). False on create unless sent. |
Response
201 Created Errors: 400 401 403 409 415 422 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Work",
"color": "teal",
"address_host": "calendar.google.com",
"source": "google",
"sync_every": "30_minutes",
"alert_minutes": 0,
"hidden_from_today": true,
"last_synced_at": "2026-10-10T09:30:00Z",
"last_attempt_at": "2026-10-10T09:30:00Z",
"sync_error": {
"code": "not_found",
"http_status": 100,
"failures": 1
},
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Sync every calendar feed now
POST/calendar_feeds/sync
Fetches every feed (up to 4 at once, each waiting up to 30 seconds), ignoring their schedules and backoff, and
returns them all, by name. A feed that couldn't be read is still in the 200, with sync_error set. Takes no
body; ignores Idempotency-Key.
curl -X POST "$NIFTY_URL/api/v1/calendar_feeds/sync" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Response
200 OK Errors: 400 401 403 415 429
{
"data": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Work",
"color": "teal",
"address_host": "calendar.google.com",
"source": "google",
"sync_every": "30_minutes",
"alert_minutes": 0,
"hidden_from_today": true,
"last_synced_at": "2026-10-10T09:30:00Z",
"last_attempt_at": "2026-10-10T09:30:00Z",
"sync_error": {
"code": "not_found",
"http_status": 100,
"failures": 1
},
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
]
}
Get a calendar feed
GET/calendar_feeds/{id}
curl "$NIFTY_URL/api/v1/calendar_feeds/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": "Work",
"color": "teal",
"address_host": "calendar.google.com",
"source": "google",
"sync_every": "30_minutes",
"alert_minutes": 0,
"hidden_from_today": true,
"last_synced_at": "2026-10-10T09:30:00Z",
"last_attempt_at": "2026-10-10T09:30:00Z",
"sync_error": {
"code": "not_found",
"http_status": 100,
"failures": 1
},
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Change a calendar feed
PATCH/calendar_feeds/{id}
Only the fields sent change. A new address is fetched first, as on create (a 422 when it can't be read,
and nothing changes); the name, colour, frequency, alerts and hidden_from_today save without a fetch. A new sync_every moves the next
sync.
curl -X PATCH "$NIFTY_URL/api/v1/calendar_feeds/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"calendar_feed":{"name":"Work","address":"https://calendar.example.com/feeds/work.ics","color":"blue","sync_every":"hourly","alert_minutes":5,"hidden_from_today":false}}'
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Request body
| Name | Description |
|---|---|
calendar_feedobject | |
calendar_feed.namestring or null · optional | Spaces are squished. Unique ignoring case. Blank on create takes the calendar's own name, else the address's host; blank on update is a 422. |
calendar_feed.addressstring · optional | Required on create. An https://, http:// or webcal:// iCal address (webcal is fetched over https). Write-only: stored encrypted, never returned; only its host is (address_host). |
calendar_feed.colorstring · optional | A key from the vocabulary's colors. Required on create. |
calendar_feed.sync_everyCalendarSyncFrequency · optional | How often the feed is fetched; the words are the vocabulary's calendar_sync_frequencies. |
calendar_feed.alert_minutesinteger or null · optional | Minutes before an event starts that the desktop app alerts (0: as it starts); null turns alerts off. Off on create unless sent. |
calendar_feed.hidden_from_todayboolean · optional | Leave its events off Today, This week and Next week (Calendar still shows them). False on create unless sent. |
Response
200 OK Errors: 400 401 403 404 415 422 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Work",
"color": "teal",
"address_host": "calendar.google.com",
"source": "google",
"sync_every": "30_minutes",
"alert_minutes": 0,
"hidden_from_today": true,
"last_synced_at": "2026-10-10T09:30:00Z",
"last_attempt_at": "2026-10-10T09:30:00Z",
"sync_error": {
"code": "not_found",
"http_status": 100,
"failures": 1
},
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Remove a calendar feed
DELETE/calendar_feeds/{id}
Permanent; its events leave the calendar (the calendar itself isn't changed). Writes a calendar_feed deletion
record. Event type rules with a calendar condition on it are deleted too, each with a calendar_type_rule
deletion record.
curl -X DELETE "$NIFTY_URL/api/v1/calendar_feeds/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Response
204 Deleted Errors: 401 403 404 429
Sync a calendar feed now
POST/calendar_feeds/{id}/sync
Fetches the feed now (waiting up to 30 seconds), ignoring its schedule and backoff. A feed that couldn't be
read is still a 200, with sync_error set. Takes no body; ignores Idempotency-Key.
curl -X POST "$NIFTY_URL/api/v1/calendar_feeds/01j9zq3k8m5x2v7c4n6b0t1r9e/sync" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Response
200 OK Errors: 400 401 403 404 415 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Work",
"color": "teal",
"address_host": "calendar.google.com",
"source": "google",
"sync_every": "30_minutes",
"alert_minutes": 0,
"hidden_from_today": true,
"last_synced_at": "2026-10-10T09:30:00Z",
"last_attempt_at": "2026-10-10T09:30:00Z",
"sync_error": {
"code": "not_found",
"http_status": 100,
"failures": 1
},
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
List event type rules
GET/calendar_type_rules
The owner's rules for an event's type, in their order. When an event is first added, the rules are tried in
order, before Nifty's own; the first whose conditions all match gives its type_key (null: no type) and no
other rule is tried.
curl "$NIFTY_URL/api/v1/calendar_type_rules" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Query parameters
| Name | Description |
|---|---|
orderposition | id | 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",
"type_key": "string",
"conditions": [
{
"kind": "name_contains",
"text": "string"
}
],
"position": 1,
"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 event type rule
POST/calendar_type_rules
Goes last unless position is sent. Then every rule, in order, is tried on each event that has no type and
whose type the owner hasn't chosen (Nifty's own rules aren't run again); meta.assigned is how many events
(a repeating event once, its moved occurrences with it) were given a type. An event's type, once set,
never changes by a rule. At most 100 rules.
curl -X POST "$NIFTY_URL/api/v1/calendar_type_rules" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"calendar_type_rule":{"type_key":"meeting","conditions":[{"kind":"name_contains","text":"standup"},{"kind":"starts_between","from":"09:00","to":"10:00"}]}}'
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 |
|---|---|
calendar_type_ruleobject | |
calendar_type_rule.type_keyRuleTypeKey · optional | A key from event_types, or null for No type (Nifty's own rules then don't type the event). Null on create unless sent. |
calendar_type_rule.conditionsCalendarTypeRuleCondition[] · optional | Required on create. All must match. A problem with one is a 422 on conditions[i].<field> (from 0). |
calendar_type_rule.positioninteger · optional | 1-based; out-of-range values are clamped. Moves the rule there. |
Response
201 Created Errors: 400 401 403 409 415 422 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"type_key": "string",
"conditions": [
{
"kind": "name_contains",
"text": "string"
}
],
"position": 1,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
},
"meta": {
"assigned": 0
}
}
Count the events a rule would type
POST/calendar_type_rules/preview
Saves nothing. matching is how many events with no type, whose type the owner hasn't chosen, these
conditions match and no rule before them does: what saving the rule would give its type (or keep with no
type). The rule goes last, or, with id, where that rule is. Conditions with problems are a 422, as when
saving. Ignores Idempotency-Key.
curl -X POST "$NIFTY_URL/api/v1/calendar_type_rules/preview" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"calendar_type_rule":{"conditions":[{"kind":"name_contains","text":"standup"}]}}'
Request body
| Name | Description |
|---|---|
calendar_type_ruleobject | |
calendar_type_rule.idULID · optional | The rule being edited: it's counted in its place, without its saved conditions. |
calendar_type_rule.conditionsCalendarTypeRuleCondition[] |
Response
200 OK Errors: 400 401 403 404 415 422 429
{
"data": {
"matching": 0
}
}
Get an event type rule
GET/calendar_type_rules/{id}
curl "$NIFTY_URL/api/v1/calendar_type_rules/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",
"type_key": "string",
"conditions": [
{
"kind": "name_contains",
"text": "string"
}
],
"position": 1,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
}
}
Change or move an event type rule
PATCH/calendar_type_rules/{id}
Only the fields sent change; conditions replaces them all. Sending position moves the rule there and
renumbers every rule 1..n; the others whose position changed get a new updated_at. A change to
type_key or conditions then tries every rule on the events with no type, as adding one does
(meta.assigned); a move alone doesn't (meta.assigned is 0). Events the rule typed before keep their type.
curl -X PATCH "$NIFTY_URL/api/v1/calendar_type_rules/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"calendar_type_rule":{"type_key":"meeting","conditions":[{"kind":"name_contains","text":"standup"},{"kind":"starts_between","from":"09:00","to":"10:00"}]}}'
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Request body
| Name | Description |
|---|---|
calendar_type_ruleobject | |
calendar_type_rule.type_keyRuleTypeKey · optional | A key from event_types, or null for No type (Nifty's own rules then don't type the event). Null on create unless sent. |
calendar_type_rule.conditionsCalendarTypeRuleCondition[] · optional | Required on create. All must match. A problem with one is a 422 on conditions[i].<field> (from 0). |
calendar_type_rule.positioninteger · optional | 1-based; out-of-range values are clamped. Moves the rule there. |
Response
200 OK Errors: 400 401 403 404 415 422 429
{
"data": {
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"type_key": "string",
"conditions": [
{
"kind": "name_contains",
"text": "string"
}
],
"position": 1,
"created_at": "2026-10-10T09:30:00Z",
"updated_at": "2026-10-10T09:30:00Z"
},
"meta": {
"assigned": 0
}
}
Delete an event type rule
DELETE/calendar_type_rules/{id}
Permanent. Writes a calendar_type_rule deletion record. Events it typed keep their type; other rules keep
their positions (gaps are fine).
curl -X DELETE "$NIFTY_URL/api/v1/calendar_type_rules/01j9zq3k8m5x2v7c4n6b0t1r9e" \
-H "Authorization: Bearer $NIFTY_TOKEN"
Path parameters
| Name | Description |
|---|---|
idULID | A record's ID. |
Response
204 Deleted Errors: 401 403 404 429