whatsapp-cloud 0.0.4 → 0.0.5
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/CHANGELOG.md +6 -0
- package/agent_docs/INCOMING_MESSAGES_BRAINSTORM.md +500 -0
- package/cloud-api-docs/webhooks/endpoint.md +112 -0
- package/cloud-api-docs/webhooks/overview.md +154 -0
- package/package.json +6 -2
- package/src/client/HttpClient.ts +43 -6
- package/src/client/WhatsAppClient.ts +3 -0
- package/src/examples/main.ts +9 -0
- package/src/examples/template.ts +134 -0
- package/src/schemas/client.ts +2 -2
- package/src/schemas/index.ts +1 -0
- package/src/schemas/templates/component.ts +145 -0
- package/src/schemas/templates/index.ts +4 -0
- package/src/schemas/templates/request.ts +78 -0
- package/src/schemas/templates/response.ts +64 -0
- package/src/services/accounts/AccountsClient.ts +6 -14
- package/src/services/accounts/AccountsService.ts +19 -21
- package/src/services/accounts/methods/list-phone-numbers.ts +1 -2
- package/src/services/business/BusinessClient.ts +1 -9
- package/src/services/business/BusinessService.ts +19 -21
- package/src/services/business/methods/list-accounts.ts +1 -2
- package/src/services/messages/MessagesClient.ts +2 -6
- package/src/services/messages/MessagesService.ts +42 -22
- package/src/services/templates/TemplatesClient.ts +35 -0
- package/src/services/templates/TemplatesService.ts +117 -0
- package/src/services/templates/index.ts +3 -0
- package/src/services/templates/methods/create.ts +27 -0
- package/src/services/templates/methods/delete.ts +38 -0
- package/src/services/templates/methods/get.ts +23 -0
- package/src/services/templates/methods/list.ts +36 -0
- package/src/services/templates/methods/update.ts +35 -0
- package/src/types/index.ts +1 -0
- package/src/types/templates/component.ts +33 -0
- package/src/types/templates/index.ts +4 -0
- package/src/types/templates/request.ts +28 -0
- package/src/types/templates/response.ts +34 -0
- package/tsconfig.json +2 -3
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Nav-Logo
|
|
2
|
+
Build with us
|
|
3
|
+
Docs
|
|
4
|
+
Blog
|
|
5
|
+
Resources
|
|
6
|
+
Developer centers
|
|
7
|
+
Meine Apps
|
|
8
|
+
Dokumente
|
|
9
|
+
Übersicht
|
|
10
|
+
Webhooks
|
|
11
|
+
Webhooks
|
|
12
|
+
Aktualisiert: 07.11.2025
|
|
13
|
+
In diesem Dokument werden Webhooks beschrieben und wie sie auf der WhatsApp Business-Plattform verwendet werden.
|
|
14
|
+
Webhooks sind HTTP-Anfragen mit JSON-Payloads, die von Meta-Servern an einen von dir bestimmten Server gesendet werden. Die WhatsApp Business-Plattform verwendet Webhooks, um dich über eingehende Nachrichten, den Status ausgehender Nachrichten und andere wichtige Informationen zu informieren, wie z. B. Änderungen an deinem Kontostatus, Upgrades der Nachrichtenfunktion und Änderungen an der Qualitätsbewertungen deiner Vorlagen.
|
|
15
|
+
Hier siehst du einen Webhook, der eine Nachricht beschreibt, die von einem*einer WhatsApp-Benutzer*in an ein Unternehmen gesendet wurde:
|
|
16
|
+
{
|
|
17
|
+
"object": "whatsapp_business_account",
|
|
18
|
+
"entry": [
|
|
19
|
+
{
|
|
20
|
+
"id": "102290129340398",
|
|
21
|
+
"changes": [
|
|
22
|
+
{
|
|
23
|
+
"value": {
|
|
24
|
+
"messaging_product": "whatsapp",
|
|
25
|
+
"metadata": {
|
|
26
|
+
"display_phone_number": "15550783881",
|
|
27
|
+
"phone_number_id": "106540352242922"
|
|
28
|
+
},
|
|
29
|
+
"contacts": [
|
|
30
|
+
{
|
|
31
|
+
"profile": {
|
|
32
|
+
"name": "Sheena Nelson"
|
|
33
|
+
},
|
|
34
|
+
"wa_id": "16505551234"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"messages": [
|
|
38
|
+
{
|
|
39
|
+
"from": "16505551234",
|
|
40
|
+
"id": "wamid.HBgLMTY1MDM4Nzk0MzkVAgASGBQzQTRBNjU5OUFFRTAzODEwMTQ0RgA=",
|
|
41
|
+
"timestamp": "1749416383",
|
|
42
|
+
"type": "text"
|
|
43
|
+
"text": {
|
|
44
|
+
"body": "Does it come in another color?"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
"field": "messages"
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
Einen Webhook-Endpunkt erstellen
|
|
57
|
+
Um Webhooks zu empfangen, musst du einen Webhook-Endpunkt erstellen und konfigurieren. Im Dokument Webhook-Endpunkt erstellen erfährst du, wie du deinen eigenen Endpunkt erstellst.
|
|
58
|
+
Wenn du noch nicht bereit bist, deinen eigenen Endpunkt zu erstellen, kannst du einen Test-Webhook-Endpunkt erstellen, der Webhook-Payloads an die Konsole exportiert. Beachte jedoch, dass du deinen eigenen Endpunkt erstellen musst, bevor du deine App in einer Produktionsumgebung verwenden kannst.
|
|
59
|
+
Berechtigungen
|
|
60
|
+
Du benötigst die folgenden Berechtigungen, um Webhooks zu erhalten:
|
|
61
|
+
whatsapp_business_messaging – für messages-Webhooks
|
|
62
|
+
whatsapp_business_management – für alle anderen Webhooks
|
|
63
|
+
Wenn du ein*e direkte*r Entwickler*in bist, gewähre deiner App über deine*n Systemnutzer*in diese Berechtigungen, wenn du dein Systemtoken generierst.
|
|
64
|
+
Wenn du als Lösungsanbieter*in diese Berechtigungen benötigst, um deinen Unternehmenskunden entsprechende Services zur Verfügung zu stellen, musst du vor der App-Review die Genehmigung für erweiterten Zugriff auf die Berechtigungen erhalten. Erst dann können deine Unternehmenskunden deiner App während des Onboardings diese Berechtigungen erteilen.
|
|
65
|
+
Felder
|
|
66
|
+
Sobald du deinen Webhook-Endpunkt erstellt und konfiguriert hast, kannst du die folgenden Webhook-Felder abonnieren.
|
|
67
|
+
Name des Feldes Beschreibung
|
|
68
|
+
account_alerts
|
|
69
|
+
Der account_alerts-Webhook benachrichtigt dich über Änderungen des Messaging-Limits, des Unternehmensprofils und des Status als offizielles Unternehmenskonto einer Unternehmenstelefonnummer.
|
|
70
|
+
account_review_update
|
|
71
|
+
Der account_review_update-Webhook benachrichtigt dich, wenn ein WhatsApp-Unternehmenskonto hinsichtlich der Einhaltung unserer Richtlinien überprüft wurde.
|
|
72
|
+
account_update
|
|
73
|
+
Der account_update-Webhook benachrichtigt dich über Änderungen an der partnergeführten Unternehmensverifizierung eines WhatsApp-Unternehmenskontos, seiner Berechtigung für den Tarif für internationale Authentifizierung oder seinem Hauptgeschäftsstandort, wenn das Konto mit einem*einer Lösungsanbieter*in geteilt wird, bei Richtlinien- oder Nutzungsbedingungsverstößen oder wenn es gelöscht wird.
|
|
74
|
+
automatic_events
|
|
75
|
+
Der automatic_events-Webhook benachrichtigt dich, wenn wir ein Kauf- oder Lead-Event in einem Chat-Thread zwischen dir und einem*einer WhatsApp-Nutzer*in erkennen, der*die dir über deine Click-to-WhatsApp Ad eine Nachricht gesendet hat. Dazu musst du Berichte für automatische Events aktiviert haben.
|
|
76
|
+
business_capability_update
|
|
77
|
+
Der business_capability_update-Webhook informiert dich über Änderungen der Funktionen für WhatsApp-Unternehmenskonto oder Business-Portfolio (Messaging-Limits, Telefonnummer-Limits usw.).
|
|
78
|
+
history
|
|
79
|
+
Der history-Webhook wird verwendet, um den Chatverlauf in der WhatsApp Business-App eines Unternehmenskunden zu synchronisieren, der von einem*einer Lösungsanbieter*in freigeschaltet wurde.
|
|
80
|
+
message_template_components_update
|
|
81
|
+
Der message_template_components_update-Webhook benachrichtigt dich über Änderungen an den Komponenten einer Vorlage.
|
|
82
|
+
message_template_quality_update
|
|
83
|
+
Der message_template_quality_update-Webhook benachrichtigt dich über Änderungen an der Qualitätsbewertung einer Vorlage.
|
|
84
|
+
message_template_status_update
|
|
85
|
+
Der message_template_status_update-Webhook benachrichtigt dich über Änderungen am Status einer vorhandenen Vorlage.
|
|
86
|
+
messages
|
|
87
|
+
Der messages-Webhook beschreibt Nachrichten, die von einem*einer WhatsApp-Nutzer*in an ein Unternehmen gesendet werden, und den Status von Nachrichten, die von einem Unternehmen an eine*n WhatsApp-Nutzer*in gesendet werden.
|
|
88
|
+
partner_solutions
|
|
89
|
+
Der partner_solutions-Webhook beschreibt Änderungen am Status einer Multi-Partner-Lösung.
|
|
90
|
+
payment_configuration_update
|
|
91
|
+
Der payment_configuration_update-Webhook informiert dich über Änderungen an Zahlungskonfigurationen für die Payments API für Indien und die Payments API für Brasilien.
|
|
92
|
+
phone_number_name_update
|
|
93
|
+
Der phone_number_name_update-Webhook informiert dich über die Verifizierung des Display-Namens der Unternehmenstelefonnummer.
|
|
94
|
+
phone_number_quality_update
|
|
95
|
+
Der phone_number_quality_update-Webhook benachrichtigt dich über Änderungen an der Durchsatzrate einer Unternehmenstelefonnummer.
|
|
96
|
+
security
|
|
97
|
+
Der security-Webhook benachrichtigt dich über Änderungen an den Sicherheitseinstellungen einer Unternehmenstelefonnummer.
|
|
98
|
+
smb_app_state_sync
|
|
99
|
+
Der smb_app_state_sync-Webhook wird zum Synchronisieren von Kontakten von Nutzer*innen der WhatsApp Business-App verwendet, die über eine*n Lösungsanbieter*in freigeschaltet wurden.
|
|
100
|
+
smb_message_echoes
|
|
101
|
+
Der smb_message_echoes-Webhook benachrichtigt dich über Nachrichten, die über die WhatsApp Business-App oder ein begleitendes („verknüpftes“) Gerät von einem Unternehmenskunden gesendet wurden, der über eine*n Lösungsanbieter*in für die Cloud API freigeschaltet wurde.
|
|
102
|
+
template_category_update
|
|
103
|
+
Der template_category_update-Webhook benachrichtigt dich über Änderungen an der Kategorie einer Vorlage.
|
|
104
|
+
user_preferences
|
|
105
|
+
Der user_preferences-Webhook benachrichtigt dich über Änderungen an den Marketing-Nachrichten-Einstellungen eines*einer WhatsApp-Nutzer*in.
|
|
106
|
+
Webhooks überschreiben
|
|
107
|
+
Du kannst einen alternativen Webhook-Endpunkt für messages-Webhooks für dein WhatsApp-Unternehmenskonto (WABA) oder deine Unternehmenstelefonnummer verwenden. Dieser kann für Testzwecke nützlich sein oder wenn du als Lösungsanbieter eindeutige Webhook-Endpunkte für alle deine registrierten Kund\*innen verwenden möchtest.
|
|
108
|
+
In unserem Dokument zu Webhook-Überschreibungen erfährst du, wie du Webhooks überschreiben kannst.
|
|
109
|
+
Payload-Größe
|
|
110
|
+
Payload-Webhooks können bis zu 3 MB groß sein.
|
|
111
|
+
Fehler bei der Webhook-Auslieferung
|
|
112
|
+
Wenn wir eine Webhook-Anfrage an deinen Endpunkt senden und dein Server mit einem anderen HTTP-Statuscode als 200 antwortet oder wenn wir den Webhook aus einem anderen Grund nicht zustellen können, versuchen wir es so lange mit abnehmender Häufigkeit, bis die Anfrage erfolgreich ist, und zwar bis zu 7 Tage lang.
|
|
113
|
+
Beachte, dass Wiederholungsversuche an alle Apps gesendet werden, die Webhooks (und ihre entsprechenden Felder) für das WhatsApp Business-Konto abonniert haben. Dies kann zu doppelten Webhook-Benachrichtigungen führen.
|
|
114
|
+
IP-Adressen
|
|
115
|
+
Du kannst die IP-Adressen unserer Webhook-Server abrufen, indem du in deinem Terminal den folgenden Befehl ausführst:
|
|
116
|
+
whois -h whois.radb.net — '-i origin AS32934' | grep '^route' | awk '{print $2}' | sort
|
|
117
|
+
Wir ändern diese IP-Adressen in regelmäßigen Abständen. Wenn du also unsere Server auf der Positivliste hast, solltest du diese Liste gelegentlich neu erstellen und deine Positivliste entsprechend aktualisieren.
|
|
118
|
+
Problembehandlung
|
|
119
|
+
Wenn du keine Webhooks erhältst:
|
|
120
|
+
Vergewissere dich, dass dein Endpunkt Anfragen akzeptiert.
|
|
121
|
+
Sende eine Test-Payload an deinen Endpunkt über das Panel App-Dashboard > WhatsApp > Konfigurationen.
|
|
122
|
+
Stelle sicher, dass der Live-Modus für deine App aktiviert ist. Wenn deine App im Entwicklungsmodus ist, werden einige Webhooks nicht gesendet.
|
|
123
|
+
Mehr dazu
|
|
124
|
+
Siehe unseren Blogeintrag Implementierung von Webhooks mit Node.js für WhatsApp Business.
|
|
125
|
+
War diese Seite hilfreich?
|
|
126
|
+
„Daumen hoch“-Symbol
|
|
127
|
+
„Daumen runter“-Symbol
|
|
128
|
+
Meta
|
|
129
|
+
FacebookInstagramXLinkedInYouTube
|
|
130
|
+
Build with Meta
|
|
131
|
+
AI
|
|
132
|
+
Meta Horizon
|
|
133
|
+
Social technologies
|
|
134
|
+
Wearables
|
|
135
|
+
News
|
|
136
|
+
Meta for Developers
|
|
137
|
+
Blog
|
|
138
|
+
Success stories
|
|
139
|
+
Support
|
|
140
|
+
Developer Support
|
|
141
|
+
Bug tool
|
|
142
|
+
Platform status
|
|
143
|
+
Developer community forum
|
|
144
|
+
Report an incident
|
|
145
|
+
About us
|
|
146
|
+
About
|
|
147
|
+
Careers
|
|
148
|
+
Terms and policies
|
|
149
|
+
Responsible platform initiatives
|
|
150
|
+
Platform terms
|
|
151
|
+
Developer policies
|
|
152
|
+
Privacy policy
|
|
153
|
+
Cookies
|
|
154
|
+
English (US)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whatsapp-cloud",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Work in progress. A WhatsApp client tailored for LLMs—built to actually work.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"devDependencies": {
|
|
13
13
|
"@changesets/cli": "^2.29.8",
|
|
14
|
+
"@types/node": "^25.0.3",
|
|
15
|
+
"dotenv": "^17.2.3",
|
|
14
16
|
"tsup": "^8.5.1",
|
|
17
|
+
"tsx": "^4.21.0",
|
|
15
18
|
"typescript": "^5.9.3"
|
|
16
19
|
},
|
|
17
20
|
"dependencies": {
|
|
@@ -19,6 +22,7 @@
|
|
|
19
22
|
},
|
|
20
23
|
"scripts": {
|
|
21
24
|
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
22
|
-
"lint": "tsc"
|
|
25
|
+
"lint": "tsc",
|
|
26
|
+
"example": "tsx src/examples/main.ts"
|
|
23
27
|
}
|
|
24
28
|
}
|
package/src/client/HttpClient.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { ClientConfig } from "../types/client";
|
|
2
2
|
|
|
3
|
+
interface APIErrorResponse {
|
|
4
|
+
error?: {
|
|
5
|
+
message?: string;
|
|
6
|
+
code?: number;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
/**
|
|
4
11
|
* HTTP client for making requests to the WhatsApp Cloud API
|
|
5
12
|
*/
|
|
@@ -42,12 +49,12 @@ export class HttpClient {
|
|
|
42
49
|
});
|
|
43
50
|
|
|
44
51
|
if (!response.ok) {
|
|
45
|
-
const error = await response.json().catch(() => ({
|
|
52
|
+
const error = (await response.json().catch(() => ({
|
|
46
53
|
error: {
|
|
47
54
|
message: response.statusText,
|
|
48
55
|
code: response.status,
|
|
49
56
|
},
|
|
50
|
-
}));
|
|
57
|
+
}))) as APIErrorResponse;
|
|
51
58
|
throw new Error(
|
|
52
59
|
`API Error: ${error.error?.message || response.statusText} (${
|
|
53
60
|
error.error?.code || response.status
|
|
@@ -72,12 +79,12 @@ export class HttpClient {
|
|
|
72
79
|
});
|
|
73
80
|
|
|
74
81
|
if (!response.ok) {
|
|
75
|
-
const error = await response.json().catch(() => ({
|
|
82
|
+
const error = (await response.json().catch(() => ({
|
|
76
83
|
error: {
|
|
77
84
|
message: response.statusText,
|
|
78
85
|
code: response.status,
|
|
79
86
|
},
|
|
80
|
-
}));
|
|
87
|
+
}))) as APIErrorResponse;
|
|
81
88
|
throw new Error(
|
|
82
89
|
`API Error: ${error.error?.message || response.statusText} (${
|
|
83
90
|
error.error?.code || response.status
|
|
@@ -104,12 +111,42 @@ export class HttpClient {
|
|
|
104
111
|
});
|
|
105
112
|
|
|
106
113
|
if (!response.ok) {
|
|
107
|
-
const error = await response.json().catch(() => ({
|
|
114
|
+
const error = (await response.json().catch(() => ({
|
|
115
|
+
error: {
|
|
116
|
+
message: response.statusText,
|
|
117
|
+
code: response.status,
|
|
118
|
+
},
|
|
119
|
+
}))) as APIErrorResponse;
|
|
120
|
+
throw new Error(
|
|
121
|
+
`API Error: ${error.error?.message || response.statusText} (${
|
|
122
|
+
error.error?.code || response.status
|
|
123
|
+
})`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return response.json() as Promise<T>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Make a DELETE request
|
|
132
|
+
*/
|
|
133
|
+
async delete<T>(path: string): Promise<T> {
|
|
134
|
+
const url = `${this.baseURL}/${this.apiVersion}${path}`;
|
|
135
|
+
|
|
136
|
+
const response = await fetch(url, {
|
|
137
|
+
method: "DELETE",
|
|
138
|
+
headers: {
|
|
139
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
const error = (await response.json().catch(() => ({
|
|
108
145
|
error: {
|
|
109
146
|
message: response.statusText,
|
|
110
147
|
code: response.status,
|
|
111
148
|
},
|
|
112
|
-
}));
|
|
149
|
+
}))) as APIErrorResponse;
|
|
113
150
|
throw new Error(
|
|
114
151
|
`API Error: ${error.error?.message || response.statusText} (${
|
|
115
152
|
error.error?.code || response.status
|
|
@@ -4,6 +4,7 @@ import { HttpClient } from "./HttpClient";
|
|
|
4
4
|
import { MessagesService } from "../services/messages/index";
|
|
5
5
|
import { AccountsService } from "../services/accounts/index";
|
|
6
6
|
import { BusinessService } from "../services/business/index";
|
|
7
|
+
import { TemplatesService } from "../services/templates/index";
|
|
7
8
|
import { ZodError } from "zod";
|
|
8
9
|
import { transformZodError } from "../utils/zod-error";
|
|
9
10
|
import type { DebugTokenResponse } from "../types/debug";
|
|
@@ -15,6 +16,7 @@ export class WhatsAppClient {
|
|
|
15
16
|
public readonly messages: MessagesService;
|
|
16
17
|
public readonly accounts: AccountsService;
|
|
17
18
|
public readonly business: BusinessService;
|
|
19
|
+
public readonly templates: TemplatesService;
|
|
18
20
|
|
|
19
21
|
private readonly httpClient: HttpClient;
|
|
20
22
|
|
|
@@ -37,6 +39,7 @@ export class WhatsAppClient {
|
|
|
37
39
|
this.messages = new MessagesService(this.httpClient);
|
|
38
40
|
this.accounts = new AccountsService(this.httpClient);
|
|
39
41
|
this.business = new BusinessService(this.httpClient);
|
|
42
|
+
this.templates = new TemplatesService(this.httpClient);
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
/**
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { WhatsAppClient } from "../client";
|
|
3
|
+
|
|
4
|
+
const client = new WhatsAppClient({
|
|
5
|
+
accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
|
|
6
|
+
businessAccountId: process.env.WHATSAPP_BUSINESS_ACCOUNT_ID!,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
async function testTemplates() {
|
|
10
|
+
try {
|
|
11
|
+
console.log("🧪 Testing Templates API...\n");
|
|
12
|
+
|
|
13
|
+
// Test 1: Create a template
|
|
14
|
+
console.log("1️⃣ Creating template...");
|
|
15
|
+
const createResponse = await client.templates.create({
|
|
16
|
+
name: `test_template_${Date.now()}`, // Unique name to avoid conflicts
|
|
17
|
+
language: "en",
|
|
18
|
+
category: "UTILITY",
|
|
19
|
+
components: [
|
|
20
|
+
{
|
|
21
|
+
type: "BODY",
|
|
22
|
+
text: "Hello! This is a test template. Thank you for testing our WhatsApp Cloud API SDK.",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
type: "FOOTER",
|
|
26
|
+
text: "This is a test footer",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
type: "BUTTONS",
|
|
30
|
+
buttons: [
|
|
31
|
+
{
|
|
32
|
+
type: "QUICK_REPLY",
|
|
33
|
+
text: "Get Started",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: "QUICK_REPLY",
|
|
37
|
+
text: "Learn More",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
console.log("✅ Template created successfully!");
|
|
45
|
+
console.log("Response:", JSON.stringify(createResponse, null, 2));
|
|
46
|
+
console.log("\n");
|
|
47
|
+
|
|
48
|
+
// Verify create response structure
|
|
49
|
+
if (!createResponse.id) {
|
|
50
|
+
throw new Error("Create response missing 'id' field");
|
|
51
|
+
}
|
|
52
|
+
if (!createResponse.status) {
|
|
53
|
+
throw new Error("Create response missing 'status' field");
|
|
54
|
+
}
|
|
55
|
+
if (!createResponse.category) {
|
|
56
|
+
throw new Error("Create response missing 'category' field");
|
|
57
|
+
}
|
|
58
|
+
console.log("✅ Create response structure verified\n");
|
|
59
|
+
|
|
60
|
+
// Test 2: Get template by ID
|
|
61
|
+
console.log("2️⃣ Getting template by ID...");
|
|
62
|
+
const templateId = createResponse.id;
|
|
63
|
+
const getResponse = await client.templates.get(templateId);
|
|
64
|
+
|
|
65
|
+
console.log("✅ Template retrieved successfully!");
|
|
66
|
+
console.log("Response:", JSON.stringify(getResponse, null, 2));
|
|
67
|
+
console.log("\n");
|
|
68
|
+
|
|
69
|
+
// Verify get response structure
|
|
70
|
+
if (!getResponse.id) {
|
|
71
|
+
throw new Error("Get response missing 'id' field");
|
|
72
|
+
}
|
|
73
|
+
if (!getResponse.name) {
|
|
74
|
+
throw new Error("Get response missing 'name' field");
|
|
75
|
+
}
|
|
76
|
+
if (!getResponse.language) {
|
|
77
|
+
throw new Error("Get response missing 'language' field");
|
|
78
|
+
}
|
|
79
|
+
if (!getResponse.status) {
|
|
80
|
+
throw new Error("Get response missing 'status' field");
|
|
81
|
+
}
|
|
82
|
+
if (!getResponse.category) {
|
|
83
|
+
throw new Error("Get response missing 'category' field");
|
|
84
|
+
}
|
|
85
|
+
if (!Array.isArray(getResponse.components)) {
|
|
86
|
+
throw new Error("Get response missing 'components' array");
|
|
87
|
+
}
|
|
88
|
+
console.log("✅ Get response structure verified\n");
|
|
89
|
+
|
|
90
|
+
// Verify components
|
|
91
|
+
const bodyComponent = getResponse.components.find((c) => c.type === "BODY");
|
|
92
|
+
if (!bodyComponent) {
|
|
93
|
+
throw new Error("BODY component not found in response");
|
|
94
|
+
}
|
|
95
|
+
if (bodyComponent.type !== "BODY") {
|
|
96
|
+
throw new Error("BODY component type mismatch");
|
|
97
|
+
}
|
|
98
|
+
console.log("✅ Components verified\n");
|
|
99
|
+
|
|
100
|
+
// Test 3: List templates
|
|
101
|
+
console.log("3️⃣ Listing templates...");
|
|
102
|
+
const listResponse = await client.templates.list();
|
|
103
|
+
console.log(`✅ Found ${listResponse.data.length} template(s)`);
|
|
104
|
+
console.log("Response:", JSON.stringify(listResponse, null, 2));
|
|
105
|
+
console.log("\n");
|
|
106
|
+
|
|
107
|
+
// Verify list response structure
|
|
108
|
+
if (!Array.isArray(listResponse.data)) {
|
|
109
|
+
throw new Error("List response missing 'data' array");
|
|
110
|
+
}
|
|
111
|
+
console.log("✅ List response structure verified\n");
|
|
112
|
+
|
|
113
|
+
// Verify our created template is in the list
|
|
114
|
+
const foundTemplate = listResponse.data.find((t) => t.id === templateId);
|
|
115
|
+
if (!foundTemplate) {
|
|
116
|
+
console.log(
|
|
117
|
+
"⚠️ Note: Created template may not appear in list immediately (async processing)"
|
|
118
|
+
);
|
|
119
|
+
} else {
|
|
120
|
+
console.log("✅ Created template found in list\n");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log("🎉 All tests passed!");
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error("❌ Test failed:", error);
|
|
126
|
+
if (error instanceof Error) {
|
|
127
|
+
console.error("Error message:", error.message);
|
|
128
|
+
console.error("Error stack:", error.stack);
|
|
129
|
+
}
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
testTemplates();
|
package/src/schemas/client.ts
CHANGED
|
@@ -44,7 +44,7 @@ export const clientConfigSchema = z.object({
|
|
|
44
44
|
.refine((val) => val === undefined || val.trim().length > 0, {
|
|
45
45
|
message: "businessId cannot be empty or whitespace only",
|
|
46
46
|
}),
|
|
47
|
-
apiVersion: z.string().default("v18.0"),
|
|
48
|
-
baseURL: z.string().url().default("https://graph.facebook.com"),
|
|
47
|
+
apiVersion: z.string().default("v18.0").optional(),
|
|
48
|
+
baseURL: z.string().url().default("https://graph.facebook.com").optional(),
|
|
49
49
|
timeout: z.number().positive().optional(),
|
|
50
50
|
});
|
package/src/schemas/index.ts
CHANGED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Button schemas for template components
|
|
5
|
+
* Simplified version without variables for now
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Quick reply button schema
|
|
10
|
+
*/
|
|
11
|
+
export const quickReplyButtonSchema = z.object({
|
|
12
|
+
type: z.literal("QUICK_REPLY"),
|
|
13
|
+
text: z.string().min(1).max(25, "Button text must be 25 characters or less"),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* URL button schema
|
|
18
|
+
* Note: example field will be added later when we support variables
|
|
19
|
+
*/
|
|
20
|
+
export const urlButtonSchema = z.object({
|
|
21
|
+
type: z.literal("URL"),
|
|
22
|
+
text: z.string().min(1).max(25, "Button text must be 25 characters or less"),
|
|
23
|
+
url: z.string().url().max(2000, "URL must be 2000 characters or less"),
|
|
24
|
+
// example: z.array(z.string()).optional(), // For later: when URL contains variables
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Phone number button schema
|
|
29
|
+
*/
|
|
30
|
+
export const phoneNumberButtonSchema = z.object({
|
|
31
|
+
type: z.literal("PHONE_NUMBER"),
|
|
32
|
+
text: z.string().min(1).max(25, "Button text must be 25 characters or less"),
|
|
33
|
+
phone_number: z
|
|
34
|
+
.string()
|
|
35
|
+
.min(1)
|
|
36
|
+
.max(20, "Phone number must be 20 characters or less"),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Copy code button schema
|
|
41
|
+
* Note: example field will be added later
|
|
42
|
+
*/
|
|
43
|
+
export const copyCodeButtonSchema = z.object({
|
|
44
|
+
type: z.literal("COPY_CODE"),
|
|
45
|
+
// example: z.string().max(15).optional(), // For later: example value to copy
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Flow button schema (for authentication templates)
|
|
50
|
+
* Note: Will be expanded later when we support flow templates
|
|
51
|
+
*/
|
|
52
|
+
export const flowButtonSchema = z.object({
|
|
53
|
+
type: z.literal("FLOW"),
|
|
54
|
+
text: z.string().min(1).max(25, "Button text must be 25 characters or less"),
|
|
55
|
+
flow_action: z.string().optional(),
|
|
56
|
+
flow_id: z.string().optional(),
|
|
57
|
+
navigate_screen: z.string().optional(),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Union of all button types
|
|
62
|
+
*/
|
|
63
|
+
export const buttonSchema = z.discriminatedUnion("type", [
|
|
64
|
+
quickReplyButtonSchema,
|
|
65
|
+
urlButtonSchema,
|
|
66
|
+
phoneNumberButtonSchema,
|
|
67
|
+
copyCodeButtonSchema,
|
|
68
|
+
flowButtonSchema,
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Header component schema
|
|
73
|
+
* Simplified - no variables/examples for now
|
|
74
|
+
*
|
|
75
|
+
* Note:
|
|
76
|
+
* - TEXT format requires text field
|
|
77
|
+
* - IMAGE/VIDEO/DOCUMENT formats require example.header_handle (for later)
|
|
78
|
+
* - LOCATION format requires neither text nor example
|
|
79
|
+
*/
|
|
80
|
+
export const headerComponentSchema = z
|
|
81
|
+
.object({
|
|
82
|
+
type: z.literal("HEADER"),
|
|
83
|
+
format: z.enum(["TEXT", "IMAGE", "VIDEO", "DOCUMENT", "LOCATION"]),
|
|
84
|
+
text: z
|
|
85
|
+
.string()
|
|
86
|
+
.max(60, "Header text must be 60 characters or less")
|
|
87
|
+
.optional(),
|
|
88
|
+
// example: z.object({...}).optional(), // For later: when using variables or media
|
|
89
|
+
})
|
|
90
|
+
.refine(
|
|
91
|
+
(data) => {
|
|
92
|
+
// TEXT format requires text
|
|
93
|
+
if (data.format === "TEXT" && !data.text) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
// LOCATION format doesn't need text
|
|
97
|
+
if (data.format === "LOCATION") {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
// IMAGE/VIDEO/DOCUMENT will need example.header_handle (for later)
|
|
101
|
+
return true;
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
message: "TEXT format header requires text field",
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Body component schema
|
|
110
|
+
* Required component - no variables for now
|
|
111
|
+
*/
|
|
112
|
+
export const bodyComponentSchema = z.object({
|
|
113
|
+
type: z.literal("BODY"),
|
|
114
|
+
text: z
|
|
115
|
+
.string()
|
|
116
|
+
.min(1)
|
|
117
|
+
.max(1024, "Body text must be 1024 characters or less"),
|
|
118
|
+
// example: z.object({...}).optional(), // For later: when using variables
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Footer component schema
|
|
123
|
+
*/
|
|
124
|
+
export const footerComponentSchema = z.object({
|
|
125
|
+
type: z.literal("FOOTER"),
|
|
126
|
+
text: z.string().min(1).max(60, "Footer text must be 60 characters or less"),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Buttons component schema
|
|
131
|
+
*/
|
|
132
|
+
export const buttonsComponentSchema = z.object({
|
|
133
|
+
type: z.literal("BUTTONS"),
|
|
134
|
+
buttons: z.array(buttonSchema).min(1).max(10, "Maximum 10 buttons allowed"),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Union of all component types
|
|
139
|
+
*/
|
|
140
|
+
export const componentSchema = z.discriminatedUnion("type", [
|
|
141
|
+
headerComponentSchema,
|
|
142
|
+
bodyComponentSchema,
|
|
143
|
+
footerComponentSchema,
|
|
144
|
+
buttonsComponentSchema,
|
|
145
|
+
]);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { componentSchema } from "./component";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Schema for creating a template
|
|
6
|
+
* Simplified - no variables/examples for now
|
|
7
|
+
*/
|
|
8
|
+
export const createTemplateRequestSchema = z.object({
|
|
9
|
+
name: z.string().min(1).max(512, "Template name must be 512 characters or less"),
|
|
10
|
+
language: z.string().min(2).max(5, "Language code must be 2-5 characters (e.g., 'en' or 'en_US')"),
|
|
11
|
+
category: z.enum(["AUTHENTICATION", "MARKETING", "UTILITY"]),
|
|
12
|
+
components: z
|
|
13
|
+
.array(componentSchema)
|
|
14
|
+
.min(1, "At least one component is required")
|
|
15
|
+
.refine(
|
|
16
|
+
(components) => {
|
|
17
|
+
// Body component is required
|
|
18
|
+
return components.some((c) => c.type === "BODY");
|
|
19
|
+
},
|
|
20
|
+
{ message: "BODY component is required" }
|
|
21
|
+
)
|
|
22
|
+
.refine(
|
|
23
|
+
(components) => {
|
|
24
|
+
// Only one header allowed
|
|
25
|
+
const headers = components.filter((c) => c.type === "HEADER");
|
|
26
|
+
return headers.length <= 1;
|
|
27
|
+
},
|
|
28
|
+
{ message: "Only one HEADER component is allowed" }
|
|
29
|
+
)
|
|
30
|
+
.refine(
|
|
31
|
+
(components) => {
|
|
32
|
+
// Only one footer allowed
|
|
33
|
+
const footers = components.filter((c) => c.type === "FOOTER");
|
|
34
|
+
return footers.length <= 1;
|
|
35
|
+
},
|
|
36
|
+
{ message: "Only one FOOTER component is allowed" }
|
|
37
|
+
)
|
|
38
|
+
.refine(
|
|
39
|
+
(components) => {
|
|
40
|
+
// Only one buttons component allowed
|
|
41
|
+
const buttons = components.filter((c) => c.type === "BUTTONS");
|
|
42
|
+
return buttons.length <= 1;
|
|
43
|
+
},
|
|
44
|
+
{ message: "Only one BUTTONS component is allowed" }
|
|
45
|
+
),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Schema for updating a template
|
|
50
|
+
* All fields optional - only update what's provided
|
|
51
|
+
*/
|
|
52
|
+
export const updateTemplateRequestSchema = z.object({
|
|
53
|
+
category: z.enum(["AUTHENTICATION", "MARKETING", "UTILITY"]).optional(),
|
|
54
|
+
components: z.array(componentSchema).optional(),
|
|
55
|
+
language: z.string().min(2).max(5).optional(),
|
|
56
|
+
name: z.string().min(1).max(512).optional(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Schema for listing templates
|
|
61
|
+
*/
|
|
62
|
+
export const listTemplatesRequestSchema = z.object({
|
|
63
|
+
name: z.string().optional(), // Filter by template name
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Schema for deleting a template
|
|
68
|
+
* Either name or hsm_id must be provided
|
|
69
|
+
*/
|
|
70
|
+
export const deleteTemplateRequestSchema = z
|
|
71
|
+
.object({
|
|
72
|
+
name: z.string().optional(),
|
|
73
|
+
hsm_id: z.string().optional(),
|
|
74
|
+
})
|
|
75
|
+
.refine((data) => data.name || data.hsm_id, {
|
|
76
|
+
message: "Either name or hsm_id must be provided",
|
|
77
|
+
});
|
|
78
|
+
|