sently 1.0.0 → 1.1.1

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 (45) hide show
  1. package/AGENTS.md +3 -2
  2. package/CHANGELOG.md +97 -0
  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/inbucket.d.ts +196 -0
  7. package/dist/transports/inbucket.js +3 -0
  8. package/dist/transports/inbucket.js.map +10 -0
  9. package/dist/transports/mailpit.d.ts +108 -8
  10. package/dist/transports/mailpit.js +2 -2
  11. package/dist/transports/mailpit.js.map +3 -3
  12. package/dist/transports/taqnyat-sms.d.ts +85 -0
  13. package/dist/transports/taqnyat-sms.js +2 -2
  14. package/dist/transports/taqnyat-sms.js.map +3 -3
  15. package/dist/transports/taqnyat-whatsapp.d.ts +112 -4
  16. package/dist/transports/taqnyat-whatsapp.js +2 -2
  17. package/dist/transports/taqnyat-whatsapp.js.map +3 -3
  18. package/dist/transports/webpush.d.ts +24 -1
  19. package/dist/transports/webpush.js +2 -2
  20. package/dist/transports/webpush.js.map +3 -3
  21. package/dist/webhooks/sndr.js +2 -2
  22. package/dist/webhooks/sndr.js.map +3 -3
  23. package/package.json +7 -2
  24. package/site/content/docs/ai/llms-txt.mdx +2 -0
  25. package/site/content/docs/channels/email.mdx +2 -1
  26. package/site/content/docs/channels/push.mdx +3 -1
  27. package/site/content/docs/decorators/preview.mdx +2 -1
  28. package/site/content/docs/get-started/entrypoints.mdx +1 -1
  29. package/site/content/docs/get-started/support-matrix.mdx +2 -2
  30. package/site/content/docs/guides/vendor-extras-otp.mdx +2 -1
  31. package/site/content/docs/guides/webhooks.mdx +2 -0
  32. package/site/content/docs/guides/webpush-interop.mdx +9 -1
  33. package/site/content/docs/reference/exports.mdx +1 -1
  34. package/site/content/docs/reference/push-options.mdx +22 -5
  35. package/site/content/docs/transports/inbucket.mdx +200 -0
  36. package/site/content/docs/transports/index.mdx +2 -3
  37. package/site/content/docs/transports/mailpit.mdx +114 -14
  38. package/site/content/docs/transports/meta.json +5 -4
  39. package/site/content/docs/transports/smtp.mdx +3 -2
  40. package/site/content/docs/transports/sndr.mdx +68 -5
  41. package/site/content/docs/transports/taqnyat.mdx +361 -0
  42. package/site/content/docs/transports/webpush.mdx +208 -17
  43. package/site/content/docs/transports/taqnyat-mail.mdx +0 -41
  44. package/site/content/docs/transports/taqnyat-sms.mdx +0 -41
  45. package/site/content/docs/transports/taqnyat-whatsapp.mdx +0 -40
@@ -5,8 +5,12 @@ icon: Truck
5
5
  source: "src/transports/sndr.ts"
6
6
  ---
7
7
 
8
- Use SNDR when you want an HTTP email API for receipts, sign-in mail, and other app-triggered messages.
9
- Wire `SndrTransport` into `createMailer` — do not call the vendor SDK from application code.
8
+ Use SNDR for app-triggered email — receipts, sign-in mail, and other transactional sends — over HTTPS JSON.
9
+ Wire `SndrTransport` into `createMailer`; do not call `@rkiza/sndr` from application code.
10
+
11
+ <LiveVerified>
12
+ Email send against SNDR’s production API succeeded in sently’s opt-in live suite (verified domain + real recipient).
13
+ </LiveVerified>
10
14
 
11
15
  <Callout title="The one rule">
12
16
  Import from `sently/transports/sndr`, pass the transport to `await createMailer(...)`, and send
@@ -55,7 +59,7 @@ console.log(result.messageId); // e.g. em_…
55
59
  console.log(result.response); // e.g. queued
