{"_id":"@abdelrahmannasr/wa-cloud-api","_rev":"5-f6959268d853c3270328fd8d9d8318f0","name":"@abdelrahmannasr/wa-cloud-api","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@abdelrahmannasr/wa-cloud-api","version":"1.0.0","keywords":["whatsapp","cloud-api","sdk","typescript","meta","messaging","webhooks","templates"],"author":{"name":"AbdelRahman Nasr"},"license":"MIT","_id":"@abdelrahmannasr/wa-cloud-api@1.0.0","maintainers":[{"name":"abdelrahmannasr","email":"a.nasr.yocto@gmail.com"}],"homepage":"https://github.com/abdelrahmannasr/wa-cloud-sdk#readme","bugs":{"url":"https://github.com/abdelrahmannasr/wa-cloud-sdk/issues"},"dist":{"shasum":"077946f9ba0d79e5523fc78f907e07ceee948ee8","tarball":"https://registry.npmjs.org/@abdelrahmannasr/wa-cloud-api/-/wa-cloud-api-1.0.0.tgz","fileCount":76,"integrity":"sha512-H+GZl4GPQw+NGupQM4USg9ZzQHPOS4GnpUMxgnTHn5+d9VWc8+b+76+Mj3rFcAn8ZfxiXydqA2zjJZD/gZUwMA==","signatures":[{"sig":"MEYCIQDEA4ebsIKEUx3oSvZWXVq2Dn029gE0FqI0TIjBDTi40wIhALA07oSM7g8hfw+Ljk4G8REXC9qOxT/bha+fo/v2Olif","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":2942095},"main":"./dist/index.cjs","pnpm":{"onlyBuiltDependencies":["esbuild"]},"type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18.0.0"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./flows":{"import":{"types":"./dist/flows/index.d.ts","default":"./dist/flows/index.js"},"require":{"types":"./dist/flows/index.d.cts","default":"./dist/flows/index.cjs"}},"./media":{"import":{"types":"./dist/media/index.d.ts","default":"./dist/media/index.js"},"require":{"types":"./dist/media/index.d.cts","default":"./dist/media/index.cjs"}},"./errors":{"import":{"types":"./dist/errors/index.d.ts","default":"./dist/errors/index.js"},"require":{"types":"./dist/errors/index.d.cts","default":"./dist/errors/index.cjs"}},"./catalog":{"import":{"types":"./dist/catalog/index.d.ts","default":"./dist/catalog/index.js"},"require":{"types":"./dist/catalog/index.d.cts","default":"./dist/catalog/index.cjs"}},"./messages":{"import":{"types":"./dist/messages/index.d.ts","default":"./dist/messages/index.js"},"require":{"types":"./dist/messages/index.d.cts","default":"./dist/messages/index.cjs"}},"./webhooks":{"import":{"types":"./dist/webhooks/index.d.ts","default":"./dist/webhooks/index.js"},"require":{"types":"./dist/webhooks/index.d.cts","default":"./dist/webhooks/index.cjs"}},"./templates":{"import":{"types":"./dist/templates/index.d.ts","default":"./dist/templates/index.js"},"require":{"types":"./dist/templates/index.d.cts","default":"./dist/templates/index.cjs"}},"./multi-account":{"import":{"types":"./dist/multi-account/index.d.ts","default":"./dist/multi-account/index.js"},"require":{"types":"./dist/multi-account/index.d.cts","default":"./dist/multi-account/index.cjs"}},"./phone-numbers":{"import":{"types":"./dist/phone-numbers/index.d.ts","default":"./dist/phone-numbers/index.js"},"require":{"types":"./dist/phone-numbers/index.d.cts","default":"./dist/phone-numbers/index.cjs"}}},"gitHead":"a25d369dc996000efe29c8e5604b2ef03ce141c4","scripts":{"lint":"eslint src/ tests/","test":"vitest run","build":"tsup","clean":"rm -rf dist","format":"prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"","lint:fix":"eslint src/ tests/ --fix","typecheck":"tsc --noEmit","test:watch":"vitest","format:check":"prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"","test:coverage":"vitest run --coverage","prepublishOnly":"pnpm run typecheck && pnpm run build && pnpm run test"},"_npmUser":{"name":"abdelrahmannasr","email":"a.nasr.yocto@gmail.com"},"deprecated":"use wa-cloud-sdk instead https://www.npmjs.com/package/wa-cloud-sdk","repository":{"url":"git+https://github.com/abdelrahmannasr/wa-cloud-sdk.git","type":"git"},"_npmVersion":"10.8.2","description":"Zero-dependency, type-safe TypeScript SDK for the Meta WhatsApp Cloud API","directories":{},"_nodeVersion":"20.19.5","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"packageManager":"pnpm@10.23.0","devDependencies":{"tsup":"^8.0.0","eslint":"^9.0.0","vitest":"^3.0.0","prettier":"^3.8.3","@eslint/js":"^9.39.4","typescript":"^5.3.0","@types/node":"^20.19.39","semantic-release":"^25.0.3","typescript-eslint":"^8.59.1","@vitest/coverage-v8":"^3.0.0","@semantic-release/git":"^10.0.1","eslint-config-prettier":"^10.0.0","@typescript-eslint/parser":"^8.59.1","@semantic-release/changelog":"^6.0.3","@typescript-eslint/eslint-plugin":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/wa-cloud-api_1.0.0_1777476908518_0.7631266222580575","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2026-04-29T15:35:08.415Z","modified":"2026-06-13T08:11:50.281Z","1.0.0":"2026-04-29T15:35:08.759Z"},"bugs":{"url":"https://github.com/abdelrahmannasr/wa-cloud-sdk/issues"},"author":{"name":"AbdelRahman Nasr"},"license":"MIT","homepage":"https://github.com/abdelrahmannasr/wa-cloud-sdk#readme","keywords":["whatsapp","cloud-api","sdk","typescript","meta","messaging","webhooks","templates"],"repository":{"url":"git+https://github.com/abdelrahmannasr/wa-cloud-sdk.git","type":"git"},"description":"Zero-dependency, type-safe TypeScript SDK for the Meta WhatsApp Cloud API","maintainers":[{"name":"abdelrahmannasr","email":"a.nasr.yocto@gmail.com"}],"readme":"# @abdelrahmannasr/wa-cloud-api\n\n[![npm version](https://img.shields.io/npm/v/@abdelrahmannasr/wa-cloud-api.svg)](https://www.npmjs.com/package/@abdelrahmannasr/wa-cloud-api)\n[![CI](https://img.shields.io/github/actions/workflow/status/abdelrahmannasr/wa-cloud-sdk/ci.yml?branch=main&label=CI)](https://github.com/abdelrahmannasr/wa-cloud-sdk/actions/workflows/ci.yml)\n[![codecov](https://img.shields.io/codecov/c/github/abdelrahmannasr/wa-cloud-sdk)](https://codecov.io/gh/abdelrahmannasr/wa-cloud-sdk)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Node.js >= 18](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org/)\n[![TypeScript 5.x](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org/)\n[![npm downloads](https://img.shields.io/npm/dm/@abdelrahmannasr/wa-cloud-api.svg)](https://www.npmjs.com/package/@abdelrahmannasr/wa-cloud-api)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](./CONTRIBUTING.md)\n[![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release)\n\n## Overview\n\nA comprehensive, zero-dependency, type-safe TypeScript SDK for the Meta WhatsApp Cloud API. Built for Node.js 18+, this SDK provides a unified client interface for sending messages, managing media, creating templates, handling webhooks, managing phone numbers, and coordinating multiple WhatsApp Business Accounts (WABAs). Perfect for building WhatsApp integrations, chatbots, and campaign systems.\n\n## Features\n\n- **Messages** — Send text, media (images, videos, audio, documents, stickers), locations, contacts, reactions, interactive buttons/lists, and templates\n- **Media** — Upload, download, retrieve URLs, and delete media assets with client-side validation\n- **Templates** — Create, list, update, and delete message templates with a fluent TemplateBuilder API\n- **Flows** — Create, publish, update, deprecate, and delete WhatsApp Flows; send flow messages; receive flow completions as typed events\n- **Webhooks** — Parse incoming events (including flow completions), verify signatures, and integrate with Express or Next.js App Router\n- **Phone Numbers** — List, manage business profiles, request verification codes, and register/deregister numbers\n- **Multi-Account** — Manage multiple WABAs with distribution strategies (round-robin, weighted, sticky), broadcast messaging with concurrency control, and dynamic account management\n\n## Installation\n\n```bash\n# npm\nnpm install @abdelrahmannasr/wa-cloud-api\n\n# pnpm\npnpm add @abdelrahmannasr/wa-cloud-api\n\n# yarn\nyarn add @abdelrahmannasr/wa-cloud-api\n```\n\n## Quick Start\n\n```typescript\nimport { WhatsApp } from '@abdelrahmannasr/wa-cloud-api';\n\n// Initialize the client\nconst wa = new WhatsApp({\n  accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,\n  phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,\n});\n\n// Send a text message\ntry {\n  const result = await wa.messages.sendText({\n    to: '1234567890',\n    body: 'Hello from WhatsApp Cloud API!',\n  });\n\n  console.log('Message sent:', result.data.messages[0].id);\n} catch (error) {\n  // See Error Handling section for typed error handling patterns\n  console.error('Failed to send message:', error);\n}\n\n// Clean up\nwa.destroy();\n```\n\n## Configuration\n\nThe `WhatsApp` client accepts a configuration object with the following options:\n\n| Option                       | Type      | Required | Default                        | Description                                           |\n| ---------------------------- | --------- | -------- | ------------------------------ | ----------------------------------------------------- |\n| `accessToken`                | `string`  | Yes      | —                              | Meta access token for authentication                  |\n| `phoneNumberId`              | `string`  | Yes      | —                              | WhatsApp phone number ID                              |\n| `businessAccountId`          | `string`  | No       | —                              | WhatsApp Business Account ID (required for templates) |\n| `apiVersion`                 | `string`  | No       | `'v21.0'`                      | Graph API version                                     |\n| `baseUrl`                    | `string`  | No       | `'https://graph.facebook.com'` | API base URL                                          |\n| `logger`                     | `Logger`  | No       | —                              | Custom logger instance                                |\n| `rateLimitConfig.maxTokens`  | `number`  | No       | `80`                           | Max tokens in bucket                                  |\n| `rateLimitConfig.refillRate` | `number`  | No       | `80`                           | Tokens refilled per second                            |\n| `rateLimitConfig.enabled`    | `boolean` | No       | `true`                         | Enable/disable rate limiting                          |\n| `retryConfig.maxRetries`     | `number`  | No       | `3`                            | Maximum retry attempts                                |\n| `retryConfig.baseDelayMs`    | `number`  | No       | `1000`                         | Base delay between retries (ms)                       |\n| `retryConfig.maxDelayMs`     | `number`  | No       | `30000`                        | Maximum delay cap (ms)                                |\n| `retryConfig.jitterFactor`   | `number`  | No       | `0.2`                          | Jitter randomization factor (0-1)                     |\n| `timeoutMs`                  | `number`  | No       | `30000`                        | Request timeout in milliseconds                       |\n| `appSecret`                  | `string`  | No       | —                              | App secret for webhook signature verification         |\n| `webhookVerifyToken`         | `string`  | No       | —                              | Webhook verify token                                  |\n\n## Messages\n\nSend various types of messages using the `messages` module:\n\n```typescript\n// Text message\nawait wa.messages.sendText({\n  to: '1234567890',\n  body: 'Hello! 👋',\n  previewUrl: true, // Enable URL preview\n});\n\n// Image message\nawait wa.messages.sendImage({\n  to: '1234567890',\n  media: { link: 'https://example.com/image.jpg' },\n  caption: 'Check out this image!',\n});\n\n// Template message\nawait wa.messages.sendTemplate({\n  to: '1234567890',\n  templateName: 'hello_world',\n  language: 'en_US',\n});\n\n// Interactive buttons\nawait wa.messages.sendInteractiveButtons({\n  to: '1234567890',\n  body: 'Choose an option:',\n  buttons: [\n    { type: 'reply', reply: { id: 'btn1', title: 'Option 1' } },\n    { type: 'reply', reply: { id: 'btn2', title: 'Option 2' } },\n  ],\n});\n\n// Mark message as read\nawait wa.messages.markAsRead({\n  messageId: 'wamid.ABC123...',\n});\n```\n\nAdditional message types:\n\n```typescript\n// Video message\nawait wa.messages.sendVideo({\n  to: '1234567890',\n  media: { id: 'media_id_123' },\n  caption: 'Watch this video',\n});\n\n// Audio message\nawait wa.messages.sendAudio({\n  to: '1234567890',\n  media: { link: 'https://example.com/audio.mp3' },\n});\n\n// Document message\nawait wa.messages.sendDocument({\n  to: '1234567890',\n  media: { link: 'https://example.com/report.pdf' },\n  filename: 'report.pdf',\n  caption: 'Monthly report',\n});\n\n// Sticker message\nawait wa.messages.sendSticker({\n  to: '1234567890',\n  media: { id: 'sticker_media_id' },\n});\n\n// Location message\nawait wa.messages.sendLocation({\n  to: '1234567890',\n  longitude: -122.4194,\n  latitude: 37.7749,\n  name: 'San Francisco',\n  address: 'San Francisco, CA',\n});\n\n// Contacts message\nawait wa.messages.sendContacts({\n  to: '1234567890',\n  contacts: [\n    {\n      name: { formatted_name: 'Jane Doe' },\n      phones: [{ phone: '+0987654321', type: 'WORK' }],\n    },\n  ],\n});\n\n// Reaction (emoji react to an existing message)\nawait wa.messages.sendReaction({\n  to: '1234567890',\n  messageId: 'wamid.ABC123...',\n  emoji: '\\u{1F44D}', // Use empty string to remove reaction\n});\n\n// Interactive list message\nawait wa.messages.sendInteractiveList({\n  to: '1234567890',\n  body: 'Select a product:',\n  buttonText: 'View options',\n  sections: [\n    {\n      title: 'Products',\n      rows: [\n        { id: 'p1', title: 'Widget', description: 'A useful widget' },\n        { id: 'p2', title: 'Gadget', description: 'A fancy gadget' },\n      ],\n    },\n  ],\n  header: 'Our Catalog',\n  footer: 'Reply STOP to opt out',\n});\n```\n\n## Media\n\nUpload, download, and manage media files:\n\n```typescript\nimport { readFileSync } from 'fs';\n\n// Upload media\nconst uploadResult = await wa.media.upload({\n  file: readFileSync('./image.jpg'),\n  mimeType: 'image/jpeg',\n  category: 'image',\n  filename: 'image.jpg',\n});\n\nconst mediaId = uploadResult.data.id;\n\n// Get media URL\nconst urlResult = await wa.media.getUrl(mediaId);\nconsole.log('Download URL:', urlResult.data.url);\n\n// Download media\nconst downloadResult = await wa.media.download(urlResult.data.url);\nconst buffer = Buffer.from(downloadResult.data);\n\n// Delete media\nawait wa.media.delete(mediaId);\n```\n\n### Supported Media Types\n\nThe SDK validates MIME types and file sizes client-side before uploading:\n\n| Category             | MIME Types                                                                   | Max Size |\n| -------------------- | ---------------------------------------------------------------------------- | -------- |\n| `image`              | `image/jpeg`, `image/png`                                                    | 5 MB     |\n| `video`              | `video/mp4`, `video/3gpp`                                                    | 16 MB    |\n| `audio`              | `audio/aac`, `audio/mp4`, `audio/mpeg`, `audio/amr`, `audio/ogg`             | 16 MB    |\n| `document`           | `application/pdf`, `text/plain`, `text/csv`, MS Office, OpenDocument formats | 100 MB   |\n| `sticker` (static)   | `image/webp`                                                                 | 500 KB   |\n| `sticker` (animated) | `image/webp`                                                                 | 1 MB     |\n\n```typescript\n// Upload a sticker (stickerType is required for stickers)\nawait wa.media.upload({\n  file: stickerBuffer,\n  mimeType: 'image/webp',\n  category: 'sticker',\n  stickerType: 'static', // or 'animated'\n});\n```\n\n## Templates\n\nCreate and manage message templates:\n\n```typescript\nimport { TemplateBuilder } from '@abdelrahmannasr/wa-cloud-api';\n\n// List templates\nconst templates = await wa.templates.list({ limit: 10 });\n\n// Get specific template\nconst template = await wa.templates.get('hello_world');\n\n// Create template with TemplateBuilder\nconst newTemplate = new TemplateBuilder()\n  .setName('order_confirmation')\n  .setLanguage('en_US')\n  .setCategory('UTILITY')\n  .addBody('Your order {{1}} has been confirmed. Total: {{2}}.')\n  .addQuickReplyButton('Track Order')\n  .build();\n\nawait wa.templates.create(newTemplate);\n\n// Full template with all builder options\nconst promoTemplate = new TemplateBuilder()\n  .setName('summer_sale')\n  .setLanguage('en_US')\n  .setCategory('MARKETING')\n  .allowCategoryChange(true)\n  .addHeaderText('Summer Sale!')\n  .addBody('Hi {{1}}, enjoy {{2}}% off all items this week.')\n  .addFooter('Terms and conditions apply')\n  .addQuickReplyButton('Shop Now')\n  .addQuickReplyButton('Not Interested')\n  .addUrlButton('View Catalog', 'https://example.com/catalog/{{1}}')\n  .addPhoneNumberButton('Call Support', '+1234567890')\n  .build();\n\nawait wa.templates.create(promoTemplate);\n\n// Update template components\nawait wa.templates.update('template_id', [\n  { type: 'BODY', text: 'Updated: Your order {{1}} has been confirmed.' },\n]);\n\n// Delete template\nawait wa.templates.delete('old_template');\n```\n\n**TemplateBuilder methods:** `setName()`, `setLanguage()`, `setCategory()`, `allowCategoryChange()`, `addHeaderText()`, `addHeaderMedia(format, example?)`, `addBody(text, example?)`, `addFooter()`, `addQuickReplyButton()` (max 3), `addUrlButton()` (max 2), `addPhoneNumberButton()` (max 1).\n\n## Flows\n\nCreate and manage WhatsApp Flows for interactive forms, surveys, and guided journeys:\n\n### Send a Flow\n\n```typescript\n// Send a published flow to a user\nawait wa.messages.sendFlow({\n  to: '1234567890',\n  body: 'Please complete your appointment booking.',\n  flowCta: 'Book Now',\n  flowId: '9876543210',\n});\n\n// Test a draft flow before publishing\nawait wa.messages.sendFlow({\n  to: '1234567890',\n  body: 'Preview the onboarding flow',\n  flowCta: 'Start',\n  flowId: '9876543210',\n  mode: 'draft',\n});\n\n// Pre-populate initial screen data\nawait wa.messages.sendFlow({\n  to: '1234567890',\n  body: 'Review your profile',\n  flowCta: 'Continue',\n  flowId: '9876543210',\n  flowActionPayload: {\n    screen: 'EDIT_PROFILE',\n    data: { name: 'Alice', email: 'alice@example.com' },\n  },\n});\n```\n\n### Receive Flow Completions\n\nFlow completions arrive as a dedicated `FlowCompletionEvent` via the `onFlowCompletion` callback (NOT via `onMessage`):\n\n```typescript\nconst handler = wa.webhooks.createHandler({\n  onMessage: async (event) => {\n    // Text, images, button/list replies — unchanged\n  },\n  onFlowCompletion: async (event) => {\n    // Deduplicate (Meta retries on errors)\n    if (await db.isProcessed(event.messageId)) return;\n    await db.markProcessed(event.messageId);\n\n    // event.response is the parsed form data (or {} if malformed)\n    // event.responseJson is the raw string, preserved exactly\n    await saveSubmission(event.contact.waId, event.response);\n  },\n});\n```\n\n### Flow Lifecycle (CRUD)\n\nRequires `businessAccountId` in the client config:\n\n```typescript\nconst wa = new WhatsApp({\n  accessToken: '...',\n  phoneNumberId: '...',\n  businessAccountId: 'YOUR_WABA_ID',\n});\n\n// Create a flow\nconst created = await wa.flows.create({\n  name: 'customer_onboarding',\n  categories: ['SIGN_UP'],\n});\n\n// Upload flow JSON (accepts string or object — SDK stringifies objects)\nawait wa.flows.updateAssets(created.data.id, {\n  flow_json: { version: '3.0', screens: [/* ... */] },\n});\n\n// Publish\nawait wa.flows.publish(created.data.id);\n\n// List, get, update metadata, preview, deprecate, delete\nconst list = await wa.flows.list({ limit: 10 });\nconst flow = await wa.flows.get('flow_id');\nawait wa.flows.updateMetadata('flow_id', { name: 'new_name' });\nconst preview = await wa.flows.getPreview('flow_id');\nawait wa.flows.deprecate('flow_id');\nawait wa.flows.delete('draft_flow_id'); // Only draft flows can be deleted\n```\n\n### Multi-Account Broadcast with Flows\n\nFlow IDs are scoped to a single WABA. When broadcasting across accounts, maintain a per-account flow ID mapping:\n\n```typescript\nconst flowIdByAccount = {\n  us: 'flow_id_in_us_account',\n  eu: 'flow_id_in_eu_account',\n};\n\nconst result = await manager.broadcast(\n  ['15551234567', '442071234567'],\n  (account, recipient) => account.messages.sendFlow({\n    to: recipient,\n    body: 'Complete your registration',\n    flowCta: 'Get Started',\n    flowId: flowIdByAccount[account.name as keyof typeof flowIdByAccount],\n  }),\n);\n```\n\n**Flows methods:** `list()`, `get()`, `create()`, `updateMetadata()`, `updateAssets()`, `publish()`, `deprecate()`, `delete()`, `getPreview()`.\n\n## Commerce & Catalogs\n\nSend product messages, receive order notifications, and manage your product catalog programmatically. All catalog operations require `businessAccountId` in the client config.\n\n### Send a Single Product Message\n\n```typescript\n// Display one product card to a recipient\nawait wa.messages.sendProduct({\n  to: '1234567890',\n  catalogId: 'CATALOG_ID',\n  productRetailerId: 'SKU-001',\n  body: 'Check out this item — just restocked!',\n  footer: 'Limited stock',\n});\n```\n\n### Send a Multi-Product List\n\nUp to 30 products across up to 10 named sections (validated client-side before the API call):\n\n```typescript\nawait wa.messages.sendProductList({\n  to: '1234567890',\n  catalogId: 'CATALOG_ID',\n  header: \"Today's Specials\",\n  body: \"Here's our curated selection.\",\n  sections: [\n    {\n      title: 'Beverages',\n      productRetailerIds: ['cola-001', 'juice-002'],\n    },\n    {\n      title: 'Snacks',\n      productRetailerIds: ['chips-001', 'nuts-002'],\n    },\n  ],\n});\n```\n\n### Send a Catalog Message\n\nInvite the recipient to browse your entire catalog. Optionally pin a featured product as a thumbnail:\n\n```typescript\nawait wa.messages.sendCatalogMessage({\n  to: '1234567890',\n  body: 'Browse our full collection.',\n  footer: 'Free shipping on orders over $50',\n  thumbnailProductRetailerId: 'featured-item-001', // optional\n});\n```\n\n### Receive Order Notifications\n\nWhen a recipient submits a cart, the SDK fires a dedicated `OrderEvent` via `onOrder`. This event is **never** delivered to the generic `onMessage` callback:\n\n```typescript\nwa.webhooks.onOrder(async (event) => {\n  // Deduplicate using the stable platform message ID\n  if (await db.isProcessed(event.messageId)) return;\n  await db.markProcessed(event.messageId);\n\n  console.log(`Order from ${event.from}: ${event.items.length} item(s)`);\n  for (const item of event.items) {\n    console.log(`  ${item.product_retailer_id} × ${item.quantity} @ ${item.item_price} ${item.currency}`);\n  }\n\n  // Reply to confirm\n  await wa.messages.sendText({\n    to: event.from,\n    body: `Thanks! We received your order and will process it shortly.`,\n  });\n});\n```\n\n`event.raw` preserves the original JSON-stringified payload for storage or auditing. If `product_items` is malformed, `event.items` is `[]` and `event.raw` is still preserved.\n\n### Template lifecycle events\n\nReact to template approvals, rejections, and quality changes without polling the templates API. Events arrive on the same webhook URL, routed automatically by the SDK:\n\n```typescript\nwa.webhooks\n  .onTemplateStatus(async (event) => {\n    if (event.status === 'APPROVED') {\n      await db.markTemplateLive(event.templateId);\n    } else if (event.status === 'REJECTED') {\n      await alerts.notify({\n        template: event.templateName,\n        language: event.language,\n        reason: event.reason ?? 'no reason provided',\n      });\n    }\n  })\n  .onTemplateQuality(async (event) => {\n    // previousScore is undefined for first-time ratings\n    if (event.newScore === 'RED') {\n      await throttle.pauseCampaign(event.templateId);\n    }\n  });\n```\n\nTemplate events use a WABA-scoped `metadata.businessAccountId` (sourced from `entry.id`) instead of `phoneNumberId`. The `status` and `newScore` fields preserve unknown platform-added values verbatim so your code doesn't break when Meta adds new states. Requires subscribing to `message_template_status_update` and `message_template_quality_update` in the Meta App Dashboard.\n\n### Catalog Management\n\nCreate, update, and delete products from code. Requires `businessAccountId`:\n\n```typescript\n// List catalogs connected to the WABA\nconst { data } = await wa.catalog.listCatalogs();\n\n// Create a product (strict — throws ConflictError on duplicate retailer_id)\ntry {\n  await wa.catalog.createProduct('CATALOG_ID', {\n    retailer_id: 'SKU-001',\n    name: 'Wireless Headphones',\n    image_url: 'https://example.com/sku-001.jpg',\n    price: 4999,       // integer minor units ($49.99)\n    currency: 'USD',\n    availability: 'in stock',\n  });\n} catch (err) {\n  if (err instanceof ConflictError) {\n    // Fall back to upsert (create-or-update)\n    await wa.catalog.upsertProduct('CATALOG_ID', { /* same payload */ });\n  }\n}\n\n// Partial update\nawait wa.catalog.updateProduct('PRODUCT_ID', { price: 3999 });\n\n// Delete\nawait wa.catalog.deleteProduct('PRODUCT_ID');\n```\n\n**Catalog methods:** `listCatalogs()`, `getCatalog()`, `listProducts()`, `getProduct()`, `createProduct()`, `upsertProduct()`, `updateProduct()`, `deleteProduct()`.\n\n### Limitations\n\n- **No bulk product mutations** — single-product CRUD only; compose multiple calls for bulk sync.\n- **No product image hosting** — `image_url` must be a publicly accessible HTTPS URL hosted by the consumer (own CDN, S3, Cloudflare R2, etc.).\n- **No order acknowledgement messages** — reply to orders with any existing message type (e.g., `sendText`); dedicated `sendOrderStatusMessage` is out of scope for v0.4.0.\n- **No lookup by `retailer_id`** — use `listProducts` with a filter to find a product by its retailer ID, or use `upsertProduct` for create-or-update semantics.\n- **Catalog IDs are WABA-scoped** — when broadcasting across multiple accounts in different WABAs, maintain a per-account catalog ID mapping.\n\n## Webhooks\n\nHandle incoming webhook events from WhatsApp:\n\n**Express.js:**\n\n```typescript\nimport express from 'express';\nimport { createExpressMiddleware } from '@abdelrahmannasr/wa-cloud-api/webhooks';\n\nconst app = express();\n\napp.use(\n  '/webhook',\n  createExpressMiddleware(\n    {\n      appSecret: process.env.WHATSAPP_APP_SECRET!,\n      verifyToken: process.env.WHATSAPP_VERIFY_TOKEN!,\n    },\n    {\n      onMessage: (event) => {\n        console.log('Message received:', event.message.text?.body);\n      },\n      onStatus: (event) => {\n        console.log('Status update:', event.status.status);\n      },\n      onError: (event) => {\n        console.error('Error:', event.error);\n      },\n    },\n  ),\n);\n\napp.listen(3000);\n```\n\n**Next.js App Router:**\n\n```typescript\n// app/api/webhook/route.ts\nimport { createNextRouteHandler } from '@abdelrahmannasr/wa-cloud-api/webhooks';\n\nconst { GET, POST } = createNextRouteHandler(\n  {\n    appSecret: process.env.WHATSAPP_APP_SECRET!,\n    verifyToken: process.env.WHATSAPP_VERIFY_TOKEN!,\n  },\n  {\n    onMessage: (event) => {\n      console.log('Message received:', event.message.text?.body);\n    },\n    onStatus: (event) => {\n      console.log('Status update:', event.status.status);\n    },\n  },\n);\n\nexport { GET, POST };\n```\n\n**Unified Client:**\n\nWhen using the `WhatsApp` client, webhooks are available via `wa.webhooks`:\n\n```typescript\nconst wa = new WhatsApp({\n  accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,\n  phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,\n  appSecret: process.env.WHATSAPP_APP_SECRET!,\n  webhookVerifyToken: process.env.WHATSAPP_VERIFY_TOKEN!,\n});\n\n// Parse webhook payload into typed events\nconst events = wa.webhooks.parse(webhookPayload);\nfor (const event of events) {\n  if (event.type === 'message') {\n    console.log('Message from:', event.message.from);\n  }\n}\n\n// Verify webhook subscription (GET endpoint)\nconst challenge = wa.webhooks.verify(queryParams);\n\n// Verify payload signature (POST endpoint)\nwa.webhooks.verifySignature(rawBody, signatureHeader);\n\n// Or create middleware directly from the unified client\napp.use(\n  '/webhook',\n  wa.webhooks.createExpressMiddleware({\n    onMessage: (event) => console.log('Message:', event.message.text?.body),\n  }),\n);\n```\n\n## Phone Numbers\n\nManage phone numbers and business profiles:\n\n```typescript\n// List phone numbers\nconst numbers = await wa.phoneNumbers.list();\n\n// Get phone number details\nconst number = await wa.phoneNumbers.get('phone_number_id');\n\n// Get business profile\nconst profile = await wa.phoneNumbers.getBusinessProfile('phone_number_id');\n\n// Update business profile\nawait wa.phoneNumbers.updateBusinessProfile('phone_number_id', {\n  description: 'Your trusted business partner',\n  websites: ['https://example.com'],\n});\n\n// Request verification code\nawait wa.phoneNumbers.requestVerificationCode('phone_number_id', {\n  code_method: 'SMS',\n  language: 'en',\n});\n\n// Verify code\nawait wa.phoneNumbers.verifyCode('phone_number_id', { code: '123456' });\n\n// Register a verified phone number\nawait wa.phoneNumbers.register('phone_number_id', {\n  pin: '123456', // Two-step verification PIN\n});\n\n// Deregister a phone number\nawait wa.phoneNumbers.deregister('phone_number_id');\n```\n\n## Multi-Account\n\nManage multiple WhatsApp Business Accounts:\n\n```typescript\nimport { WhatsAppMultiAccount } from '@abdelrahmannasr/wa-cloud-api';\n\nconst multiAccount = new WhatsAppMultiAccount({\n  // Shared base config\n  retryConfig: { maxRetries: 3 },\n\n  // Per-account configurations\n  accounts: [\n    {\n      name: 'account1',\n      accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,\n      phoneNumberId: process.env.ACCOUNT1_PHONE_NUMBER_ID!,\n    },\n    {\n      name: 'account2',\n      accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,\n      phoneNumberId: process.env.ACCOUNT2_PHONE_NUMBER_ID!,\n    },\n  ],\n});\n\n// Send via specific account\nconst client1 = multiAccount.get('account1');\nawait client1.messages.sendText({\n  to: '1234567890',\n  body: 'Hello from account 1!',\n});\n\n// Lookup by phone number ID\nconst client2 = multiAccount.get(process.env.ACCOUNT2_PHONE_NUMBER_ID!);\n\n// Clean up all accounts\nmultiAccount.destroy();\n```\n\n### Dynamic Account Management\n\nAdd, remove, and query accounts at runtime:\n\n```typescript\n// Check if an account exists (by name or phone number ID)\nif (!manager.has('marketing')) {\n  // Add a new account dynamically\n  manager.addAccount({\n    name: 'marketing',\n    accessToken: 'TOKEN_M',\n    phoneNumberId: 'PHONE_M',\n    businessAccountId: 'WABA_M',\n  });\n}\n\n// List all registered accounts\nconst accounts = manager.getAccounts();\nfor (const [name, config] of accounts) {\n  console.log(`${name}: ${config.phoneNumberId}`);\n}\n\n// Remove an account (destroys its client if created)\nmanager.removeAccount('marketing');\n```\n\n### Distribution Strategies\n\nAutomatically distribute sends across accounts using built-in strategies:\n\n```typescript\nimport { WhatsAppMultiAccount, RoundRobinStrategy } from '@abdelrahmannasr/wa-cloud-api';\n\nconst manager = new WhatsAppMultiAccount({\n  strategy: new RoundRobinStrategy(),\n  accounts: [\n    { name: 'account-a', accessToken: 'TOKEN_A', phoneNumberId: 'PHONE_A' },\n    { name: 'account-b', accessToken: 'TOKEN_B', phoneNumberId: 'PHONE_B' },\n    { name: 'account-c', accessToken: 'TOKEN_C', phoneNumberId: 'PHONE_C' },\n  ],\n});\n\n// Each call cycles: A → B → C → A → B → ...\nconst wa = manager.getNext();\nawait wa.messages.sendText({ to: '1234567890', body: 'Hello!' });\n```\n\n### Weighted Distribution\n\nRoute traffic proportionally based on per-account weights:\n\n```typescript\nimport { WhatsAppMultiAccount, WeightedStrategy } from '@abdelrahmannasr/wa-cloud-api';\n\nconst manager = new WhatsAppMultiAccount({\n  strategy: new WeightedStrategy(\n    new Map([\n      ['enterprise', 80], // gets ~80% of traffic\n      ['business-1', 10], // gets ~10% of traffic\n      ['business-2', 10], // gets ~10% of traffic\n    ]),\n  ),\n  accounts: [\n    { name: 'enterprise', accessToken: 'TOKEN_E', phoneNumberId: 'PHONE_E' },\n    { name: 'business-1', accessToken: 'TOKEN_1', phoneNumberId: 'PHONE_1' },\n    { name: 'business-2', accessToken: 'TOKEN_2', phoneNumberId: 'PHONE_2' },\n  ],\n});\n\nconst wa = manager.getNext();\nawait wa.messages.sendText({ to: '1234567890', body: 'Hello!' });\n```\n\n### Sticky Routing\n\nEnsure the same recipient always routes to the same account for conversation continuity:\n\n```typescript\nimport { WhatsAppMultiAccount, StickyStrategy } from '@abdelrahmannasr/wa-cloud-api';\n\nconst manager = new WhatsAppMultiAccount({\n  strategy: new StickyStrategy(),\n  accounts: [\n    { name: 'account-a', accessToken: 'TOKEN_A', phoneNumberId: 'PHONE_A' },\n    { name: 'account-b', accessToken: 'TOKEN_B', phoneNumberId: 'PHONE_B' },\n  ],\n});\n\n// Same recipient always routes to the same account\nconst wa = manager.getNext('1234567890');\nawait wa.messages.sendText({ to: '1234567890', body: 'Hello!' });\n```\n\n> **Stickiness is not stable across account-set mutations.** `StickyStrategy`\n> uses a simple `hash(recipient) % accountNames.length` mapping, so calling\n> `addAccount()` or `removeAccount()` shifts the modulo result and reroutes\n> most recipients to a different account. If your deployment adds or removes\n> accounts while conversations are in flight and you need routing to survive\n> those changes, implement a custom `DistributionStrategy` using rendezvous\n> (HRW) or consistent hashing — they rebind only ~1/N of recipients on\n> mutation.\n\n### Broadcast\n\nSend a message to many recipients in parallel, distributed across accounts:\n\n```typescript\nconst recipients = ['1111111111', '2222222222', '3333333333', '4444444444'];\n\nconst result = await manager.broadcast(\n  recipients,\n  async (wa, to) => wa.messages.sendText({ to, body: 'Campaign message!' }),\n  { concurrency: 10 }, // limit to 10 concurrent sends\n);\n\nconsole.log(`Sent: ${result.successes.length}, Failed: ${result.failures.length}`);\n\nfor (const failure of result.failures) {\n  console.error(`Failed to send to ${failure.recipient}:`, failure.error);\n}\n```\n\n### Custom Strategy\n\nImplement the `DistributionStrategy` interface for custom routing logic:\n\n```typescript\nimport type { DistributionStrategy } from '@abdelrahmannasr/wa-cloud-api';\n\nclass PriorityStrategy implements DistributionStrategy {\n  select(accountNames: readonly string[], _recipient?: string): string {\n    return accountNames[0]; // Always prefer the first account\n  }\n}\n\nconst manager = new WhatsAppMultiAccount({\n  strategy: new PriorityStrategy(),\n  accounts: [\n    { name: 'primary', accessToken: 'TOKEN_P', phoneNumberId: 'PHONE_P' },\n    { name: 'fallback', accessToken: 'TOKEN_F', phoneNumberId: 'PHONE_F' },\n  ],\n});\n```\n\n## Examples\n\nComplete, runnable examples are available in the [`examples/`](./examples) directory. Each example demonstrates a specific feature with inline documentation and environment variable setup:\n\n- **[send-text.ts](./examples/send-text.ts)** — Send a simple text message with typed error handling (ApiError, RateLimitError)\n- **[media-upload.ts](./examples/media-upload.ts)** — Upload a file from disk, send as image message, handle MediaError\n- **[templates.ts](./examples/templates.ts)** — List existing templates, create new template with TemplateBuilder, send template message\n- **[webhooks-express.ts](./examples/webhooks-express.ts)** — Complete Express server with webhook middleware (GET verification, POST event handling)\n- **[webhooks-nextjs.ts](./examples/webhooks-nextjs.ts)** — Next.js App Router webhook handler (app/api/webhook/route.ts structure)\n- **[phone-numbers.ts](./examples/phone-numbers.ts)** — List phone numbers, manage business profile, request/verify verification code\n- **[multi-account.ts](./examples/multi-account.ts)** — Manage multiple WABAs with distribution strategies (round-robin, weighted, sticky), broadcast messaging, and dynamic account management\n\nRun any example with:\n\n```bash\n# Set required environment variables\nexport WHATSAPP_ACCESS_TOKEN=\"your_access_token\"\nexport WHATSAPP_PHONE_NUMBER_ID=\"your_phone_number_id\"\n\n# Run with tsx\nnpx tsx examples/send-text.ts\n```\n\n## Error Handling\n\nThe SDK uses a typed error hierarchy for precise error handling. All errors extend `WhatsAppError` for unified catch blocks:\n\n### Error Class Hierarchy\n\n```\nWhatsAppError (base)\n├── ApiError (API response errors)\n│   ├── RateLimitError (429 Too Many Requests)\n│   └── AuthenticationError (401 Unauthorized)\n├── NotFoundError (semantic \"resource missing\" — see note below)\n├── ValidationError (client-side validation)\n├── WebhookVerificationError (signature verification)\n└── MediaError (media upload/download)\n```\n\n> **Note on `NotFoundError`:** It deliberately does **not** extend `ApiError`.\n> Meta's API returns 200 with an empty `data` array for several \"missing\n> resource\" cases (e.g. `getBusinessProfile` when the profile is not\n> provisioned). The SDK surfaces those as `NotFoundError`, which a\n> `catch (err) { if (err instanceof ApiError && err.statusCode === 404) }`\n> branch will **not** catch. See the example under \"Error Handling Patterns\"\n> below for the recommended dual-catch shape.\n\n### Error Properties\n\n| Error Class                | Extends         | Properties                                                                                     |\n| -------------------------- | --------------- | ---------------------------------------------------------------------------------------------- |\n| `WhatsAppError`            | `Error`         | `code: string`                                                                                 |\n| `ApiError`                 | `WhatsAppError` | `statusCode: number`<br>`errorType: string`<br>`errorSubcode?: number`<br>`fbTraceId?: string` |\n| `RateLimitError`           | `ApiError`      | All ApiError properties<br>`retryAfterMs?: number`                                             |\n| `AuthenticationError`      | `ApiError`      | All ApiError properties                                                                        |\n| `NotFoundError`            | `WhatsAppError` | `resource?: string`                                                                            |\n| `ValidationError`          | `WhatsAppError` | `field?: string`                                                                               |\n| `WebhookVerificationError` | `WhatsAppError` | None                                                                                           |\n| `MediaError`               | `WhatsAppError` | `mediaType?: string`                                                                           |\n\n### Error Handling Patterns\n\n**1. Handling API errors with status code checks:**\n\n```typescript\nimport { ApiError, RateLimitError } from '@abdelrahmannasr/wa-cloud-api';\n\ntry {\n  await wa.messages.sendText({ to: '1234567890', body: 'Hello!' });\n} catch (error) {\n  if (error instanceof ApiError) {\n    console.error(`API Error ${error.statusCode}: ${error.message}`);\n    console.error(`Error type: ${error.errorType}`);\n    if (error.fbTraceId) {\n      console.error(`FB Trace ID: ${error.fbTraceId}`);\n    }\n  } else {\n    console.error('Unexpected error:', error);\n  }\n}\n```\n\n**2. Handling rate limits with retry delay:**\n\n```typescript\nimport { RateLimitError } from '@abdelrahmannasr/wa-cloud-api';\n\ntry {\n  await wa.messages.sendText({ to: '1234567890', body: 'Hello!' });\n} catch (error) {\n  if (error instanceof RateLimitError) {\n    const delayMs = error.retryAfterMs || 60000; // Default 60s if not provided\n    console.log(`Rate limited. Retry after ${delayMs}ms`);\n\n    // Wait and retry\n    await new Promise((resolve) => setTimeout(resolve, delayMs));\n    await wa.messages.sendText({ to: '1234567890', body: 'Hello!' });\n  } else {\n    throw error; // Re-throw if not a rate limit error\n  }\n}\n```\n\n**3. Handling validation errors with field identification:**\n\n```typescript\nimport { ValidationError } from '@abdelrahmannasr/wa-cloud-api';\n\ntry {\n  await wa.media.upload({\n    file: buffer,\n    mimeType: 'invalid/type', // Invalid MIME type\n    category: 'document',\n    filename: 'file.txt',\n  });\n} catch (error) {\n  if (error instanceof ValidationError) {\n    console.error(`Validation failed: ${error.message}`);\n    if (error.field) {\n      console.error(`Invalid field: ${error.field}`);\n    }\n  } else {\n    throw error;\n  }\n}\n```\n\n**4. Handling \"resource missing\" responses separately from 404s:**\n\n```typescript\nimport { ApiError, NotFoundError } from '@abdelrahmannasr/wa-cloud-api';\n\ntry {\n  const profile = await wa.phoneNumbers.getBusinessProfile(phoneNumberId);\n  console.log(profile.data.description);\n} catch (error) {\n  if (error instanceof NotFoundError) {\n    // Meta returned 200 with an empty data array — the resource simply\n    // isn't provisioned. `error.resource` names the specific resource.\n    console.log(`No ${error.resource} configured yet`);\n  } else if (error instanceof ApiError && error.statusCode === 404) {\n    // Explicit wire-level 404 from Meta — e.g. the phoneNumberId is bogus.\n    console.error('Unknown phone number ID');\n  } else {\n    throw error;\n  }\n}\n```\n\n## Advanced Usage\n\n### Direct Module Imports\n\nFor advanced use cases or tree-shaking, import individual modules via dedicated subpaths:\n\n```typescript\n// Import only what you need\nimport { Messages } from '@abdelrahmannasr/wa-cloud-api/messages';\nimport { Media, MEDIA_CONSTRAINTS } from '@abdelrahmannasr/wa-cloud-api/media';\nimport { Templates, TemplateBuilder } from '@abdelrahmannasr/wa-cloud-api/templates';\nimport { PhoneNumbers } from '@abdelrahmannasr/wa-cloud-api/phone-numbers';\nimport { WhatsAppMultiAccount, RoundRobinStrategy } from '@abdelrahmannasr/wa-cloud-api/multi-account';\nimport { Webhooks, createExpressMiddleware } from '@abdelrahmannasr/wa-cloud-api/webhooks';\nimport { WhatsAppError, ApiError } from '@abdelrahmannasr/wa-cloud-api/errors';\n```\n\n**Available subpath exports:**\n\n| Subpath | Primary Exports |\n|---------|----------------|\n| `@abdelrahmannasr/wa-cloud-api` | `WhatsApp` (unified client), all modules |\n| `@abdelrahmannasr/wa-cloud-api/messages` | `Messages`, all message type interfaces |\n| `@abdelrahmannasr/wa-cloud-api/media` | `Media`, `MEDIA_CONSTRAINTS`, media types |\n| `@abdelrahmannasr/wa-cloud-api/templates` | `Templates`, `TemplateBuilder`, validation constants |\n| `@abdelrahmannasr/wa-cloud-api/phone-numbers` | `PhoneNumbers`, business profile types |\n| `@abdelrahmannasr/wa-cloud-api/multi-account` | `WhatsAppMultiAccount`, distribution strategies |\n| `@abdelrahmannasr/wa-cloud-api/webhooks` | `Webhooks`, middleware factories, parser |\n| `@abdelrahmannasr/wa-cloud-api/errors` | `WhatsAppError`, `ApiError`, `RateLimitError`, etc. |\n\nAll subpaths support ESM (`import`), CommonJS (`require`), and include full TypeScript declarations.\n\n```typescript\n// Direct module usage with HttpClient\nimport { HttpClient } from '@abdelrahmannasr/wa-cloud-api';\nimport { Messages } from '@abdelrahmannasr/wa-cloud-api/messages';\n\nconst client = new HttpClient({\n  accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,\n  phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,\n});\n\nconst messages = new Messages(client, process.env.WHATSAPP_PHONE_NUMBER_ID!);\n\nawait messages.sendText({\n  to: '1234567890',\n  body: 'Hello!',\n});\n\nclient.destroy();\n```\n\n### Custom Rate Limiter\n\n```typescript\nconst wa = new WhatsApp({\n  accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,\n  phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,\n  rateLimitConfig: {\n    maxTokens: 1000, // Enterprise tier\n    refillRate: 1000,\n    enabled: true,\n  },\n});\n```\n\n### Custom Retry Configuration\n\n```typescript\nconst wa = new WhatsApp({\n  accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,\n  phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,\n  retryConfig: {\n    maxRetries: 5,\n    baseDelayMs: 2000,\n    maxDelayMs: 60000,\n    jitterFactor: 0.3,\n  },\n});\n```\n\n### Request Options\n\nPass custom options to individual requests:\n\n```typescript\nawait wa.messages.sendText(\n  {\n    to: '1234567890',\n    body: 'Hello!',\n  },\n  {\n    timeoutMs: 10000, // Override timeout for this request\n    skipRateLimit: false,\n    skipRetry: false,\n  },\n);\n```\n\n## ESM & CJS\n\nThis SDK supports both ESM and CommonJS:\n\n**ESM (recommended):**\n\n```typescript\nimport { WhatsApp } from '@abdelrahmannasr/wa-cloud-api';\n```\n\n**CommonJS:**\n\n```javascript\nconst { WhatsApp } = require('@abdelrahmannasr/wa-cloud-api');\n```\n\n## Contributing\n\nContributions are welcome. Before opening a PR, please read:\n\n- [CONTRIBUTING.md](./CONTRIBUTING.md) — quick-start, Conventional Commits conventions, development commands, and code style\n- [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) — behavioral expectations for everyone participating in this project\n\nGood first issues and feature requests are tracked on the [GitHub issues page](https://github.com/abdelrahmannasr/wa-cloud-sdk/issues).\n\n## Security\n\nFor security vulnerabilities, please follow the responsible-disclosure process described in [SECURITY.md](./SECURITY.md). Do **not** open a public GitHub issue for security reports.\n\n## License\n\nMIT © AbdelRahman Nasr\n","readmeFilename":"README.md"}