Giải phápKhách hàng
Tài liệu

29/9/2026

Tóm tắt

blog.share_blog

API reference @cogover/sdk: Notification và email

Trang này thuộc API reference của @cogover/sdk.

Notification

notifications lưu một notification vào danh sách thông báo (biểu tượng chuông) của từng người nhận và gửi nó trong web app, qua web push, mobile push và một bản sao email, theo các kênh thông báo (notification channel) của workspace. Khác với push message, notification vẫn nằm trong danh sách sau khi đã được gửi, nên phù hợp với việc một người cần xử lý sau, ví dụ yêu cầu phê duyệt. Việc gửi diễn ra bất đồng bộ và là best-effort: lời gọi hoàn tất khi Cogover đã tiếp nhận notification, không phải khi nó tới thiết bị hay hộp thư.

type NotificationContentType = "text" | "html";
type NotificationDisplay = "SIMPLE" | "FULL";
type NotificationSender = "workspace" | "actor";
type NotificationLinkTarget = "SAME_TAB" | "NEW_TAB";
 
interface NotificationLink {
  readonly url: string;
  readonly openIn?: NotificationLinkTarget;
}
 
interface NotificationMessage {
  readonly to: readonly string[];
  readonly exclude?: readonly string[];
  readonly title: string;
  readonly content: string;
  readonly contentType?: NotificationContentType;
  readonly subtitle?: string;
  readonly display?: NotificationDisplay;
  readonly link?: NotificationLink;
  readonly sender?: NotificationSender;
  readonly channel?: string;
  readonly email?: boolean;
  readonly idempotencyKey?: string;
}
 
interface NotificationResult {
  readonly requestId: string;
  readonly notified: number;
  readonly skippedPersonnelIds: readonly string[];
  readonly duplicate: boolean;
}
 
interface NotificationsApi {
  send(message: NotificationMessage): Promise<NotificationResult>;
}
const result = await notifications.send({
  to: approverIds,
  exclude: ["actor"],
  title: "Leave request waiting for your approval",
  content: "A leave request of 3 days was submitted and needs your decision.",
  link: { url: `/app/leave_request/${recordId}` },
  channel: approvalChannelId,
  idempotencyKey: `leave-approval:${recordId}`,
});
// result.notified: số tài khoản được gửi; result.skippedPersonnelIds: personnel chưa có tài khoản

notifications.send(message): Promise<NotificationResult>

Người nhận. to liệt kê từ 1 đến 200 personnel ID; ID trùng được loại bỏ. Notification được gửi tới tài khoản người dùng của từng personnel. Personnel chưa có tài khoản người dùng được bỏ qua và trả về trong skippedPersonnelIds, còn notified là số tài khoản được gửi sau khi đã loại trừ. exclude liệt kê personnel ID không bao giờ nhận notification; giá trị đặc biệt "actor" là người dùng có hành động khởi đầu execution hiện tại (người gọi HTTP route, người lưu record làm trigger chạy, người có lời gọi đã enqueue lần chạy job). Execution không có người dùng thì không có actor, nên "actor" không loại ai. Personnel ID trong to không tồn tại ném NotFoundError với resource là "personnel" trước khi gửi bất cứ gì. Khi mọi người nhận đều bị loại trừ hoặc bỏ qua, không có gì được gửi và notified là 0.

Nội dung và cách hiển thị.

  • title (bắt buộc, tối đa 200 ký tự) là tiêu đề của notification và là subject của bản sao email.
  • content (bắt buộc, tối đa 16.384 ký tự) là nội dung. Với contentType: "text" (mặc định), đây là văn bản thuần. Với "html", nội dung được rút gọn về định dạng cơ bản trước khi lưu — đoạn văn, tiêu đề, danh sách, bảng, liên kết và nhấn mạnh; script, style và hình ảnh bị loại bỏ — và vẫn phải còn chữ hiển thị được, nếu không sẽ ném ValidationError.
  • subtitle (tối đa 200 ký tự) chỉ hiển thị ở chế độ "FULL". display mặc định là "FULL" khi có subtitle và "SIMPLE" khi không có; subtitle đi kèm display: "SIMPLE" ném ValidationError.
  • link là trang được mở khi người nhận bấm vào notification. url là một path của web app Cogover bắt đầu bằng /, hoặc một URL tuyệt đối http hay https, tối đa 2.048 ký tự. openIn là "SAME_TAB" hoặc "NEW_TAB"; mặc định "SAME_TAB" với path và "NEW_TAB" với URL tuyệt đối.
  • sender là "workspace" (mặc định) hoặc "actor", hiển thị tên và ảnh đại diện của người dùng có hành động khởi đầu execution. "actor" trong execution không có người dùng ném ValidationError.

