🔗Integrations

🔗 将 Cowlendar 连接到你的 CRM、ERP 或自定义应用(Public API 与 Webhooks)

使用 Cowlendar 的 Public API 与 Webhooks,将预约同步到 CRM、ERP 或你的自定义工具

8 min read
2 views
Updated May 22, 2026

如果您经营一家依赖预订的企业,您可能需要在多个地方保存预订数据。您的 CRM 需要知道新客户何时预订。您的 ERP 需要收入数据。您的营销工具需要触发欢迎电子邮件。您的自定义应用程序需要以编程方式创建预订。

Cowlendar 的 公共 API Webhooks 使这一切成为可能。该 API 允许您读取您的服务和预订,并从任何外部系统创建新的预订。每次发生事件时,Webhook 都会向您的服务器推送实时通知:创建、确认、取消、重新安排或发生订阅事件。

这两个功能目前都处于 测试版

ℹ️Info
包含所有模式和响应示例的完整交互式 API 参考位于 https://app.cowlendar.com/public-api/docs

您可以使用 Cowlendar API 构建什么?

以下是 Cowlendar 商家目前正在构建的真实集成场景:

将每个预订同步到 HubSpot、Salesforce 或任何 CRM。 当客户预订理发、瑜伽课程或租赁时,网络钩子会立即触发,并包含完整的预订数据(姓名、电子邮件、电话、服务、日期、价格、团队成员)。您的 CRM 自动创建或更新联系人,无需手动输入数据。 构建您自己的预订页面或 移动应用。 使用 API 获取您的服务,包括持续时间、团队成员、设备和参与者类别,并从您自己的前端创建预订。预订会触发与通过 Cowlendar 小部件进行的任何预订相同的确认电子邮件、短信通知和 Google Calendar 同步。 使用 Zapier 或 Make 实现工作流程自动化,无需代码。 将 Cowlendar Webhook 指向您的 Zapier 或 Make Webhook URL。每一次预订事件都会成为一个触发器。当有人预订时发送 Slack 通知、向 Google 表格添加行、创建 Trello 卡片或在 Klaviyo 或 Mailchimp 中触发电子邮件序列。 将预订数据输入到您的报告或 BI 工具中。 使用列表预订端点来提取某个日期范围内的所有预订,并按状态、服务、出勤或客户电子邮件进行筛选。导出到您的数据仓库、Google Sheets 或 Airtable 以用于自定义仪表板。 将预订链接到订阅积分。 通过 API 创建预订时,传递 subscription_id 会自动从客户的计划中扣除一个积分,例如 10 节瑜伽通行证。 将 POS 或管理预订连接到外部系统。 Webhook 适用于所有预订源:店面小部件、管理面板、POS 和 API 本身。没有任何事情会被遗漏。

认证

每个 API 请求都需要由您的 Cowlendar 管理员生成的 不记名令牌 。令牌对您商店的预订和服务具有完全的读写权限。像对待密码一样对待它们:不要将它们提交给版本控制或在公共渠道中共享它们。

如何生成 API 令牌

步骤 1: 在您的 Shopify 管理员中,打开 Cowlendar 应用程序。

步骤 2: 转到 Settings > Public API

步骤 3: 单击“ Generate token ”。

步骤 4: 为您的令牌指定一个描述性名称,以便您以后可以识别它,例如“HubSpot sync”、“Zapier”或“Mobile app”。

步骤 5: 单击“ Generate ”。 Cowlendar 向您显示完整的令牌 仅一次 。立即复制它并将其存储在安全位置,例如密码管理器或服务器的环境变量。

您可以随时从同一页面撤销任何令牌。撤销令牌会立即停止使用它的所有 API 请求。

使用您的令牌

将令牌包含在每个 API 请求的 Authorization 标头中:

Authorization: Bearer YOUR_TOKEN_HERE

卷曲示例:

curl https://app.cowlendar.com/public-api/v1/services -H "Authorization: Bearer YOUR_TOKEN_HERE"

如果令牌丢失或无效,API 将返回 401

API端点

