sently 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +60 -0
  3. package/README.md +18 -16
  4. package/dist/adapters/bun.js +1 -1
  5. package/dist/adapters/cf.js +2 -2
  6. package/dist/adapters/cf.js.map +1 -1
  7. package/dist/adapters/deno.js +1 -1
  8. package/dist/adapters/node.js +1 -1
  9. package/dist/auth/oauth2.js +2 -2
  10. package/dist/auth/oauth2.js.map +1 -1
  11. package/dist/channel-result.d.ts +6 -0
  12. package/dist/channel-result.js +1 -0
  13. package/dist/{chunk-skqhj0rm.js → chunk-1ke3nmyg.js} +3 -3
  14. package/dist/{chunk-skqhj0rm.js.map → chunk-1ke3nmyg.js.map} +1 -1
  15. package/dist/{chunk-7gy2q9sh.js → chunk-5es0v1j7.js} +2 -2
  16. package/dist/{chunk-7gy2q9sh.js.map → chunk-5es0v1j7.js.map} +1 -1
  17. package/dist/{chunk-0qxws3kj.js → chunk-7wtfjx0d.js} +2 -2
  18. package/dist/{chunk-0qxws3kj.js.map → chunk-7wtfjx0d.js.map} +1 -1
  19. package/dist/chunk-8kpgbrba.js +4 -0
  20. package/dist/{chunk-93vqxxj2.js.map → chunk-8kpgbrba.js.map} +2 -2
  21. package/dist/{chunk-vjpds2pf.js → chunk-a057jzsy.js} +2 -2
  22. package/dist/{chunk-vjpds2pf.js.map → chunk-a057jzsy.js.map} +1 -1
  23. package/dist/{chunk-8njyga62.js → chunk-bhr7w3gg.js} +2 -2
  24. package/dist/{chunk-8njyga62.js.map → chunk-bhr7w3gg.js.map} +1 -1
  25. package/dist/chunk-cs1tvehn.js +4 -0
  26. package/dist/chunk-cs1tvehn.js.map +10 -0
  27. package/dist/{chunk-29z7fkzy.js → chunk-d5aw5vag.js} +3 -3
  28. package/dist/{chunk-29z7fkzy.js.map → chunk-d5aw5vag.js.map} +1 -1
  29. package/dist/chunk-f5x8vesr.js +4 -0
  30. package/dist/{chunk-b602dhck.js.map → chunk-f5x8vesr.js.map} +2 -2
  31. package/dist/chunk-fv0bemgf.js +4 -0
  32. package/dist/{chunk-yp311efr.js.map → chunk-fv0bemgf.js.map} +3 -3
  33. package/dist/{chunk-ywq6s10g.js → chunk-hnzmn4s4.js} +2 -2
  34. package/dist/{chunk-ywq6s10g.js.map → chunk-hnzmn4s4.js.map} +1 -1
  35. package/dist/chunk-htdg075t.js +4 -0
  36. package/dist/{chunk-qtkd5bak.js.map → chunk-htdg075t.js.map} +4 -3
  37. package/dist/{chunk-0gqy32pe.js → chunk-q2mahnb7.js} +3 -3
  38. package/dist/chunk-q2mahnb7.js.map +10 -0
  39. package/dist/chunk-sbgt4165.js +4 -0
  40. package/dist/chunk-sbgt4165.js.map +10 -0
  41. package/dist/{chunk-6npp3x3c.js → chunk-th4cwrpb.js} +2 -2
  42. package/dist/{chunk-6npp3x3c.js.map → chunk-th4cwrpb.js.map} +1 -1
  43. package/dist/{chunk-tamww15j.js → chunk-ttbwyxmh.js} +2 -2
  44. package/dist/{chunk-tamww15j.js.map → chunk-ttbwyxmh.js.map} +1 -1
  45. package/dist/chunk-w4c9kj70.js +13 -0
  46. package/dist/{chunk-n0qeyzqm.js.map → chunk-w4c9kj70.js.map} +2 -2
  47. package/dist/chunk-w9sabmb0.js +4 -0
  48. package/dist/chunk-w9sabmb0.js.map +10 -0
  49. package/dist/{chunk-8dty8c4p.js → chunk-yg3kj723.js} +3 -3
  50. package/dist/{chunk-8dty8c4p.js.map → chunk-yg3kj723.js.map} +2 -2
  51. package/dist/chunk-z1589fjk.js +4 -0
  52. package/dist/chunk-z1589fjk.js.map +10 -0
  53. package/dist/core/channel-result.d.ts +36 -0
  54. package/dist/core/channel-result.js +3 -0
  55. package/dist/core/channel-result.js.map +10 -0
  56. package/dist/core/decorated-transport.d.ts +30 -0
  57. package/dist/core/decorator-hooks.d.ts +17 -0
  58. package/dist/core/errors.js +2 -2
  59. package/dist/core/errors.js.map +1 -1
  60. package/dist/core/provider-label.d.ts +7 -3
  61. package/dist/core/push-endpoint.d.ts +5 -0
  62. package/dist/core/push-types.d.ts +48 -14
  63. package/dist/core/sms-types.d.ts +12 -0
  64. package/dist/core/smtp.js +2 -2
  65. package/dist/core/smtp.js.map +1 -1
  66. package/dist/core/whatsapp-types.d.ts +12 -0
  67. package/dist/detect.js +2 -2
  68. package/dist/detect.js.map +1 -1
  69. package/dist/dkim.js +2 -2
  70. package/dist/dkim.js.map +2 -2
  71. package/dist/idempotency.js +2 -2
  72. package/dist/idempotency.js.map +1 -1
  73. package/dist/index.d.ts +4 -0
  74. package/dist/index.js +5 -0
  75. package/dist/mailer.js +2 -2
  76. package/dist/mailer.js.map +1 -1
  77. package/dist/observability/console.js +1 -1
  78. package/dist/plugins/react.js +2 -2
  79. package/dist/plugins/react.js.map +1 -1
  80. package/dist/plugins/template.js +1 -1
  81. package/dist/pool/pool.js +2 -2
  82. package/dist/pool/pool.js.map +1 -1
  83. package/dist/push.d.ts +1 -0
  84. package/dist/push.js +2 -2
  85. package/dist/push.js.map +3 -3
  86. package/dist/sms.d.ts +1 -0
  87. package/dist/sms.js +2 -2
  88. package/dist/sms.js.map +3 -3
  89. package/dist/smtp-mailer.js +2 -2
  90. package/dist/smtp-mailer.js.map +1 -1
  91. package/dist/transports/brevo.js +2 -2
  92. package/dist/transports/brevo.js.map +2 -2
  93. package/dist/transports/cloudflare-email.js +2 -2
  94. package/dist/transports/cloudflare-email.js.map +2 -2
  95. package/dist/transports/fallback.d.ts +32 -6
  96. package/dist/transports/fallback.js +2 -2
  97. package/dist/transports/fallback.js.map +1 -1
  98. package/dist/transports/fcm.d.ts +55 -0
  99. package/dist/transports/fcm.js +4 -0
  100. package/dist/transports/fcm.js.map +10 -0
  101. package/dist/transports/loops.js +2 -2
  102. package/dist/transports/loops.js.map +1 -1
  103. package/dist/transports/mailersend.js +2 -2
  104. package/dist/transports/mailersend.js.map +2 -2
  105. package/dist/transports/mailgun.js +2 -2
  106. package/dist/transports/mailgun.js.map +2 -2
  107. package/dist/transports/mailtrap.js +2 -2
  108. package/dist/transports/mailtrap.js.map +2 -2
  109. package/dist/transports/msegat.js +2 -2
  110. package/dist/transports/msegat.js.map +1 -1
  111. package/dist/transports/plunk.js +2 -2
  112. package/dist/transports/plunk.js.map +1 -1
  113. package/dist/transports/postmark.js +2 -2
  114. package/dist/transports/postmark.js.map +2 -2
  115. package/dist/transports/preview.js +2 -2
  116. package/dist/transports/preview.js.map +1 -1
  117. package/dist/transports/resend.js +2 -2
  118. package/dist/transports/resend.js.map +2 -2
  119. package/dist/transports/retry.d.ts +28 -9
  120. package/dist/transports/retry.js +2 -2
  121. package/dist/transports/retry.js.map +3 -3
  122. package/dist/transports/sendgrid.js +2 -2
  123. package/dist/transports/sendgrid.js.map +2 -2
  124. package/dist/transports/ses.js +2 -2
  125. package/dist/transports/ses.js.map +2 -2
  126. package/dist/transports/smtp.js +2 -2
  127. package/dist/transports/smtp.js.map +1 -1
  128. package/dist/transports/sndr.js +2 -2
  129. package/dist/transports/sndr.js.map +1 -1
  130. package/dist/transports/sparkpost.js +2 -2
  131. package/dist/transports/sparkpost.js.map +2 -2
  132. package/dist/transports/taqnyat-mail.js +2 -2
  133. package/dist/transports/taqnyat-mail.js.map +1 -1
  134. package/dist/transports/taqnyat-sms.js +2 -2
  135. package/dist/transports/taqnyat-sms.js.map +1 -1
  136. package/dist/transports/taqnyat-whatsapp.js +2 -2
  137. package/dist/transports/taqnyat-whatsapp.js.map +1 -1
  138. package/dist/transports/twilio-sms.js +2 -2
  139. package/dist/transports/twilio-sms.js.map +2 -2
  140. package/dist/transports/unifonic.d.ts +81 -0
  141. package/dist/transports/unifonic.js +3 -0
  142. package/dist/transports/unifonic.js.map +10 -0
  143. package/dist/transports/webpush.js +2 -2
  144. package/dist/transports/webpush.js.map +3 -3
  145. package/dist/transports/weighted-fallback.d.ts +11 -9
  146. package/dist/transports/weighted-fallback.js +2 -2
  147. package/dist/transports/weighted-fallback.js.map +3 -3
  148. package/dist/transports/whatsapp-cloud.js +2 -2
  149. package/dist/transports/whatsapp-cloud.js.map +1 -1
  150. package/dist/webhooks/brevo.js +2 -2
  151. package/dist/webhooks/brevo.js.map +1 -1
  152. package/dist/webhooks/mailgun.js +2 -2
  153. package/dist/webhooks/mailgun.js.map +1 -1
  154. package/dist/webhooks/postmark.js +2 -2
  155. package/dist/webhooks/postmark.js.map +1 -1
  156. package/dist/webhooks/resend.js +2 -2
  157. package/dist/webhooks/resend.js.map +1 -1
  158. package/dist/webhooks/sendgrid.js +2 -2
  159. package/dist/webhooks/sendgrid.js.map +1 -1
  160. package/dist/webhooks/ses.js +2 -2
  161. package/dist/webhooks/ses.js.map +1 -1
  162. package/dist/webhooks/sndr.js +2 -2
  163. package/dist/webhooks/sndr.js.map +1 -1
  164. package/dist/webhooks/timing-safe-equal.js +1 -1
  165. package/dist/webhooks/twilio-sms.d.ts +20 -0
  166. package/dist/webhooks/twilio-sms.js +3 -0
  167. package/dist/webhooks/twilio-sms.js.map +10 -0
  168. package/dist/webhooks/types.d.ts +37 -3
  169. package/dist/webhooks/types.js +3 -0
  170. package/dist/webhooks/types.js.map +9 -0
  171. package/dist/webhooks/unifonic.d.ts +13 -0
  172. package/dist/webhooks/unifonic.js +3 -0
  173. package/dist/webhooks/unifonic.js.map +10 -0
  174. package/dist/webhooks/whatsapp-cloud.d.ts +14 -0
  175. package/dist/webhooks/whatsapp-cloud.js +3 -0
  176. package/dist/webhooks/whatsapp-cloud.js.map +10 -0
  177. package/dist/webhooks.d.ts +10 -5
  178. package/dist/webhooks.js +11 -0
  179. package/dist/whatsapp.d.ts +1 -0
  180. package/dist/whatsapp.js +2 -2
  181. package/dist/whatsapp.js.map +3 -3
  182. package/package.json +26 -2
  183. package/site/content/docs/channels/hooks.mdx +6 -5
  184. package/site/content/docs/channels/index.mdx +2 -2
  185. package/site/content/docs/channels/push.mdx +27 -21
  186. package/site/content/docs/channels/sms.mdx +4 -1
  187. package/site/content/docs/decorators/fallback.mdx +41 -14
  188. package/site/content/docs/decorators/index.mdx +29 -12
  189. package/site/content/docs/decorators/retry.mdx +32 -9
  190. package/site/content/docs/decorators/weighted-fallback.mdx +27 -6
  191. package/site/content/docs/get-started/entrypoints.mdx +6 -5
  192. package/site/content/docs/get-started/installation.mdx +3 -2
  193. package/site/content/docs/get-started/introduction.mdx +1 -1
  194. package/site/content/docs/guides/webhooks.mdx +35 -20
  195. package/site/content/docs/index.mdx +1 -1
  196. package/site/content/docs/quick-start/push.mdx +22 -5
  197. package/site/content/docs/reference/channel-result.mdx +82 -0
  198. package/site/content/docs/reference/exports.mdx +5 -3
  199. package/site/content/docs/reference/meta.json +1 -0
  200. package/site/content/docs/reference/push-options.mdx +44 -3
  201. package/site/content/docs/reference/sms-options.mdx +18 -2
  202. package/site/content/docs/reference/webhook-events.mdx +38 -14
  203. package/site/content/docs/reference/whatsapp-options.mdx +19 -1
  204. package/site/content/docs/transports/fcm.mdx +80 -0
  205. package/site/content/docs/transports/index.mdx +3 -1
  206. package/site/content/docs/transports/meta.json +3 -1
  207. package/site/content/docs/transports/unifonic.mdx +85 -0
  208. package/dist/chunk-0gqy32pe.js.map +0 -10
  209. package/dist/chunk-5915vbcj.js +0 -4
  210. package/dist/chunk-5915vbcj.js.map +0 -10
  211. package/dist/chunk-93vqxxj2.js +0 -4
  212. package/dist/chunk-b602dhck.js +0 -4
  213. package/dist/chunk-gp4fwrs8.js +0 -4
  214. package/dist/chunk-gp4fwrs8.js.map +0 -10
  215. package/dist/chunk-n0qeyzqm.js +0 -13
  216. package/dist/chunk-q8k3m6qe.js +0 -4
  217. package/dist/chunk-q8k3m6qe.js.map +0 -10
  218. package/dist/chunk-qtkd5bak.js +0 -4
  219. package/dist/chunk-yp311efr.js +0 -4