Kênh thông báo và bản sao email. channel là record ID của một notification channel của workspace. Kênh quyết định hình thức gửi nào được bật — trong web app, web push, mobile push và email — và mỗi người dùng có thể ghi đè lựa chọn đó trong cài đặt thông báo của riêng mình. Không có channel thì mọi hình thức gửi đều được bật. Kênh không tồn tại ném NotFoundError với resource là "notificationChannel"; workspace chưa thiết lập notification ném NotFoundError với resource là "object" và resourceId là "notification_channel".

email: false bỏ bản sao email kể cả khi kênh bật email. Bản sao dùng title làm subject và content làm nội dung, được gửi từ một địa chỉ thông báo của Cogover chứ không phải từ hộp thư của workspace, và được tính vào hạn mức email thông báo hằng ngày theo gói của workspace. Để gửi email từ hộp thư của workspace, dùng email.send.

Idempotency. idempotencyKey bắt đầu bằng chữ cái hoặc chữ số và chứa tối đa 128 chữ cái, chữ số, ., _, : hoặc -. Lời gọi có key mà project đã dùng cho một notification trong 7 ngày gần nhất sẽ không gửi gì và trả về kết quả của lời gọi đầu tiên với duplicate: true. Lời gọi thực hiện trong lúc lời gọi đầu tiên cùng key vẫn đang gửi ném CogoverApiError với code: "DUPLICATE_IN_PROGRESS"; hãy thử lại sau giây lát. Lời gọi thất bại không giữ key, nên có thể thử lại. Key thuộc về project và tách biệt với key của email.send. Trong trigger after-change hoặc job, vốn có thể chạy nhiều hơn một lần, hãy tạo key từ trigger.changeId và record.id, hoặc từ job.id hay một key nghiệp vụ. Cogover không so sánh nội dung: lời gọi dùng lại key với người nhận hoặc nội dung khác vẫn trả về kết quả đầu tiên và không gửi gì.

Quy tắc

  • notifications có trong script, route, trigger after-change, background job, lời gọi inbound webhook và Development Session có quyền ghi. Trigger before-change là chỉ đọc và nhận PermissionDeniedError có details.reason là "TRIGGER_READ_ONLY"; preview chỉ đọc và Development Session chỉ đọc cũng từ chối.
  • Notification không cần grant trong identity policy: project được gửi notification tới mọi personnel của workspace.
  • Mặc định mỗi project được gửi tới tối đa 5.000 người nhận mỗi giờ. Lời gọi làm vượt hạn mức này ném RateLimitError và không gửi gì.
  • Input được SDK kiểm tra và Cogover kiểm tra lại; input không hợp lệ ném ValidationError trước khi gửi bất cứ gì. Mỗi lời gọi tính một capability call.
  • Khi deployment tắt notification, mọi lời gọi ném CogoverApiError với code NOTIFICATIONS_DISABLED.
  • Notification đã gửi không thể thu hồi. Chạy lại một execution thất bại có thể gửi lại notification nếu lời gọi không dùng idempotencyKey.

Email

email gửi email từ hộp thư của workspace: một hộp thư dùng chung của workspace, hoặc hộp thư cá nhân mặc định của người dùng có hành động khởi đầu execution. Identity policy đã duyệt của Project quyết định project được dùng những hộp thư nào. Khi có record, email còn được ghi nhận là một hoạt động email trên timeline của record đó, giống email gửi từ trang record. Việc gửi diễn ra bất đồng bộ: lời gọi hoàn tất khi Cogover đã tiếp nhận email để gửi, và lỗi gửi xảy ra sau đó không được báo lại cho script.

type EmailSender = { readonly mailbox: string } | "actor";
 
type EmailRecipient =
  | string
  | { readonly email: string; readonly name?: string }
  | { readonly personnelId: string };
 
interface EmailRecordLink<ObjectSlug extends string = string> {
  readonly object: ObjectSlug;
  readonly recordId: string;
}
 
interface EmailAttachment<ObjectSlug extends string = string> {
  readonly object: ObjectSlug;
  readonly recordId: string;
  readonly field: string;
}
 
type EmailMessage<ObjectSlug extends string = string> = {
  readonly from: EmailSender;
  readonly to?: readonly EmailRecipient[];
  readonly cc?: readonly EmailRecipient[];
  readonly bcc?: readonly EmailRecipient[];
  readonly subject: string;
  readonly record?: EmailRecordLink<ObjectSlug>;
  readonly recordEmailFields?: readonly string[];
  readonly attachments?: readonly EmailAttachment<ObjectSlug>[];
  readonly appendSignature?: boolean;
  readonly idempotencyKey?: string;
} & (
  | { readonly html: string; readonly text?: never }
  | { readonly text: string; readonly html?: never }
);
 
