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_v2per entry indata.calendars[]. A calendar withcurrentFolderIdsset is a smart calendar and takes a smallloadingStrategybody. A calendar withcurrentFolderIds: nullis 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.
| Case | calendarGroupId | Calls after call 1 | Raw files |
|---|---|---|---|
| 1 | 2181566 | 5, one per calendar | sample_0 … sample_5 |
| 2 | 2181585 | 2, one per calendar | sample_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#
| calendarId | title | eventSource | currentFolderIds | Form | Entity types | Captured in | Events |
|---|---|---|---|---|---|---|---|
| 3417512 | 4-SmartTasks-FullDuration | Tasks | [4519445009] | smart | [-11] | case 1 / sample_1 | 7 |
| 3417513 | 5-Classic-EndDate | Tasks | null | classic | n/a | case 1 / sample_2 | 1 |
| 3360010 | 3-SmartTasks-EndDate | Tasks | [4497604621] | smart | [-11] | case 1 / sample_5 | 17 |
| 3417183 | 1-Classic-Full Duration | Tasks | null | classic | n/a | case 1 / sample_3 | 0 |
| 3417182 | 2-SmartProjects-EndDate | Projects | [4497604621] | smart | [-12] | case 1 / sample_4 | 0 |
| 3360068 | Collab | Tasks | [4519445009] | smart | [-11] | case 2 / sample_2 | 7 |
| 3360069 | Team | Tasks | [4547354009] | smart | [-11] | case 2 / sample_1 | 0 |
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 field | Source |
|---|---|
calendarIds | [calendars[i].calendarId] |
currentFolderIds | calendars[i].currentFolderIds, copied as is |
loadingStrategy.showDescendants | calendars[i].showDescendants |
loadingStrategy.entityTypeIds | from settings.eventSource: Tasks → [-11], Projects → [-12] |
loadingStrategy.viewPortStart / viewPortEnd | the visible date range, see Viewport |
useUniversalFilters | true in every sample. accountCalendarUiSettings.betaFlags.useUniversalFilters is also true, so it probably mirrors that flag. |
between, filterRoot | constants [] 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 field | Value |
|---|---|
calendarIds | [calendars[i].calendarId] |
between | [{ "from": viewPortStart, "to": viewPortEnd }] |
dateFrame | { "from": viewPortStart, "to": viewPortEnd } |
showDescendants | true (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.firstDayOfWeekis1and the classic bodies sendfirstDayOfWeek: 0. Either1does not mean Monday here, or the viewport ignores that setting. With week-start 1, the grid would be2026-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:
| Header | Value |
|---|---|
x-b3-traceid | new 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
cookieor 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
parentFoldersequals the calendar'scurrentFolderIdsandcalendarIdsis[]. Case 13417512and case 23360068use 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 owncalendarIds: [3417513]. subscriptionIdis a fresh GUID for smart calendars andnullfor classic ones. It is probably for live updates, which is a guess.- Display settings are not in the request or the response.
colorandeventDisplayMode(FullvsLastDay) exist only in call 1, so the client must apply them itself.LastDaycalendars still receive the fullstartDateandfinishDate. - Dates look like
"2026-10-03 09:00:00"with no time zone.updatedDateis epoch milliseconds. - Item types vary:
itemTypeIcon.symbolwasb-meeting,edit-noteandtask. - Localized group title.
calendarGroups[0].titlewas 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#
- Week start for the viewport (see above). Needs one capture with a different
firstDayOfWeek. - Other
eventSourcevalues. OnlyTasksandProjectsare observed. The builder flags anything else rather than guessing anentityTypeIds. - What exactly picks "smart".
currentFolderIdsnon-null is the only difference observed, and it is confounded withshowDescendants: nullon classic calendars. An empty array, a calendar with afilter, orshowDescendants: truehas not been captured. - Other view modes. Every capture is
Month. Week, day, year and multi-year views may use a different range or endpoint. - Paging.
eventsFetchLimitPerLayeris500, which matches the classiclimitandfetchLimit. What happens past 500 events is unknown, and smart calls send no limit at all. - Several calendars in one call. Every call used a single calendar id.
- Authentication. See the warning under Headers.