@@ -1,12 +1,13 @@
1
1
  ---
2
2
  title: Webhooks
3
- description: Parse provider delivery events into a shared EmailEvent shape, with optional signature checks.
3
+ description: Parse provider delivery events into EmailEvent or DeliveryEvent shapes, with optional signature checks.
4
4
  icon: Webhook
5
5
  source: "src/webhooks.ts"
6
6
  ---
7
7
 
8
- Use webhooks when a provider posts delivery updates (delivered, bounced, and similar).
9
- sently turns each provider payload into `EmailEvent[]` so your app can share one handler.
8
+ Use webhooks when a provider posts delivery updates (delivered, bounced, failed, and similar).
9
+ Email parsers return `EmailEvent[]`. SMS and WhatsApp parsers return `DeliveryEvent[]`.
10
+ Map email events with `toDeliveryEvent` when you want one ingest pipeline.
10
11
 
11
12
  <Callout title="The one rule">
12
13
  Verify the signature against the unmodified raw request body before you trust or act on the event.
@@ -53,7 +54,7 @@ const events = parse(JSON.parse(rawBody));
53
54
 
54
55
  ```ts
55
56
  for (const event of events) {
56
- if (event.type === "bounced") {
57
+ if (event.type === "bounced" || event.type === "failed") {
57
58
  // suppress or alert using event.messageId / event.recipient
58
59
  }
59
60
  }
@@ -65,19 +66,32 @@ for (const event of events) {
65
66
 
66
67
  ## Provider entrypoints
67
68
 
68
- | Import | Parse export | Verify export |
69
- | ------ | ------------ | ------------- |
70
- | `sently/webhooks/brevo` | `parse` | — |
71
- | `sently/webhooks/mailgun` | `parse` | `verifySignature`, `verifyPayload` |
72
- | `sently/webhooks/postmark` | `parse` | — |
73
- | `sently/webhooks/resend` | `parse` | `verifySignature` |
74
- | `sently/webhooks/sendgrid` | `parse` | — |
75
- | `sently/webhooks/ses` | `parse` | — |
76
- | `sently/webhooks/sndr` | `parse` | `verifySignature` |
77
-
78
- The convenience barrel `sently/webhooks` re-exports named helpers such as `parseSndrWebhook` and `verifySndrSignature`.
69
+ | Import | Channel | Parse export | Verify export |
70
+ | ------ | ------- | ------------ | ------------- |
71
+ | `sently/webhooks/brevo` | Email | `parse` → `EmailEvent[]` | — |
72
+ | `sently/webhooks/mailgun` | Email | `parse` | `verifySignature`, `verifyPayload` |
73
+ | `sently/webhooks/postmark` | Email | `parse` | — |
74
+ | `sently/webhooks/resend` | Email | `parse` | `verifySignature` |
75
+ | `sently/webhooks/sendgrid` | Email | `parse` | — |
76
+ | `sently/webhooks/ses` | Email | `parse` | — |
77
+ | `sently/webhooks/sndr` | Email | `parse` | `verifySignature` |
78
+ | `sently/webhooks/twilio-sms` | SMS | `parse` → `DeliveryEvent[]` | `verifySignature` (`X-Twilio-Signature`) |
79
+ | `sently/webhooks/unifonic` | SMS | `parse` `DeliveryEvent[]` | — |
80
+ | `sently/webhooks/whatsapp-cloud` | WhatsApp | `parse` → `DeliveryEvent[]` | `verifySignature` (`X-Hub-Signature-256`) |
81
+
82
+ The convenience barrel `sently/webhooks` re-exports named helpers such as `parseSndrWebhook`, `parseTwilioSmsWebhook`, and `toDeliveryEvent`.
79
83
  Bundlers tree-shake unused names; Node/Deno without a bundler evaluate every re-export.
80
84
 
85
+ ## SMS status callbacks
86
+
87
+ Twilio posts form fields (`MessageSid`, `MessageStatus`, `To`). Pass a parsed object or `URLSearchParams`:
88
+
89
+ ```ts
90
+ import { parse } from "sently/webhooks/twilio-sms";
91
+
92
+ const events = parse(await request.formData().then((fd) => Object.fromEntries(fd)));
93
+ ```
94
+
81
95
  ## SNDR signatures
82
96
 
83
97
  SNDR signs with `X-Sndr-Signature: t=<unix>,v1=<hex>`.
@@ -99,7 +113,9 @@ Most frameworks parse JSON before your handler runs. Read the raw body first (`r
99
113
 