type EmailDelivery = "activity" | "direct";
 
interface EmailSendResult {
  readonly requestId: string;
  readonly delivery: EmailDelivery;
  readonly recipients: number;
  readonly duplicate: boolean;
}
 
type EmailSenderKind = "workspace" | "actor";
 
interface EmailSenderInfo {
  readonly id: string;
  readonly kind: EmailSenderKind;
  readonly email: string;
  readonly displayName: string;
}
 
interface EmailApi<Schema extends object = EffectiveWorkspaceObjects> {
  send(message: EmailMessage<StringKeyOf<Schema>>): Promise<EmailSendResult>;
  senders(): Promise<readonly EmailSenderInfo[]>;
}
const result = await email.send({
  from: { mailbox: "EMWXXXXXXXXXXXX" },
  bcc: ["sales-archive@example.com"],
  subject: `Quote ${quote.fields.code}`,
  html: "<p>Dear customer,</p><p>Please find our quote attached.</p>",
  record: { object: "quote", recordId: quote.id },
  recordEmailFields: ["contact_email"],
  attachments: [{ object: "quote", recordId: quote.id, field: "quote_pdf" }],
  idempotencyKey: `quote-email:${quote.id}`,
});
// result.delivery === "activity": email xuất hiện trên timeline của báo giá.

email.send(message): Promise<EmailSendResult>

Người gửi. from: { mailbox: id } gửi từ một hộp thư dùng chung của workspace; from: "actor" gửi từ hộp thư cá nhân mặc định của người dùng có hành động khởi đầu execution. Người gửi phải được phép trong mục email của identity policy đã duyệt của Project: workspaceMailboxIds liệt kê các hộp thư dùng chung và allowActorMailbox cho phép "actor". Giống phần còn lại của policy, mục này chỉ áp dụng cho các caller được chọn trong callerPersonnelIds, và cho execution không có người dùng chỉ khi policy đặt allowInternalSystem: true. Project có policy không có mục email thì không gửi được email. Người gửi không được phép ném PermissionDeniedError có details.reason là "EMAIL_SENDER_NOT_GRANTED"; bước kiểm tra này diễn ra trước mọi bước tra cứu khác. Hộp thư không tồn tại hoặc đã bị vô hiệu hoá, và actor không có hộp thư cá nhân đang hoạt động, ném NotFoundError với resource là "mailbox". "actor" trong execution không có người dùng ném ValidationError.

Người nhận.

  • to, cc và bcc nhận một chuỗi địa chỉ, { email, name? } hoặc { personnelId }. Personnel được đổi thành email công việc của họ, hoặc email của tài khoản người dùng khi không có email công việc; personnel không có cả hai ném ValidationError, và personnel không tồn tại ném NotFoundError với resource là "personnel".
  • Địa chỉ trùng được loại bỏ trong từng danh sách, không phân biệt hoa thường. Ba danh sách cộng lại nhận tối đa 50 người nhận. name là một dòng, tối đa 200 ký tự.
  • to phải có ít nhất một người nhận, trừ khi có recordEmailFields.