基本网址:https://app.cowlendar.com

列出服务

GET /public-api/v1/services

Returns all non-archived services in your shop.在通过 API 创建预订之前,使用此端点来发现您的服务 ID、类型、持续时间、团队成员、设备和参与者类别。

查询参数:

limit(可选,1-100):每页结果数。

cursor(可选):来自先前响应的分页光标。

is_active(可选,true 或 false):按活动或非活动服务过滤。

type(可选):按服务类型过滤。值:classic-checkoutclassic-no-checkoutmultiday-checkoutmultiday-no-checkoutmultislot-checkoutmultislot-no-checkoutfullday-checkoutfullday-no-checkout

q(可选,最多 120 个字符):按标题搜索服务。

sort(可选):-id(默认,最新的在前)、id(最旧的在前)、title(A 到 Z)、-title(Z 到 A)。

请求示例:

curl "https://app.cowlendar.com/public-api/v1/services?is_active=true&limit=10" -H "Authorization: Bearer YOUR_TOKEN_HERE"

响应示例(缩短):

{
 "data": [
 {
 "id": "6789abcdef0123456789abcd",
 "title": "Haircut",
 "type": "classic-checkout",
 "is_active": true,
 "timezone": "Europe/Paris",
 "durations": [30, 45, 60],
 "default_duration": 30,
 "avs_type": "teammates",
 "meeting_location": "in_person",
 "enable_participants": false,
 "participants": [],
 "equipments": [],
 "teammates": [
 {
 "id": "aaa111bbb222ccc333ddd444",
 "firstname": "Marie",
 "lastname": "Dupont",
 "email": "[email protected]",
 "thumbnail": null
 }
 ],
 "product": {
 "handle": "haircut",
 "title": "Haircut",
 "image": "https://cdn.shopify.com/..."
 },
 "created_at": "2025-09-01T10:00:00.000Z",
 "updated_at": "2026-05-10T08:30:00.000Z"
 }
 ],
 "pagination": {
 "has_more": false,
 "next_cursor": null
 }
}

了解关键服务领域

avs_type 告诉您此服务如何管理可用性并确定您在创建预订时是否需要传递 teammate_id

avs_type: "teammates"表示每个团队成员都有自己独立的日程安排。创建预订时,从 teammates 数组传递 teammate_id。如果省略它,店主将用作后备。

avs_type: "service" 表示服务本身拥有其可用性。指定的团队成员是根据预订时间自动确定的。您仍然可以传递 teammate_id 来强制分配特定人员。

avs_type: "equipment" 表示可用性是针对每台设备的。与服务模式类似,但与设备容量挂钩。

enable_participants 告诉您服务是否使用类型化的参与者类别,例如“成人”、“儿童”或“老年人”。当 true 时,participants 数组包含类别及其 ID、最小和最大数量以及默认值。创建包含详细参与者细分的预订时,您需要这些 ID。 设备 包含可预订的资源,例如房间、球场、车辆或自行车及其变体和 SKU。对于配备 tracking_type: "same" 的设备,所有单元均可互换,您可以使用任何 SKU,通常是 base。对于 tracking_type: "unique",每个单元都有自己的 SKU,您必须从 variants[].sku 中进行选择。 类型 告诉您预订模式。后缀 -checkout-no-checkout 指示服务是否使用 Shopify 签出。前缀表示调度模型:单时段为classic,酒店住宿等多日为multiday,连续多时段为multislot,全天预订为fullday

列出预订

GET /public-api/v1/bookings

返回您商店的预订。这是您用于 CRM 同步、报告导出和自定义预订仪表板的端点。

查询参数:

limit(可选,1-100):每页结果。

cursor(可选):分页光标。

start(可选,ISO 8601 日期时间):仅限在此日期或之后开始的预订。

end(可选,ISO 8601 日期时间):仅在此日期之前开始的预订。

service_id(可选,ID 数组):按一项或多项服务过滤。

subscription_id(可选,ID 数组):按订阅过滤。

status(可选,数组):confirmedpendingdeclinedcanceled