56
60
  ```
57
61
 
58
- `from` must use a domain verified in the SNDR dashboard.
62
+ `from` must use a domain verified in the [SNDR dashboard](https://www.sndr.sh/) (SPF, DKIM, DMARC).
59
63
 
60
64
  </Step>
61
65
 
@@ -82,10 +86,12 @@ console.log(result.response); // e.g. queued
82
86
  | `idempotencyKey` / `messageId` | `Idempotency-Key` | Sent when present. |
83
87
  | `data` | `variables` | Only when a template id is set. |
84
88
 
89
+ `POST /v1/send` is idempotent when `Idempotency-Key` is supplied.
90
+
85
91
  ## Templates
86
92
 
87
93
  Set a template with the `x-sndr-template-id` header (or `defaultTemplateId` on the transport).
88
- Pass template variables through `data`.
94
+ Pass template variables through `data` (`{{ variable }}` placeholders on SNDR).
89
95
 
90
96
  ```ts
91
97
  import { SNDR_TEMPLATE_ID_HEADER } from "sently/transports/sndr";
@@ -105,13 +111,57 @@ await mailer.send({
105
111
 
106
112
  Failed HTTP responses throw `SndrError` (`SentlyError`) with the API `error.message` when present.
107
113
 
114
+ | SNDR `error.code` (common) | Meaning |
115
+ | --- | --- |
116
+ | `invalid_request` | Malformed payload |
117
+ | `unauthenticated` | Missing or invalid API key |
118
+ | `rate_limited` | Slow down |
119
+ | `domain_not_verified` | Verify the sending domain first |
120
+ | `recipient_suppressed` | Address is on the suppression list |
121
+
122
+ ## Webhooks
123
+
124
+ Import from `sently/webhooks/sndr`. Verify `X-Sndr-Signature` (`t=…,v1=…`) over the **raw** body, then `parse`.
125
+
126
+ | SNDR event | Normalized `EmailEvent.type` |
127
+ | --- | --- |
128
+ | `email.queued` | `deferred` |
129
+ | `email.delivered` | `delivered` |
130
+ | `email.bounced` | `bounced` |
131
+ | `email.failed` | `unknown` |
132
+ | `email.complained` | `complained` |
133
+ | `email.opened` | `opened` |
134
+ | `email.clicked` | `clicked` |
135
+ | `email.unsubscribed` | `unknown` |
136
+
137
+ See [Webhooks](/docs/guides/webhooks#sndr-signatures) for the HMAC details.
138
+
139
+ ## What sently covers vs SNDR platform
140
+
141
+ | Surface | In sently | Notes |
142
+ | --- | --- | --- |
143
+ | Send (`POST /v1/send`) | Yes — `SndrTransport` | Channel: `createMailer` |
144
+ | Templates + variables | Yes | Header / `defaultTemplateId` + `data` |
145
+ | Idempotency | Yes | `idempotencyKey` / `messageId` |
146
+ | Domain verify | Yes — `verify()` | `GET /v1/domains` |
147
+ | Delivery webhooks | Yes — `sently/webhooks/sndr` | Signed parse |
148
+ | Contacts / contact groups / broadcasts | No | SNDR dashboard / their API |
149
+ | Analytics / suppressions admin | No | Use SNDR dashboard |
150
+ | Attachments on HTTP send | Not mapped | Prefer SMTP or ask SNDR if their send API gains attachments |
151
+
108
152
  ## Troubleshooting
109
153
 
110
154
  <Accordions>
111
155
 
112
156
  <Accordion title="from domain is not verified">
113
157
 
114
- SNDR rejects sends from unverified domains. Verify DNS in the SNDR dashboard, then retry with that domain in `from`.
158
+ SNDR rejects sends from unverified domains. Add SPF, DKIM, and DMARC in the dashboard Domains page, click Verify, then retry with that domain in `from`.
159
+
160
+ </Accordion>
161
+
162
+ <Accordion title="recipient_suppressed">
163
+
164
+ The address is on SNDR’s suppression list (bounce/complaint). Remove it only if the recipient opted back in; otherwise pick another address.
115
165
 
116
166
  </Accordion>
117
167
 
@@ -129,6 +179,19 @@ No. Use `createMailer` plus `SndrTransport`. Keep vendor extras off the shared e
129
179
 
130
180
  </Accordions>
131
181
 
182
+ ## Contact & resources
183
+
184
+ | Contact | Detail |
185
+ | --- | --- |
186
+ | Direct email | [sndr@rkiza.sa](mailto:sndr@rkiza.sa) |
187
+ | Contact form | [sndr.sh/contact](https://www.sndr.sh/contact) |
188
+ | Docs | [sndr.sh/docs](https://www.sndr.sh/docs) |
189
+ | API reference | [API Reference](https://www.sndr.sh/docs/api-reference) |
190
+ | Status | [sndr.sh/status](https://www.sndr.sh/status) |
191
+ | X / Twitter | [@usesndr](https://x.com/usesndr) |
192
+ | LinkedIn | [SNDR company](https://www.linkedin.com/company/104143949) |
193
+ | GitHub | [github.com/rkiza/sndr](https://github.com/rkiza/sndr) |
194
+
132
195
  ## Learn more
133
196
 
134
197
  - [Email channel](/docs/channels/email) — mailer options and send pipeline
@@ -0,0 +1,361 @@
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
+ | Option | Type | Default or requirement |
274
+ | --- | --- | --- |
275
+ | `bearerToken` | `string` | required |
276
+ | `campaignName` | `string` | required |
277
+
278
+ `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.
279
+
280
+ ### Features
281
+
282
+ <Tabs items={["Send"]}>
283
+ <Tab value="Send">
284
+
285
+ Transactional email via the channel mailer.
286
+
287
+ ```ts
288
+ import { createMailer } from "sently/mailer";
289
+ import { TaqnyatMailTransport } from "sently/transports/taqnyat-mail";
290
+
291
+ const mailer = await createMailer({
292
+ transport: new TaqnyatMailTransport({
293
+ bearerToken: process.env.TAQNYAT_MAIL_TOKEN!,
294
+ campaignName: "sently",
295
+ }),
296
+ });
297
+
298
+ await mailer.send({
299
+ from: "noreply@example.com",
300
+ to: "person@example.com",
301
+ subject: "Hello",
302
+ text: "Hi",
303
+ // html: "<p>Hi</p>",
304
+ });
305
+ ```
306
+
307
+ Body is `html` when set, otherwise `text` (`msg` on Taqnyat’s API).
308
+
309
+ </Tab>
310
+ </Tabs>
311
+
312
+ ## Troubleshooting
313
+
314
+ <Accordions>
315
+ <Accordion title="SMS — Sender Name not active / not accepted">
316
+ Open the **Senders** branch and call `listSenders()`, or use an active portal sender exactly as shown (trial accounts often use `Taqnyat.sa`).
317
+ </Accordion>
318
+ <Accordion title="WhatsApp — sandbox rejects the number">
319
+ Add the destination under Manage WhatsApp → Sandbox, then use the **Opt-in** branch before business-initiated templates.
320
+ </Accordion>
321
+ <Accordion title="WhatsApp — template not approved / not found">
322
+ Use the **Templates** branch (`listTemplates`) and pick status **approved** — name + language must match exactly.
323
+ </Accordion>
324
+ <Accordion title="Email — Error 14 error From">
325
+ Taqnyat rejected the `from` address. Confirm email is enabled and the sender is approved, then retry with that address.
326
+ </Accordion>
327
+ <Accordion title="Error 102 IP not authorized">
328
+ Developers → Security Settings: authorize your public IP or turn off the IP allowlist.
329
+ </Accordion>
330
+ <Accordion title="Should I call the provider SDK?">
331
+ No. Use the matching sently channel sender; open a feature branch above for vendor extras on the transport.
332
+ </Accordion>
333
+ </Accordions>
334
+
335
+ ## Contact & resources
336
+
337
+ Provider contact details from Taqnyat (account setup, packages, and platform guides).
338
+
339
+ | Contact | Detail |
340
+ | --- | --- |
341
+ | Unified number | [920015404](tel:920015404) |
342
+ | Support email | [support@taqnyat.sa](mailto:support@taqnyat.sa) |
343
+ | Point of contact | [a.ghaith@taqnyat.sa](mailto:a.ghaith@taqnyat.sa) |
344
+ | Packages & pricing | [Offers & packages](https://taqnyat.sa/en/offers/packages/) |
345
+ | Platform explanations | [Technical explanations](https://portal.taqnyat.sa/technical_explanations/en/index.html) |
346
+
347
+ 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/).
348
+
349
+ ## Learn more
350
+
351
+ - [Email channel](/docs/channels/email) — mailer options and send pipeline
352
+ - [Sms channel](/docs/channels/sms) — SMS sender options
353
+ - [Whatsapp channel](/docs/channels/whatsapp) — template and session text
354
+ - [Vendor OTP extras](/docs/guides/vendor-extras-otp) — OTP helpers on concrete transports
355
+
356
+ ## Next
357
+
358
+ <Cards>
359
+ <Card title="Transports" href="/docs/transports" />
360
+ <Card title="Vendor OTP extras" href="/docs/guides/vendor-extras-otp" />
361
+ </Cards>