Skip to content
Niftyhelp 2026.10.2 Support

Calendar

Events from your calendar feeds and your own events on My Nifty Calendar.

On this page

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 before from but is still on, a multi-day all-day event), by start, all-day ones first on a day; then title, then key.
  • tasks: live tasks, open or completed, whose deadline_on is in the range (an overdue open task only on its own date), by deadline_on, then title.
  • project_dates: live projects that are planned, active or on_hold, with a deadline or start_date in 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

NameDescription
event_typesstring · optionalComma-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.
fromdateThe first day, YYYY-MM-DD.
todateThe 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

NameDescription
event_typeobject
event_type.keystringAn occurrence key from GET /calendar.
event_type.feed_idULIDA record's ID.
event_type.datedate
event_type.type_keyEventTypeKeyA 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

NameDescription
Idempotency-Keystring · optionalMakes a POST safe to retry for 24 hours. Needs a bearer token (400 under session auth).

Request body

NameDescription
hidden_eventobject
hidden_event.keystringThe occurrence's key (CalendarEvent.key).
hidden_event.feed_idULIDThe occurrence's feed (CalendarEvent.feed_id): removing that feed drops the hide.
hidden_event.datedateThe 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

NameDescription
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

NameDescription
Idempotency-Keystring · optionalMakes a POST safe to retry for 24 hours. Needs a bearer token (400 under session auth).

Request body

NameDescription
ignored_eventobject
ignored_event.keystringThe occurrence's key (CalendarEvent.key).
ignored_event.feed_idULIDThe occurrence's feed (CalendarEvent.feed_id): removing that feed drops the ignore.
ignored_event.datedateThe day the occurrence starts, in the owner's time zone (its start_on, or starts_at's date there).
ignored_event.repeatsboolean · optionalEvery 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

NameDescription
keystring

Query parameters

NameDescription
feed_idULIDThe occurrence's feed.
datedateThe 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

NameDescription
nifty_calendarobject
nifty_calendar.colorstring · optionalA key from the vocabulary's colors.
nifty_calendar.hidden_from_todayboolean · optionalLeave 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

NameDescription
event_typesstring · optionalComma-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 · optionaldate sorts by day, with id as the tie-breaker.
directionasc | desc · optionalasc (ascending) or desc (descending), for order.
updated_sincedate-time · optionalOnly records updated at or after this time (ISO 8601 with Z or a UTC offset), ordered by updated_at, id.
cursorstring · optionalmeta.next_cursor from the previous page.
limitinteger · optionalItems per page, up to 100; a larger number is taken as 100.
counttrue | false · optionaltrue 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

NameDescription
Idempotency-Keystring · optionalMakes a POST safe to retry for 24 hours. Needs a bearer token (400 under session auth).

Request body

NameDescription
calendar_entryobject
calendar_entry.type_keyEventTypeKey · optionalA 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 · optionalRequired on create; spaces are squished; blank is a 422.
calendar_entry.datedate · optionalRequired on create. The day, in time_zone for a timed event.
calendar_entry.all_dayboolean · optionalFalse on create unless sent. True clears start_time, end_time and time_zone.
calendar_entry.start_timestring or null · optionalHH:MM, 24-hour, in time_zone. Required for a timed event.
calendar_entry.end_timestring or null · optionalHH:MM, after start_time on the same day. Required for a timed event.
calendar_entry.time_zonestring or null · optionalAn IANA zone (Europe/London); null or unsent on a timed event takes the owner's.
calendar_entry.locationstring or null · optionalPlain text; blank is null.
calendar_entry.notesstring or null · optionalPlain 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

NameDescription
idULIDA 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

NameDescription
idULIDA record's ID.

Request body