Nội dung. subject là bắt buộc: một dòng, tối đa 255 ký tự. Cung cấp đúng một trong html và text; nội dung tối đa 192 KB (196.608 byte) theo UTF-8, đo trên dạng đã mã hoá JSON: mỗi dấu ngoặc kép, dấu gạch chéo ngược hoặc ký tự điều khiển như xuống dòng được tính theo chuỗi escape của nó (\" là 2 byte, \n là 2 byte, \u0001 là 6 byte). Nội dung dài hơn ném ValidationError trước khi gọi.

Record. record: { object, recordId } ghi nhận email là một hoạt động email trên timeline của record đó: Cogover theo dõi lượt mở và lượt bấm liên kết, gom các thư trả lời vào cùng luồng, và result.delivery là "activity". Record phải đọc được bằng danh tính mặc định của execution: record không tồn tại ném NotFoundError với resource là "record", còn record mà danh tính đó không có quyền đọc ném PermissionDeniedError. Không có record thì email chỉ được gửi từ hộp thư và delivery là "direct". Các tuỳ chọn sau cần record; thiếu record chúng ném ValidationError:

  • recordEmailFields liệt kê tối đa 20 field email của record đó, địa chỉ trong các field này được thêm vào to. Field không phải field email ném ValidationError.
  • attachments đính kèm mọi file lưu trong file field field của từng record được liệt kê: tối đa 10 file và tổng cộng 20 MB. Các record phải đọc được bằng danh tính mặc định; field không phải file field ném ValidationError.
  • appendSignature: true thêm chữ ký email mặc định của người gửi. Tuỳ chọn này cần hộp thư cá nhân, tức from: "actor"; nếu không sẽ ném ValidationError.

Kết quả. requestId định danh lần gửi. recipients đếm các địa chỉ trong to, cc và bcc sau khi loại trùng; địa chỉ của recordEmailFields được xác định sau nên không được đếm. duplicate là true khi một lời gọi trước đó cùng idempotencyKey đã gửi email.

Idempotency. idempotencyKey theo cùng quy tắc với notification: lời gọi lặp lại cùng key trong 7 ngày trả về kết quả đầu tiên với duplicate: true thay vì gửi lại, và lời gọi lặp lại trong lúc lời gọi đầu tiên vẫn đang gửi ném CogoverApiError với code: "DUPLICATE_IN_PROGRESS". Key của email tách biệt với key của notification. Giống notification, nội dung không được so sánh: dùng lại key cho một email khác vẫn trả về kết quả đầu tiên và không gửi gì.

email.senders(): Promise<readonly EmailSenderInfo[]>

Trả về các hộp thư đang hoạt động mà execution này được dùng để gửi: một mục kind: "workspace" cho mỗi hộp thư dùng chung đang hoạt động được policy áp dụng liệt kê trong workspaceMailboxIds, dùng dưới dạng { mailbox: id }, và, khi policy đặt allowActorMailbox và actor có hộp thư cá nhân đang hoạt động, tối đa một mục kind: "actor" mô tả hộp thư mà from: "actor" sử dụng. Danh sách rỗng khi policy không cấp người gửi nào cho execution này. Dùng method này để hiển thị các người gửi khả dụng hoặc chọn người gửi theo địa chỉ email thay vì hard-code ID. Method không gửi gì, nên cũng dùng được trong preview chỉ đọc và Development Session chỉ đọc.

Quy tắc

  • email có trong script, route, trigger after-change, background job, lời gọi inbound webhook và Development Session. Trigger before-change nhận PermissionDeniedError có details.reason là "TRIGGER_READ_ONLY" với cả hai method; preview chỉ đọc và Development Session chỉ đọc từ chối email.send.
  • Mặc định mỗi project được gửi tối đa 500 email mỗi giờ; vượt quá sẽ ném RateLimitError và không gửi gì. Một lời gọi là một email, bất kể có bao nhiêu người nhận.
  • Input được SDK kiểm tra và Cogover kiểm tra lại; input không hợp lệ ném ValidationError trước khi gửi bất cứ gì. Mỗi lời gọi tính một capability call.
  • Khi deployment tắt email, mọi lời gọi ném CogoverApiError với code EMAIL_DISABLED.
  • Email đã gửi không thể thu hồi. Chạy lại một execution thất bại có thể gửi lại email nếu lời gọi không dùng idempotencyKey.
avatar

blog.blogs_related

Giải pháp tùy biến và hợp nhất

Số hóa và tự động hóa hoàn toàn công tác vận hành và quản trị doanh nghiệp với Cogover!

Bắt đầu đổi mới phương thức vận hành và tự chủ hệ thống quản trị công việc của bạn

Dùng thử ngay
CogoverCogover

CÔNG TY CỔ PHẦN STRINGEE

GPKD số 0107938504, đăng ký ngày 27-07-2017, nơi cấp Sở Tài chính Thành phố Hà Nội
Địa chỉ: Tầng 19, Leadvisors Tower, số 643 Phạm Văn Đồng, Phường Nghĩa Đô, Thành phố Hà Nội, Việt Nam

Văn phòng

Hà Nội: Tầng 17, VP2, tòa Sun Square, 21 Lê Đức Thọ, phường Từ Liêm, TP Hà Nội
TP Hồ Chí Minh: Tòa nhà Lô A, Khu chung cư Tân Phước, 153 Lý Thường Kiệt, phường Minh Phụng, TP Hồ Chí Minh
Mỹ: 838 Walker Road, Suite 21-2 Dover, Delaware 19904, United States
Ấn Độ: 804/805, Kosha Kommercial Komplex, Primal Nagar, Malad East, Mumbai - 400097
Dubai: CWS-1V-792740, Amber Gem Tower, Ajman, United Arab Emirates
Đã xác nhận
© 2026 Cogover LLC