Engineering Portal

Wrike CalendarLink: deriving the event calls from calendar_list

Question#

We know how to make the first call, calendar_list, given a calendarGroupId. We want to construct the rest of the calls (the ones that return the events to draw) from that first response, without watching the Wrike UI make them.

These are internal, undocumented UI endpoints on app-eu.wrike.com, not the public Wrike REST API. Everything below is inferred from captured traffic, so it can change without notice.

Finding Short answer. Make one POST /ui/get_view_calendar_v2 per entry in data.calendars[]. A calendar with currentFolderIds set is a smart calendar and takes a small loadingStrategy body. A calendar with currentFolderIds: null is a classic calendar and takes the long legacy filter body. The only other input is the visible date range. The rules reproduce all 7 captured request bodies exactly (npm test).

Evidence#

Two cases were captured. In each, sample_0 is the calendar_list call and the later samples are the follow-up calls.

CasecalendarGroupIdCalls after call 1Raw files
121815665, one per calendarsample_0 … sample_5
221815852, one per calendarsample_0 … sample_2

The call chain#

calendar_list      { calendarGroupId }
   └─ data.calendars[]            ── one entry per calendar in the group
        └─ get_view_calendar_v2   ── one call per calendar, independent of each other
              └─ data.events[]    ── events overlapping the viewport

The calls after the first have no dependency on each other. In the samples the capture order differs from the order of data.calendars[], so do not rely on ordering.

Rule 1: one call per calendar#

Every calendar in data.calendars[] produced exactly one get_view_calendar_v2 call, and each call carries a single id in calendarIds: the calendar's calendarId.

Rule 2: currentFolderIds selects the body format#

calendarIdtitleeventSourcecurrentFolderIdsFormEntity typesCaptured inEvents
34175124-SmartTasks-FullDurationTasks[4519445009]smart[-11]case 1 / sample_17
34175135-Classic-EndDateTasksnullclassicn/acase 1 / sample_21
33600103-SmartTasks-EndDateTasks[4497604621]smart[-11]case 1 / sample_517
34171831-Classic-Full DurationTasksnullclassicn/acase 1 / sample_30
34171822-SmartProjects-EndDateProjects[4497604621]smart[-12]case 1 / sample_40
3360068CollabTasks[4519445009]smart[-11]case 2 / sample_27
3360069TeamTasks[4547354009]smart[-11]case 2 / sample_10

The titles in case 1 spell this out: Smart calendars are folder-based, Classic calendars are not. The two forms are used in exactly the split the table shows.

Rule 3: smart calendar body#