attendance(可选,数组):bookedpendingarrivedstartedcompletedno-showdelayedpaid

customer_email(可选):按确切的客户电子邮件过滤。

sort(可选):-id(默认,最新的在前)、id(最旧的在前)、start_date(最早的在前)、-start_date(最新的在前)。

示例:获取下周的所有已确认预订:

curl "https://app.cowlendar.com/public-api/v1/bookings?status=confirmed&start=2026-05-25T00:00:00Z&end=2026-06-01T00:00:00Z&sort=start_date" -H "Authorization: Bearer YOUR_TOKEN_HERE"

示例:查找特定客户的所有预订:

curl "https://app.cowlendar.com/public-api/v1/[email protected]" -H "Authorization: Bearer YOUR_TOKEN_HERE"

示例:导出特定服务中的所有缺席情况:

curl "https://app.cowlendar.com/public-api/v1/bookings?attendance=no-show&service_id=6789abcdef0123456789abcd" -H "Authorization: Bearer YOUR_TOKEN_HERE"

响应示例(缩短):

{
 "data": [
 {
 "id": "abc123def456ghi789jkl012",
 "booking_str": "COW-1234",
 "service": {
 "id": "6789abcdef0123456789abcd",
 "title": "Haircut",
 "type": "classic-checkout"
 },
 "start_date": "2026-05-27T14:00:00.000Z",
 "end_date": "2026-05-27T14:30:00.000Z",
 "timezone": "Europe/Paris",
 "customer": {
 "name": "Jane Smith",
 "email": "[email protected]",
 "phone": "+33612345678",
 "locale": "en"
 },
 "form_data": {
 "Special requests": "Window seat"
 },
 "quantity": 1,
 "unit_quantity": 1,
 "quantity_details": [],
 "price": { "amount": 35, "currency": "EUR" },
 "confirmation_status": "confirmed",
 "attendance": "booked",
 "financial_status": "paid",
 "is_canceled": false,
 "teammates": [
 {
 "id": "aaa111bbb222ccc333ddd444",
 "firstname": "Marie",
 "lastname": "Dupont"
 }
 ],
 "order_id": "gid://shopify/Order/123456789",
 "subscription_id": null,
 "created_at": "2026-05-20T09:15:00.000Z",
 "updated_at": "2026-05-20T09:15:00.000Z"
 }
 ],
 "pagination": {
 "has_more": false,
 "next_cursor": null
 }
}

了解关键预订字段

数量单位数量 一起工作。对于理发等经典服务,quantity 是参加的顾客数量,unit_quantity1。对于酒店住宿等多日服务,quantity 是房间数,unit_quantity 是住宿天数。对于多时隙服务,quantity 始终是 1unit_quantity 是预订的时隙数。总计费单位始终为 quantity x unit_quantity数量_详细信息 是详细的细分。它可以包含三种类型的条目:default 用于匿名计数,participant 用于键入参与者(如“成人”或“儿童”),或 equipment 用于可预订资源(如“Court 3”)。旧预订此处可能有一个空数组。 confirmation_status confirmedpendingdeclined。某些服务需要管理员在预订生效之前进行手动确认。 出勤 跟踪客户的出勤生命周期:bookedpendingarrivedstartedcompletedno-showdelayedpaid order_id 是预订通过 Shopify 结账时的 Shopify 订单 ID。用于手动预订和 API 创建预订的 null subscription_id 将预订链接到订阅计划,例如 10 级通行证。

通过 API 创建预订

POST /public-api/v1/bookings

以编程方式创建预订。这被视为 手动预订 ,意味着不会创建 Shopify 订单。然而,所有常见的自动化仍然正常触发:向客户发送确认电子邮件、向团队发送通知电子邮件和短信,以及与 Google Calendar 或 Outlook 进行日历同步。

您在构建自定义预订前端、移动应用、自助服务终端或从其他系统导入预订时使用此端点。

必填字段:

service_id(字符串):服务的 ID,您从列表服务中获取。

start_date(ISO 8601 日期时间):预订开始时。

end_date(ISO 8601日期时间):预订结束时。

