The Calendar entry object
Every item in the entries array returned by GET /api/v1/calendar shares the same shape. Which fields carry a value depends on kind.
| Field | Type | Notes |
|---|---|---|
id | string | A typed, stable id — see "The id format" below. Also usable as an ICS-style UID. |
kind | string | One of task, deal-close, campaign, followup, birthday |
date | string (YYYY-MM-DD) | The calendar day this entry belongs to |
startAt | string (ISO 8601) | null | Full instant, only for a timed entry — null for all-day entries |
endAt | string (ISO 8601) | null | Same rule as startAt |
allDay | boolean | true for every kind except a timed task |
title | string | Display title |
href | string | In-app agentlyleads URL — a deep link back into the CRM |
meta | string | null | One-line human-readable context, or null |
done | boolean | task entries only — whether the task is complete |
taskId | string | task entries only — the raw task ID |
seriesId | string | null | task entries only — the repeating series this occurrence belongs to |
The id format
id is built from the kind and the underlying record, and is stable across requests:
| Kind | id shape | Example |
|---|---|---|
task | task:<taskId> | "task:cmq7task0alk34567890" |
deal-close | deal:<dealId> | "deal:cmq3deal0alk34567890" |
campaign | campaign:<campaignId> | "campaign:cmq9camp0alk34567890" |
followup | followup:<emailId> | "followup:cmq2mail0alk34567890" |
birthday | birthday:<contactId>:<year> | "birthday:cmq5cont0alk34567890:2026" |
A birthday recurs yearly, so its id includes the occurrence's year — the same contact's birthday next year is a different entry with a different id.
date vs. startAt/endAt
date is always set — it is which calendar day the entry belongs to, and is what a month grid or day list buckets on. startAt/endAt are only set when the entry is a real moment in time (a timed task/appointment); never read one against the other's accessor. All entries other than a timed task are allDay: true with startAt/endAt both null — a deal close, a scheduled campaign send, a follow-up, and a birthday are always all-day.
Kind by kind
task
A Task row whose dueDate falls in the range — the same underlying record as GET /api/v1/tasks, reshaped into the merged calendar format. A timed task (dueDate carries a clock time) is a real appointment: allDay is false and startAt/endAt are set. An all-day task (a plain to-do, or one with no endAt beyond its due date) has allDay: true and null startAt/endAt.
Only task entries carry done, taskId, and seriesId. href is the generic /tasks list, not a per-task deep link. meta is the linked contact's or deal's name, whichever is set (null if neither).
{
"id": "task:cmq7task0alk34567890",
"kind": "task",
"date": "2026-09-10",
"startAt": "2026-09-10T18:00:00.000Z",
"endAt": "2026-09-10T18:30:00.000Z",
"allDay": false,
"title": "Discovery call with Acme",
"href": "/tasks",
"meta": "Jane Buyer",
"done": false,
"taskId": "cmq7task0alk34567890",
"seriesId": null
}deal-close
An open deal (stage not WON/LOST) whose expectedCloseDate falls in the range. Always all-day. title is "Close: <deal name>"; meta is "$<value> · <stage>"; href links to the deal.
{
"id": "deal:cmq3deal0alk34567890",
"kind": "deal-close",
"date": "2026-09-12",
"startAt": null,
"endAt": null,
"allDay": true,
"title": "Close: Acme Corp — Annual Contract",
"href": "/deals/cmq3deal0alk34567890",
"meta": "$12,000 · QUALIFIED"
}campaign
A SCHEDULED campaign whose scheduledAt falls in the range. Always all-day — a send has a scheduled instant, but it isn't something anyone attends, so it doesn't carry startAt/endAt. title is "Campaign: <name>"; meta is always "scheduled send"; href links to the campaign.
{
"id": "campaign:cmq9camp0alk34567890",
"kind": "campaign",
"date": "2026-09-09",
"startAt": null,
"endAt": null,
"allDay": true,
"title": "Campaign: September newsletter",
"href": "/campaigns/cmq9camp0alk34567890",
"meta": "scheduled send"
}followup
An email thread that's gone quiet and is due for a follow-up. Always dated today (in the workspace's timezone) — it only appears if today falls inside the requested range, regardless of from/to. title is "Follow up: <contact name>"; meta is "<N>d quiet · <subject>"; href links to the email thread.
{
"id": "followup:cmq2mail0alk34567890",
"kind": "followup",
"date": "2026-09-08",
"startAt": null,
"endAt": null,
"allDay": true,
"title": "Follow up: Jane Buyer",
"href": "/emails/cmq2mail0alk34567890",
"meta": "3d quiet · Re: pricing question"
}birthday
A contact with a birthday set, surfaced on every yearly occurrence that falls in the range — a contact whose birthday you're tracking across a range spanning two years appears twice, once per year. Always all-day, meta is always null. href links to the contact.
{
"id": "birthday:cmq5cont0alk34567890:2026",
"kind": "birthday",
"date": "2026-09-11",
"startAt": null,
"endAt": null,
"allDay": true,
"title": "Jane Buyer's birthday",
"href": "/contacts/cmq5cont0alk34567890",
"meta": null
}external is never returned
Internally, the CRM calendar has a sixth kind, external — a user's connected Google/Outlook calendar, read in as busy time. GET /api/v1/calendar never returns it: those events are only fetched when the caller is a specific signed-in user, and a workspace API key has no single "current user" to attribute them to. Returning them here would leak whichever user's personal calendar to every caller holding the key, so they're deliberately left out — this is not a gap to be filled later, it's the contract.