100
114
  <Accordion title="Which providers are supported?">
101
115
 
102
- Brevo, Mailgun, Postmark, Resend, SendGrid, SES, and SNDR. Import each from its `sently/webhooks/<provider>` subpath.
116
+ Email: Brevo, Mailgun, Postmark, Resend, SendGrid, SES, SNDR.
117
+ SMS / WhatsApp: Twilio SMS, Unifonic, WhatsApp Cloud.
118
+ Import each from its `sently/webhooks/<provider>` subpath.
103
119
 
104
120
  </Accordion>
105
121
 
@@ -107,14 +123,13 @@ Brevo, Mailgun, Postmark, Resend, SendGrid, SES, and SNDR. Import each from its
107
123
 
108
124
  ## Learn more
109
125
 
110
- - [Webhook events](/docs/reference/webhook-events) — `EmailEvent` fields and type unions
111
- - [SNDR transport](/docs/transports/sndr) — send path that produces those message ids
126
+ - [Webhook events](/docs/reference/webhook-events) — `EmailEvent` / `DeliveryEvent` fields
112
127
  - [Exports](/docs/reference/exports) — package entrypoints
113
128
 
114
129
  ## Next
115
130
 
116
131
  <Cards>
117
- <Card title="Webhook events" description="Normalized EmailEvent fields." href="/docs/reference/webhook-events" />
118
- <Card title="SNDR" description="Send email through SNDR." href="/docs/transports/sndr" />
132
+ <Card title="Webhook events" description="Normalized event fields." href="/docs/reference/webhook-events" />
133
+ <Card title="Channel send result" description="Shared accepted mapping." href="/docs/reference/channel-result" />
119
134
  <Card title="Exports" description="Public package entrypoints." href="/docs/reference/exports" />
