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.
- package/AGENTS.md +3 -2
- package/CHANGELOG.md +110 -1
- package/README.md +20 -5
- package/dist/chunk-z1589fjk.js.map +2 -2
- package/dist/core/push-types.d.ts +53 -4
- package/dist/transports/hostinger.d.ts +128 -0
- package/dist/transports/hostinger.js +3 -0
- package/dist/transports/hostinger.js.map +10 -0
- package/dist/transports/inbucket.d.ts +196 -0
- package/dist/transports/inbucket.js +3 -0
- package/dist/transports/inbucket.js.map +10 -0
- package/dist/transports/mailpit.d.ts +108 -8
- package/dist/transports/mailpit.js +2 -2
- package/dist/transports/mailpit.js.map +3 -3
- package/dist/transports/taqnyat-sms.d.ts +85 -0
- package/dist/transports/taqnyat-sms.js +2 -2
- package/dist/transports/taqnyat-sms.js.map +3 -3
- package/dist/transports/taqnyat-whatsapp.d.ts +112 -4
- package/dist/transports/taqnyat-whatsapp.js +2 -2
- package/dist/transports/taqnyat-whatsapp.js.map +3 -3
- package/dist/transports/webpush.d.ts +19 -0
- package/dist/transports/webpush.js +2 -2
- package/dist/transports/webpush.js.map +3 -3
- package/dist/webhooks/sndr.js +2 -2
- package/dist/webhooks/sndr.js.map +3 -3
- package/package.json +12 -2
- package/site/content/docs/ai/llms-txt.mdx +2 -0
- package/site/content/docs/channels/email.mdx +2 -1
- package/site/content/docs/channels/push.mdx +3 -1
- package/site/content/docs/decorators/preview.mdx +2 -1
- package/site/content/docs/get-started/entrypoints.mdx +1 -1
- package/site/content/docs/get-started/support-matrix.mdx +2 -2
- package/site/content/docs/guides/vendor-extras-otp.mdx +2 -1
- package/site/content/docs/guides/webhooks.mdx +2 -0
- package/site/content/docs/guides/webpush-interop.mdx +9 -1
- package/site/content/docs/reference/exports.mdx +1 -1
- package/site/content/docs/reference/push-options.mdx +22 -5
- package/site/content/docs/transports/hostinger.mdx +435 -0
- package/site/content/docs/transports/inbucket.mdx +200 -0
- package/site/content/docs/transports/index.mdx +3 -3
- package/site/content/docs/transports/mailpit.mdx +114 -14
- package/site/content/docs/transports/meta.json +6 -4
- package/site/content/docs/transports/smtp.mdx +3 -2
- package/site/content/docs/transports/sndr.mdx +68 -5
- package/site/content/docs/transports/taqnyat.mdx +365 -0
- package/site/content/docs/transports/webpush.mdx +201 -17
- package/site/content/docs/transports/taqnyat-mail.mdx +0 -41
- package/site/content/docs/transports/taqnyat-sms.mdx +0 -41
- 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
|
-
<
|
|
11
|
+
<LiveVerified>
|
|
12
|
+
Browser notification send with VAPID succeeded against a real push service (okengine live verification).
|
|
13
|
+
</LiveVerified>
|
|
11
14
|
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
32
|
-
|
|
52
|
+
</Step>
|
|
53
|
+
|
|
54
|
+
<Step>
|
|
55
|
+
### Send
|
|
33
56
|
|
|
34
57
|
```ts
|
|
35
|
-
await
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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>
|