{
  "loadingStrategy": {
    "useUniversalFilters": true,
    "showDescendants": false,
    "entityTypeIds": [-11],
    "viewPortStart": "2026-09-27",
    "viewPortEnd": "2026-10-31"
  },
  "calendarIds": [3417512],
  "currentFolderIds": [4519445009],
  "between": [],
  "filterRoot": null
}
Body fieldSource
calendarIds[calendars[i].calendarId]
currentFolderIdscalendars[i].currentFolderIds, copied as is
loadingStrategy.showDescendantscalendars[i].showDescendants
loadingStrategy.entityTypeIdsfrom settings.eventSource: Tasks → [-11], Projects → [-12]
loadingStrategy.viewPortStart / viewPortEndthe visible date range, see Viewport
useUniversalFilterstrue in every sample. accountCalendarUiSettings.betaFlags.useUniversalFilters is also true, so it probably mirrors that flag.
between, filterRootconstants [] and null (the calendars' own filter and filterRoot were all null)

Rule 4: classic calendar body#

The classic body is the long legacy filter object. Only four things vary; everything else is a constant default ([], null or false):

Body fieldValue
calendarIds[calendars[i].calendarId]
between[{ "from": viewPortStart, "to": viewPortEnd }]
dateFrame{ "from": viewPortStart, "to": viewPortEnd }
showDescendantstrue (the calendar's own value was null, so this is not copied)

Fixed values worth knowing: limit: 500, fetchLimit: 500, offset: 0, showSubtasks: true, withRights: true, firstDayOfWeek: 0. The full body is generated by the builder below and by calendarlink.js.

Viewport#

Every sample used 2026-09-27 → 2026-10-31. That is the visible grid of the October 2026 month view: the 5 weeks from the Sunday before 1 October to the last day of the month.

calendarSelectedDate in userCalendarUiSettings (call 1) is 1792644186039 = 2026-10-22, and calendarViewMode is Month. Taking the month of that date and expanding to whole weeks gives the captured range for both cases.

Open The grid starts on a Sunday, yet userCalendarUiSettings.firstDayOfWeek is 1 and the classic bodies send firstDayOfWeek: 0. Either 1 does not mean Monday here, or the viewport ignores that setting. With week-start 1, the grid would be 2026-09-28 → 2026-11-01. Only the Sunday-first range has been observed.

Headers#

Across all 10 calls the headers were identical except for two:

HeaderValue
x-b3-traceidnew per call (a trace id, so any fresh hex string is probably fine)
wrike-navigation-path#/single-calendar?id={calendarGroupId}&spaceId={spaceId} with spaceId from calendarGroups[0].spaceId

Constant for the session: wrike-client-id, wrike-version, x-w-client, x-w-account (the account id, 3128883), origin and referer. The account id is not in the call 1 response body. It is in the page URL (workspace.htm?acc=3128883) and in each event's accountId.

Warning The samples contain no cookie or authorization header. The calls only work with a signed-in Wrike session, and how that session is passed has not been captured.

What the responses tell us#

  • Same shape for both forms: { success, async: false, data: { events[], subscriptionId } }.
  • Overlap, not containment. "Team - 2026-09 September Goal" runs 1–30 Sep and was returned for a viewport starting 27 Sep. Events that overlap the range are returned.
  • Smart events are matched by folder. Their parentFolders equals the calendar's currentFolderIds and calendarIds is []. Case 1 3417512 and case 2 3360068 use the same folder and the same entity type, and returned the same 7 events.
  • Classic events are matched by explicit membership. The one event returned, "Test Event 1" (parentFolders: [4372380543]), lists the calendar in its own calendarIds: [3417513].
  • subscriptionId is a fresh GUID for smart calendars and null for classic ones. It is probably for live updates, which is a guess.
  • Display settings are not in the request or the response. color and eventDisplayMode (Full vs LastDay) exist only in call 1, so the client must apply them itself. LastDay calendars still receive the full startDate and finishDate.
  • Dates look like "2026-10-03 09:00:00" with no time zone. updatedDate is epoch milliseconds.
  • Item types vary: itemTypeIcon.symbol was b-meeting, edit-note and task.
  • Localized group title. calendarGroups[0].title was Tamil in case 1 and Chinese in case 2, because the default group is named in the user's language. Do not match on it.

Verification#

test/verify-calendarlink.mjs builds the calls from each case's sample_0 and compares them with the real request bodies, matched by calendarIds. All 7 bodies match exactly, and the viewport derivation matches in both cases. Run npm test in the portal repo.

This is a replay of the captured traffic, not a test against live Wrike, so every untested input below is still open.

Try it#

Paste a calendar_list response to get the request bodies. The viewport is derived from calendarSelectedDate unless you fill in both dates.

Open questions#

  1. Week start for the viewport (see above). Needs one capture with a different firstDayOfWeek.
  2. Other eventSource values. Only Tasks and Projects are observed. The builder flags anything else rather than guessing an entityTypeIds.
  3. What exactly picks "smart". currentFolderIds non-null is the only difference observed, and it is confounded with showDescendants: null on classic calendars. An empty array, a calendar with a filter, or showDescendants: true has not been captured.
  4. Other view modes. Every capture is Month. Week, day, year and multi-year views may use a different range or endpoint.
  5. Paging. eventsFetchLimitPerLayer is 500, which matches the classic limit and fetchLimit. What happens past 500 events is unknown, and smart calls send no limit at all.
  6. Several calendars in one call. Every call used a single calendar id.
  7. Authentication. See the warning under Headers.