120
135
  </Cards>
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: sently handbook
3
- description: Channel-first messaging for email, SMS, WhatsApp, and web push.
3
+ description: Channel-first messaging for email, SMS, WhatsApp, and push (Web Push / FCM).
4
4
  icon: BookOpen
5
5
  source: "README.md"
6
6
  ---
@@ -1,14 +1,20 @@
1
1
  ---
2
2
  title: Push
3
- description: Send a browser notification through a channel sender.
3
+ description: Send a Web Push or FCM notification through a channel sender.
4
4
  icon: Bell
5
5
  source: "src/push.ts"
6
6
  ---
7
7
 
8
- <Callout title="The one rule">Store the browser subscription and send it back unchanged as `subscription`.</Callout>
8
+ Send a browser Web Push or FCM device notification through `createPushSender`.
9
+
10
+ <Callout title="The one rule">
11
+ Use `subscription` with `WebPushTransport`, or `token` with `FcmTransport` — not both shapes on the wrong transport.
12
+ </Callout>
13
+
14
+ ## Quick start
9
15
 
10
16
  <Steps>
11
- <Step title="Create the sender">
17
+ <Step title="Create a Web Push sender">
12
18
 
13
19
  ```ts
14
20
  import { createPushSender } from "sently/push";
@@ -27,10 +33,21 @@ const push = createPushSender({
27
33
  <Step title="Send">
28
34
 
29
35
  ```ts