timezone(字符串):IANA 时区,例如 Europe/ParisAmerica/New_YorkAsia/Tokyo

customer(对象):必须至少包含emailphone。它还可以包括 namefirstnamelastnamelocale,这是客户通知电子邮件的语言代码。

可选字段:

quantity(整数,默认1):预订的顶级单元。如果提供了 quantity_details,则忽略。

unit_quantity(整数,默认 1):每个顶级单元的子单元。

quantity_details(数组):按参与者类型或设备详细分类。如果提供,quantity 会自动计算为总和。

teammate_id(字符串):强制分配特定团队成员。

subscription_id(字符串):将此预订链接到现有订阅并扣除一个积分。

form_data(对象):预订表单中的自定义键或值对。

price(数字,默认 0):预订的净总价。

currency(字符串):ISO 4217 代码,如 USDEURGBP。默认为您商店的货币。

示例:创建一个简单的预订

一位顾客与特定造型师预订了 30 分钟的理发服务:

curl -X POST https://app.cowlendar.com/public-api/v1/bookings -H "Authorization: Bearer YOUR_TOKEN_HERE" -H "Content-Type: application/json" -d '{
 "service_id": "6789abcdef0123456789abcd",
 "start_date": "2026-06-01T10:00:00.000Z",
 "end_date": "2026-06-01T10:30:00.000Z",
 "timezone": "Europe/Paris",
 "customer": {
 "email": "[email protected]",
 "name": "John Doe",
 "phone": "+33612345678",
 "locale": "en"
 },
 "teammate_id": "aaa111bbb222ccc333ddd444",
 "quantity": 1,
 "price": 35,
 "currency": "EUR"
}'

Cowlendar 创建预订并为客户和团队成员触发确认电子邮件、短信和日历邀请。

示例:与键入的参与者进行团体预订

一家人预订导游:3名成人和2名儿童,不同价位:

curl -X POST https://app.cowlendar.com/public-api/v1/bookings -H "Authorization: Bearer YOUR_TOKEN_HERE" -H "Content-Type: application/json" -d '{
 "service_id": "6789abcdef0123456789abcd",
 "start_date": "2026-06-15T09:00:00.000Z",
 "end_date": "2026-06-15T11:00:00.000Z",
 "timezone": "America/New_York",
 "customer": {
 "email": "[email protected]",
 "firstname": "Sarah",
 "lastname": "Miller",
 "phone": "+15551234567",
 "locale": "en"
 },
 "quantity_details": [
 {
 "type": "participant",
 "name": "Adult",
 "quantity": 3,
 "participant_id": "adult_id_from_service"
 },
 {
 "type": "participant",
 "name": "Child (under 12)",
 "quantity": 2,
 "participant_id": "child_id_from_service"
 }
 ],
 "price": 175,
 "currency": "USD"
}'

participant_id 值来自列表服务响应中服务的 participants[] 数组。总 quantity 自动设置为 5 (3 + 2)。

示例:使用设备预订

客户预订网球场1小时:

curl -X POST https://app.cowlendar.com/public-api/v1/bookings -H "Authorization: Bearer YOUR_TOKEN_HERE" -H "Content-Type: application/json" -d '{
 "service_id": "6789abcdef0123456789abcd",
 "start_date": "2026-06-10T16:00:00.000Z",
 "end_date": "2026-06-10T17:00:00.000Z",
 "timezone": "Europe/London",
 "customer": {
 "email": "[email protected]",
 "name": "Tom Wilson"
 },
 "quantity_details": [
 {
 "type": "equipment",
 "name": "Court 3",
 "quantity": 1,
 "equipment_id": "eee555fff666ggg777hhh888",
 "equipment_sku": "COURT3",
 "capacity": 4
 }
 ],
 "price": 25,
 "currency": "GBP"
}'

equipment_idequipment_sku 来自服务的 equipments[] 阵列。对于配备 tracking_type: "same" 的设备,可使用任何 SKU,通常是 base。对于 tracking_type: "unique",请使用适合您所需装置的特定 variants[].sku