NameDescription
calendar_entryobject
calendar_entry.type_keyEventTypeKey · optionalA 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 · optionalRequired on create; spaces are squished; blank is a 422.
calendar_entry.datedate · optionalRequired on create. The day, in time_zone for a timed event.
calendar_entry.all_dayboolean · optionalFalse on create unless sent. True clears start_time, end_time and time_zone.
calendar_entry.start_timestring or null · optionalHH:MM, 24-hour, in time_zone. Required for a timed event.
calendar_entry.end_timestring or null · optionalHH:MM, after start_time on the same day. Required for a timed event.
calendar_entry.time_zonestring or null · optionalAn IANA zone (Europe/London); null or unsent on a timed event takes the owner's.
calendar_entry.locationstring or null · optionalPlain text; blank is null.
calendar_entry.notesstring or null · optionalPlain 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

NameDescription
idULIDA 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

NameDescription
idULIDA record's ID.

Headers

NameDescription
Idempotency-Keystring · optionalMakes 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

NameDescription
ordername | id | updated_at · optionalname sorts ignoring case and accents, with id as the tie-breaker.
directionasc | desc · optionalasc (ascending) or desc (descending), for order.
updated_sincedate-time · optionalOnly records updated at or after this time (ISO 8601 with Z or a UTC offset), ordered by updated_at, id.
cursorstring · optionalmeta.next_cursor from the previous page.
limitinteger · optionalItems per page, up to 100; a larger number is taken as 100.
counttrue | false · optionaltrue 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

NameDescription
Idempotency-Keystring · optionalMakes a POST safe to retry for 24 hours. Needs a bearer token (400 under session auth).

Request body

NameDescription
calendar_feedobject
calendar_feed.namestring or null · optionalSpaces 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 · optionalRequired 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 · optionalA key from the vocabulary's colors. Required on create.
calendar_feed.sync_everyCalendarSyncFrequency · optionalHow often the feed is fetched; the words are the vocabulary's calendar_sync_frequencies.
calendar_feed.alert_minutesinteger or null · optionalMinutes 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 · optionalLeave 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

NameDescription
idULIDA 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

NameDescription
idULIDA record's ID.

Request body

NameDescription
calendar_feedobject
calendar_feed.namestring or null · optionalSpaces 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 · optionalRequired 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 · optionalA key from the vocabulary's colors. Required on create.
calendar_feed.sync_everyCalendarSyncFrequency · optionalHow often the feed is fetched; the words are the vocabulary's calendar_sync_frequencies.
calendar_feed.alert_minutesinteger or null · optionalMinutes 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 · optionalLeave 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

NameDescription
idULIDA 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

NameDescription
idULIDA 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

NameDescription
orderposition | id | updated_at · optionalThe field to sort by; ties are broken by id, so the order is stable.
directionasc | desc · optionalasc (ascending) or desc (descending), for order.
updated_sincedate-time · optionalOnly records updated at or after this time (ISO 8601 with Z or a UTC offset), ordered by updated_at, id.
cursorstring · optionalmeta.next_cursor from the previous page.
limitinteger · optionalItems per page, up to 100; a larger number is taken as 100.
counttrue | false · optionaltrue 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

NameDescription
Idempotency-Keystring · optionalMakes a POST safe to retry for 24 hours. Needs a bearer token (400 under session auth).

Request body

NameDescription
calendar_type_ruleobject
calendar_type_rule.type_keyRuleTypeKey · optionalA 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[] · optionalRequired on create. All must match. A problem with one is a 422 on conditions[i].<field> (from 0).
calendar_type_rule.positioninteger · optional1-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

NameDescription
calendar_type_ruleobject
calendar_type_rule.idULID · optionalThe 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

NameDescription
idULIDA 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

NameDescription
idULIDA record's ID.

Request body

NameDescription
calendar_type_ruleobject
calendar_type_rule.type_keyRuleTypeKey · optionalA 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[] · optionalRequired on create. All must match. A problem with one is a 422 on conditions[i].<field> (from 0).
calendar_type_rule.positioninteger · optional1-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

NameDescription
idULIDA record's ID.

Response

204 Deleted Errors: 401 403 404 429

Menu

2026.10.2Contact support