30
- await push.send({ subscription, title: "Update", body: "Your report is ready." });
36
+ await push.send({
37
+ subscription,
38
+ title: "Update",
39
+ body: "Your report is ready.",
40
+ });
31
41
  ```
32
42
 
33
43
  </Step>
34
44
  </Steps>
35
45
 
36
- <Cards><Card title="Push channel" href="/docs/channels/push" /></Cards>
46
+ For FCM device tokens, use `FcmTransport` and `token` instead of `subscription` — see [FCM](/docs/transports/fcm).
47
+
48
+ ## Next
49
+
50
+ <Cards>
51
+ <Card title="Push channel" href="/docs/channels/push" />
52
+ <Card title="FCM transport" href="/docs/transports/fcm" />
53
+ </Cards>
@@ -0,0 +1,82 @@
1
+ ---
2
+ title: Channel send result
3
+ description: Map email, SMS, WhatsApp, and push results to a shared accepted shape.
4
+ icon: Boxes
5
+ source: "src/core/channel-result.ts"
6
+ ---
7
+
8
+ Adapters that span channels can normalize every send into
9
+ `{ messageId, provider, accepted }` without inventing per-provider glue.
10
+ Use this after `mailer.send`, `sms.send`, `whatsapp.send`, or `push.send`.
11
+
12
+ <Callout title="The one rule">
13
+ Keep channel-specific fields on the original result; use `toChannelSendResult` only for the shared slice.
14
+ </Callout>
15
+
16
+ ## Quick start
17
+
18
+ <Steps>
19
+ <Step title="Send on any channel">
20
+
21
+ ```ts
22
+ import { createSmsSender } from "sently/sms";
23
+ import { toChannelSendResult } from "sently/channel-result";
24
+ import { UnifonicTransport } from "sently/transports/unifonic";
25
+
26
+ const sms = createSmsSender({
27
+ transport: new UnifonicTransport({
28
+ appSid: process.env.UNIFONIC_APPSID!,
29
+ senderId: "MyBrand",
30
+ }),
31
+ });
32
+ const result = await sms.send({ to: "+15551234567", body: "Hi" });
33
+ ```
34
+
35
+ </Step>
36
+ <Step title="Normalize for your adapter">
37
+
38
+ ```ts
39
+ const shared = toChannelSendResult(result);
40
+ // { messageId, provider?, accepted }
41
+ ```
42
+
43
+ </Step>
44
+ </Steps>
45
+
46
+ ## ChannelSendResult
47
+
48
+ | Field | Type | Meaning |
49
+ | --- | --- | --- |
50
+ | `messageId` | `string` | Provider or client message id |
51
+ | `provider` | `string?` | Transport id when set |
52
+ | `accepted` | `boolean` | Provider accepted the send (not end-user delivery) |
53
+
54
+ ## Mapping
55
+
56
+ | Channel result | `accepted` |
57
+ | --- | --- |
58
+ | Email `SendResult` | `accepted.length > 0` and `rejected.length === 0` |
59
+ | SMS / WhatsApp / Push | `status` in `accepted` / `queued` / `sending` / `sent` / `delivered` / `read` / `success` (case-insensitive) |
60
+
61
+ Import from `sently/channel-result` or the main `sently` barrel.
62
+
63
+ ## Troubleshooting
64
+
65
+ <Accordions>
66
+ <Accordion title="Why is accepted true but the user never got the message?">
67
+ Acceptance means the provider took the message. Use [webhooks](/docs/guides/webhooks) for delivered / failed outcomes.
68
+ </Accordion>
69
+ </Accordions>
70
+
71
+ ## Learn more
72
+
73
+ - [Webhook events](./webhook-events)
74
+ - [SMS options](./sms-options)
75
+ - [Exports](./exports)
76
+
77
+ ## Next
78
+
79
+ <Cards>
80
+ <Card title="Webhook events" href="/docs/reference/webhook-events" />
81
+ <Card title="Exports" href="/docs/reference/exports" />
82
+ </Cards>
@@ -17,12 +17,13 @@ The main `sently` package is for shared types and factories — not every provid
17
17
 
18
18
  | Import | Primary API |
19
19
  | ------ | ----------- |
20
- | `sently` | `createMailer`, `createSMTPMailer`, types, OAuth2, `SentlyError`, `consoleObserver`, `FallbackTransport`, `WeightedFallbackTransport`, `CloudflareEmailTransport` |
20
+ | `sently` | `createMailer`, `createSMTPMailer`, types, OAuth2, `SentlyError`, `consoleObserver`, `toChannelSendResult`, `FallbackTransport`, `WeightedFallbackTransport`, `CloudflareEmailTransport` |
21
21
  | `sently/mailer` | `createMailer` only |
22
22
  | `sently/smtp` | `createSMTPMailer` |
23
23
  | `sently/sms` | `createSmsSender` |
24
24
  | `sently/whatsapp` | `createWhatsAppSender` |
25
25
  | `sently/push` | `createPushSender` |
26
+ | `sently/channel-result` | `toChannelSendResult`, `ChannelSendResult` |
26
27
  | `sently/errors` | `SentlyError` hierarchy |
27
28
  | `sently/idempotency` | `IdempotencyTransport` |
28
29
  | `sently/dkim` | DKIM signing |
@@ -36,7 +37,7 @@ The main `sently` package is for shared types and factories — not every provid
36
37
 
37
38
  | Import | Use |
38
39
  | ------ | --- |
39
- | `sently/transports/<name>` | One provider or email decorator (e.g. `sndr`, `resend`, `twilio-sms`) |
40
+ | `sently/transports/<name>` | One provider or decorator (e.g. `sndr`, `unifonic`, `fcm`, `retry`) |
40
41
 
41
42
  HTTP providers such as SNDR, Resend, and Plunk are **not** re-exported from `sently`.
42
43
 
@@ -47,7 +48,8 @@ HTTP providers such as SNDR, Resend, and Plunk are **not** re-exported from `sen
47
48
  | `sently/webhooks/<provider>` | One parser (+ verify helper when exported) |