示例:订阅链接预订

瑜伽馆会员可使用其每月 10 节课程通行证中的一个积分:

curl -X POST https://app.cowlendar.com/public-api/v1/bookings -H "Authorization: Bearer YOUR_TOKEN_HERE" -H "Content-Type: application/json" -d '{
 "service_id": "6789abcdef0123456789abcd",
 "start_date": "2026-06-05T08:00:00.000Z",
 "end_date": "2026-06-05T09:00:00.000Z",
 "timezone": "America/Los_Angeles",
 "customer": {
 "email": "[email protected]",
 "name": "Lisa Chen",
 "locale": "en"
 },
 "subscription_id": "fff000eee111ddd222ccc333",
 "price": 0
}'

订阅必须是active,属于同一服务,并且有remaining_credits > 0。 Cowlendar 自动减少信用计数并向客户发送订阅更新电子邮件。

错误代码

400 验证错误。必填字段缺失或格式错误。错误正文告诉您哪个字段。

401 令牌丢失或无效。

403 商店处于非活动状态或访问受到限制。

404 未找到服务。

422 预订被拒绝。时间段已被占用、团队成员无法使用、设备已满或订阅没有剩余积分。具体原因请查看错误信息。

429 超出速率限制。间隔您的请求并稍后重试。

分页

所有列表端点都使用基于光标的分页。当响应包含 "has_more": true 时,将 next_cursor 值作为 cursor 参数传递到下一个请求中:

curl "https://app.cowlendar.com/public-api/v1/bookings?cursor=eyJpZCI6IjY3ODlhYmNkIn0=&limit=50" -H "Authorization: Bearer YOUR_TOKEN_HERE"

排序顺序在页面之间保留。最大页面大小为 100 个结果。

Webhooks:实时预订通知

API 允许您按需提取数据,而 Webhook 可以立即将数据推送给您。每次创建、确认、取消或重新安排预订时,Cowlendar 都会将签名的 HTTP POST 发送到您配置的 URL。

Webhooks 对于构建实时集成至关重要:CRM 同步、Slack 通知、自动电子邮件序列、实时仪表板或任何需要对预订事件发生时做出反应的工作流程。

设置 Webhook 端点

步骤 1: 在您的 Shopify 管理员中,打开 Cowlendar 应用程序。

步骤 2: 转到 Settings > Webhooks

步骤 3: 单击“ Add endpoint ”。

步骤 4: 输入端点的名称,例如“HubSpot CRM”、“Zapier”或“Analytics pipeline”。

步骤 5: 粘贴您的 HTTPS URL 。 URL 必须使用 HTTPS。如果您使用的是 Zapier 或 Make,请粘贴它们提供的 Webhook URL。

步骤 6: 检查您想要接收的事件。您可以选择任何组合。

步骤 7: 单击“ Create ”。

Cowlendar 显示以 whsec_ 开头的 签名秘密 。立即复制并安全保存。此秘密仅显示一次,您需要它来验证服务器上传入的 Webhook 请求。

您可以稍后编辑任何端点以更改 URL 或启用或禁用特定事件:

8 个 Webhook 事件

booking.created 当从任何来源创建新预订时触发:店面小部件、管理面板、POS 或公共 API。 booking.confirmed 当管理员手动确认待处理的预订时触发。 booking.declined 当待处理的预订被拒绝时触发。 booking.canceled 当客户或管理员取消预订时触发。 booking.rescheduled 当预订日期或分配的团队成员更改时触发。 booking.attendance_changed 当出勤状态更改时触发,例如从“已预订”到“已完成”或标记为“缺席”时。 subscription.created 在创建新的定期订阅时触发。 subscription.billing_failed 当订阅计费尝试失败时触发。连续3次失败后,Shopify上的认购合约自动取消,本地状态变为canceled

Webhook 负载是什么样子的

每个 Webhook 传递都是带有 JSON 正文的 HTTP POST。这是一个完整的 booking.created 示例:

