Calendar
Events from your calendar feeds.
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, 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 |
|---|---|
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": [
{
"key": "string",
"feed_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"
}
],
"tasks": [
{
"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",
"person_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"project_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"position": 1,
"description_html": "<p>Ask about the boiler too.</p>",
"description_text": "Ask about the boiler too.",
"has_documents": true,
"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",
"my_role": "Organiser",
"external_reference": "JIRA-123",
"external_url": "https://example.com",
"description_html": "<p>New units and worktops.</p>",
"description_text": "New units and worktops.",
"members": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"person_id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"role": "string"
}
],
"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"
},
"relationships": [
"spouse"
]
}
}
],
"people": [
{
"id": "01j9zq3k8m5x2v7c4n6b0t1r9e",
"name": "Maya Okafor",
"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"
}
],
"relationships": [
"spouse"
],
"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"
}
]
}
}
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",
"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: 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"}}'
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. |
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",
"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",
"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",
"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}
Also PUT /calendar_feeds/{id}, the same.
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 and frequency 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"}}'
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. |
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",
"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.
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/{calendar_feed_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 |
|---|---|
calendar_feed_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",
"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"
}
}