48
49
  | `sently/webhooks` | Convenience barrel of all named helpers |
49
50
 
50
- Providers: `brevo`, `mailgun`, `postmark`, `resend`, `sendgrid`, `ses`, `sndr`.
51
+ Email: `brevo`, `mailgun`, `postmark`, `resend`, `sendgrid`, `ses`, `sndr`.
52
+ SMS / WhatsApp: `twilio-sms` (+ verify), `unifonic`, `whatsapp-cloud` (+ verify).
51
53
 
52
54
  ## Adapters
53
55
 
@@ -9,6 +9,7 @@
9
9
  "whatsapp-options",
10
10
  "push-options",
11
11
  "transport-contracts",
12
+ "channel-result",
12
13
  "errors",
13
14
  "webhook-events"
14
15
  ]
@@ -1,13 +1,21 @@
1
1
  ---
2
2
  title: Push options
3
- description: Fields accepted by `PushSender.send`.
3
+ description: Fields accepted by `PushSender.send` for Web Push and FCM.
4
4
  icon: Bell
5
5
  source: "src/core/push-types.ts"
6
6
  ---
7
7
 
8
+ `PushOptions` is `WebPushOptions | FcmPushOptions`.
9
+ Pick the shape that matches your transport.
10
+
11
+ <Callout title="The one rule">
12
+ Pass `subscription` to `WebPushTransport` and `token` to `FcmTransport`.
13
+ </Callout>
14
+
15
+ ## Shared fields
16
+
8
17
  | Field | Type | Required |
9
18
  | --- | --- | --- |
10
- | `subscription` | `PushSubscription` | yes |
11
19
  | `title` | `string` | yes |
12
20
  | `body` | `string` | yes |
13
21
  | `data` | `Record<string, unknown>` | no |
@@ -15,6 +23,39 @@ source: "src/core/push-types.ts"
15
23
  | `ttl` | `number` | no |
16
24
  | `messageId` | `string` | no |
17
25
 
26
+ ## Web Push
27
+
28
+ | Field | Type | Required |
29
+ | --- | --- | --- |
30
+ | `subscription` | `PushSubscription` | yes |
31
+
18
32
  A subscription contains `endpoint`, `keys.p256dh`, and `keys.auth`.
19
33
 
20
- <Cards><Card title="Push channel" href="/docs/channels/push" /></Cards>
34
+ ## FCM
35
+
36
+ | Field | Type | Required |
37
+ | --- | --- | --- |
38
+ | `token` | `string` | yes |
39
+ | `image` | `string` | no |
40
+
41
+ FCM stringifies non-string `data` values before send.
42
+
43
+ ## Result
44
+
45
+ | Field | Type | Notes |
46
+ | --- | --- | --- |
47
+ | `messageId` | `string` | Provider or client id |
48
+ | `status` | `string` | e.g. `"accepted"` |
49
+ | `response` | `string` | Often the HTTP status or FCM name |
50
+ | `provider` | `string?` | `"webpush"` / `"fcm"`; set by fallback |
51
+ | `providerIndex` | `number?` | Fallback chain index |
52
+
53
+ Map any channel result with [channel send result](./channel-result).
54
+
55
+ ## Next
56
+
57
+ <Cards>
58
+ <Card title="Push channel" href="/docs/channels/push" />
59
+ <Card title="FCM transport" href="/docs/transports/fcm" />
60
+ <Card title="Channel send result" href="/docs/reference/channel-result" />
61
+ </Cards>
@@ -12,6 +12,22 @@ source: "src/core/sms-types.ts"
12
12
  | `from` | `string` | no |
13
13
  | `messageId` | `string` | no |
14
14
 
15
- The result includes `messageId`, `to`, `status`, `response`, and optional `provider`.
15
+ ## Result
16
16
 
17
- <Cards><Card title="SMS channel" href="/docs/channels/sms" /></Cards>
17
+ | Field | Type | Notes |
18
+ | --- | --- | --- |
19
+ | `messageId` | `string` | Provider or client id |
20
+ | `to` | `string` | Recipient |
21
+ | `status` | `string` | e.g. `"accepted"`, `"queued"` |
22
+ | `response` | `string` | Provider text |
23
+ | `provider` | `string?` | Transport id; set by fallback |
24
+ | `providerIndex` | `number?` | Fallback chain index |
25
+
26
+ Map any channel result to `{ messageId, provider, accepted }` with [channel send result](./channel-result).
27
+
28
+ ## Next
29
+
30
+ <Cards>
31
+ <Card title="SMS channel" href="/docs/channels/sms" />
32
+ <Card title="Channel send result" href="/docs/reference/channel-result" />
33
+ </Cards>
@@ -1,17 +1,29 @@
1
1
  ---