{
 "id": "evt_2b86f80a-1c3d-4e5f-9a8b-7c6d5e4f3a2b",
 "type": "booking.created",
 "created_at": "2026-05-20T09:15:00.000Z",
 "shop_domain": "yourshop.myshopify.com",
 "data": {
 "id": "abc123def456ghi789jkl012",
 "booking_str": "COW-1234",
 "service": {
 "id": "6789abcdef0123456789abcd",
 "title": "Haircut",
 "type": "classic-checkout"
 },
 "start_date": "2026-05-27T14:00:00.000Z",
 "end_date": "2026-05-27T14:30:00.000Z",
 "timezone": "Europe/Paris",
 "customer": {
 "name": "Jane Smith",
 "email": "[email protected]",
 "phone": "+33612345678",
 "locale": "en"
 },
 "form_data": {},
 "quantity": 1,
 "unit_quantity": 1,
 "quantity_details": [],
 "price": { "amount": 35, "currency": "EUR" },
 "confirmation_status": "confirmed",
 "attendance": "booked",
 "financial_status": null,
 "is_canceled": false,
 "teammates": [
 {
 "id": "aaa111bbb222ccc333ddd444",
 "firstname": "Marie",
 "lastname": "Dupont"
 }
 ],
 "order_id": null,
 "subscription_id": null,
 "created_at": "2026-05-20T09:15:00.000Z",
 "updated_at": "2026-05-20T09:15:00.000Z"
 }
}

data 字段包含完整的预订或订阅对象,其结构与 API 响应完全相同。

对于 subscription.billing_failed 事件,负载包括一个额外的 previous 对象以及当前尝试之前的 failure_count,因此您可以跟踪升级情况,例如在第二次失败后发送紧急电子邮件。

每个 Webhook 传递的请求标头

X-Cowlendar-Event-Id 是事件的唯一 ID。 Cowlendar 在重试失败的传递时重复使用相同的 ID。使用它在您这边进行重复数据删除。

X-Cowlendar-Event-Type 是事件类型字符串,例如 booking.created

X-Cowlendar-Timestamp 是 Cowlendar 签署请求时的 Unix 时间戳(以秒为单位)。

X-Cowlendar-Signaturet=<timestamp>,v1=<hex> 格式的 HMAC-SHA256 签名。

验证 webhook 签名

每个 Webhook 请求都经过签名,因此您可以确认它确实来自 Cowlendar 并且未被篡改。使用端点的签名密钥将签名计算为 HMAC-SHA256(secret, timestamp + "." + raw_body)

您还应该拒绝时间戳早于 5 分钟的请求,以防止重放攻击。

⚠️Warning
始终通过 RAW 请求主体(发送的确切字节 Cowlendar)计算签名,而不是解析和重新序列化的 JSON 对象。解析和重新序列化可能会更改空格或键顺序,从而破坏签名。

Node.js(Express):

import crypto from "crypto";
import express from "express";

const app = express();

app.post(
 "/webhooks/cowlendar",
 express.raw({ type: "application/json" }),
 (req, res) => {
 const sigHeader =
 req.header("X-Cowlendar-Signature") || "";
 const timestamp =
 req.header("X-Cowlendar-Timestamp") || "";
 const rawBody = req.body.toString("utf8");

 // Reject requests older than 5 minutes
 if (
 Math.abs(Date.now() / 1000 - parseInt(timestamp, 10))
 > 300
 ) {
 return res.status(400).send("Timestamp too old");
 }

 const expected = crypto
 .createHmac(
 "sha256",
 process.env.COWLENDAR_WEBHOOK_SECRET
 )
 .update(`${timestamp}.${rawBody}`)
 .digest("hex");

 const received = sigHeader
 .split(",")
 .find((p) => p.startsWith("v1="))
 ?.slice(3);

 if (
 !received ||
 !crypto.timingSafeEqual(
 Buffer.from(expected),
 Buffer.from(received)
 )
 ) {
 return res.status(401).send("Invalid signature");
 }

 const event = JSON.parse(rawBody);

 // Short-circuit test pings
 if (event.type === "webhook.ping") {
 return res.status(200).send("pong");
 }

 // Handle real events here
 res.status(200).send("ok");
 }
);

