Cal.diy 平台 API v2 Event Types 实操指南:从事件类型 CRUD 到团队调度与 Webhook 的完整配置
【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy
Cal.diy 以 "Scheduling infrastructure for absolutely everyone" 为定位,在其开放平台 API v2 中,Event Types(事件类型)是定义"可被预约的会议形态"的核心资源——时长、地点、可用规则、自定义表单均由它承载。本文以仓库内置的 event-types.md 为骨架,结合 apps/api/v2 中对应的 NestJS 控制器、输入输出 DTO、转换器与 e2e 测试,系统讲解事件类型的增删改查、8 种地点类型、6 种自定义预约字段,以及团队/组织级调度和事件类型级 Webhook 的完整接入方法。读完本文,你将能够独立调用/v2/event-types系列端点,为你的产品搭建一套可被预约的会议类型。
适用前提:文中所有路由、参数与示例均以当前仓库
apps/api/v2的实现与官方参考文档为准。调用前需先阅读 authentication.md 获取 Bearer API Key;API 基础地址为https://api.cal.com/v2,所有请求都要求携带Authorization: Bearer cal_<your_api_key>(Key 以cal_live_或cal_test_开头)。
一、端点总览
| Method | Endpoint | Description |
|---|---|---|
| GET | /v2/event-types | List event types |
| POST | /v2/event-types | Create an event type |
| GET | /v2/event-types/{eventTypeId} | Get an event type |
| PATCH | /v2/event-types/{eventTypeId} | Update an event type |
| DELETE | /v2/event-types/{eventTypeId} | Delete an event type |
在仓库实现中,这一组路由由两个"版本化"的 NestJS 控制器共同提供:
- event-types_2024_06_14/controllers/event-types.controller.ts 使用
@Controller({ path: "/v2/event-types", version: VERSION_2024_06_14_VALUE })承载/、/:eventTypeId各写读方法,并声明了cal-api-version请求头,未传正确版本值时会回退到旧版本; - event-types_2024_04_15/controllers/event-types.controller.ts 同时服务于
VERSION_2024_04_15与VERSION_2024_06_11两个旧版本。
两者都通过@UseGuards(PermissionsGuard)做全局权限拦截,并用EVENT_TYPE_READ/EVENT_TYPE_WRITE两个权限常量区分读、写操作(源码见 event-types_2024_04_15 控制器 的@Permissions([EVENT_TYPE_WRITE])用法)。每类方法对应的 e2e 测试位于 event-types.controller.e2e-spec.ts,可作为请求/响应的可运行样例。
二、列出事件类型:GET /v2/event-types
GET /v2/event-typesQuery Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| take | number | No | Number of results (default: 10, max: 250) |
| skip | number | No | Pagination offset |
响应示例
{ "status": "success", "data": [ { "id": 123, "title": "30 Minute Meeting", "slug": "30min", "description": "A quick 30 minute call", "lengthInMinutes": 30, "locations": [ { "type": "integration", "integration": "cal-video" } ], "bookingFields": [], "disableGuests": false, "slotInterval": null, "minimumBookingNotice": 120, "beforeEventBuffer": 0, "afterEventBuffer": 0, "schedulingType": null, "metadata": {}, "requiresConfirmation": false, "price": 0, "currency": "usd", "hidden": false } ] }响应字段速览:status固定为"success";data中lengthInMinutes即会议时长(分钟),slug会出现在预约 URL 中(如/{username}/30min),minimumBookingNotice表示"提前多少分钟才允许被预约"(此处 120 即最早可约 2 小时后),slotInterval为null表示相邻可预约时段之间无强制间隔。
关于 hidden 事件类型的可见性:从控制器源码看,GET 列表会调用getResponseEventTypesWithoutHiddenFields对结果做隐藏字段过滤——也就是说公开调用时hidden: true的事件类型不会出现在列表里,只有携带所属 owner 认证信息时才被返回。此外当前 2024-06-14 版本还支持可选的sortCreatedAt查询参数("asc"/"desc")按创建顺序排序,详见控制器getEventTypes的 API 描述。
三、创建事件类型:POST /v2/event-types
POST /v2/event-types请求体示例
{ "title": "30 Minute Meeting", "slug": "30min", "description": "A quick 30 minute call to discuss your needs", "lengthInMinutes": 30, "locations": [ { "type": "integration", "integration": "cal-video" } ], "bookingFields": [ { "type": "textarea", "name": "notes", "label": "Additional Notes", "required": false, "placeholder": "Any additional information..." } ], "disableGuests": false, "slotInterval": 15, "minimumBookingNotice": 120, "beforeEventBuffer": 5, "afterEventBuffer": 5, "scheduleId": 1, "requiresConfirmation": false, "hidden": false }必填字段
| Field | Type | Description |
|---|---|---|
| title | string | Display name of the event type |
| slug | string | URL-friendly identifier |
| lengthInMinutes | number | Duration of the event |
可选字段
| Field | Type | Description |
|---|---|---|
| description | string | Description shown on booking page |
| locations | array | Meeting location options |
| bookingFields | array | Custom form fields |
| disableGuests | boolean | Prevent attendees from adding guests |
| slotInterval | number | Minutes between available slots |
| minimumBookingNotice | number | Minimum minutes before booking |
| beforeEventBuffer | number | Buffer time before event (minutes) |
| afterEventBuffer | number | Buffer time after event (minutes) |
| scheduleId | number | ID of schedule to use |
| requiresConfirmation | boolean | Require host confirmation |
| hidden | boolean | Hide from public profile |
字段语义补充:beforeEventBuffer/afterEventBuffer是"会议前后各预留的缓冲分钟数",例如 30 分钟会议前后各留 5 分钟,相邻两场约会被自动拉开到 40 分钟;scheduleId指向用户在某张"可用性排班表(Schedule)"上开放预约,关于排班表可参阅 schedules.md;price/currency用于付费事件(本示例为 0/usd)。
源码中的处理管线
创建请求并非直接落库,而是先经过inputEventTypesService.transformAndValidateCreateEventTypeInput做输入转换与校验,再交给eventTypesService.createUserEventType写入,最后通过EventTypeResponseTransformPipe统一输出格式(见 2024-06-14 控制器 createEventType)。这里"转换"指把 API 友好的locations、bookingFields、seats、recurrence等结构翻译为内部事件类型模型——对应目录 event-types_2024_06_14/transformers/api-to-internal 下的locations.ts、booking-fields.ts、recurrence.ts、seats.ts等转换器;出参方向的逆转换则在transformers/internal-to-api与pipes/event-type-response.transformer.ts完成。这意味着你按下文格式传入的locations、bookingFields都是经过映射器支持的官方输入格式,字段拼写错误会在这一层被校验拦截。
四、获取单个事件类型:GET /v2/event-types/{eventTypeId}
GET /v2/event-types/{eventTypeId}Path Parameters
| Parameter | Type | Description |
|---|---|---|
| eventTypeId | number | Event type ID |
授权模型(重要):该 GET 端点并非"拿到 ID 就能读",控制器注释明确列出了授权范围——系统管理员、事件类型 owner、该事件类型的 Host/被指派用户、所属团队的管理员、所属组织(含团队父组织)的管理员/owner 均可读取;而UPDATE 与 DELETE 仍仅限事件类型 owner 本人(见 getEventTypeById 的 API 描述)。服务层实现为getEventTypeByIdIfAuthorized,未授权时返回NotFoundException("Event type with id … not found")。
五、更新事件类型:PATCH /v2/event-types/{eventTypeId}
PATCH /v2/event-types/{eventTypeId}请求体
PATCH 为局部更新语义,只需包含要修改的字段:
{ "title": "Updated Meeting Title", "lengthInMinutes": 45, "hidden": false }该请求需EVENT_TYPE_WRITE权限;服务端先执行transformAndValidateUpdateEventTypeInput(结合当前用户与eventTypeId校验变更后的整体合法性),再调用updateEventType落库,成功响应为HTTP 200+status: "success"及更新后的事件对象。对应的集成测试覆盖见 event-types.controller.e2e-spec.ts。
六、删除事件类型:DELETE /v2/event-types/{eventTypeId}
DELETE /v2/event-types/{eventTypeId}删除同样要求EVENT_TYPE_WRITE且仅限 owner。删除成功后响应体返回被删事件的关键信息(id、lengthInMinutes、slug、title),便于客户端做本地清理:
{ "status": "success", "data": { "id": 123, "lengthInMinutes": 30, "slug": "30min", "title": "30 Minute Meeting" } }七、地点类型(Location Types)全解
locations数组决定了"会议在哪里进行"。所有官方地点类型都以type区分,以下是文档列出的 8 种写法:
Cal Video(内置视频会议)
{ "type": "integration", "integration": "cal-video" }Zoom
{ "type": "integration", "integration": "zoom" }Google Meet
{ "type": "integration", "integration": "google-meet" }Microsoft Teams
{ "type": "integration", "integration": "msteams" }线下地址(In-Person)
{ "type": "address", "address": "123 Main St, City, Country" }电话(Host 主动拨打)
{ "type": "userPhone" }电话(Attendee 提供号码)
{ "type": "attendeePhone" }自定义链接
{ "type": "link", "link": "https://custom-meeting.com/room" }仓库中locations的转换逻辑分别位于 transformers/api-to-internal/locations.ts(API 输入 → 内部模型)与transformers/internal-to-api/locations.ts(内部模型 → API 输出),其配套单测api-to-internal.spec.ts覆盖了各类integration与自定义地点的映射,是排查"地点类型传参无效"时的第一参照。
八、自定义预约字段(Booking Fields)
bookingFields用于在预约表单上向 Attendee 收集额外信息。除示例中的placeholder外,通用字段结构为type + name + label + required;选项类字段还需options数组。
文本框(Text)
{ "type": "text", "name": "company", "label": "Company Name", "required": true, "placeholder": "Enter your company name" }多行文本(Textarea)
{ "type": "textarea", "name": "notes", "label": "Additional Notes", "required": false }下拉选择(Select)
{ "type": "select", "name": "topic", "label": "Meeting Topic", "required": true, "options": [ { "value": "sales", "label": "Sales Inquiry" }, { "value": "support", "label": "Support" }, { "value": "other", "label": "Other" } ] }单选按钮(Radio)
{ "type": "radio", "name": "preference", "label": "Preferred Contact Method", "required": true, "options": [ { "value": "email", "label": "Email" }, { "value": "phone", "label": "Phone" } ] }复选框(Checkbox)
{ "type": "checkbox", "name": "terms", "label": "I agree to the terms", "required": true }电话号码(Phone)
{ "type": "phone", "name": "phone", "label": "Phone Number", "required": false }提示:
options中value是提交给 API 的实际取值,label是展示文案;required决定该字段是否阻塞预约提交。若需要在预约后把这些字段转发到下游系统,可在事件类型级 Webhook 中接收携带字段值的事件(见下文"事件类型 Webhooks")。字段结构的双向转换由 booking-fields.ts 等转换器实现,提交前会对字段类型与选项做校验。
九、团队事件类型(Team Event Types)
团队场景请使用team-scoped端点,资源归属校验会落到团队维度:
列出团队事件类型
GET /v2/teams/{teamId}/event-types创建团队事件类型
POST /v2/teams/{teamId}/event-types团队事件类型在基础必填字段之上,额外支持schedulingType与hosts:
{ "title": "Team Meeting", "slug": "team-meeting", "lengthInMinutes": 30, "schedulingType": "ROUND_ROBIN", "hosts": [ { "userId": 1, "isFixed": false }, { "userId": 2, "isFixed": false } ] }hosts声明可承接预约的团队成员;isFixed: false表示非固定 Host,可参与轮转分配。
调度类型(Scheduling Types)
| Type | Description |
|---|---|
| ROUND_ROBIN | Distributes bookings among team members |
| COLLECTIVE | All team members must attend |
| MANAGED | Parent event type that creates child event types |
调度类型的取值在仓库中有枚举定义可对照:scheduling-type.ts(ROUND_ROBIN = "ROUND_ROBIN"、COLLECTIVE = "COLLECTIVE"、MANAGED = "MANAGED")。团队事件类型的服务层与出参序列化位于 modules/teams/event-types/services/teams-event-types.service.ts 与 output-team-event-types-response.pipe.ts;值得注意的是,即使是单机/v2/event-types/:eventTypeId的 GET,一旦发现事件归属团队(带teamId),控制器也会切换走团队专用输出管道来格式化响应。
十、私有链接(Private Links)
hidden事件类型用于"不公开但可预约";而Private Link更进一步:为事件生成绕过公开页可见性的专属预约链接,适合发给特定客户。
列出私有链接
GET /v2/event-types/{eventTypeId}/private-links创建私有链接
POST /v2/event-types/{eventTypeId}/private-links仓库中该功能的控制器路径为/v2/event-types/:eventTypeId/private-links(见 event-types-private-links.controller.ts),并比文档更进一步地实现了PATCH /private-links/{linkId}(更新)与DELETE /private-links/{linkId}(删除)两个管理端点。所有操作都叠加了ApiAuthGuard + EventTypeOwnershipGuard双重校验——即使拿到eventTypeId,非 owner 也无法读写其私有链接。完整的链路(创建→列表→更新→删除)在 event-types-private-links.controller.e2e-spec.ts 中有端到端覆盖。
十一、组织级(Organization)事件类型
当事件类型归属于某个组织下的团队时,可走组织范围的 team-scoped 端点。先定位组织下的团队:
列出组织团队
GET /v2/organizations/{orgId}/teams响应示例
{ "status": "success", "data": [ { "id": 1, "name": "Sales Team", "slug": "sales", "parentId": null, "isOrganization": false } ] }isOrganization用于区分"组织根节点"与"普通团队",parentId指向父组织/父团队。
组织范围获取团队事件类型
GET /v2/organizations/{orgId}/teams/{teamId}/event-types组织范围创建团队事件类型
POST /v2/organizations/{orgId}/teams/{teamId}/event-types组织下的团队事件类型同样支持三种调度模式,语义如下:
| Mode | Description |
|---|---|
| COLLECTIVE | All team members must attend the meeting |
| ROUND_ROBIN | Distributes bookings among team members based on availability and priority |
| MANAGED | Parent event type that creates child event types for team members |
三者差异:COLLECTIVE要求所有成员同时出现在同一场会议;ROUND_ROBIN依据可用性(availability)与优先级把预约轮流分发给成员;MANAGED是"父事件类型",由平台为每个成员自动派生"子事件类型",适合统一管理一个团队的对外服务目录。
十二、事件类型级 Webhooks
除了全局 Webhook,还可以把 Webhook 绑定到单个事件类型,只在它被预约/取消等时刻收到通知:
列出事件类型 Webhooks
GET /v2/event-types/{eventTypeId}/webhooks创建事件类型 Webhook
POST /v2/event-types/{eventTypeId}/webhooks{ "subscriberUrl": "https://your-app.com/webhook", "triggers": ["BOOKING_CREATED"], "active": true }subscriberUrl:接收通知的 HTTPS 回调地址;triggers:触发事件数组,BOOKING_CREATED表示"预约创建时"推送(完整的可用触发器清单与负载格式可查阅 webhooks.md);active:是否启用该订阅。
仓库实现位于 event-types-webhooks.controller.ts,控制器路径为/v2/event-types/:eventTypeId/webhooks,通过IsUserEventTypeWebhookGuard校验"当前用户确实是该事件类型 owner";请求体会经WebhookInputPipe清洗后写入,e2e 测试见同目录的event-types-webhooks.controller.e2e-spec.ts。
十三、从零开始的接入流程(Checklist)
结合 calcom-api/SKILL.md 的通用工作流,一次典型接入可以这样走:
- 鉴权准备:生成 API Key(
cal_live_/cal_test_),请求头携带Authorization: Bearer cal_<key>;2024-06-14 版端点还要求cal-api-version头,详见 authentication.md。 - 确认排班:先通过
GET /v2/schedules(参考 schedules.md)拿到scheduleId,再创建事件类型并绑定。 - 创建事件类型:
POST /v2/event-types,至少给出title+slug+lengthInMinutes;按需组装locations、bookingFields、缓冲区与确认策略。 - 公开预约链路:用返回的
id、slug生成 booking URL;对外可只暴露hidden/private-link 形态的入口。 - 团队化(可选):通过
/v2/teams/{teamId}/event-types或组织范围端点创建,选择ROUND_ROBIN/COLLECTIVE/MANAGED并配置hosts。 - 订阅通知:
POST /v2/event-types/{eventTypeId}/webhooks注册回调;收到事件后可用文档 webhooks.md 中说明的签名头做验签。
关键实现文件索引
想深入代码层面验证以上行为,可优先查看:
- 2024-06-14 主控制器:apps/api/v2/src/platform/event-types/event-types_2024_06_14/controllers/event-types.controller.ts
- 旧版本(2024-04-15 / 2024-06-11)控制器与其公开端点:event-types_2024_04_15/controllers/event-types.controller.ts
- 调度类型枚举:scheduling-type.ts
- 双向字段转换器目录:transformers/api-to-internal 与
transformers/internal-to-api - 私有链接控制器:event-types-private-links.controller.ts
- 团队事件类型服务:modules/teams/event-types/services/teams-event-types.service.ts
- 事件类型级 Webhook 控制器:modules/event-types/controllers/event-types-webhooks.controller.ts
以上文件与文档共同构成了"文档描述 → 控制器路由 → DTO 校验 → 转换器映射 → 服务层落库"的完整证据链,可放心作为你接入或二次开发 Event Types 功能时的依据。
【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考