2
2
  title: Webhook events
3
- description: Normalized EmailEvent fields returned by every provider webhook parser.
3
+ description: Normalized EmailEvent and DeliveryEvent fields from provider webhook parsers.
4
4
  icon: Webhook
5
5
  source: "src/webhooks/types.ts"
6
6
  ---
7
7
 
8
- Every parser returns `EmailEvent[]` so delivery webhooks can share one application pipeline.
9
- Verify signatures in the [webhooks guide](/docs/guides/webhooks) before you process events.
8
+ Email parsers return `EmailEvent[]`. SMS and WhatsApp parsers return `DeliveryEvent[]`.
9
+ Use `toDeliveryEvent` to put email events on the same ingest path as SMS / WhatsApp.
10
10
 
11
11
  <Callout title="The one rule">
12
12
  Branch on `event.type` and keep provider-specific details in `event.raw` when you need them.
13
13
  </Callout>
14
14
 
15
+ ## DeliveryEvent
16
+
17
+ | Field | Type | Meaning |
18
+ | ----- | ---- | ------- |
19
+ | `channel` | `"email" \| "sms" \| "whatsapp" \| "push"` | Messaging channel |
20
+ | `provider` | `string` | Parser id (e.g. `"twilio-sms"`) |
21
+ | `type` | union below | Normalized lifecycle type |
22
+ | `messageId` | `string?` | Provider message id when present |
23
+ | `recipient` | `string?` | Address or phone when present |
24
+ | `timestamp` | `Date?` | Event time when present |
25
+ | `raw` | `unknown` | Original provider payload |
26
+
15
27
  ## EmailEvent
16
28
 
17
29
  | Field | Type | Meaning |
@@ -25,15 +37,27 @@ Verify signatures in the [webhooks guide](/docs/guides/webhooks) before you proc
25
37
 
26
38
  ## Normalized `type` values
27
39
 
28
- | Value | Meaning |
29
- | ----- | ------- |
30
- | `delivered` | Accepted / delivered to the mailbox provider. |
31
- | `bounced` | Hard failure or bounce. |
32
- | `complained` | Spam complaint. |
33
- | `opened` | Open tracking. |
34
- | `clicked` | Click tracking. |
35
- | `deferred` | Temporary deferral. |
36
- | `unknown` | Unmapped or informational provider event. |
40
+ | Value | Channels | Meaning |
41
+ | ----- | -------- | ------- |
42
+ | `queued` | SMS | Accepted / queued by the provider |
43
+ | `sent` | SMS / WhatsApp | Handed to the network |
44
+ | `delivered` | All | Delivered (or mailbox accepted for email) |
45
+ | `failed` | SMS / WhatsApp | Permanent failure |
46
+ | `read` | SMS / WhatsApp | Read receipt |
47
+ | `bounced` | Email | Hard failure or bounce |
48
+ | `complained` | Email | Spam complaint |
49
+ | `opened` | Email | Open tracking |
50
+ | `clicked` | Email | Click tracking |
51
+ | `deferred` | Email | Temporary deferral |
52
+ | `unknown` | All | Unmapped or informational event |
53
+
54
+ ## Mapping helpers
55
+
56
+ ```ts
57
+ import { toDeliveryEvent } from "sently/webhooks";
58
+
59
+ const delivery = toDeliveryEvent(emailEvent);
60
+ ```
37
61
 
38
62
  ## SNDR mapping
39
63
 