Python(烧瓶):

import hmac, hashlib, os, time, json
from flask import Flask, request

app = Flask(__name__)

@app.post("/webhooks/cowlendar")
def cowlendar_webhook():
 sig_header = request.headers.get(
 "X-Cowlendar-Signature", ""
 )
 timestamp = request.headers.get(
 "X-Cowlendar-Timestamp", ""
 )
 raw_body = request.get_data(as_text=True)

 # Reject requests older than 5 minutes
 if abs(time.time() - int(timestamp)) > 300:
 return "Timestamp too old", 400

 expected = hmac.new(
 os.environ["COWLENDAR_WEBHOOK_SECRET"].encode(),
 f"{timestamp}.{raw_body}".encode(),
 hashlib.sha256,
 ).hexdigest()

 received = next(
 (p[3:] for p in sig_header.split(",")
 if p.startswith("v1=")),
 None,
 )
 if not received or not hmac.compare_digest(
 expected, received
 ):
 return "Invalid signature", 401

 event = json.loads(raw_body)

 if event["type"] == "webhook.ping":
 return "pong", 200

 # Handle real events here
 return "ok", 200

PHP:

<?php
$secret = getenv("COWLENDAR_WEBHOOK_SECRET");

$rawBody = file_get_contents("php://input");
$sigHeader = $_SERVER["HTTP_X_COWLENDAR_SIGNATURE"] ?? "";
$timestamp = $_SERVER["HTTP_X_COWLENDAR_TIMESTAMP"] ?? "";

// Reject requests older than 5 minutes
if (abs(time() - (int)$timestamp) > 300) {
 http_response_code(400);
 exit("Timestamp too old");
}

$expected = hash_hmac(
 "sha256",
 $timestamp . "." . $rawBody,
 $secret
);

$received = null;
foreach (explode(",", $sigHeader) as $part) {
 if (str_starts_with($part, "v1=")) {
 $received = substr($part, 3);
 break;
 }
}

if (!$received || !hash_equals($expected, $received)) {
 http_response_code(401);
 exit("Invalid signature");
}

$event = json_decode($rawBody, true);

if ($event["type"] === "webhook.ping") {
 http_response_code(200);
 exit("pong");
}

// Handle real events here
http_response_code(200);
echo "ok";

上线前测试您的 webhook

每个 Webhook 端点在管理中都有一个 Test 按钮。单击它会将合成的 webhook.ping 事件发送到您的 URL。此事件使用与真实事件相同的签名、标头和重试行为,但具有无害的测试负载:

{
 "id": "evt_2b86f80a...",
 "type": "webhook.ping",
 "created_at": "2026-05-13T14:57:57.704Z",
 "shop_domain": "yourshop.myshopify.com",
 "data": {
 "message": "This is a test event from Cowlendar. If you can read this, your endpoint is reachable."
 }
}

使用它来确认 Cowlendar 可以到达您的 URL,DNS、HTTPS 和防火墙设置正常工作,并验证您的 HMAC 签名验证码 无需创建真正的预订 。真正的预订会向客户发送电子邮件、创建日历事件,并可能触发 Shopify 订单。

单击测试后,检查 See deliveries 按钮以查看端点返回的 HTTP 状态代码。绿色的 200 表示一切正常。

重试行为并自动禁用

如果您的终端返回 5xx 错误、429 或网络超时,Cowlendar 将按以下计划重试:

1分钟后、5分钟后、25分钟后、2小时后、10小时后、50小时后。总共 6 次尝试,持续大约 3 天。

在所有事件中连续大约 20 次失败后,端点将自动禁用,并且 Cowlendar 会向店主发送电子邮件。服务器重新上线后,您可以从 Settings > Webhooks 重新启用端点。

重试时会重复使用相同的 X-Cowlendar-Event-Id。始终存储已处理的事件 ID 并跳过重复项。

任何 2xx 响应均视为成功。您不需要返回特定的正文或内容类型。

Webhook 最佳实践

