sently 1.0.1 → 1.2.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 (49) hide show
  1. package/AGENTS.md +3 -2
  2. package/CHANGELOG.md +110 -1
  3. package/README.md +20 -5
  4. package/dist/chunk-z1589fjk.js.map +2 -2
  5. package/dist/core/push-types.d.ts +53 -4
  6. package/dist/transports/hostinger.d.ts +128 -0
  7. package/dist/transports/hostinger.js +3 -0
  8. package/dist/transports/hostinger.js.map +10 -0
  9. package/dist/transports/inbucket.d.ts +196 -0
  10. package/dist/transports/inbucket.js +3 -0
  11. package/dist/transports/inbucket.js.map +10 -0
  12. package/dist/transports/mailpit.d.ts +108 -8
  13. package/dist/transports/mailpit.js +2 -2
  14. package/dist/transports/mailpit.js.map +3 -3
  15. package/dist/transports/taqnyat-sms.d.ts +85 -0
  16. package/dist/transports/taqnyat-sms.js +2 -2
  17. package/dist/transports/taqnyat-sms.js.map +3 -3
  18. package/dist/transports/taqnyat-whatsapp.d.ts +112 -4
  19. package/dist/transports/taqnyat-whatsapp.js +2 -2
  20. package/dist/transports/taqnyat-whatsapp.js.map +3 -3
  21. package/dist/transports/webpush.d.ts +19 -0
  22. package/dist/transports/webpush.js +2 -2
  23. package/dist/transports/webpush.js.map +3 -3
  24. package/dist/webhooks/sndr.js +2 -2
  25. package/dist/webhooks/sndr.js.map +3 -3
  26. package/package.json +12 -2
  27. package/site/content/docs/ai/llms-txt.mdx +2 -0
  28. package/site/content/docs/channels/email.mdx +2 -1
  29. package/site/content/docs/channels/push.mdx +3 -1
  30. package/site/content/docs/decorators/preview.mdx +2 -1
  31. package/site/content/docs/get-started/entrypoints.mdx +1 -1
  32. package/site/content/docs/get-started/support-matrix.mdx +2 -2
  33. package/site/content/docs/guides/vendor-extras-otp.mdx +2 -1
  34. package/site/content/docs/guides/webhooks.mdx +2 -0
  35. package/site/content/docs/guides/webpush-interop.mdx +9 -1
  36. package/site/content/docs/reference/exports.mdx +1 -1
  37. package/site/content/docs/reference/push-options.mdx +22 -5
  38. package/site/content/docs/transports/hostinger.mdx +435 -0
  39. package/site/content/docs/transports/inbucket.mdx +200 -0
  40. package/site/content/docs/transports/index.mdx +3 -3
  41. package/site/content/docs/transports/mailpit.mdx +114 -14
  42. package/site/content/docs/transports/meta.json +6 -4
  43. package/site/content/docs/transports/smtp.mdx +3 -2
  44. package/site/content/docs/transports/sndr.mdx +68 -5
  45. package/site/content/docs/transports/taqnyat.mdx +365 -0
  46. package/site/content/docs/transports/webpush.mdx +201 -17
  47. package/site/content/docs/transports/taqnyat-mail.mdx +0 -41
  48. package/site/content/docs/transports/taqnyat-sms.mdx +0 -41
  49. package/site/content/docs/transports/taqnyat-whatsapp.mdx +0 -40
