用 Rovela 附件 API 发送日历邀请(.ics)邮件(2026)
生成 VEVENT .ics,以 text/calendar 经 base64 挂到 POST /emails,并用 Idempotency-Key = cal-invite-{bookingId}。会议/预约视角——区别于发票 PDF 与附件总览。Rovela 限制仅对齐文档。
为什么预约类产品发 .ics 而不是 PDF
用户约了产品演示、咨询或课程席位时,需要的是把事件写进自己的日历,而不是一份静态 PDF。开放格式是 iCalendar(RFC 5545):一小段 text/calendar,常见文件名 invite.ics,内含 VEVENT(UID、DTSTART、DTEND、SUMMARY、ORGANIZER、ATTENDEE)。
Rovela 不会替你生成 ICS。你的预约服务负责拼出 .ics 字节,再用与其它附件相同的方式发出:POST https://api.rovela.dev/emails,attachments[] = { filename, content (base64), content_type },并带上 Idempotency-Key,避免 webhook 重试时重复邀请。
本文对齐线上文档:带附件的发票邮件 与 发送邮件 的 attachments 字段。它不是用 HTTP API 发送带 PDF 附件的发票邮件(财务 PDF),也不是邮件附件指南(格式总览)。相关事务邮:Stripe webhook 收据、Shopify 订单 webhook、团队邀请链接邮件。
你负责什么,Rovela 负责什么
你的排期 / 预约任务负责:
- 生成合法 ICS(
BEGIN:VCALENDAR…END:VCALENDAR),每个预约使用稳定的UID - 把标题、时间、时区、入会链接、地点同时写进 ICS 以及 HTML/纯文本正文(手机预览很少打开附件)
- 将
.ics编成单行 base64,并设置content_type: "text/calendar" - 用预约 ID 作为
Idempotency-Key(例如cal-invite-bk_8f3a2c) - 更倾向对每位受邀者发一次
POST /emails(或按产品设计使用多个to),而不是把大包 ICS 塞进/emails/batch
Rovela 负责:
- 接收 JSON、解码附件、组装 MIME multipart、DKIM 签名、入队、重试与事件历史
- 执行文档中的请求体大小上限(见下)
返回 200 且带 {"object":"email","id":"..."} 只表示已接受并入队,不代表「已写入 Google 日历」。
前置条件
- 在 Rovela 控制台完成发信域名验证。未验证的
from域名会返回400(快速开始)。 - 形如
re_…的 API Key。生产邀请发送建议用sending_access(Authentication)。 - 可在 worker / webhook 重试时复用的预约 ID,用作幂等键。
大小限制(来自发票附件文档)
线上发票示例写明:整个 JSON 请求体上限为 2 MB。Base64 大约膨胀三分之一,附件合计大约剩 1.5 MB。超限会在发送前返回 413。
单个 .ics 通常只有几 KB,远低于上限。若还要附带演示文稿或录像,请记住同一 2 MB JSON 预算,或把大文件放到自有存储并在正文给链接。详见带附件的发票邮件。
拼一份最小的 VEVENT
在服务端生成 ICS。让 UID 对同一预约保持稳定,便于日后改期时客户端更新或取消。下面时间用 UTC(Z);若要用本地浮动时间,应自行加 VTIMEZONE——Rovela 只负责传输你附上的字节,不会改写 ICS。
function buildInviteIcs({ uid, summary, description, startUtc, endUtc, organizerEmail, attendeeEmail, location, url }) {
const lines = [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//Acme Booking//EN",
"CALSCALE:GREGORIAN",
"METHOD:REQUEST",
"BEGIN:VEVENT",
`UID:${uid}`,
`DTSTAMP:${startUtc}`,
`DTSTART:${startUtc}`,
`DTEND:${endUtc}`,
`SUMMARY:${summary}`,
`DESCRIPTION:${description}`,
`ORGANIZER:mailto:${organizerEmail}`,
`ATTENDEE:mailto:${attendeeEmail}`,
location ? `LOCATION:${location}` : null,
url ? `URL:${url}` : null,
"STATUS:CONFIRMED",
"SEQUENCE:0",
"END:VEVENT",
"END:VCALENDAR",
].filter(Boolean);
return lines.join("\r\n") + "\r\n";
}
示例值:
uid:bk_8f3a2c@mail.acme.comstartUtc/endUtc:20261105T150000Z/20261105T153000Zsummary:Acme product demourl:Meet / Zoom / 应用内会议室链接
若标题可能含逗号、分号或换行,按 RFC 5545 自行转义。转义是你的责任;Rovela 不会改写 ICS 内容。
curl:去掉换行再 POST
GNU base64 默认每 76 字符换行。Rovela 解码器拒绝 base64 中的换行,因此按发票文档同样方式剥离:
ICS=$(base64 < invite.ics | tr -d '\n')
curl -X POST https://api.rovela.dev/emails \
-H "Authorization: Bearer re_your_sending_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: cal-invite-bk_8f3a2c" \
-d @- <<EOF
{
"from": "Acme Scheduling <schedules@mail.acme.com>",
"to": "alex@customer.com",
"reply_to": "schedules@acme.com",
"subject": "Calendar invite: Acme product demo — Nov 5",
"html": "<p>Hi Alex,</p><p>Your <strong>Acme product demo</strong> is confirmed for <strong>Nov 5, 2026 · 15:00–15:30 UTC</strong>.</p><p>Join: <a href=\"https://meet.acme.com/bk_8f3a2c\">https://meet.acme.com/bk_8f3a2c</a></p><p>The attached <code>invite.ics</code> adds the event to your calendar.</p>",
"text": "Hi Alex,\n\nYour Acme product demo is confirmed for Nov 5, 2026 · 15:00–15:30 UTC.\nJoin: https://meet.acme.com/bk_8f3a2c\n\nThe attached invite.ics adds the event to your calendar.\n",
"attachments": [
{
"filename": "invite.ics",
"content": "$ICS",
"content_type": "text/calendar"
}
]
}
EOF
为什么要 content_type: "text/calendar"
不设 content_type 时,部件会按 application/octet-stream 发出。许多客户端会显示「未知文件」,而不是「添加到日历」。请始终设置 "content_type": "text/calendar"——与文档对发票使用 application/pdf 的要求相同。
为什么要 Idempotency-Key
预约 webhook、队列 worker 与「重发邀请」按钮会在超时后再次 POST。使用 Idempotency-Key: cal-invite-bk_8f3a2c(最长 256 字符;24 小时内重试返回原响应),客人只会收到一封邀请。见发送邮件的幂等。
Node.js:生成 ICS → base64 → fetch
export async function sendCalendarInvite({ booking, attendeeEmail }) {
const ics = buildInviteIcs({
uid: `${booking.id}@mail.acme.com`,
summary: booking.title,
description: `Join: ${booking.joinUrl}`,
startUtc: booking.startUtc,
endUtc: booking.endUtc,
organizerEmail: "schedules@acme.com",
attendeeEmail,
location: booking.location || booking.joinUrl,
url: booking.joinUrl,
});
const res = await fetch("https://api.rovela.dev/emails", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.ROVELA_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "cal-invite-" + booking.id,
},
body: JSON.stringify({
from: "Acme Scheduling <schedules@mail.acme.com>",
to: attendeeEmail,
reply_to: "schedules@acme.com",
subject: "Calendar invite: " + booking.title,
html:
"<p>Your <strong>" +
booking.title +
"</strong> is confirmed.</p><p>Join: <a href=\"" +
booking.joinUrl +
"\">" +
booking.joinUrl +
"</a></p><p>Open the attached invite.ics to add it to your calendar.</p>",
text:
"Your " +
booking.title +
" is confirmed.\nJoin: " +
booking.joinUrl +
"\n\nOpen the attached invite.ics to add it to your calendar.\n",
attachments: [
{
filename: "invite.ics",
content: Buffer.from(ics, "utf8").toString("base64"),
content_type: "text/calendar",
},
],
}),
});
if (!res.ok) throw new Error("calendar invite email failed: " + res.status);
return (await res.json()).id;
}
reply_to 把回复导向真人可读的邮箱,同时 from 仍使用已验证发信子域——与发票示例同一模式。
会议邀请的好习惯
- 正文优先,ICS 其次。 把时间、时区、入会链接写进 HTML 与 text。预览窗和锁屏不会打开附件。
- 文件名不要空格或个人数据。 用
invite.ics或demo-bk_8f3a2c.ics,避免Alex Smith Meeting.ics。 - 稳定 UID + SEQUENCE。 改期时递增
SEQUENCE,重建 ICS,并用新的幂等键重发(例如cal-invite-bk_8f3a2c-seq-1),以免被当成原发送的重复请求吞掉。 - 需要个性化时一人一封。 单封邮件
to/cc/bcc合计最多 50 人(发送邮件),但日历客户端常期望单一 ATTENDEE。演示与 1:1 预约建议每位客人一次 POST。 - 带附件的邀请慎用
/emails/batch。 发票文档指出 batch 共用同一 2 MB 体,一条非法条目可能整批拒绝;ICS 包请同样谨慎。见批量与定时发送。
你会真正碰到的错误
来自发送邮件与发票附件示例:
400— 缺字段、base64 无法解码(常见是残留换行)、或from域名不属于你的组织401— Key 无效/缺失,或用sending_access去读/取消接口413— JSON 体超过 2 MB 请求上限429— 限流;退避后用同一个 Idempotency-Key 重试
本文不是什么
- Rovela 不托管日历、忙闲或 RSVP 回调。Outlook/Google 里的接受/拒绝是客户端 ICS 行为;若要 RSVP webhook,在自己的产品里接。
- 不要把
method=REQUEST之类 MIME 参数、或Content-Disposition: inline编造成 Rovela API 字段——文档中的附件形状只有filename、content、content_type。若客户端需要 METHOD,把METHOD:REQUEST写进 ICS 内部,如上所示。 - 大附件或非日历文件,请从邮件附件指南与带附件的发票邮件入手。
下一步
- 验证发信域名并创建
sending_accessKey。 - 为一次真实预约生成
.ics,用content_type: "text/calendar"发出。 - 确认返回
id,再在 Gmail / Apple Mail / Outlook 中打开并添加事件。 - 把
Idempotency-Key = cal-invite-{bookingId}与预约行一起存,保证重试安全。
文档:发送邮件 · 带附件的发票邮件 · Authentication