在 10 秒内响应。 如果您的处理程序需要执行繁重的工作,例如调用另一个 API、写入数据库或运行复杂的操作,请异步执行。立即使用 200 接受 Webhook,然后在后台作业或队列中处理它。 使用事件 ID 进行重复数据删除。 存储您处理的每个 X-Cowlendar-Event-Id。如果您再次收到相同的 ID,请跳过它。 Cowlendar 失败时使用相同的 ID 重试。 对签名使用常量时间比较。 Node.js 中为 crypto.timingSafeEqual,Python 中为 hmac.compare_digest,PHP 中为 hash_equals。像 === 这样的常规字符串比较很容易受到定时攻击。 请勿从您这边重试。 Cowlendar 自动处理重试。如果您的服务器出现临时问题,请返回 5xx,Cowlendar 将按其重试计划恢复。 显式处理 webhook.ping 针对 ping 事件立即返回 200。这使您可以使用“测试”按钮,而无需触发业务逻辑。

目前的限制

通过 API 创建的预订是手动预订。 它们不会创建 Shopify 订单。如果您的服务通常使用 Shopify 结帐,则 API 会绕过它。确认电子邮件、短信和日历同步仍然正常工作。 尚无可用性端点。 当前的 API 允许您读取服务和预订,以及创建预订。它不公开可用的时间段。要检查时间段是否可用,请尝试创建预订并在该时间段已被占用时处理 422 响应。未来的更新中可能会添加可用性端点。 尚无更新或删除端点。 您可以创建和读取预订,但无法通过 API 更新、重新安排或取消它们。目前通过 Cowlendar 管理员管理这些操作。 适用速率限制。 如果您在短时间内发送过多请求,API 将返回 429。对于导出所有预订等批量操作,请在分页请求之间添加短暂的延迟,例如 200 到 500 毫秒。 Webhook 机密仅显示一次。 如果您丢失了签名机密,请删除端点并创建一个新端点以获取新的机密。 Webhook 需要 HTTPS。 不接受 HTTP 端点。

常见问题解答

我可以在不编写代码的情况下将 Cowlendar API 与 Zapier 或 Make 一起使用吗?

是的。对于 Webhook,从 Cowlendar 接收事件,在 Zapier 或 Make 中创建 "Custom Webhook" 触发器,复制它们为您提供的 URL,并将其添加为 Cowlendar Settings > Webhooks 中的端点。每个预订事件都会触发您的自动化。对于 API,向 Cowlendar 发送请求,使用 Zapier 或 Make 中的 "HTTP Request" 操作使用您的不记名令牌调用任何 Cowlendar API 端点。

通过 API 创建的预订是否会向客户发送电子邮件?

是的。 API 创建的预订的处理方式与手动预订完全相同。确认电子邮件、短信通知和日历同步都会自动触发,就像通过管理面板进行预订一样。

如果我的 webhook 端点出现故障会发生什么?

Cowlendar 在大约 3 天内重试失败的交付 6 次。连续大约 20 次失败后,端点将自动禁用,并且您会收到一封电子邮件。服务器恢复后,从 Settings > Webhooks 重新启用它。

我可以有多个 Webhook 端点吗?

是的。根据需要创建任意多个,每个都有不同的事件选择和 URL。例如,您的 CRM 的一个端点用于侦听 booking.createdbooking.canceled,另一个端点用于您的 analytics pipeline 侦听所有事件。

我可以撤销 API 令牌吗? 是的。转到 Settings > Public API 并单击 Revoke 按钮。令牌立即停止工作。 是否有沙箱或暂存环境?

目前还没有。使用 webhook.ping 测试按钮验证您的 Webhook 设置,而无需创建实际预订。对于 API 测试,我们建议在您的 Cowlendar 管理员中创建一个仅用于开发的测试服务。

完整的 API 参考在哪里?

包含所有架构、参数和响应示例的交互式 API 文档位于 https://app.cowlendar.com/public-api/docs

Cowlendar
Ready to use Cowlendar?
Open the app directly in your Shopify store.