@@ -51,7 +75,7 @@ SNDR `data.email_id` becomes `messageId`. Recipients come from `data.to` or `dat
51
75
 
52
76
  <Accordion title="Why is unsubscribed mapped to unknown?">
53
77
 
54
- The shared `EmailEvent.type` union has no unsubscribe value. Keep the vendor payload in `raw` if you need unsubscribe-specific handling.
78
+ The shared email `type` union has no unsubscribe value. Keep the vendor payload in `raw` if you need unsubscribe-specific handling.
55
79
 
56
80
  </Accordion>
57
81
 
@@ -60,7 +84,7 @@ The shared `EmailEvent.type` union has no unsubscribe value. Keep the vendor pay
60
84
  ## Learn more
61
85
 
62
86
  - [Webhooks guide](/docs/guides/webhooks) — verify + parse flow
63
- - [SNDR transport](/docs/transports/sndr) — sending side
87
+ - [Channel send result](/docs/reference/channel-result) — send-time accepted mapping
64
88
 
65
89
  ## Next
66
90
 
@@ -12,4 +12,22 @@ source: "src/core/whatsapp-types.ts"
12
12
 
13
13
  Both shapes accept optional `messageId`. Template components can include `header`, `body`, or `button` parameters.
14
14
 
15
- <Cards><Card title="WhatsApp channel" href="/docs/channels/whatsapp" /></Cards>
15
+ ## Result
16
+
17
+ | Field | Type | Notes |
18
+ | --- | --- | --- |
19
+ | `messageId` | `string` | Provider or client id |
20
+ | `to` | `string` | Recipient |
21
+ | `status` | `string` | e.g. `"accepted"` |
22
+ | `response` | `string` | Provider text |
23
+ | `provider` | `string?` | Transport id; set by fallback |
24
+ | `providerIndex` | `number?` | Fallback chain index |
25
+
26
+ Map any channel result with [channel send result](./channel-result).
27
+
28
+ ## Next
29
+
30
+ <Cards>
31
+ <Card title="WhatsApp channel" href="/docs/channels/whatsapp" />
32
+ <Card title="Channel send result" href="/docs/reference/channel-result" />
33
+ </Cards>
@@ -0,0 +1,80 @@
1
+ ---
2
+ title: FCM
3
+ description: Send mobile push through Firebase Cloud Messaging HTTP v1.
4
+ icon: Bell
5
+ source: "src/transports/fcm.ts"
6
+ ---
7
+
8
+ Send device-token push through Firebase Cloud Messaging HTTP v1 for native Android and iOS clients.
9
+ Use [Web Push](./webpush) when you have a browser Push API subscription instead.
10
+
11
+ <Callout title="The one rule">Pass `token` in push options — not a Web Push `subscription`.</Callout>
12
+
13
+ ## Quick start
14
+
15
+ <Steps>
16
+ <Step title="Configure the transport">
17
+
18
+ ```ts
19
+ import { createPushSender } from "sently/push";
20
+ import { FcmTransport } from "sently/transports/fcm";
21
+
22
+ const push = createPushSender({
23
+ transport: new FcmTransport({
24
+ projectId: process.env.FCM_PROJECT_ID!,
25
+ clientEmail: process.env.FCM_CLIENT_EMAIL!,
26
+ privateKey: process.env.FCM_PRIVATE_KEY!,
27
+ }),
28
+ });
29
+ ```
30
+
31
+ </Step>
32
+ <Step title="Send to a device token">
33
+
34
+ ```ts
35
+ const result = await push.send({
36
+ token: deviceRegistrationToken,
37
+ title: "Hello",
38
+ body: "World",
39
+ });
40
+ console.log(result.messageId, result.status);
41
+ ```
42
+
43
+ </Step>
44
+ </Steps>
45
+
46
+ ## Configuration
47
+
48
+ | Option | Type | Default or requirement |
49
+ | --- | --- | --- |
50
+ | `projectId` | `string` | required |
51
+ | `clientEmail` | `string` | service account email |
52
+ | `privateKey` | `string` | PEM private key (env / secrets only) |
53
+ | `getAccessToken` | `() => Promise<string>` | optional override for tests |
54
+
55
+ Literal `\n` sequences in `privateKey` env strings are normalized to newlines.
56
+ The transport mints a short-lived OAuth2 token with scope `firebase.messaging` — no Google SDK.
57
+
58
+ ## Troubleshooting
59
+
60
+ <Accordions>
61
+ <Accordion title="Can I pass a browser subscription?">
62
+ No. `FcmTransport` requires `token`. Use `WebPushTransport` for VAPID subscriptions.
63
+ </Accordion>
64
+ <Accordion title="Where do I get the private key?">
65
+ Download a Firebase / GCP service account JSON and pass `project_id`, `client_email`, and `private_key` from that file via environment variables.
66
+ </Accordion>
67
+ </Accordions>
68
+
69
+ ## Learn more
70
+
71
+ - [Push channel](/docs/channels/push)
72
+ - [Push options](/docs/reference/push-options)
73
+ - [Web Push](./webpush)
74
+
75
+ ## Next
76
+
77
+ <Cards>
78
+ <Card title="Push channel" href="/docs/channels/push" />
79
+ <Card title="Web Push" href="/docs/transports/webpush" />
80
+ </Cards>
@@ -63,9 +63,11 @@ Every import below is an exported package subpath.
63
63
  | [Twilio SMS](./twilio-sms) | SMS | `sently/transports/twilio-sms` |
64
64
  | [Taqnyat SMS](./taqnyat-sms) | SMS | `sently/transports/taqnyat-sms` |
65
65
  | [Msegat](./msegat) | SMS | `sently/transports/msegat` |
66
+ | [Unifonic](./unifonic) | SMS | `sently/transports/unifonic` |
66
67
  | [WhatsApp Cloud](./whatsapp-cloud) | WhatsApp | `sently/transports/whatsapp-cloud` |
67
68
  | [Taqnyat WhatsApp](./taqnyat-whatsapp) | WhatsApp | `sently/transports/taqnyat-whatsapp` |
68
69
  | [Web Push](./webpush) | Push | `sently/transports/webpush` |
70
+ | [FCM](./fcm) | Push | `sently/transports/fcm` |
69
71
 
70
72
  For retry, fallback, preview, and idempotency wrappers, see [Decorators](../decorators).
71
73
 
@@ -85,7 +87,7 @@ For retry, fallback, preview, and idempotency wrappers, see [Decorators](../deco
85
87
  No. A transport implements one channel contract. Choose a transport listed for the channel sender you use.
86
88
  </Accordion>
87
89
  <Accordion title="How do I add retries or fallback?">
88
- Wrap an email transport with a [decorator](../decorators), then pass the wrapper to `createMailer`.
90
+ Wrap any channel transport with a [decorator](../decorators), then pass the wrapper to the matching sender.
89
91
  </Accordion>
90
92
  </Accordions>
91
93
 
@@ -23,10 +23,12 @@
23
23
  "twilio-sms",
24
24
  "taqnyat-sms",
25
25
  "msegat",
26
+ "unifonic",
26
27
  "---WhatsApp---",
27
28
  "whatsapp-cloud",
28
29
  "taqnyat-whatsapp",
29
30
  "---Push---",
30
- "webpush"
31
+ "webpush",
32
+ "fcm"
31
33
  ]
32
34
  }