@@ -0,0 +1,365 @@
1
+ ---
2
+ title: Taqnyat
3
+ description: Send email, SMS, and WhatsApp through Taqnyat — one provider, three channel transports.
4
+ icon: Truck
5
+ source: "src/transports/taqnyat-sms.ts"
6
+ ---
7
+
8
+ Taqnyat is a multi-channel provider. Wire each product into its sently channel sender — do not build a mega Taqnyat client.
9
+
10
+ <Callout title="The one rule">
11
+ Use `createMailer`, `createSmsSender`, or `createWhatsAppSender` with the matching `sently/transports/taqnyat-*` transport.
12
+ Vendor extras stay on the concrete transport instance — never on the shared channel sender.
13
+ </Callout>
14
+
15
+ | Channel | Transport | Import |
16
+ | --- | --- | --- |
17
+ | SMS | `TaqnyatSmsTransport` | `sently/transports/taqnyat-sms` |
18
+ | WhatsApp | `TaqnyatWhatsAppTransport` | `sently/transports/taqnyat-whatsapp` |
19
+ | Email | `TaqnyatMailTransport` | `sently/transports/taqnyat-mail` |
20
+
21
+ ## SMS
22
+
23
+ <LiveVerified>
24
+ SMS send against Taqnyat’s production API succeeded in sently’s opt-in live suite (trial sender + Saudi destination).
25
+ </LiveVerified>
26
+
27
+ | Option | Type | Default or requirement |
28
+ | --- | --- | --- |
29
+ | `bearerToken` | `string` | required |
30
+ | `sender` | `string` | required — active portal sender name (case-sensitive) |
31
+
32
+ Recipients are normalized to international digits (no `+` / leading `00`).
33
+
34
+ ### Setup
35
+
36
+ ```ts
37
+ import { createSmsSender } from "sently/sms";
38
+ import { TaqnyatSmsTransport } from "sently/transports/taqnyat-sms";
39
+
40
+ const taqnyat = new TaqnyatSmsTransport({
41
+ bearerToken: process.env.TAQNYAT_TOKEN!,
42
+ sender: "Taqnyat.sa",
43
+ });
44
+ const sms = createSmsSender({ transport: taqnyat });
45
+ ```
46
+
47
+ ### Features
48
+
49
+ Pick a branch. Channel send goes through `sms`; everything else is called on `taqnyat`.
50
+
51
+ <Tabs items={["Send", "OTP", "Balance", "Senders", "Schedule"]}>
52
+ <Tab value="Send">
53
+
54
+ Immediate SMS via the channel sender.
55
+
56
+ ```ts
57
+ const result = await sms.send({
58
+ to: "+9665xxxxxxxx",
59
+ body: "Hello from sently",
60
+ });
61
+ console.log(result.messageId, result.response);
62
+ ```
63
+
64
+ Optional `from` overrides the transport `sender` for that send.
65
+
66
+ </Tab>
67
+ <Tab value="OTP">
68
+
69
+ Taqnyat Verify API on the transport (`sendOtp` → user enters code → `verifyOtp`).
70
+
71
+ ```ts
72
+ await taqnyat.sendOtp({
73
+ to: "+9665xxxxxxxx",
74
+ requestId: "login-1",
75
+ lang: "en", // or "ar" (default)
76
+ note: "Acme login", // optional
77
+ });
78
+
79
+ await taqnyat.verifyOtp({
80
+ to: "+9665xxxxxxxx",
81
+ requestId: "login-1",
82
+ code: "6240",
83
+ lang: "en",
84
+ });
85
+ ```
86
+
87
+ Success send code is `5`. Verify accepts `10`, `13`, or `19`.
88
+
89
+ </Tab>
90
+ <Tab value="Balance">
91
+
92
+ Account balance for preflight and ops (`GET /account/balance`).
93
+
94
+ ```ts
95
+ const balance = await taqnyat.getBalance();
96
+ // { accountStatus, balance, currency, accountExpiryDate?, provider }
97
+ console.log(balance.balance, balance.currency);
98
+ ```
99
+
100
+ </Tab>
101
+ <Tab value="Senders">
102
+
103
+ List registered sender names before you hard-code `sender`.
104
+
105
+ ```ts
106
+ const senders = await taqnyat.listSenders();
107
+ // [{ senderName: "Taqnyat.sa", status: "active" }, ...]
108
+ const active = senders.filter((s) => s.status === "active");
109
+ ```
110
+
111
+ Trial accounts may return an empty list while the portal still allows `Taqnyat.sa`.
112
+
113
+ </Tab>
114
+ <Tab value="Schedule">
115
+
116
+ Schedule a send, then delete it with the same `deleteId` if you need to cancel.
117
+
118
+ ```ts
119
+ await taqnyat.schedule({
120
+ to: "+9665xxxxxxxx",
121
+ body: "Later",
122
+ scheduledDatetime: "2030-01-01T10:00", // Taqnyat format
123
+ deleteId: "demo-1",
124
+ });
125
+
126
+ await taqnyat.deleteScheduled("demo-1");
127
+ ```
128
+
129
+ Useful for dry-runs: schedule → confirm in portal → delete to reclaim cost when the account allows it.
130
+
131
+ </Tab>
132
+ </Tabs>
133
+
134
+ ## WhatsApp
135
+
136
+ <LiveVerified>
137
+ Template send against Taqnyat’s WhatsApp API succeeded in sently’s opt-in live suite (approved sandbox template + authorized number).
138
+ </LiveVerified>
139
+
140
+ | Option | Type | Default or requirement |
141
+ | --- | --- | --- |
142
+ | `bearerToken` | `string` | required |
143
+
144
+ Business-initiated chats must start with an **approved** template. Session `text` is only allowed after the user replies (24h window).
145
+ Queued accepts may return `statuses: "PENDING"` with no `message_id` yet — still treated as accepted.
146
+
147
+ ### Setup
148
+
149
+ ```ts
150
+ import { createWhatsAppSender } from "sently/whatsapp";
151
+ import { TaqnyatWhatsAppTransport } from "sently/transports/taqnyat-whatsapp";
152
+
153
+ const transport = new TaqnyatWhatsAppTransport({
154
+ bearerToken: process.env.TAQNYAT_WHATSAPP_TOKEN!,
155
+ });
156
+ const wa = createWhatsAppSender({ transport });
157
+ ```
158
+
159
+ ### Features
160
+
161
+ Pick a branch. Template/session send goes through `wa`; extras stay on `transport`.
162
+
163
+ <Tabs items={["Template", "Session text", "Opt-in", "Templates", "Failover"]}>
164
+ <Tab value="Template">
165
+
166
+ Business-initiated message with an approved template.
167
+
168
+ ```ts
169
+ await transport.optIn("+9665xxxxxxxx"); // once per recipient when required
170
+
171
+ const result = await wa.send({
172
+ to: "+9665xxxxxxxx",
173
+ template: {
174
+ name: "demotest1_testr11",
175
+ language: "ar",
176
+ // components: [...] // when the template has variables
177
+ },
178
+ });
179
+ console.log(result.messageId, result.response);
180
+ ```
181
+
182
+ Sandbox: recipient must be on the authorized numbers list.
183
+
184
+ </Tab>
185
+ <Tab value="Session text">
186
+
187
+ Free-form text only inside an open 24h customer-care session (after the user replies).
188
+
189
+ ```ts
190
+ await wa.send({
191
+ to: "+9665xxxxxxxx",
192
+ text: "Thanks — how can we help?",
193
+ });
194
+ ```
195
+
196
+ Do not use this as the first business-initiated message.
197
+
198
+ </Tab>
199
+ <Tab value="Opt-in">
200
+
201
+ Record consent before template campaigns (`POST` / `DELETE` provision opt-in).
202
+
203
+ ```ts
204
+ await transport.optIn("+9665xxxxxxxx");
205
+ // or several:
206
+ await transport.optIn(["+9665xxxxxxxx", "9665yyyyyyyy"]);
207
+
208
+ await transport.optOut("+9665xxxxxxxx");
209
+ ```
210
+
211
+ Numbers are normalized the same way as send (`+` / `00` stripped).
212
+
213
+ </Tab>
214
+ <Tab value="Templates">
215
+
216
+ List, create, and delete WhatsApp templates on the transport.
217
+
218
+ ```ts
219
+ const templates = await transport.listTemplates();
220
+ const approved = templates.filter((t) => t.status === "approved");
221
+
222
+ const created = await transport.createTemplate({
223
+ name: "sently_test",
224
+ language: "ar",
225
+ category: "UTILITY",
226
+ components: [{ type: "BODY", text: "Hello from sently" }],
227
+ });
228
+ // { id?, category?, status: "PENDING", provider }
229
+
230
+ await transport.deleteTemplate({
231
+ name: "sently_test",
232
+ id: created.id!,
233
+ });
234
+ ```
235
+
236
+ Meta must approve a template before you can send it.
237
+
238
+ </Tab>
239
+ <Tab value="Failover">
240
+
241
+ Same WhatsApp send URL with nested SMS and/or email fallback branches.
242
+
243
+ ```ts
244
+ await transport.sendWithFailover(
245
+ {
246
+ to: "+9665xxxxxxxx",
247
+ template: { name: "welcome", language: "ar" },
248
+ },
249
+ {
250
+ sms: {
251
+ sender: "Taqnyat.sa",
252
+ campaign: "sently",
253
+ body: "SMS fallback",
254
+ },
255
+ mail: {
256
+ from: "hi@example.com",
257
+ to: "user@example.com",
258
+ campaign: "sently",
259
+ subject: "Fallback",
260
+ msg: "Email fallback",
261
+ },
262
+ },
263
+ );
264
+ ```
265
+
266
+ Keep `campaign` aligned with email `campaignName` when both channels share a campaign.
267
+
268
+ </Tab>
269
+ </Tabs>
270
+
271
+ ## Email
272
+
273
+ <LiveVerified>
274
+ Email send against Taqnyat’s production API succeeded in sently’s opt-in live suite (approved sender + real recipient).
275
+ </LiveVerified>
276
+
277
+ | Option | Type | Default or requirement |
278
+ | --- | --- | --- |
279
+ | `bearerToken` | `string` | required |
280
+ | `campaignName` | `string` | required |
281
+
282
+ `from` should be a sender address your Taqnyat account is allowed to use. See [Sender Approval](https://docs.taqnyat.sa/sender-approval) if email is not enabled in the portal yet.
283
+
284
+ ### Features
285
+
286
+ <Tabs items={["Send"]}>
287
+ <Tab value="Send">
288
+
289
+ Transactional email via the channel mailer.
290
+
291
+ ```ts
292
+ import { createMailer } from "sently/mailer";
293
+ import { TaqnyatMailTransport } from "sently/transports/taqnyat-mail";
294
+
295
+ const mailer = await createMailer({
296
+ transport: new TaqnyatMailTransport({
297
+ bearerToken: process.env.TAQNYAT_MAIL_TOKEN!,
298
+ campaignName: "sently",
299
+ }),
300
+ });
301
+
302
+ await mailer.send({
303
+ from: "noreply@example.com",
304
+ to: "person@example.com",
305
+ subject: "Hello",
306
+ text: "Hi",
307
+ // html: "<p>Hi</p>",
308
+ });
309
+ ```
310
+
311
+ Body is `html` when set, otherwise `text` (`msg` on Taqnyat’s API).
312
+
313
+ </Tab>
314
+ </Tabs>
315
+
316
+ ## Troubleshooting
317
+
318
+ <Accordions>
319
+ <Accordion title="SMS — Sender Name not active / not accepted">
320
+ Open the **Senders** branch and call `listSenders()`, or use an active portal sender exactly as shown (trial accounts often use `Taqnyat.sa`).
321
+ </Accordion>
322
+ <Accordion title="WhatsApp — sandbox rejects the number">
323
+ Add the destination under Manage WhatsApp → Sandbox, then use the **Opt-in** branch before business-initiated templates.
324
+ </Accordion>
325
+ <Accordion title="WhatsApp — template not approved / not found">
326
+ Use the **Templates** branch (`listTemplates`) and pick status **approved** — name + language must match exactly.
327
+ </Accordion>
328
+ <Accordion title="Email — Error 14 error From">
329
+ Taqnyat rejected the `from` address. Confirm email is enabled and the sender is approved, then retry with that address.
330
+ </Accordion>
331
+ <Accordion title="Error 102 IP not authorized">
332
+ Developers → Security Settings: authorize your public IP or turn off the IP allowlist.
333
+ </Accordion>
334
+ <Accordion title="Should I call the provider SDK?">
335
+ No. Use the matching sently channel sender; open a feature branch above for vendor extras on the transport.
336
+ </Accordion>
337
+ </Accordions>
338
+
339
+ ## Contact & resources
340
+
341
+ Provider contact details from Taqnyat (account setup, packages, and platform guides).
342
+
343
+ | Contact | Detail |
344
+ | --- | --- |
345
+ | Unified number | [920015404](tel:920015404) |
346
+ | Support email | [support@taqnyat.sa](mailto:support@taqnyat.sa) |
347
+ | Point of contact | [a.ghaith@taqnyat.sa](mailto:a.ghaith@taqnyat.sa) |
348
+ | Packages & pricing | [Offers & packages](https://taqnyat.sa/en/offers/packages/) |
349
+ | Platform explanations | [Technical explanations](https://portal.taqnyat.sa/technical_explanations/en/index.html) |
350
+
351
+ API reference: [SMS](https://dev.taqnyat.sa/ar/doc/sms/), [WhatsApp](https://dev.taqnyat.sa/ar/doc/whatsapp/), [Verify](https://dev.taqnyat.sa/en/doc/verify/), [Mail](https://dev.taqnyat.sa/ar/doc/mail/).
352
+
353
+ ## Learn more
354
+
355
+ - [Email channel](/docs/channels/email) — mailer options and send pipeline
356
+ - [Sms channel](/docs/channels/sms) — SMS sender options
357
+ - [Whatsapp channel](/docs/channels/whatsapp) — template and session text
358
+ - [Vendor OTP extras](/docs/guides/vendor-extras-otp) — OTP helpers on concrete transports
359
+
360
+ ## Next
361
+
362
+ <Cards>
363
+ <Card title="Transports" href="/docs/transports" />
364
+ <Card title="Vendor OTP extras" href="/docs/guides/vendor-extras-otp" />
365
+ </Cards>
@@ -5,46 +5,230 @@ icon: Truck
5
5
  source: "src/transports/webpush.ts"
6
6
  ---
7
7
 
8
- Encrypt and send browser notifications with VAPID.
8
+ Encrypt and send browser notifications with VAPID — the welcome ping, the
9
+ “report ready” alert, or a silent data sync to a service worker.
9
10
 
10
- <Callout title="The one rule">Create this transport under the matching sently channel sender.</Callout>
11
+ <LiveVerified>
12
+ Browser notification send with VAPID succeeded against a real push service (okengine live verification).
13
+ </LiveVerified>
11
14
 
12
- ## Configuration
15
+ <Callout title="The one rule">
16
+ Create this transport under `createPushSender` and pass a browser
17
+ `subscription` — not an FCM device token.
18
+ </Callout>
13
19
 
14
- | Option | Type | Default or requirement |
15
- | --- | --- | --- |
16
- | `vapidPublicKey` | `string` | required |
17
- | `vapidPrivateKey` | `string` | required |
18
- | `subject` | `string` | required — `mailto:you@example.com` or `https://example.com/contact` (validated at construction) |
19
- | `allowedEndpointHosts` | `string[]` | optional |
20
+ ## Quick start
20
21
 
21
22
  <Steps>
22
- <Step title="Configure the transport">
23
+
24
+ <Step>
25
+ ### Generate VAPID keys
26
+
27
+ ```ts
28
+ import { generateVapidKeys } from "sently/transports/webpush";
29
+
30
+ const { publicKey, privateKey } = await generateVapidKeys();
31
+ // Store privateKey in env / secrets manager. Use publicKey in the browser subscribe call.
32
+ ```
33
+
34
+ </Step>
35
+
36
+ <Step>
37
+ ### Configure the sender
23
38
 
24
39
  ```ts
25
40
  import { createPushSender } from "sently/push";
26
41
  import { WebPushTransport } from "sently/transports/webpush";
27
42
 
28
- const sender = createPushSender({ transport: new WebPushTransport({ vapidPublicKey: "...", vapidPrivateKey: "...", subject: "mailto:you@example.com" }) });
43
+ const push = createPushSender({
44
+ transport: new WebPushTransport({
45
+ vapidPublicKey: process.env.VAPID_PUBLIC_KEY!,
46
+ vapidPrivateKey: process.env.VAPID_PRIVATE_KEY!,
47
+ subject: "mailto:you@example.com",
48
+ }),
49
+ });
29
50
  ```
30
51
 
31
- </Step>
32
- <Step title="Send with the channel API">
52
+ </Step>
53
+
54
+ <Step>
55
+ ### Send
33
56
 
34
57
  ```ts
35
- await sender.send({ subscription, title: "Hello", body: "World" });
58
+ await push.send({
59
+ subscription,
60
+ title: "Report ready",
61
+ body: "Your weekly report is ready to view.",
62
+ urgency: "high",
63
+ topic: "report-ready",
64
+ });
36
65
  ```
37
66
 
38
- </Step>
67
+ </Step>
68
+
39
69
  </Steps>
40
70
 
71
+ ## Configuration
72
+
73
+ | Option | Type | Default or requirement |
74
+ | --- | --- | --- |
75
+ | `vapidPublicKey` | `string` | required — base64url uncompressed P-256 (65 bytes) |
76
+ | `vapidPrivateKey` | `string` | required — base64url raw private key (32 bytes) |
77
+ | `subject` | `string` | required — `mailto:you@example.com` or `https://example.com/contact` |
78
+ | `allowedEndpointHosts` | `string[]` | optional — exact hostnames for private push relays |
79
+
80
+ ## Send options (Web Push)
81
+
82
+ | Field | Type | Notes |
83
+ | --- | --- | --- |
84
+ | `subscription` | `PushSubscription` | required — `endpoint`, `keys.p256dh`, `keys.auth` |
85
+ | `title` / `body` | `string` | required together for a visible notification |
86
+ | `data` | `Record<string, unknown>` | optional; required for `silent` / data-only |
87
+ | `icon` / `badge` / `image` | `string` | Notification API URLs |
88
+ | `tag` | `string` | replace an existing notification with the same tag |
89
+ | `actions` | `{ action, title, icon? }[]` | action buttons |
90
+ | `requireInteraction` | `boolean` | keep open until the user interacts |
91
+ | `renotify` | `boolean` | re-alert when replacing by `tag` |
92
+ | `ttl` | `number` | seconds (default `2419200` / 28 days) |
93
+ | `urgency` | `"very-low" \| "low" \| "normal" \| "high"` | RFC 8030 `Urgency` header |
94
+ | `topic` | `string` | RFC 8030 `Topic` — 1–32 printable ASCII; collapses pending messages |
95
+ | `silent` | `boolean` | encrypt only `data` (no visible fields); requires `data` |
96
+ | `messageId` | `string` | optional client id |
97
+
98
+ ## Features
99
+
100
+ Pick a branch. Channel send goes through `push`; key generation is imported from
101
+ `sently/transports/webpush`.
102
+
103
+ <Tabs items={["Send", "Urgency", "Topic", "Rich", "Silent", "Keys"]}>
104
+ <Tab value="Send">
105
+
106
+ Visible notification via the channel sender.
107
+
108
+ ```ts
109
+ await push.send({
110
+ subscription,
111
+ title: "Report ready",
112
+ body: "Your weekly report is ready to view.",
113
+ });
114
+ ```
115
+
116
+ </Tab>
117
+ <Tab value="Urgency">
118
+
119
+ RFC 8030 `Urgency` header — delivery priority hint for the push service.
120
+
121
+ ```ts
122
+ await push.send({
123
+ subscription,
124
+ title: "Payment failed",
125
+ body: "Update your card to keep service running.",
126
+ urgency: "high",
127
+ });
128
+ ```
129
+
130
+ </Tab>
131
+ <Tab value="Topic">
132
+
133
+ RFC 8030 `Topic` — a newer message replaces a pending one with the same topic.
134
+
135
+ ```ts
136
+ await push.send({
137
+ subscription,
138
+ title: "Order update",
139
+ body: "Your package is out for delivery.",
140
+ topic: "order-42",
141
+ ttl: 3600,
142
+ });
143
+ ```
144
+
145
+ </Tab>
146
+ <Tab value="Rich">
147
+
148
+ Notification API fields encrypted into the JSON payload for the service worker.
149
+
150
+ ```ts
151
+ await push.send({
152
+ subscription,
153
+ title: "Report ready",
154
+ body: "Tap to open your weekly report.",
155
+ icon: "https://example.com/icon.png",
156
+ badge: "https://example.com/badge.png",
157
+ image: "https://example.com/hero.png",
158
+ tag: "report-ready",
159
+ requireInteraction: true,
160
+ renotify: true,
161
+ actions: [{ action: "open", title: "Open" }],
162
+ data: { reportId: "wk-12" },
163
+ });
164
+ ```
165
+
166
+ </Tab>
167
+ <Tab value="Silent">
168
+
169
+ Data-only / silent push — encrypt `data` without visible notification fields.
170
+ The service worker must handle `push` without calling `showNotification`.
171
+
172
+ ```ts
173
+ await push.send({
174
+ subscription,
175
+ silent: true,
176
+ data: { sync: "inbox", since: "2026-08-02T00:00:00Z" },
177
+ });
178
+
179
+ // Same shape without the flag: omit title/body and pass data.
180
+ await push.send({
181
+ subscription,
182
+ data: { ping: 1 },
183
+ });
184
+ ```
185
+
186
+ </Tab>
187
+ <Tab value="Keys">
188
+
189
+ Generate a VAPID key pair (`generateVapidKeys` — not on `createPushSender`).
190
+
191
+ ```ts
192
+ import { generateVapidKeys } from "sently/transports/webpush";
193
+
194
+ const { publicKey, privateKey } = await generateVapidKeys();
195
+ // Store privateKey in secrets. Use publicKey in pushManager.subscribe.
196
+ ```
197
+
198
+ </Tab>
199
+ </Tabs>
200
+
201
+ Invalid `urgency` / `topic`, `silent` without `data`, or a partial visible
202
+ payload (`title` without `body`) throw `WebPushError` (`provider: "webpush"`)
203
+ with status `400` before fetch.
204
+
205
+ ## Troubleshooting
206
+
41
207
  <Accordions>
42
208
  <Accordion title="Should I call the provider SDK?">
43
- No. Use the sently sender; provider-specific extras stay on the transport instance.
209
+ No. Use the sently sender; provider-specific extras stay on the transport module.
44
210
  </Accordion>
45
211
  <Accordion title="Why does construction throw on subject?">
46
212
  VAPID `subject` must be a real `mailto:` address or `https:` URL. Values like `@oke.local` (no prefix) are rejected immediately as `WebPushError` so push services never return a confusing 403 later.
47
213
  </Accordion>
214
+ <Accordion title="Why does silent send fail?">
215
+ `silent: true` requires `data`. Without it the transport throws `WebPushError` with status `400`.
216
+ </Accordion>
217
+ <Accordion title="Why was my topic rejected?">
218
+ `topic` must be 1–32 printable ASCII characters (no spaces). Invalid values throw before fetch.
219
+ </Accordion>
48
220
  </Accordions>
49
221
 
50
- <Cards><Card title="Push channel" href="/docs/channels/push" /></Cards>
222
+ ## Learn more
223
+
224
+ - [Push channel](/docs/channels/push) — sender, hooks, plugins
225
+ - [Push options](/docs/reference/push-options) — union fields for Web Push and FCM
226
+ - [Web Push interoperability](/docs/guides/webpush-interop) — browser subscription shape
227
+
228
+ ## Next
229
+
230
+ <Cards>
231
+ <Card title="Push channel" href="/docs/channels/push" />
232
+ <Card title="Push options" href="/docs/reference/push-options" />
233
+ <Card title="FCM" href="/docs/transports/fcm" />
234
+ </Cards>
@@ -1,41 +0,0 @@
1
- ---
2
- title: Taqnyat Mail
3
- description: Send email through the Taqnyat Email API.
4
- icon: Truck
5
- source: "src/transports/taqnyat-mail.ts"
6
- ---
7
-
8
- Send email through the Taqnyat Email API.
9
-
10
- <Callout title="The one rule">Create this transport under the matching sently channel sender.</Callout>
11
-
12
- ## Configuration
13
-
14
- | Option | Type | Default or requirement |
15
- | --- | --- | --- |
16
- | `bearerToken` | `string` | required |
17
- | `campaignName` | `string` | required |
18
-
19
- <Steps>
20
- <Step title="Configure the transport">
21
-
22
- ```ts
23
- import { createMailer } from "sently/mailer";
24
- import { TaqnyatMailTransport } from "sently/transports/taqnyat-mail";
25
-
26
- const sender = createMailer({ transport: new TaqnyatMailTransport({ bearerToken: process.env.TAQNYAT_MAIL_TOKEN!, campaignName: "transactional" }) });
27
- ```
28
-
29
- </Step>
30
- <Step title="Send with the channel API">
31
-
32
- ```ts
33
- await sender.send({ from: "hello@example.com", to: "person@example.com", subject: "Hello", text: "Hi" });
34
- ```
35
-
36
- </Step>
37
- </Steps>
38
-
39
- <Accordions><Accordion title="Should I call the provider SDK?">No. Use the sently sender; provider-specific extras stay on the transport instance.</Accordion></Accordions>
40
-
41
- <Cards><Card title="Email channel" href="/docs/channels/email" /></Cards>
@@ -1,41 +0,0 @@
1
- ---
2
- title: Taqnyat SMS
3
- description: Send SMS through the Taqnyat API.
4
- icon: Truck
5
- source: "src/transports/taqnyat-sms.ts"
6
- ---
7
-
8
- Send SMS through the Taqnyat API.
9
-
10
- <Callout title="The one rule">Create this transport under the matching sently channel sender.</Callout>
11
-
12
- ## Configuration
13
-
14
- | Option | Type | Default or requirement |
15
- | --- | --- | --- |
16
- | `bearerToken` | `string` | required |
17
- | `sender` | `string` | required |
18
-
19
- <Steps>
20
- <Step title="Configure the transport">
21
-
22
- ```ts
23
- import { createSmsSender } from "sently/sms";
24
- import { TaqnyatSmsTransport } from "sently/transports/taqnyat-sms";
25
-
26
- const sender = createSmsSender({ transport: new TaqnyatSmsTransport({ bearerToken: "...", sender: "MyBrand" }) });
27
- ```
28
-
29
- </Step>
30
- <Step title="Send with the channel API">
31
-
32
- ```ts
33
- await sender.send({ to: "+15551234567", body: "Hello" });
34
- ```
35
-
36
- </Step>
37
- </Steps>
38
-
39
- <Accordions><Accordion title="Should I call the provider SDK?">No. Use the sently sender; provider-specific extras stay on the transport instance.</Accordion></Accordions>
40
-
41
- <Cards><Card title="Sms channel" href="/docs/channels/sms" /></Cards>