├── .all-contributorsrc ├── .firebaserc ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── renovate.json └── workflows │ ├── alpha-release.yml │ ├── codeql-analysis.yml │ ├── firebase-hosting-pull-request.yml │ ├── npm-deploy.yml │ ├── on-pull-request.yml │ └── web-release.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── BREAKING.md ├── CONTRIBUTING.md ├── ENVIRONMENTS.md ├── EXAMPLES ├── README.md ├── contacts.md ├── interactive.md ├── location.md ├── main.md ├── media.md ├── reaction.md ├── template.md └── text.md ├── LICENSE ├── README.md ├── SECURITY.md ├── build.js ├── docs_statics └── 404.html ├── eslint.config.js ├── firebase.json ├── package-lock.json ├── package.json ├── src ├── emitters.ts ├── errors.ts ├── index.ts ├── messages │ ├── contacts.ts │ ├── globals.ts │ ├── index.ts │ ├── interactive.ts │ ├── location.ts │ ├── media.ts │ ├── reaction.ts │ ├── template.ts │ └── text.ts ├── middleware │ ├── adonis.ts │ ├── azure.ts │ ├── bun.ts │ ├── deno.ts │ ├── express.ts │ ├── globals.ts │ ├── index.ts │ ├── next.ts │ ├── node-http.ts │ ├── sveltekit.ts │ ├── vercel.ts │ └── web-standard.ts ├── setup │ ├── bun.ts │ ├── deno.ts │ ├── index.ts │ ├── node.ts │ └── web.ts ├── standalone.ts ├── types.ts └── utils.ts ├── test ├── index.test.js ├── server.mocks.js └── webhooks.mocks.js ├── tsconfig.json ├── tsdoc.json └── typedoc.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "whatsapp-api-js", 3 | "projectOwner": "Secreto31126", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": ["README.md"], 7 | "imageSize": 100, 8 | "commit": false, 9 | "commitConvention": "eslint", 10 | "contributors": [ 11 | { 12 | "login": "DiegoCarrillogt", 13 | "name": "Diego Carrillo", 14 | "avatar_url": "https://avatars.githubusercontent.com/u/29462621?v=4", 15 | "profile": "https://github.com/DiegoCarrillogt", 16 | "contributions": ["financial"] 17 | }, 18 | { 19 | "login": "HysMX", 20 | "name": "Omar", 21 | "avatar_url": "https://avatars.githubusercontent.com/u/50180189?v=4", 22 | "profile": "https://github.com/HysMX", 23 | "contributions": ["bug", "financial"] 24 | }, 25 | { 26 | "login": "RahulLanjewar93", 27 | "name": "Rahul Lanjewar", 28 | "avatar_url": "https://avatars.githubusercontent.com/u/63550998?v=4", 29 | "profile": "https://github.com/RahulLanjewar93", 30 | "contributions": ["code", "doc", "ideas"] 31 | }, 32 | { 33 | "login": "felixarjuna", 34 | "name": "Felix Arjuna", 35 | "avatar_url": "https://avatars.githubusercontent.com/u/79026094?v=4", 36 | "profile": "https://github.com/felixarjuna", 37 | "contributions": ["bug", "security"] 38 | } 39 | ], 40 | "contributorsPerLine": 7, 41 | "linkToUsage": false 42 | } 43 | 44 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "whatsappapijs" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: Secreto31126 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Simple and straight to the point failing snippet 16 | 2. See error 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Desktop (please complete the following information):** 22 | 23 | - OS: [e.g. Windows] 24 | - Runtime [e.g. node@19, deno@1.33.4] 25 | - Library Version [e.g. 1.0.0] 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Please don't delete this checklist! Before submitting the PR, please make sure you do the following: 2 | 3 | - [ ] It's really useful if your PR references an issue where it is discussed ahead of time. 4 | - [ ] This message body should clearly illustrate what problems it solves. 5 | - [ ] Ideally, include a test that fails without this PR but passes with it. 6 | 7 | ### Tests 8 | 9 | - [ ] Run the tests with `npm run test` and lint the project with `npm run lint` and `npm run prettier` 10 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseBranches": ["main"], 3 | "dependencyDashboard": true, 4 | "extends": [ 5 | "config:recommended", 6 | "group:allNonMajor", 7 | "helpers:disableTypesNodeMajor", 8 | "schedule:earlyMondays" 9 | ], 10 | "commitMessageAction": "", 11 | "commitMessageTopic": "{{depName}}", 12 | "commitMessageExtra": "{{#if isMajor}}v{{{newMajor}}}{{else}}{{#if isSingleVersion}}v{{{newVersion}}}{{else}}{{{newValue}}}{{/if}}{{/if}}", 13 | "labels": ["dependencies"], 14 | "prBodyTemplate": "{{{header}}}{{{table}}}{{{notes}}}{{{changelogs}}}{{{controls}}}{{{footer}}}", 15 | "prHeader": "", 16 | "prFooter": "", 17 | "semanticCommits": "enabled", 18 | "packageRules": [ 19 | { 20 | "matchPackageNames": ["esbuild"], 21 | "versioning": "regex:^(?\\d+)\\.(?\\d+)\\.(?\\d+)?$" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/alpha-release.yml: -------------------------------------------------------------------------------- 1 | name: Deploy alpha release to GitHub 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build: 11 | name: Build from source 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | ref: ${{ github.ref }} 17 | 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: 22.x 21 | 22 | - name: Build 23 | run: | 24 | npm ci 25 | npm run build 26 | 27 | - name: Store builds 28 | uses: actions/upload-artifact@v4 29 | with: 30 | name: build 31 | path: | 32 | package.json 33 | README.md 34 | LICENSE 35 | lib 36 | 37 | alpha-branch: 38 | name: Deploy alpha 39 | runs-on: ubuntu-latest 40 | needs: build 41 | steps: 42 | - uses: actions/checkout@v4 43 | with: 44 | ref: alpha 45 | 46 | - name: Clear old build 47 | run: find . -not -wholename './.git*' -delete 48 | 49 | - uses: actions/download-artifact@v4 50 | with: 51 | name: build 52 | 53 | - name: Push to alpha 54 | uses: EndBug/add-and-commit@v9 55 | with: 56 | default_author: github_actor 57 | message: New alpha release 58 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: ["main"] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: ["main"] 20 | schedule: 21 | - cron: "45 22 * * 6" 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ["javascript"] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 56 | # If this step fails, then you should remove it and run the build manually (see below) 57 | - name: Autobuild 58 | uses: github/codeql-action/autobuild@v3 59 | 60 | # ℹ️ Command-line programs to run using the OS shell. 61 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 62 | 63 | # If the Autobuild fails above, remove it and uncomment the following three lines. 64 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 65 | 66 | # - run: | 67 | # echo "Run, Build Application using script" 68 | # ./location_of_script_within_repo/buildscript.sh 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v3 72 | -------------------------------------------------------------------------------- /.github/workflows/firebase-hosting-pull-request.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Preview with Firebase Hosting 5 | 6 | on: 7 | pull_request: 8 | paths: 9 | - "src/**" 10 | - "README.md" 11 | - "package.json" 12 | - "docs_statics/*.html" 13 | 14 | jobs: 15 | build_and_preview: 16 | name: Build documentation and preview 17 | if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - run: | 22 | npm ci --only=dev 23 | npm run document 24 | 25 | - uses: FirebaseExtended/action-hosting-deploy@v0 26 | with: 27 | repoToken: "${{ secrets.GITHUB_TOKEN }}" 28 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_WHATSAPPAPIJS }}" 29 | projectId: whatsappapijs 30 | -------------------------------------------------------------------------------- /.github/workflows/npm-deploy.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Deploy to NPM 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | name: Build and Document 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | 19 | - name: Setup 20 | run: npm ci 21 | 22 | - name: Build 23 | run: npm run build 24 | 25 | - name: Store builds 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: build 29 | path: lib 30 | 31 | test: 32 | name: Test 33 | needs: build 34 | 35 | runs-on: ubuntu-latest 36 | strategy: 37 | matrix: 38 | node-version: [18.x, 20.x, 22.x] 39 | 40 | steps: 41 | - uses: actions/checkout@v4 42 | - uses: actions/download-artifact@v4 43 | with: 44 | name: build 45 | path: lib 46 | 47 | - name: Use Node.js ${{ matrix.node-version }} 48 | uses: actions/setup-node@v4 49 | with: 50 | node-version: ${{ matrix.node-version }} 51 | cache: npm 52 | 53 | - name: Setup 54 | run: npm ci 55 | 56 | - name: Test 57 | run: npm run test 58 | 59 | publish-npm: 60 | name: Public Release 61 | needs: test 62 | 63 | runs-on: ubuntu-latest 64 | permissions: 65 | id-token: write 66 | 67 | if: ${{ !github.event.release.prerelease }} 68 | 69 | steps: 70 | - uses: actions/checkout@v4 71 | - uses: actions/setup-node@v4 72 | with: 73 | node-version: 22.x 74 | registry-url: https://registry.npmjs.org/ 75 | - uses: actions/download-artifact@v4 76 | with: 77 | name: build 78 | path: lib 79 | 80 | - name: Give me 1 minute to reconsider my life actions 81 | run: sleep 60s 82 | 83 | - name: Ok, now publish it 84 | run: npm publish --provenance 85 | env: 86 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 87 | 88 | preview-npm: 89 | name: Beta Release 90 | needs: test 91 | 92 | runs-on: ubuntu-latest 93 | permissions: 94 | id-token: write 95 | 96 | if: ${{ github.event.release.prerelease }} 97 | 98 | steps: 99 | - uses: actions/checkout@v4 100 | - uses: actions/setup-node@v4 101 | with: 102 | node-version: 22.x 103 | registry-url: https://registry.npmjs.org/ 104 | - uses: actions/download-artifact@v4 105 | with: 106 | name: build 107 | path: lib 108 | 109 | - name: Set package version to prerelease tag 110 | run: npm version --no-git-tag-version ${GITHUB_REF/refs\/tags\//} 111 | 112 | - name: Beta Release 113 | run: npm publish --tag beta --provenance 114 | env: 115 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 116 | 117 | web-release: 118 | name: Deploy website 119 | needs: publish-npm 120 | 121 | uses: ./.github/workflows/web-release.yml 122 | secrets: inherit 123 | with: 124 | tag_name: ${{ github.event.release.tag_name }} 125 | -------------------------------------------------------------------------------- /.github/workflows/on-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Run Pull Request Checks 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | tests: 7 | name: Run Tests 8 | 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: [18.x, 20.x, 22.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | cache: npm 21 | - name: Install 22 | run: npm ci 23 | - name: Compile 24 | run: npm run build:code 25 | - name: Tests 26 | run: npm run test 27 | 28 | lint_and_format: 29 | name: Run Lint and Format 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: actions/setup-node@v4 35 | with: 36 | node-version: 22.x 37 | cache: npm 38 | - name: Install 39 | run: npm ci 40 | - name: Linted 41 | run: npm run lint 42 | - name: Nice 43 | run: npm run prettier 44 | -------------------------------------------------------------------------------- /.github/workflows/web-release.yml: -------------------------------------------------------------------------------- 1 | name: Deploy websites update 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | tag_name: 7 | description: "Tag to deploy" 8 | required: true 9 | workflow_call: 10 | inputs: 11 | tag_name: 12 | description: "Tag to deploy" 13 | required: true 14 | type: string 15 | 16 | jobs: 17 | build: 18 | name: Build Documentation 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-node@v4 24 | 25 | - name: Setup 26 | run: npm ci 27 | 28 | - name: Document 29 | run: npm run document 30 | 31 | - name: Store docs 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: docs 35 | path: docs 36 | 37 | firebase: 38 | name: Deploy website to Firebase 39 | needs: build 40 | 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v4 44 | 45 | - run: mkdir docs 46 | - name: Copy docs 47 | uses: actions/download-artifact@v4 48 | with: 49 | name: docs 50 | path: docs 51 | 52 | - uses: FirebaseExtended/action-hosting-deploy@v0 53 | with: 54 | repoToken: "${{ secrets.GITHUB_TOKEN }}" 55 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_WHATSAPPAPIJS }}" 56 | channelId: live 57 | projectId: whatsappapijs 58 | 59 | archive: 60 | name: Deploy website to Github Pages 61 | needs: build 62 | 63 | runs-on: ubuntu-latest 64 | 65 | steps: 66 | - uses: actions/checkout@v4 67 | with: 68 | ref: gh-pages 69 | 70 | - name: Check if tag exists 71 | run: | 72 | if [ $(git tag -l "${{ inputs.tag_name }}") ]; then 73 | echo "Tag doesn't exists" 74 | exit 1 75 | fi 76 | 77 | - name: Create Endpoint 78 | # The endpoint shouldn't exists 79 | # The code is only executed on releases 80 | run: mkdir ${{ inputs.tag_name }} 81 | 82 | - name: Copy docs into Endpoint 83 | uses: actions/download-artifact@v4 84 | with: 85 | name: docs 86 | path: ${{ inputs.tag_name }} 87 | 88 | - name: Add Endpoint to the index 89 | run: bash build.sh 90 | 91 | - name: Commit changes 92 | uses: EndBug/add-and-commit@v9 93 | with: 94 | default_author: github_actor 95 | message: Added ${{ inputs.tag_name }} docs 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # Firebase cache 101 | .firebase/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | # VS Code data 110 | .vscode/ 111 | 112 | # Test code 113 | playground.[tj]s 114 | playground.[cm][tj]s 115 | 116 | # Documentation 117 | docs_temp/ 118 | docs/ 119 | 120 | # Builds 121 | lib/ 122 | 123 | # JetBrains IDEs 124 | .idea 125 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | .all-contributorsrc 3 | playground.js 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": false, 6 | "endOfLine": "lf" 7 | } 8 | -------------------------------------------------------------------------------- /BREAKING.md: -------------------------------------------------------------------------------- 1 | # Breaking changes 2 | 3 | ## 5.0.0 4 | 5 | ### post() and get() errors 6 | 7 | The webhook methods no longer throw simple numbers as errors, but rather 8 | custom classes extending the new WhatsAppAPIError class. This change was 9 | made to allow for more detailed error handling, as the new classes contain 10 | the error message, recommended status code and lots of docs to help. 11 | 12 | ```ts 13 | import { WhatsAppAPIError } from "whatsapp-api-js/errors"; 14 | 15 | const Whatsapp = new WhatsAppAPI({ token, secure: false }); 16 | 17 | // Assuming post is called on a POST request to your server 18 | async function post(e) { 19 | try { 20 | await Whatsapp.post(e.data); 21 | return 200; 22 | } catch (e) { 23 | console.error(e); 24 | 25 | if (e instanceof WhatsAppAPIError) { 26 | console.log("For more info, check", e.docs); 27 | return e.httpStatus; 28 | } 29 | 30 | return 500; 31 | } 32 | } 33 | ``` 34 | 35 | As you might notice, the example above first checks if the error is an 36 | instance of WhatsAppAPIError, and returns 500 if it isn't. That's because... 37 | 38 | ### post() error handling 39 | 40 | The post() method no longer catches errors thrown by the `message` or 41 | `status` emitters. This means that any error within the handlers will be 42 | propagated to the caller, which can then manage it as needed. 43 | 44 | ```ts 45 | import { WhatsAppAPIError } from "whatsapp-api-js/errors"; 46 | 47 | const Whatsapp = new WhatsAppAPI({ token, secure: false }); 48 | 49 | Whatsapp.on.message = () => { 50 | throw new Error("This is an error on my code"); 51 | }; 52 | 53 | async function post(e) { 54 | try { 55 | await Whatsapp.post(e.data); 56 | } catch (e) { 57 | if (e instanceof WhatsAppAPIError) { 58 | console.log("This is a library error"); 59 | } else { 60 | console.log("This is my faulty code"); 61 | } 62 | } 63 | 64 | return 418; 65 | } 66 | ``` 67 | 68 | This change does NOT impact the middlewares, as they still catch 69 | the errors and asserts the return values are the documented ones. 70 | 71 | ### ActionProduct signature change 72 | 73 | The ActionProduct class no longer takes the catalog ID and product as 74 | two separate arguments, but rather as a single CatalogProduct object. 75 | 76 | ```ts 77 | import { 78 | Interactive, 79 | ActionProduct, 80 | CatalogProduct 81 | } from "whatsapp-api-js/messages"; 82 | 83 | const interactive_single_product_message = new Interactive( 84 | new ActionProduct(new CatalogProduct("product_id", "catalog_id")) 85 | ); 86 | ``` 87 | 88 | ### Drop support for CJS 89 | 90 | Nine out of ten releases, there was a compatibility issue between ESM and CJS 91 | exports. Although the library was designed to support both, the complexity 92 | it brought didn't justify the effort, specially now as many other big libraries 93 | are dropping native CJS support too. 94 | 95 | If you 100% need CJS in Node.js, you can now use the release candidate feature 96 | to synchonously require ESM modules available since v22. The library is fully 97 | synchonous (contains no top-level await), so it should work just fine. 98 | 99 | https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require 100 | 101 | In order to keep the library easier to use with CJS, the code will still not 102 | use default exports. 103 | 104 | ### Bumped API version 105 | 106 | The default API version was bumped to `v21.0`. 107 | 108 | ## 4.0.0 109 | 110 | ### Emitters and post() signature change 111 | 112 | This might be one of the most requested features since the 113 | beginning of the library. It is now possible to return 114 | custom information from the emitters, which can be accessed 115 | from the promise returned by the method `post()`. 116 | 117 | ```ts 118 | type EmitterReturn = { replied: boolean; status: number }; 119 | 120 | const Whatsapp = new WhatsAppAPI({ 121 | token, 122 | appSecret 123 | }); 124 | 125 | Whatsapp.on.message = async ({ reply }) => { 126 | try { 127 | await reply(new Text("Hello!")); 128 | return { replied: true, status: 200 }; 129 | } catch (e) { 130 | return { replied: false, status: 500 }; 131 | } 132 | }; 133 | 134 | Whatsapp.on.status = ({ id, status }) => { 135 | return { replied: false, status: 200 }; 136 | }; 137 | 138 | // Assuming post is called on a POST request to your server 139 | async function post(e) { 140 | const body = JSON.parse(e.data); 141 | 142 | let status: number, replied = false; 143 | try { 144 | { replied, status } = await Whatsapp.post(body, e.data, e.headers["x-hub-signature-256"]); 145 | } catch (e) { 146 | status = e; 147 | } 148 | 149 | console.log(`Replied: ${replied}, Status: ${status}`); 150 | return status; 151 | } 152 | ``` 153 | 154 | Some things to note: 155 | 156 | 1. As you may notice, the method no longer returns 200 by default, 157 | but rather a promise of the custom type. 158 | 159 | 2. Any error thrown within the emitters will be caught by `post()` and 160 | throw a 500 status code. 161 | 162 | 3. OnSent is not affected by this change as it is not invoked 163 | by the method `post()`. 164 | 165 | 4. Middlewares are also not affected by this, as they still 166 | return a promise of 200 with no option to change it. They, however, 167 | will return 500 if an error is thrown within the emitter, as 168 | they internally call `post()`, which will catch the error as 169 | mentioned in point 2. 170 | 171 | ### offload_functions option removed 172 | 173 | The option `offload_functions` was removed from the constructor. 174 | All emitters are now always executed synchronously. In order to 175 | offload the execution of the handlers, you can use the new `offload` 176 | method, which can be called from the emitters' parameters. 177 | 178 | ```ts 179 | const Whatsapp = new WhatsAppAPI({ token, appSecret }); 180 | 181 | Whatsapp.on.message = ({ reply, offload }) => { 182 | offload(() => { 183 | reply(new Text(AI.text())); 184 | }); 185 | 186 | return 202; 187 | }; 188 | ``` 189 | 190 | ### broadcastMessage() signature change 191 | 192 | The method no longer returns a promise, and rather than waiting 193 | within the function to send the message, it immediately returns 194 | an array of timeout promises which will execute `sendMessage()`. 195 | 196 | ### Bumped API version 197 | 198 | The default API version was bumped to `v20.0`. Not much else to say. 199 | 200 | ## 3.0.0 201 | 202 | In the last few years, the library has been growing and changing, and 203 | with that, not only new functionalities are added, but also new bugs 204 | are created, which sometimes require breaking changes to be fixed. This 205 | version is one of those cases. 206 | 207 | ### Removed default exports 208 | 209 | Due to interoperability issues between ESM and CJS, all the default 210 | exports were removed to assure a consistent behavior across all 211 | runtimes. The list of affected imports by this change is: 212 | 213 | - `whatsapp-api-js` 214 | - `whatsapp-api-js/messages/location` 215 | - `whatsapp-api-js/messages/reaction` 216 | - `whatsapp-api-js/messages/text` 217 | - `whatsapp-api-js/middleware/*` (yeah, all of them, issue [#306](https://github.com/Secreto31126/whatsapp-api-js/issues/306)) 218 | - `whatsapp-api-js/setup/bun` 219 | - `whatsapp-api-js/setup/deno` 220 | - `whatsapp-api-js/setup/web` 221 | 222 | ESM example: 223 | 224 | ```js 225 | import { WhatsAppAPI } from "whatsapp-api-js"; 226 | ``` 227 | 228 | CJS example: 229 | 230 | ```js 231 | const { WhatsAppAPI } = require("whatsapp-api-js"); 232 | ``` 233 | 234 | ### Once again, classes splitted and renamed 235 | 236 | In order to improve the typing in Interactive's constructor, the 237 | `ActionProduct` class was splitted into `ActionProduct` and 238 | `ActionProductList`. If you are using the former to send multi product 239 | messages, updating is as simple as renaming the class to the new one. 240 | 241 | ### Version pinning warning 242 | 243 | Although not a breaking change, it's worth mentioning that the library 244 | will now show a warning if the API version is not pinned. This is to 245 | encourage good practices and avoid unexpected changes in the API 246 | at production. 247 | 248 | ## 2.0.0 249 | 250 | ### Classes renamed, splitted and moved 251 | 252 | With the release of **"full catalog"** support for Cloud API, the library needed 253 | some files and classes renaming to avoid confusion between the new features and 254 | the original catalog messages. 255 | 256 | Among the affected classes and files are: 257 | 258 | - `interactive.ts` 259 | 260 | - `ActionCatalog` -> `ActionProduct`: `ActionCatalog` is now used for the 261 | Catalog messages. The original class was renamed as `ActionProduct`. 262 | - `Product` and `ProductSection`: These classes were moved from the file 263 | `messages/interactive.ts` to `messages/globals.ts`, as they are now also 264 | used in templates. 265 | - `Section`: The class was moved from the file `messages/interactive.ts` to 266 | `types.ts`. 267 | 268 | - `template.ts` 269 | 270 | - `ButtonComponent` -> `URLComponent`, `PayloadComponent`, `CatalogComponent`, 271 | `MPMComponent`, `CopyComponent`, `SkipButtonComponent`: As the API now 272 | supports mixing button types, the `ButtonComponent` was splitted into 273 | different classes, each one representing a button types. For example, an URL 274 | component was updated from `new ButtonComponent("url", "example")` to 275 | `new URLComponent("example")`. 276 | - `Template`: The constructor now receives multiple button components instead 277 | of a single one. 278 | - `ButtonComponent`: The class was replaced with an abstract class. 279 | - `ButtonParameter`: The class was replaced with a type. 280 | 281 | - `types.ts` 282 | - `ClientBuildableMessageComponent`: The class was replaced with an interface. 283 | - `PostData`: `PostData.entry[].changes[].value.contacts` may be undefined. 284 | 285 | ### Node min version bumped 286 | 287 | Node 14 and 16 support was dropped as they reached EoL. The engine requirement 288 | is now `>=16`. 289 | 290 | ## 1.0.0 291 | 292 | The module was rewritten in TypeScript, which allows for better type support and 293 | documentation, reducing the number of runtime checks and improving the overall 294 | performance. 295 | 296 | Most of the import syntax was updated to ES6 in order to support tree-shaking. 297 | CommonJS is still supported. 298 | 299 | Examples: 300 | 301 | ```js 302 | // ESM 303 | import WhatsAppAPI from "whatsapp-api-js"; 304 | import { Document, Image, Text } from "whatsapp-api-js/messages"; 305 | import Location from "whatsapp-api-js/messages/location"; 306 | ``` 307 | 308 | ```js 309 | // CommonJS 310 | const WhatsAppAPI = require("whatsapp-api-js").default; 311 | const { Text, Image, Document } = require("whatsapp-api-js/messages"); 312 | const Location = require("whatsapp-api-js/messages/location").default; 313 | ``` 314 | 315 | The main contructor now takes named arguments instead of positional arguments. 316 | Also bumped the default API version to v17.0. 317 | 318 | ```js 319 | import WhatsAppAPI from "whatsapp-api-js"; 320 | 321 | const Whatsapp = new WhatsAppAPI({ 322 | token: "123", 323 | appSecret: "456", 324 | webhookVerifyToken: "789", 325 | v: "v16.0", 326 | parsed: true 327 | }); 328 | ``` 329 | 330 | Both post and get wizards were moved within the WhatsAppAPI class and got major 331 | signatures updates. 332 | 333 | get() no longer takes the verify_token as a parameter, as it's now configured on 334 | the main class. 335 | 336 | ```js 337 | import WhatsAppAPI from "whatsapp-api-js"; 338 | 339 | const Whatsapp = new WhatsAppAPI({ 340 | token: "123", 341 | appSecret: "456", 342 | webhookVerifyToken: "789" 343 | }); 344 | 345 | // Assuming get is called on a GET request to your server 346 | function get(e) { 347 | return Whatsapp.get(JSON.parse(e.params)); 348 | } 349 | ``` 350 | 351 | post() now requires both the raw body and the signature header _if_ secure is 352 | set to true. 353 | 354 | ```js 355 | import WhatsAppAPI from "whatsapp-api-js"; 356 | 357 | const Whatsapp = new WhatsAppAPI({ token: "123", appSecret: "456" }); 358 | 359 | // Assuming post is called on a POST request to your server 360 | function post(e) { 361 | return Whatsapp.post( 362 | JSON.parse(e.data), 363 | e.data, 364 | e.headers["x-hub-signature-256"] 365 | ); 366 | } 367 | ``` 368 | 369 | If you want to skip the signature verification, you can set secure to false as 370 | follows. 371 | 372 | ```js 373 | import WhatsAppAPI from "whatsapp-api-js"; 374 | 375 | const Whatsapp = new WhatsAppAPI({ token: "123", secure: false }); 376 | 377 | // Assuming post is called on a POST request to your server 378 | function post(e) { 379 | return Whatsapp.post(JSON.parse(e.data)); 380 | } 381 | ``` 382 | 383 | The callbacks for OnMessage and OnStatus are now configured separately on the 384 | main class. 385 | 386 | ```js 387 | import WhatsAppAPI from "whatsapp-api-js"; 388 | 389 | const Whatsapp = new WhatsAppAPI({ token: "123", appSecret: "456" }); 390 | 391 | Whatsapp.on.message = ({ name, from }) => { 392 | console.log(`Got message from ${name} (${from})`); 393 | }; 394 | 395 | Whatsapp.on.status = ({ id, status }) => { 396 | console.log(`Message ${id} status changed to ${status}`); 397 | }; 398 | 399 | Whatsapp.on.sent = ({ id, to }) => { 400 | console.log(`Message ${id} was sent to ${to}`); 401 | }; 402 | 403 | // Assuming post is called on a POST request to your server 404 | function post(e) { 405 | return Whatsapp.post( 406 | JSON.parse(e.data), 407 | e.data, 408 | e.headers["x-hub-signature-256"] 409 | ); 410 | } 411 | ``` 412 | 413 | With the implementation of the new callbacks, the logSentMessages function was 414 | removed. 415 | 416 | The default undici fallback was also removed, and the module now uses the 417 | enviroment fetch implementation. Ponyfilling is still possible via the new 418 | argument at the WhatsAppAPI() constructor: 419 | 420 | ```js 421 | import WhatsAppAPI from "whatsapp-api-js"; 422 | import { fetch } from "undici"; 423 | 424 | const Whatsapp = new WhatsAppAPI({ 425 | token: "YOUR_TOKEN_HERE", 426 | appSecret: "YOUR_SECRET_HERE", 427 | ponyfill: { 428 | fetch 429 | } 430 | }); 431 | ``` 432 | 433 | This change also restores the compatibility with previous Node.js versions, 434 | making the module more server agnostic. 435 | 436 | There had been some minor changes to the messages classes, although the most 437 | noticeable one is the reduction of the Text class usage across classes, replaced 438 | by normal strings. 439 | 440 | ## 0.8.0 441 | 442 | The module changed from using "cross-fetch" to "undici" as the fallback fetch 443 | implementation in order to use FormData for the Media upload support, which is 444 | not (easily) available in "cross-fetch". 445 | 446 | Although this change doesn't affect existing code, it forces the Node.js version 447 | to be at least 16. If the module is downloaded using a lower version, npm will 448 | throw an error. 449 | 450 | ## 0.7.0 451 | 452 | With the release of cart support for Cloud API, some naming changes where made 453 | within the interactive's classes. The Section class, which was a component of 454 | the ActionList, was renamed to ListSection, to avoid confusion with the new 455 | ProductSection. 456 | 457 | ## 0.6.0 458 | 459 | Since 0.6.0, the module will no longer return the raw fetch request, now it's 460 | internally parsed and returned. This change was made in order to improve the 461 | logSentMessages function, as it can now log the server response too. To get the 462 | raw request as before, you can use the `parsed` property of the main object as 463 | follows. 464 | 465 | ```js 466 | const parsed = false; 467 | const Whatsapp = new WhatsAppAPI("YOUR_TOKEN", undefined, parsed); 468 | // All the API operations, like sendMessage, will now return the raw request. 469 | // Keep in mind, now when using logSentMessage the id and response parameters will be undefined. 470 | ``` 471 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing: 2 | 3 | This project accepts PR! Just keep in mind the following rules: 4 | 5 | 1. If you are planning to write a new feature or bug fix, 6 | consider opening an issue to get a confirmation 7 | 8 | - Minor changes like typos fixes in documentation can be 9 | created without previous discussion 10 | 11 | 2. If deemed necesary, add unit tests that cover the feature or prevents the bug fix 12 | 13 | 3. Before committing, remember to run prettier and eslint to fix 14 | syntax and format issues (will setup husky in the future) 15 | 16 | 4. Follow the server agnostic "philosophy" of the library, avoid using runtime specific features unless strictly required 17 | 18 | ## Setup 19 | 20 | ```sh 21 | git clone https://github.com/secreto31126/whatsapp-api-js.git 22 | ``` 23 | 24 | ```sh 25 | cd whatsapp-api-js 26 | ``` 27 | 28 | ```sh 29 | npm ci 30 | ``` 31 | 32 | ```sh 33 | npm run test:build 34 | ``` 35 | -------------------------------------------------------------------------------- /ENVIRONMENTS.md: -------------------------------------------------------------------------------- 1 | # Environments 2 | 3 | The code is server agnostic, which allows it to work with most runtimes and 4 | frameworks. In order to simplify the hassle of setting it up, we provide some 5 | helpers for the most popular choices. 6 | 7 | ## Node.js 8 | 9 | If using ESM, you can import the module like this: 10 | 11 | ```js 12 | import { WhatsAppAPI } from "whatsapp-api-js"; 13 | ``` 14 | 15 | If using CommonJS, you can require the package: 16 | 17 | ```js 18 | const { WhatsAppAPI } = require("whatsapp-api-js"); 19 | ``` 20 | 21 | For each version of Node, you can use the `setup` function to simplify the 22 | process. 23 | 24 | - Node 19 and above (using Express and it's middleware): 25 | 26 | ```js 27 | import { WhatsAppAPI } from "whatsapp-api-js/middleware/express"; 28 | import { NodeNext } from "whatsapp-api-js/setup/node"; 29 | 30 | import express from "express"; 31 | 32 | const Whatsapp = new WhatsAppAPI( 33 | NodeNext({ 34 | token: "123", 35 | appSecret: "123" 36 | }) 37 | ); 38 | 39 | const app = express(); 40 | 41 | app.post("/message", async (req, res) => { 42 | res.status(await Whatsapp.handle_post(req)); 43 | }); 44 | ``` 45 | 46 | - Node 18: 47 | 48 | ```js 49 | import { WhatsAppAPI } from "whatsapp-api-js"; 50 | import { Node18 } from "whatsapp-api-js/setup/node"; 51 | 52 | const Whatsapp = new WhatsAppAPI( 53 | Node18({ 54 | token: "123", 55 | appSecret: "123" 56 | }) 57 | ); 58 | ``` 59 | 60 | - Node 15 to 17 (deprecated): 61 | 62 | ```js 63 | import { WhatsAppAPI } from "whatsapp-api-js"; 64 | import { Node15 } from "whatsapp-api-js/setup/node"; 65 | 66 | // As fetch isn't available until Node 18, you will need to pass a ponyfill as a parameter 67 | import fetch from "node-fetch"; // or any other fetch implementation 68 | 69 | const Whatsapp = new WhatsAppAPI( 70 | Node15( 71 | { 72 | token: "123", 73 | appSecret: "123" 74 | }, 75 | fetch 76 | ) 77 | ); 78 | ``` 79 | 80 | ## Deno 81 | 82 | With the release of Deno 1.25.0, now you can import npm modules directly to 83 | Deno. It's really simple to use: 84 | 85 | ```js 86 | import { WhatsAppAPI } from "npm:whatsapp-api-js"; 87 | ``` 88 | 89 | If you want to use prior versions of Deno, use 90 | [https://esm.sh/whatsapp-api-js](https://esm.sh/whatsapp-api-js) to import the 91 | code. 92 | 93 | Deno also counts with setup and middleware helpers: 94 | 95 | ```js 96 | import { WhatsAppAPI } from "npm:whatsapp-api-js/middleware/deno"; 97 | import { Deno } from "whatsapp-api-js/setup/deno"; 98 | 99 | const Whatsapp = new WhatsAppAPI( 100 | Deno({ 101 | token: "123", 102 | appSecret: "123" 103 | }) 104 | ); 105 | 106 | Deno.serve(async (req) => { 107 | if (req.url === "/message" && req.method === "POST") { 108 | return new Response(null, { 109 | status: await Whatsapp.handle_post(req) 110 | }); 111 | } 112 | }); 113 | ``` 114 | 115 | ## Bun 116 | 117 | Bun _should_ also work by running: 118 | 119 | ```sh 120 | bun install whatsapp-api-js 121 | ``` 122 | 123 | ```js 124 | import { WhatsAppAPI } from "whatsapp-api-js/middleware/bun"; 125 | import { Bun } from "whatsapp-api-js/setup/bun"; 126 | 127 | const Whatsapp = new WhatsAppAPI( 128 | Bun({ 129 | token: "123", 130 | appSecret: "123" 131 | }) 132 | ); 133 | 134 | Bun.serve({ 135 | fetch: async (req) => { 136 | if (req.url === "/message" && req.method === "POST") { 137 | req.respond({ status: await Whatsapp.handle_post(req) }); 138 | } 139 | } 140 | }); 141 | ``` 142 | 143 | ## Websites 144 | 145 | HTML module example: 146 | 147 | ```html 148 | 152 | ``` 153 | 154 | ## Google App Script 155 | 156 | The library is exported as standalone to Google App Script. You can import it 157 | using the code: 158 | 159 | ``` 160 | 1iTMl1x_CayBWuLYBBZH0DM72eXqjuavZ0nAwSZ6y3jj9ELXOMckcHmW6 161 | ``` 162 | 163 | Unfortunately, type definitions, middlewares and setup methods aren't available, 164 | as the standalone version doesn't include them while bundling. 165 | 166 | ```js 167 | const { WhatsAppAPI, Text } = WhatsAppScript; // Or whatever name you gave to the library 168 | 169 | async function fetchPonyfill(url, options = {}) { 170 | if (options?.body) { 171 | options.payload = options.body; 172 | delete options.body; 173 | } 174 | 175 | if (options?.headers?.["Content-Type"]) { 176 | options.contentType = options.headers["Content-Type"]; 177 | delete options.headers["Content-Type"]; 178 | } 179 | 180 | if (options?.method) { 181 | options.method = options.method.toLowerCase(); 182 | } 183 | 184 | const request = UrlFetchApp.fetch(url, { 185 | ...options, 186 | muteHttpExceptions: true 187 | }); 188 | 189 | return { 190 | text: async () => request.getContentText, 191 | json: async () => JSON.parse(request.getContentText()) 192 | }; 193 | } 194 | 195 | const Whatsapp = new WhatsAppAPI({ 196 | token: YOUR_TOKEN, 197 | webhookVerifyToken: YOUR_WEBHOOK_VERIFY_TOKEN, 198 | // GAS doesn't support crypto nor getting the headers of a request, there's no way to verify payloads 199 | secure: false, 200 | ponyfill: { 201 | fetch: fetchPonyfill 202 | } 203 | }); 204 | 205 | Whatsapp.on.message = ({ name, reply }) => { 206 | reply(new Text(`Hello ${name}!`)); 207 | return 200; 208 | }; 209 | 210 | async function doPost(e) { 211 | const data = JSON.parse(e.postData.contents); 212 | 213 | let status_code; 214 | try { 215 | status_code = await Whatsapp.post(data); 216 | } catch (e) { 217 | status_code = e?.httpStatus ?? 500; 218 | } 219 | 220 | // GAS doesn't support sending custom status codes 221 | return ContentService.createTextOutput(status_code); 222 | } 223 | ``` 224 | -------------------------------------------------------------------------------- /EXAMPLES/README.md: -------------------------------------------------------------------------------- 1 | # Tutorials 2 | 3 | Here you can find tutorials for the different message types and 4 | the most basic methods or features. 5 | 6 | ## Index 7 | 8 | - [Main](main.md) 9 | - [Simple Whatsapp creation](main.md#simple-whatsapp-creation) 10 | - [Using the setup helpers](main.md#using-the-setup-helpers) 11 | - [Using the middlewares](main.md#using-the-middlewares) 12 | - [Sending a message](main.md#sending-a-message) 13 | - [Replying to a message](main.md#replying-to-a-message) 14 | - [Text](text.md) 15 | - [Simple text](text.md#simple-text) 16 | - [Text with URL preview](text.md#text-with-url-preview) 17 | - [Reaction](reaction.md) 18 | - [Add reaction](reaction.md#add-reaction) 19 | - [Remove reaction](reaction.md#remove-reaction) 20 | - [Location](location.md) 21 | - [Coordinates](location.md#coordinates) 22 | - [Coordinates with name](location.md#coordinates-with-name) 23 | - [Media](media.md) 24 | - [Image](media.md#image) 25 | - [Video](media.md#video) 26 | - [Audio](media.md#audio) 27 | - [Document](media.md#document) 28 | - [Sticker](media.md#sticker) 29 | - [Interactive](interactive.md) 30 | - [Reply buttons](interactive.md#reply-buttons) 31 | - [Options list](interactive.md#options-list) 32 | - [Single Product](interactive.md#single-product) 33 | - [Multi Product](interactive.md#multi-product) 34 | - [Catalog](interactive.md#catalog) 35 | - [Call To Action](interactive.md#call-to-action) 36 | - [Navigate Flow](interactive.md#navigate-flow) 37 | - [Data Exchange Flow](interactive.md#data-exchange-flow) 38 | - [Payments and Location request](interactive.md#payments-and-location-request) 39 | - [Contacts](contacts.md) 40 | - [Simple contact](contacts.md#simple-contact) 41 | - [Multiple contacts](contacts.md#multiple-contacts) 42 | - [Complex contact](contacts.md#complex-contact) 43 | - [Templates](templates.md) 44 | - [Simple template with no variables](template.md#simple-template-with-no-variables) 45 | - [Simple template with simple variables in header and body](template.md#simple-template-with-simple-variables-in-header-and-body) 46 | - [Simple template with header media](template.md#simple-template-with-header-media) 47 | - [Complex template with reply buttons](template.md#complex-template-with-reply-buttons) 48 | - [Complex template with call to action url](template.md#complex-template-with-call-to-action-url) 49 | - [Complex template with copy coupon button](template.md#complex-template-with-copy-coupon-button) 50 | - [Complex template with flow component](template.md#complex-template-with-flow-component) 51 | - [Complex template with combination of buttons](template.md#complex-template-with-combination-of-buttons) 52 | - [Complex template with catalog](template.md#complex-template-with-catalog) 53 | - [Complex template with Single-Product Message](template.md#complex-template-with-single-product-message) 54 | - [Complex template with Multi-Product Message](template.md#complex-template-with-multi-product-message) 55 | - [Complex template with Carousel](template.md#complex-template-with-carousel) 56 | - [Complex template with Product Carousel](template.md#complex-template-with-product-carousel) 57 | - [Complex template with Limited-Time Offer](template.md#complex-template-with-limited-time-offer) 58 | - [OTP prefab template](template.md#otp-prefab-template) 59 | -------------------------------------------------------------------------------- /EXAMPLES/contacts.md: -------------------------------------------------------------------------------- 1 | # Contacts 2 | 3 | ## Simple contact 4 | 5 | ```ts 6 | import { Contacts, Name, Address, Phone } from "whatsapp-api-js/messages"; 7 | 8 | const contact_message = new Contacts([ 9 | new Name("John Doe", "John", "Doe", undefined, "Mr.", "Jr."), 10 | new Address( 11 | "United States", 12 | "US", 13 | "FL", 14 | "Miami", 15 | "221B Baker Street", 16 | "33101", 17 | "Mystery" 18 | ), 19 | new Phone("+123456789", "Mystery", "123456789") 20 | ]); 21 | ``` 22 | 23 | ## Multiple contacts 24 | 25 | ```ts 26 | import { Contacts, Name, Address, Phone } from "whatsapp-api-js/messages"; 27 | 28 | const multi_contacts_message = new Contacts( 29 | [ 30 | new Name("John Doe", "John", "Doe", undefined, "Mr.", "Jr."), 31 | new Address( 32 | "United States", 33 | "US", 34 | "FL", 35 | "Miami", 36 | "221A Baker Street", 37 | "33101", 38 | "Mystery" 39 | ), 40 | new Phone("+123456789", "Mystery", "123456789") 41 | ], 42 | [ 43 | new Name("John Another Doe", "John", "Doe", "Another"), 44 | new Address( 45 | "United Kindom", 46 | "UK", 47 | "BS", 48 | "London", 49 | "221B Baker Street", 50 | "33101", 51 | "Mystery" 52 | ), 53 | new Phone("+123456789", "Mystery", "123456789") 54 | ] 55 | ); 56 | ``` 57 | 58 | ## Complex contact 59 | 60 | There's a lot more options for each contact field, 61 | some which can be repeated, some which are optional. 62 | Check out the documentation for more information. 63 | 64 | ## Documentation 65 | 66 | https://whatsappapijs.web.app/classes/messages.Contacts.html 67 | https://whatsappapijs.web.app/classes/types.ContactMultipleComponent.html 68 | https://whatsappapijs.web.app/classes/types.ContactUniqueComponent.html 69 | -------------------------------------------------------------------------------- /EXAMPLES/interactive.md: -------------------------------------------------------------------------------- 1 | # Interactive 2 | 3 | ## Reply Buttons 4 | 5 | ```ts 6 | import { 7 | Interactive, 8 | ActionButtons, 9 | Button, 10 | Body 11 | } from "whatsapp-api-js/messages"; 12 | 13 | const interactive_button_message = new Interactive( 14 | new ActionButtons( 15 | new Button("reply_1", "Hello"), 16 | new Button("reply_2", "World") 17 | ), 18 | new Body("Hello World") 19 | ); 20 | ``` 21 | 22 | ## Options list 23 | 24 | ```ts 25 | import { Interactive, ActionList, Row, Body } from "whatsapp-api-js/messages"; 26 | 27 | const interactive_list_message = new Interactive( 28 | new ActionList( 29 | "Button text", 30 | new ListSection( 31 | undefined, 32 | new Row("reply_1", "Hello", "Hello description"), 33 | new Row("reply_2", "World", "World description") 34 | ) 35 | ), 36 | new Body("Hello World") 37 | ); 38 | ``` 39 | 40 | ## Single Product 41 | 42 | ```ts 43 | import { 44 | Interactive, 45 | ActionProduct, 46 | CatalogProduct 47 | } from "whatsapp-api-js/messages"; 48 | 49 | const interactive_single_product_message = new Interactive( 50 | new ActionProduct(new CatalogProduct("product_id", "catalog_id")) 51 | ); 52 | ``` 53 | 54 | ## Multi Product 55 | 56 | ```ts 57 | import { 58 | Interactive, 59 | ActionProductList, 60 | ProductSection, 61 | Product 62 | } from "whatsapp-api-js/messages"; 63 | 64 | const interactive_multi_product_message = new Interactive( 65 | new ActionProductList( 66 | "catalog_id", 67 | new ProductSection( 68 | "Product section title", 69 | new Product("product_id"), 70 | new Product("product_id") 71 | ) 72 | ), 73 | new Body("Hello World"), 74 | new Header("Hello World Header") 75 | ); 76 | ``` 77 | 78 | ## Catalog 79 | 80 | ```ts 81 | import { 82 | Interactive, 83 | ActionCatalog, 84 | Product, 85 | Body 86 | } from "whatsapp-api-js/messages"; 87 | 88 | const interactive_catalog_message = new Interactive( 89 | new ActionCatalog(new Product("hello")), 90 | new Body("Hello World") 91 | ); 92 | ``` 93 | 94 | ## Call To Action 95 | 96 | ```ts 97 | import { Interactive, ActionCTA, Body } from "whatsapp-api-js/messages"; 98 | 99 | const interactive_catalog_message = new Interactive( 100 | new ActionCTA("Open Google", "https://google.com"), 101 | new Body("You should google it") 102 | ); 103 | ``` 104 | 105 | ## Navigate Flow 106 | 107 | ```ts 108 | import { Interactive, ActionFlow, Body } from "whatsapp-api-js/messages"; 109 | 110 | const interactive_navigate_flow_message = new Interactive( 111 | new ActionFlow({ 112 | flow_action: "navigate", 113 | flow_token: "5f9b3b4f-2b7a-4f4f-8f4f-4f4f4f4f4f4f", 114 | flow_name: "my_welcome_flow", // Can also use flow_id instead 115 | flow_cta: "Start the Flow!", 116 | mode: "published", 117 | flow_action_payload: { 118 | screen: "FIRST_SCREEN", 119 | data: { name: "John" } 120 | } 121 | }), 122 | new Body("How was your experience today?") 123 | ); 124 | ``` 125 | 126 | ## Data Exchange Flow 127 | 128 | ```ts 129 | import { Interactive, ActionFlow, Body } from "whatsapp-api-js/messages"; 130 | 131 | const interactive_data_exchange_flow_message = new Interactive( 132 | new ActionFlow({ 133 | flow_action: "data_exchange", 134 | flow_token: "5f9b3b4f-2b7a-4f4f-8f4f-4f4f4f4f4f4f", 135 | flow_name: "my_welcome_flow", // Can also use flow_id instead 136 | flow_cta: "Start the Flow!", 137 | mode: "published" 138 | }), 139 | new Body("Hello World") 140 | ); 141 | ``` 142 | 143 | ## Location Request 144 | 145 | ```ts 146 | import { Interactive, ActionLocation, Body } from "whatsapp-api-js/messages"; 147 | 148 | const interactive_catalog_message = new Interactive( 149 | new ActionLocation(), 150 | new Body("Show me where you live") 151 | ); 152 | ``` 153 | 154 | ## Payments and Address Request 155 | 156 | Check out [#154](https://github.com/Secreto31126/whatsapp-api-js/issues/154) for more information. 157 | 158 | ## Documentation 159 | 160 | https://whatsappapijs.web.app/classes/messages.Interactive.html 161 | https://whatsappapijs.web.app/classes/messages.ActionButtons.html 162 | https://whatsappapijs.web.app/classes/messages.ActionList.html 163 | https://whatsappapijs.web.app/classes/messages.ActionProduct.html 164 | https://whatsappapijs.web.app/classes/messages.ActionProductList.html 165 | https://whatsappapijs.web.app/classes/messages.ActionCatalog.html 166 | https://whatsappapijs.web.app/classes/messages.ActionCTA.html 167 | https://whatsappapijs.web.app/classes/messages.ActionNavigateFlow.html 168 | https://whatsappapijs.web.app/classes/messages.ActionDataExchangeFlow.html 169 | https://whatsappapijs.web.app/classes/messages.ActionLocation.html 170 | -------------------------------------------------------------------------------- /EXAMPLES/location.md: -------------------------------------------------------------------------------- 1 | # Location 2 | 3 | ## Coordinates 4 | 5 | ```ts 6 | import { Location } from "whatsapp-api-js/messages"; 7 | 8 | const location_message = new Location(0, 0); 9 | ``` 10 | 11 | ## Coordinates with name 12 | 13 | ```ts 14 | import { Location } from "whatsapp-api-js/messages"; 15 | 16 | const location_named_message = new Location(0, 0, "My Store", "Address"); 17 | ``` 18 | -------------------------------------------------------------------------------- /EXAMPLES/main.md: -------------------------------------------------------------------------------- 1 | # Main 2 | 3 | ## Simple Whatsapp creation 4 | 5 | ```ts 6 | import { WhatsAppAPI } from "whatsapp-api-js"; 7 | 8 | const Whatsapp = new WhatsAppAPI({ 9 | token: "", 10 | appSecret: "" 11 | }); 12 | ``` 13 | 14 | ## Using the setup helpers 15 | 16 | ```ts 17 | import { WhatsAppAPI } from "whatsapp-api-js"; 18 | import { Node18 } from "whatsapp-api-js/setup/node"; 19 | 20 | const Whatsapp = new WhatsAppAPI( 21 | Node18({ 22 | token: "", 23 | appSecret: "" 24 | }) 25 | ); 26 | ``` 27 | 28 | ## Using the middlewares 29 | 30 | ```ts 31 | import express from "express"; 32 | import { WhatsAppAPI } from "whatsapp-api-js/middleware/express"; 33 | 34 | const Whatsapp = new WhatsAppAPI({ 35 | token: "", 36 | appSecret: "", 37 | webhookVerifyToken: "" 38 | }); 39 | 40 | const app = express(); 41 | 42 | app.post("/webhook", async (req, res) => { 43 | res.sendStatus(await Whatsapp.handle_post(req)); 44 | }); 45 | ``` 46 | 47 | ## Sending a message 48 | 49 | ```ts 50 | import { Text } from "whatsapp-api-js/messages"; 51 | 52 | const text_message = new Text("Hello world!"); 53 | 54 | Whatsapp.sendMessage( 55 | "from (bot phoneID)", 56 | "to (phone number/wa_id)", 57 | text_message 58 | ); 59 | ``` 60 | 61 | ## Replying to a message 62 | 63 | ```ts 64 | import { Text } from "whatsapp-api-js/messages"; 65 | 66 | Whatsapp.on.message = async ({ reply }) => { 67 | await reply(new Text("Hello!")); 68 | }; 69 | ``` 70 | 71 | ## Documentation 72 | 73 | https://whatsappapijs.web.app/classes/WhatsAppAPI.WhatsAppAPI.html 74 | https://whatsappapijs.web.app/modules/setup.html 75 | https://whatsappapijs.web.app/modules/middleware.html 76 | https://whatsappapijs.web.app/modules/emitters.html 77 | -------------------------------------------------------------------------------- /EXAMPLES/media.md: -------------------------------------------------------------------------------- 1 | # Media 2 | 3 | ## Image 4 | 5 | ```ts 6 | import { Image } from "whatsapp-api-js/messages"; 7 | 8 | const image_message = new Image("https://i.imgur.com/4QfKuz1.png"); 9 | const image_id_message = new Image("12345678", true); 10 | const image_caption_message = new Image( 11 | "https://i.imgur.com/4QfKuz1.png", 12 | false, 13 | "Hello world!" 14 | ); 15 | ``` 16 | 17 | ## Video 18 | 19 | ```ts 20 | import { Video } from "whatsapp-api-js/messages"; 21 | 22 | const video_message = new Video("https://www.example.com/video.mp4"); 23 | const video_id_message = new Video("12345678", true); 24 | const video_caption_message = new Video( 25 | "https://www.example.com/video.mp4", 26 | false, 27 | "Hello world!" 28 | ); 29 | ``` 30 | 31 | ## Audio 32 | 33 | ```ts 34 | import { Audio } from "whatsapp-api-js/messages"; 35 | 36 | const audio_message = new Audio("https://www.example.com/audio.mp3"); 37 | const audio_id_message = new Audio("12345678", true); 38 | ``` 39 | 40 | ## Document 41 | 42 | ```ts 43 | import { Document } from "whatsapp-api-js/messages"; 44 | 45 | const document_message = new Document("https://www.example.com/document.pdf"); 46 | const document_id_message = new Document("12345678", true); 47 | const document_caption_message = new Document( 48 | "https://www.example.com/document.pdf", 49 | false, 50 | "Hello world!" 51 | ); 52 | const document_filename_message = new Document( 53 | "https://www.example.com/document.pdf", 54 | false, 55 | undefined, 56 | "a weird filename.pdf" 57 | ); 58 | ``` 59 | 60 | ## Sticker 61 | 62 | ```ts 63 | import { Sticker } from "whatsapp-api-js/messages"; 64 | 65 | const sticker_message = new Sticker("https://www.example.com/sticker.webp"); 66 | const sticker_id_message = new Sticker("12345678", true); 67 | ``` 68 | 69 | ## Documentation 70 | 71 | https://whatsappapijs.web.app/classes/messages.Media.html 72 | https://whatsappapijs.web.app/classes/messages.Image.html 73 | https://whatsappapijs.web.app/classes/messages.Video.html 74 | https://whatsappapijs.web.app/classes/messages.Audio.html 75 | https://whatsappapijs.web.app/classes/messages.Document.html 76 | https://whatsappapijs.web.app/classes/messages.Sticker.html 77 | -------------------------------------------------------------------------------- /EXAMPLES/reaction.md: -------------------------------------------------------------------------------- 1 | # Reaction 2 | 3 | ## Add reaction 4 | 5 | ```ts 6 | import { Reaction } from "whatsapp-api-js/messages"; 7 | 8 | const reaction_message = new Reaction("message_id", "👍"); 9 | ``` 10 | 11 | ## Remove reaction 12 | 13 | ```ts 14 | import { Reaction } from "whatsapp-api-js/messages"; 15 | 16 | const reaction_remove_message = new Reaction("message_id"); 17 | ``` 18 | 19 | ## Documentation 20 | 21 | https://whatsappapijs.web.app/classes/messages.Reaction.html 22 | -------------------------------------------------------------------------------- /EXAMPLES/template.md: -------------------------------------------------------------------------------- 1 | # Template 2 | 3 | ## Simple template with no variables 4 | 5 | ```ts 6 | import { Template } from "whatsapp-api-js/messages"; 7 | 8 | const template_message = new Template("template_name", "en"); 9 | ``` 10 | 11 | ## Simple template with simple variables in header and body 12 | 13 | ```ts 14 | import { 15 | Template, 16 | HeaderComponent, 17 | HeaderParameter, 18 | BodyComponent, 19 | BodyParameter, 20 | Currency, 21 | DateTime 22 | } from "whatsapp-api-js/messages"; 23 | 24 | const template_variables_message = new Template( 25 | "template_name", 26 | "en", 27 | new HeaderComponent( 28 | new HeaderParameter("Hello"), 29 | new HeaderParameter(new Currency(1.5 * 1000, "USD", "U$1.5")), 30 | new HeaderParameter(new DateTime("01/01/2023")) 31 | ), 32 | new BodyComponent( 33 | new BodyParameter("Hello"), 34 | new BodyParameter(new Currency(1.5 * 1000, "USD", "U$1.5")), 35 | new BodyParameter(new DateTime("01/01/2023")) 36 | ) 37 | ); 38 | ``` 39 | 40 | ## Simple template with header media 41 | 42 | ```ts 43 | import { 44 | Template, 45 | HeaderComponent, 46 | HeaderParameter, 47 | Video 48 | } from "whatsapp-api-js/messages"; 49 | 50 | const template_media_message = new Template( 51 | "template_name", 52 | "en", 53 | new HeaderComponent( 54 | new HeaderParameter( 55 | // Can also be image, document, location or catalog product 56 | new Video("https://www.w3schools.com/html/mov_bbb.mp4") 57 | ) 58 | ) 59 | ); 60 | ``` 61 | 62 | ## Complex template with reply buttons 63 | 64 | ```ts 65 | import { Template, PayloadComponent } from "whatsapp-api-js/messages"; 66 | 67 | const template_reply_buttons_message = new Template( 68 | "template_name", 69 | "en", 70 | new PayloadComponent("reply_1"), 71 | new PayloadComponent("reply_2") 72 | ); 73 | ``` 74 | 75 | ## Complex template with call to action urls 76 | 77 | ```ts 78 | import { Template, URLComponent } from "whatsapp-api-js/messages"; 79 | 80 | const template_call_to_action_message = new Template( 81 | "template_name", 82 | "en", 83 | new URLComponent("?user_id=123"), 84 | new URLComponent("?user_id=456") 85 | ); 86 | ``` 87 | 88 | ## Complex template with copy coupon button 89 | 90 | ```ts 91 | import { Template, CopyComponent } from "whatsapp-api-js/messages"; 92 | 93 | const template_copy_coupon_message = new Template( 94 | "template_name", 95 | "en", 96 | new CopyComponent("PROMO10") 97 | ); 98 | ``` 99 | 100 | ## Complex template with flow component 101 | 102 | ```ts 103 | import { Template, FlowComponent } from "whatsapp-api-js/messages"; 104 | 105 | const template_flow_message = new Template( 106 | "template_name", 107 | "en", 108 | new FlowComponent("rating", { user: "name" }) 109 | ); 110 | ``` 111 | 112 | ## Complex template with combination of buttons 113 | 114 | ```ts 115 | import { 116 | Template, 117 | CopyComponent, 118 | URLComponent, 119 | PayloadComponent 120 | } from "whatsapp-api-js/messages"; 121 | 122 | const template_mixed_buttons_message = new Template( 123 | "template_name", 124 | "en", 125 | new CopyComponent("PROMO10"), 126 | new URLComponent("?code=PROMO10"), 127 | new PayloadComponent("send_catalog") 128 | ); 129 | ``` 130 | 131 | ## Complex template with catalog 132 | 133 | ```ts 134 | import { Template, CatalogComponent, Product } from "whatsapp-api-js/messages"; 135 | 136 | const template_catalog_message = new Template( 137 | "template_name", 138 | "en", 139 | new CatalogComponent(new Product("thumbnail")) 140 | ); 141 | ``` 142 | 143 | ## Complex template with Single-Product Message 144 | 145 | ```ts 146 | import { 147 | Template, 148 | HeaderComponent, 149 | HeaderParameter, 150 | CatalogProduct 151 | } from "whatsapp-api-js/messages"; 152 | 153 | const template_single_product_message = new Template( 154 | "template_name", 155 | "en", 156 | new HeaderComponent( 157 | new HeaderParameter(new CatalogProduct("product_id", "catalog_id")) 158 | ) 159 | ); 160 | ``` 161 | 162 | ## Complex template with Multi-Product Message 163 | 164 | ```ts 165 | import { 166 | Template, 167 | MPMComponent, 168 | Product, 169 | ProductSection 170 | } from "whatsapp-api-js/messages"; 171 | 172 | const template_multi_product_message = new Template( 173 | "template_name", 174 | "en", 175 | new MPMComponent( 176 | new Product("thumbnail"), 177 | new ProductSection( 178 | "Section Title", 179 | new Product("product_1"), 180 | new Product("product_2") 181 | ), 182 | new ProductSection( 183 | "Another Section", 184 | new Product("product_3"), 185 | new Product("product_4") 186 | ) 187 | ) 188 | ); 189 | ``` 190 | 191 | ## Complex template with Carousel 192 | 193 | ```ts 194 | import { 195 | Template, 196 | BodyComponent, 197 | CarouselComponent, 198 | CarouselCard, 199 | Image, 200 | URLComponent 201 | } from "whatsapp-api-js/messages"; 202 | 203 | const template_carousel_message = new Template( 204 | "template_name", 205 | "en", 206 | new BodyComponent(new BodyParameter("PROMO10")), 207 | new CarouselComponent( 208 | new CarouselCard( 209 | new Image("image_url"), 210 | new URLComponent("?code=PROMO10&product=1") 211 | ), 212 | new CarouselCard( 213 | new Image("image_url"), 214 | new URLComponent("?code=PROMO10&product=2") 215 | ) 216 | ) 217 | ); 218 | ``` 219 | 220 | ## Complex template with Product Carousel 221 | 222 | ```ts 223 | import { 224 | Template, 225 | BodyComponent, 226 | CarouselComponent, 227 | CarouselCard, 228 | CatalogProduct 229 | } from "whatsapp-api-js/messages"; 230 | 231 | const template_product_carousel_message = new Template( 232 | "template_name", 233 | "en", 234 | new BodyComponent(new BodyParameter("PROMO10")), 235 | new CarouselComponent( 236 | new CarouselCard( 237 | new CatalogProduct("product_id_1", "catalog_id"), 238 | new URLComponent("?code=PROMO10&product=1") 239 | ), 240 | new CarouselCard( 241 | new CatalogProduct("product_id_2", "catalog_id"), 242 | new URLComponent("?code=PROMO10&product=2") 243 | ) 244 | ) 245 | ); 246 | ``` 247 | 248 | ## Complex template with Limited-Time Offer 249 | 250 | ```ts 251 | import { 252 | Template, 253 | LTOComponent, 254 | URLComponent, 255 | CopyComponent 256 | } from "whatsapp-api-js/messages"; 257 | 258 | const template_limited_time_offer_message = new Template( 259 | "template_name", 260 | "en", 261 | new LTOComponent(1696622508595), 262 | new CopyComponent("PROMO10"), 263 | new URLComponent("?code=PROMO10&product=1") 264 | ); 265 | ``` 266 | 267 | ## OTP prefab template 268 | 269 | ```ts 270 | import { Template } from "whatsapp-api-js/messages"; 271 | 272 | const template_otp_message = Template.OTP("template_name", "en", "123456"); 273 | ``` 274 | 275 | ## Documentation 276 | 277 | https://whatsappapijs.web.app/classes/messages.Template.html 278 | https://whatsappapijs.web.app/classes/messages.HeaderComponent.html 279 | https://whatsappapijs.web.app/classes/messages.BodyComponent.html 280 | https://whatsappapijs.web.app/classes/messages.URLComponent.html 281 | https://whatsappapijs.web.app/classes/messages.PayloadComponent.html 282 | https://whatsappapijs.web.app/classes/messages.CopyComponent.html 283 | https://whatsappapijs.web.app/classes/messages.FlowComponent.html 284 | https://whatsappapijs.web.app/classes/messages.CatalogComponent.html 285 | https://whatsappapijs.web.app/classes/messages.MPMComponent.html 286 | https://whatsappapijs.web.app/classes/messages.CarouselComponent.html 287 | https://whatsappapijs.web.app/classes/messages.LTOComponent.html 288 | -------------------------------------------------------------------------------- /EXAMPLES/text.md: -------------------------------------------------------------------------------- 1 | # Text 2 | 3 | ## Simple text 4 | 5 | ```ts 6 | import { Text } from "whatsapp-api-js/messages"; 7 | 8 | const text_message = new Text("Hello world!"); 9 | ``` 10 | 11 | ## Text with URL preview 12 | 13 | ```ts 14 | import { Text } from "whatsapp-api-js/messages"; 15 | 16 | const text_preview_message = new Text("Hello URL!", true); 17 | ``` 18 | 19 | ## Documentation 20 | 21 | https://whatsappapijs.web.app/classes/messages.Text.html 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Tomás Raiti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # whatsapp-api-js v5 2 | 3 | [![npm](https://img.shields.io/npm/v/whatsapp-api-js?color=4ccc1c)](https://www.npmjs.com/package/whatsapp-api-js) 4 | [![Contributors](https://img.shields.io/github/all-contributors/Secreto31126/whatsapp-api-js)](#contributors) 5 | 6 | A TypeScript server agnostic Whatsapp's Official API framework. 7 | 8 | ## List of contents 9 | 10 | - [whatsapp-api-js v5](#whatsapp-api-js-v5) 11 | - [List of contents](#list-of-contents) 12 | - [Set up](#set-up) 13 | - [Examples and Tutorials](#examples-and-tutorials) 14 | - [Types](#types) 15 | - [Changelog](#changelog) 16 | - [Documentation](#documentation) 17 | - [Contributors](#contributors) 18 | - [Contributions](#contributions) 19 | - [Breaking changes](#breaking-changes) 20 | - [Beta releases](#beta-releases) 21 | 22 | ## Set up 23 | 24 | Before all, you will need a Meta Bussiness App with WhatsApp API activated. You 25 | can create your first app following 26 | [this steps](https://developers.facebook.com/docs/whatsapp/cloud-api/get-started). 27 | 28 | - Get the API token, either a temporal or a 29 | [permanent one](https://developers.facebook.com/docs/whatsapp/business-management-api/get-started). 30 | - Get your App secret from the dashboard in App Settings > Basic > App Secret. 31 | - More in-depth information on how to set and retrieve this values is available 32 | at 33 | [the module documentation](https://whatsappapijs.web.app/types/types.TheBasicConstructorArguments.html) 34 | 35 | You can now install the module using npm: 36 | 37 | ```sh 38 | npm install whatsapp-api-js 39 | ``` 40 | 41 | Which will let you write code like this: 42 | 43 | ```js 44 | import { WhatsAppAPI } from "whatsapp-api-js"; 45 | import { Document, Image, Text } from "whatsapp-api-js/messages"; 46 | 47 | // Kind reminder to not hardcode your token and secret 48 | const TOKEN = "YOUR_TOKEN"; 49 | const APP_SECRET = "YOUR_SECRET"; 50 | 51 | /** @type WhatsAppAPI */ 52 | const Whatsapp = new WhatsAppAPI({ token: TOKEN, appSecret: APP_SECRET }); 53 | 54 | // Assuming post is called on a POST request to your server 55 | async function post(e) { 56 | // Too long? Read https://whatsappapijs.web.app/modules/middleware.html 57 | return await Whatsapp.post( 58 | JSON.parse(e.data), 59 | e.data, 60 | e.headers["x-hub-signature-256"] 61 | ); 62 | } 63 | 64 | Whatsapp.on.message = async ({ phoneID, from, message, name, reply }) => { 65 | console.log( 66 | `User ${name} (${from}) sent to bot ${phoneID} ${JSON.stringify( 67 | message 68 | )}` 69 | ); 70 | 71 | let response; 72 | 73 | if (message.type === "text") { 74 | response = await reply( 75 | new Text(`*${name}* said:\n\n${message.text.body}`), 76 | true 77 | ); 78 | } 79 | 80 | if (message.type === "image") { 81 | response = await reply( 82 | new Image(message.image.id, true, `Nice photo, ${name}`) 83 | ); 84 | } 85 | 86 | if (message.type === "document") { 87 | response = await reply( 88 | new Document(message.document.id, true, undefined, "Our document") 89 | ); 90 | } 91 | 92 | console.log( 93 | response ?? 94 | "There are more types of messages, such as contacts, " + 95 | "locations, templates, interactive, reactions and " + 96 | "all the other media types." 97 | ); 98 | 99 | Whatsapp.markAsRead(phoneID, message.id); 100 | 101 | return 200; 102 | }; 103 | 104 | Whatsapp.on.sent = ({ phoneID, to, message }) => { 105 | console.log(`Bot ${phoneID} sent to user ${to} ${message}`); 106 | }; 107 | ``` 108 | 109 | To receive the messages updates, you must set-up the webhook at your Meta app. 110 | Back in the dashboard, click on WhatsApp > Settings, write down your webhook 111 | URL, and make sure to subscribe to the messages event. You will also be asked 112 | for a Verify Token. This can be any string you want. 113 | 114 | The package also includes a GET handler for the webhook authentication: 115 | 116 | ```js 117 | import { WhatsAppAPI } from "whatsapp-api-js"; 118 | 119 | const TOKEN = "YOUR_TOKEN"; 120 | const APP_SECRET = "YOUR_SECRET"; 121 | const VERIFY_TOKEN = "YOUR_VERIFY_TOKEN"; 122 | 123 | const Whatsapp = new WhatsAppAPI({ 124 | token: TOKEN, 125 | appSecret: APP_SECRET, 126 | webhookVerifyToken: VERIFY_TOKEN 127 | }); 128 | 129 | // Assuming get is called on a GET request to your server 130 | function get(e) { 131 | // Too long!? Read https://whatsappapijs.web.app/modules/middleware.html 132 | return Whatsapp.get(e.query); 133 | } 134 | ``` 135 | 136 | And that's it! Now you have a functioning Whatsapp Bot connected to your server. 137 | For more information on the setup process for specific runtimes and frameworks, 138 | check out the 139 | [Environments.md file](https://github.com/Secreto31126/whatsapp-api-js/blob/main/ENVIRONMENTS.md). 140 | 141 | ## Examples and Tutorials 142 | 143 | There are a few examples that cover how to create each type of message, and how 144 | to use the basic methods of the library. 145 | 146 | Check them out in the 147 | [examples folder](https://github.com/Secreto31126/whatsapp-api-js/blob/main/EXAMPLES/). 148 | 149 | ## Types 150 | 151 | The library is fully typed. Most types are available by importing `/types` or 152 | `/emitters` files: 153 | 154 | ```ts 155 | import { GetParams, PostData } from "whatsapp-api-js/types"; 156 | import { OnMessage, OnSent, OnStatus } from "whatsapp-api-js/emitters"; 157 | ``` 158 | 159 | ## Changelog 160 | 161 | To know what changed between updates, check out the 162 | [releases on Github](https://github.com/Secreto31126/whatsapp-api-js/releases). 163 | 164 | ## Documentation 165 | 166 | The latest release documentation is available at 167 | [whatsappapijs.web.app](https://whatsappapijs.web.app/), and previous versions 168 | are archived at 169 | [secreto31126.github.io/whatsapp-api-js](https://secreto31126.github.io/whatsapp-api-js/). 170 | 171 | ## Contributors 172 | 173 | [Emoji key ✨](https://allcontributors.org/docs/en/emoji-key) 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 |
Diego Carrillo
Diego Carrillo

💵
Omar
Omar

🐛 💵
Rahul Lanjewar
Rahul Lanjewar

💻 📖 🤔
Felix Arjuna
Felix Arjuna

🐛 🛡️
188 | 189 | 190 | 191 | 192 | 193 | 194 | ## Contributions 195 | 196 | If you have some free time and really want to improve the library or fix dumb 197 | bugs, feel free to read 198 | [CONTRIBUTING.md file](https://github.com/Secreto31126/whatsapp-api-js/blob/main/CONTRIBUTING.md). 199 | 200 | ## Breaking changes 201 | 202 | You can get a full list of breaking changes in the 203 | [BREAKING.md file](https://github.com/Secreto31126/whatsapp-api-js/blob/main/BREAKING.md). 204 | 205 | ## Beta releases 206 | 207 | Install the latest beta release with `npm install whatsapp-api-js@beta`. As any 208 | beta, it is 110% likely to break. I also use this tag to test npm releases. Use 209 | it at your own risk. 210 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 5 | :white_check_mark: | 8 | | < 5 | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | To report a vulnerability, create a new issue on GitHub, select 13 | "Report a vulnerability" option, complete the form and submit. 14 | A reply will be sent ASAP, and if accepted, an update will be 15 | released soon after. 16 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | import { build } from "esbuild"; 2 | import { glob } from "glob"; 3 | 4 | await build({ 5 | entryPoints: await glob("src/**/*.ts", { ignore: ["src/standalone.ts"] }), 6 | sourcemap: true, 7 | platform: "node", 8 | format: "esm", 9 | outdir: "lib" 10 | }); 11 | -------------------------------------------------------------------------------- /docs_statics/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Not Found 7 | 8 | 79 | 80 | 81 |
82 |

404

83 |

Page Not Found

84 |

85 | The specified file was not found on this website. Please check 86 | the URL for mistakes and try again. 87 |

88 |

Why am I seeing this?

89 |

90 | This page was generated by the Firebase Command-Line Interface. 91 | To modify it, edit the 404.html file in your 92 | project's configured public directory. 93 |

94 |
95 | 96 | 97 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import ts from "typescript-eslint"; 3 | import prettier from "eslint-config-prettier"; 4 | import tsdoc from "eslint-plugin-tsdoc"; 5 | import globals from "globals"; 6 | 7 | export default ts.config( 8 | js.configs.recommended, 9 | ...ts.configs.recommended, 10 | prettier, 11 | { 12 | languageOptions: { 13 | globals: globals.node 14 | } 15 | }, 16 | { 17 | files: ["src/*"], 18 | 19 | plugins: { 20 | tsdoc 21 | }, 22 | 23 | rules: { 24 | "tsdoc/syntax": "warn" 25 | } 26 | }, 27 | { 28 | files: ["test/*"], 29 | 30 | languageOptions: { 31 | globals: { 32 | ...globals.mocha 33 | } 34 | }, 35 | 36 | rules: { 37 | "@typescript-eslint/no-require-imports": "off" 38 | } 39 | } 40 | ); 41 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "docs", 4 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whatsapp-api-js", 3 | "version": "5.3.0", 4 | "author": "Secreto31126", 5 | "description": "A TypeScript server agnostic Whatsapp's Official API framework", 6 | "license": "MIT", 7 | "type": "module", 8 | "sideEffects": false, 9 | "engines": { 10 | "node": ">=16" 11 | }, 12 | "files": [ 13 | "lib/**/*" 14 | ], 15 | "types": "lib/index.d.ts", 16 | "module": "lib/index.js", 17 | "exports": { 18 | ".": { 19 | "types": "./lib/index.d.ts", 20 | "import": "./lib/index.js", 21 | "require": "./lib/index.js" 22 | }, 23 | "./messages": { 24 | "types": "./lib/messages/index.d.ts", 25 | "import": "./lib/messages/index.js", 26 | "require": "./lib/messages/index.js" 27 | }, 28 | "./messages/*": { 29 | "types": "./lib/messages/*.d.ts", 30 | "import": "./lib/messages/*.js", 31 | "require": "./lib/messages/*.js" 32 | }, 33 | "./setup": null, 34 | "./setup/index": null, 35 | "./setup/*": { 36 | "types": "./lib/setup/*.d.ts", 37 | "import": "./lib/setup/*.js", 38 | "require": "./lib/setup/*.js" 39 | }, 40 | "./middleware": null, 41 | "./middleware/index": null, 42 | "./middleware/*": { 43 | "types": "./lib/middleware/*.d.ts", 44 | "import": "./lib/middleware/*.js", 45 | "require": "./lib/middleware/*.js" 46 | }, 47 | "./emitters": { 48 | "types": "./lib/emitters.d.ts" 49 | }, 50 | "./types": { 51 | "types": "./lib/types.d.ts", 52 | "import": "./lib/types.js", 53 | "require": "./lib/types.js" 54 | }, 55 | "./errors": { 56 | "types": "./lib/errors.d.ts", 57 | "import": "./lib/errors.js", 58 | "require": "./lib/errors.js" 59 | } 60 | }, 61 | "//": [ 62 | "https://github.com/andrewbranch/example-subpath-exports-ts-compat/blob/main/examples/node_modules/types-versions-wildcards/package.json", 63 | "Without this, IntelliSense will throw an error when importing subpaths ONLY on JS files, although it does run fine with Node.", 64 | "Even more stunishing: if included, IntelliSense works even BETTER than with TS." 65 | ], 66 | "typesVersions": { 67 | "*": { 68 | ".": [ 69 | "lib/index.d.ts" 70 | ], 71 | "messages": [ 72 | "lib/messages/index.d.ts" 73 | ], 74 | "messages/*": [ 75 | "lib/messages/*.d.ts" 76 | ], 77 | "setup/index": [], 78 | "setup/*": [ 79 | "lib/setup/*.d.ts" 80 | ], 81 | "middleware/index": [], 82 | "middleware/*": [ 83 | "lib/middleware/*.d.ts" 84 | ], 85 | "emitters": [ 86 | "lib/emitters.d.ts" 87 | ], 88 | "types": [ 89 | "lib/types.d.ts" 90 | ], 91 | "errors": [ 92 | "lib/errors.d.ts" 93 | ] 94 | } 95 | }, 96 | "scripts": { 97 | "build": "npm run build:code && npm run build:types", 98 | "build:dev": "npm run build:code:dev && npm run build:types:dev", 99 | "build:code": "node -r dotenv/config build.js", 100 | "build:code:dev": "npm run build:code", 101 | "build:types": "tsc", 102 | "build:types:dev": "tsc --noCheck", 103 | "build:standalone": "esbuild src/standalone.ts --outfile=lib/standalone.js --bundle --platform=neutral --target=node10 --minify-syntax", 104 | "test": "node --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout", 105 | "test:build": "npm run build && npm run test", 106 | "test:watch": "node --test --watch", 107 | "lint": "eslint src", 108 | "lint:fix": "eslint src --fix", 109 | "prettier": "prettier --check .", 110 | "prettier:write": "prettier --write .", 111 | "coverage": "c8 node --test --test-reporter=@reporters/silent", 112 | "document": "typedoc && cp -r docs_statics/* docs/", 113 | "clear": "rm -rf docs/ docs_temp/ lib/ coverage/" 114 | }, 115 | "keywords": [ 116 | "whatsapp", 117 | "cloud", 118 | "api", 119 | "framework", 120 | "whatsapp-cloud", 121 | "cloud-api", 122 | "whatsapp-cloud-api", 123 | "whatsapp-business", 124 | "whatsapp-business-api", 125 | "bot", 126 | "whatsapp-bot", 127 | "chatbot", 128 | "bot-framework", 129 | "typescript", 130 | "server-agnostic", 131 | "nodejs", 132 | "deno", 133 | "bun", 134 | "bot-api", 135 | "whatsapp-api", 136 | "business-api" 137 | ], 138 | "repository": { 139 | "type": "git", 140 | "url": "https://github.com/Secreto31126/whatsapp-api-js.git" 141 | }, 142 | "devDependencies": { 143 | "@adonisjs/http-server": "7.6.0", 144 | "@azure/functions": "^4.7.0", 145 | "@eslint/js": "9.25.1", 146 | "@reporters/github": "1.7.2", 147 | "@reporters/silent": "1.2.7", 148 | "@types/express": "5.0.1", 149 | "@types/node": "18.19.86", 150 | "@vercel/node": "5.1.14", 151 | "all-contributors-cli": "6.26.1", 152 | "c8": "10.1.3", 153 | "dotenv": "16.5.0", 154 | "esbuild": "0.25.3", 155 | "eslint": "9.25.1", 156 | "eslint-config-prettier": "10.1.2", 157 | "eslint-plugin-tsdoc": "0.4.0", 158 | "glob": "11.0.2", 159 | "globals": "16.0.0", 160 | "prettier": "3.5.3", 161 | "sinon": "20.0.0", 162 | "typedoc": "0.28.3", 163 | "typescript": "5.8.3", 164 | "typescript-eslint": "8.31.0", 165 | "undici": "7.8.0" 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/emitters.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ClientMessage, 3 | ClientMessageRequest, 4 | ClientTypingIndicators, 5 | ServerMessage, 6 | ServerMessageResponse, 7 | ServerConversation, 8 | ServerPricing, 9 | ServerError, 10 | PostData 11 | } from "./types.d.ts"; 12 | import type { WhatsAppAPI } from "./index.d.ts"; 13 | import type { MaybePromise } from "./utils.d.ts"; 14 | 15 | /** 16 | * Callback for "sent" event 17 | * 18 | * @public 19 | * @param args - The arguments object 20 | */ 21 | export type OnSent = (args: OnSentArgs) => unknown; 22 | 23 | /** 24 | * @public 25 | */ 26 | export type OnSentArgs = { 27 | /** 28 | * The bot's phoneID from where the message was sent 29 | */ 30 | phoneID: string; 31 | /** 32 | * The user's phone number 33 | */ 34 | to: string; 35 | /** 36 | * The message type 37 | */ 38 | type: string; 39 | /** 40 | * The message object 41 | */ 42 | message: ClientMessage; 43 | /** 44 | * The object sent to the server 45 | */ 46 | request: ClientMessageRequest; 47 | /** 48 | * The message id, undefined if parsed is set to false 49 | */ 50 | id?: string; 51 | /** 52 | * If true, the message send was delayed until quality can be validated and it will 53 | * either be sent or dropped at this point. Undefined if parsed is set to false or 54 | * the message_status property is not present in the response. 55 | */ 56 | held_for_quality_assessment?: boolean; 57 | /** 58 | * The parsed response from the server, undefined if parsed is set to false 59 | */ 60 | response?: ServerMessageResponse; 61 | /** 62 | * Utility function for offloading code from the main thread, 63 | * useful for long running tasks such as AI generation 64 | */ 65 | offload: typeof WhatsAppAPI.offload; 66 | /** 67 | * The WhatsAppAPI instance that emitted the event 68 | */ 69 | Whatsapp: InstanceType; 70 | }; 71 | 72 | /** 73 | * Callback for "message" event 74 | * 75 | * @public 76 | * @template Returns - The return type of the callback, defined by WhatsAppAPI generic parameter 77 | * @param args - The arguments object 78 | */ 79 | export type OnMessage = (args: OnMessageArgs) => MaybePromise; 80 | 81 | /** 82 | * @public 83 | */ 84 | export type OnMessageArgs = { 85 | /** 86 | * The bot's phoneID 87 | */ 88 | phoneID: string; 89 | /** 90 | * The user's phone number 91 | */ 92 | from: string; 93 | /** 94 | * The messages object 95 | */ 96 | message: ServerMessage; 97 | /** 98 | * The username 99 | */ 100 | name?: string; 101 | /** 102 | * The raw data from the API 103 | */ 104 | raw: PostData; 105 | /** 106 | * A method to easily reply to the user who sent the message 107 | * 108 | * @param response - The message to send as a reply 109 | * @param context - Wether to mention the current message, defaults to false 110 | * @param biz_opaque_callback_data - An arbitrary 512B string, useful for tracking 111 | * @returns The {@link WhatsAppAPI.sendMessage} return value 112 | */ 113 | reply: ( 114 | response: ClientMessage, 115 | context?: boolean, 116 | biz_opaque_callback_data?: string 117 | ) => Promise; 118 | /** 119 | * Mark the received message as read, and optionally display a typing indicator 120 | * 121 | * @param indicator - The type of reply indicator 122 | * @returns The {@link WhatsAppAPI.markAsRead} return value 123 | */ 124 | received: ( 125 | indicator?: ClientTypingIndicators 126 | ) => ReturnType; 127 | /** 128 | * Block the user who sent the message 129 | * 130 | * @returns The {@link WhatsAppAPI.blockUser} return value 131 | */ 132 | block: () => ReturnType; 133 | /** 134 | * Utility function for offloading code from the main thread, 135 | * useful for long running tasks such as AI generation 136 | */ 137 | offload: typeof WhatsAppAPI.offload; 138 | /** 139 | * The WhatsAppAPI instance that emitted the event 140 | */ 141 | Whatsapp: InstanceType; 142 | }; 143 | 144 | /** 145 | * Callback for "status" event 146 | * 147 | * @public 148 | * @template Returns - The return type of the callback, defined by WhatsAppAPI generic parameter 149 | * @param args - The arguments object 150 | */ 151 | export type OnStatus = (args: OnStatusArgs) => MaybePromise; 152 | 153 | /** 154 | * @public 155 | */ 156 | export type OnStatusArgs = { 157 | /** 158 | * The bot's phoneID 159 | */ 160 | phoneID: string; 161 | /** 162 | * The user's phone number 163 | */ 164 | phone: string; 165 | /** 166 | * The message status 167 | */ 168 | status: string; 169 | /** 170 | * The message ID 171 | */ 172 | id: string; 173 | /** 174 | * The message timestamp 175 | */ 176 | timestamp: string; 177 | /** 178 | * The conversation object 179 | */ 180 | conversation?: ServerConversation; 181 | /** 182 | * The pricing object 183 | */ 184 | pricing?: ServerPricing; 185 | /** 186 | * The error object 187 | */ 188 | error?: ServerError; 189 | /** 190 | * Arbitrary string included in sent messages 191 | */ 192 | biz_opaque_callback_data?: string; 193 | /** 194 | * The raw data from the API 195 | */ 196 | raw: PostData; 197 | /** 198 | * Utility function for offloading code from the main thread, 199 | * useful for long running tasks such as AI generation 200 | */ 201 | offload: typeof WhatsAppAPI.offload; 202 | /** 203 | * The WhatsAppAPI instance that emitted the event 204 | */ 205 | Whatsapp: InstanceType; 206 | }; 207 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module errors 3 | * 4 | * @description 5 | * This module contains the custom errors that are thrown by the 6 | * {@link WhatsAppAPI.get} and {@link WhatsAppAPI.post} methods. 7 | * 8 | * I did my best to explain why each error happens, include examples, 9 | * a few tips, and links to sources for further reading. 10 | * 11 | * This file is 300 lines of docs and the remaining is the actual code. 12 | * So yeah, enjoy reading :] 13 | */ 14 | 15 | // This import makes the docs' links work 16 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 17 | import type { WhatsAppAPI } from "./index.d.ts"; 18 | 19 | /** 20 | * The library's base exception class. 21 | */ 22 | export abstract class WhatsAppAPIError extends Error { 23 | /** 24 | * The HTTP status code of the error 25 | */ 26 | readonly httpStatus: number; 27 | 28 | /** 29 | * @internal 30 | */ 31 | constructor(message: string, httpStatus: number) { 32 | super(message); 33 | this.name = "WhatsAppAPIError"; 34 | this.httpStatus = httpStatus; 35 | } 36 | 37 | protected url(name: string) { 38 | return `https://whatsappapijs.web.app/classes/errors.${name}.html`; 39 | } 40 | 41 | /** 42 | * @returns The URL to the error's documentation 43 | */ 44 | abstract get docs(): string; 45 | } 46 | 47 | /** 48 | * Thrown when the request body is missing 49 | * 50 | * @description 51 | * In order to validate the request, the raw body (original string) of the request is required to do the signature verification. 52 | * 53 | * If you are using a middleware, make sure you aren't consuming the request body before the API can access it. 54 | * Otherwise, feel free to open an issue on {@link https://github.com/Secreto31126/whatsapp-api-js/issues | GitHub}. 55 | * 56 | * @example 57 | * ```ts 58 | * await whatsapp.post(JSON.parse(req.body), req.body, req.headers.get("x-hub-signature-256")); 59 | * ``` 60 | * 61 | * @see https://whatsappapijs.web.app/classes/WhatsAppAPI.WhatsAppAPI.html#post 62 | */ 63 | export class WhatsAppAPIMissingRawBodyError extends WhatsAppAPIError { 64 | /** 65 | * @internal 66 | */ 67 | constructor() { 68 | super("Missing raw body", 400); 69 | } 70 | 71 | /** 72 | * @override 73 | */ 74 | get docs() { 75 | return this.url("WhatsAppAPIMissingRawBodyError"); 76 | } 77 | } 78 | 79 | /** 80 | * Thrown when the request's signature is missing 81 | * 82 | * @description 83 | * In order to validate the request, the `x-hub-signature-256` header is required to do the signature verification. 84 | * 85 | * If you are NOT using a middleware, make sure you are passing the headers correctly (check case sensitivity). 86 | * Otherwise, feel free to open an issue on {@link https://github.com/Secreto31126/whatsapp-api-js/issues | GitHub}. 87 | * 88 | * @example 89 | * ```ts 90 | * await whatsapp.post(JSON.parse(req.body), req.body, req.headers.get("x-hub-signature-256")); 91 | * ``` 92 | * 93 | * @see https://whatsappapijs.web.app/classes/WhatsAppAPI.WhatsAppAPI.html#post 94 | */ 95 | export class WhatsAppAPIMissingSignatureError extends WhatsAppAPIError { 96 | /** 97 | * @internal 98 | */ 99 | constructor() { 100 | super("Missing signature", 401); 101 | } 102 | 103 | /** 104 | * @override 105 | */ 106 | get docs() { 107 | return this.url("WhatsAppAPIMissingSignatureError"); 108 | } 109 | } 110 | 111 | /** 112 | * Thrown when the App Secret isn't provided in the constructor 113 | * 114 | * @description 115 | * The App Secret is a private key that is used to verify the authenticity of the incoming requests. 116 | * It can be found in your Meta's app dashboard, inside App Settings -\> Basic. 117 | * 118 | * @example 119 | * ```ts 120 | * new WhatsAppAPI({ 121 | * appSecret: "your-app-secret", 122 | * // other options 123 | * }); 124 | * ``` 125 | * 126 | * @see https://whatsappapijs.web.app/types/types.TheBasicConstructorArguments.html 127 | */ 128 | export class WhatsAppAPIMissingAppSecretError extends WhatsAppAPIError { 129 | /** 130 | * @internal 131 | */ 132 | constructor() { 133 | super("Missing app secret", 500); 134 | } 135 | 136 | /** 137 | * @override 138 | */ 139 | get docs() { 140 | return this.url("WhatsAppAPIMissingAppSecretError"); 141 | } 142 | } 143 | 144 | /** 145 | * Thrown when the `crypto.subtle` API isn't available in the current environment 146 | * 147 | * @description 148 | * The `crypto.subtle` API is required to verify the signature of the incoming requests. 149 | * However, it isn't available in all environments. If your environment doesn't support it, 150 | * you can provide a ponyfill for it in the `ponyfill.subtle` option of the `WhatsAppAPI` 151 | * constructor. 152 | * 153 | * @example 154 | * ```ts 155 | * new WhatsAppAPI({ 156 | * appSecret: "your-app-secret", 157 | * ponyfill: { 158 | * subtle: my_custom_crypto.subtle 159 | * }, 160 | * // other options 161 | * }); 162 | * ``` 163 | * 164 | * @see https://whatsappapijs.web.app/types/types.TheBasicConstructorArguments.html 165 | * @see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto (specifically, `importKey` and `sign` methods) 166 | */ 167 | export class WhatsAppAPIMissingCryptoSubtleError extends WhatsAppAPIError { 168 | /** 169 | * @internal 170 | */ 171 | constructor() { 172 | super("Missing crypto subtle", 501); 173 | } 174 | 175 | /** 176 | * @override 177 | */ 178 | get docs() { 179 | return this.url("WhatsAppAPIMissingCryptoSubtleError"); 180 | } 181 | } 182 | 183 | /** 184 | * Thrown when the signature provided in the request's headers isn't valid 185 | * 186 | * @description 187 | * The signature provided in the request's headers isn't valid. 188 | * Either they are hacking you, or your App Secret is invalid. 189 | * 190 | * Make sure you provided the correct app secret on initialization. 191 | * It can be found in your Meta's app dashboard, inside App Settings -\> Basic. 192 | * 193 | * @example 194 | * ```ts 195 | * new WhatsAppAPI({ 196 | * appSecret: "your-app-secret", 197 | * // other options 198 | * }); 199 | * ``` 200 | * 201 | * @description 202 | * It might also be possible you didn't provide the correct request header 203 | * for the signature (x-hub-signature-256). If you are using a middleware and 204 | * this seems to be the case, consider opening an issue on 205 | * {@link https://github.com/Secreto31126/whatsapp-api-js/issues | GitHub}. 206 | * 207 | * ⚠ If you are just testing the API, you can disable the signature verification 208 | * by setting the `secure` option to `false` on the `WhatsAppAPI` constructor. 209 | * Obviously, this is not recommended for production. 210 | * 211 | * @example 212 | * ```ts 213 | * new WhatsAppAPI({ 214 | * // appSecret: "Not required", 215 | * secure: false, 216 | * // other options 217 | * }); 218 | * ``` 219 | * 220 | * @see https://whatsappapijs.web.app/classes/WhatsAppAPI.WhatsAppAPI.html#post 221 | * @see https://whatsappapijs.web.app/types/types.TheBasicConstructorArguments.html 222 | * @see https://developer.mozilla.org/docs/Web/HTTP/Headers 223 | */ 224 | export class WhatsAppAPIFailedToVerifyError extends WhatsAppAPIError { 225 | /** 226 | * @internal 227 | */ 228 | constructor() { 229 | super("Signature doesn't match", 401); 230 | } 231 | 232 | /** 233 | * @override 234 | */ 235 | get docs() { 236 | return this.url("WhatsAppAPIFailedToVerifyError"); 237 | } 238 | } 239 | 240 | /** 241 | * Thrown when the Webhook Verify Token isn't provided in the constructor 242 | * 243 | * @description 244 | * The Webhook Verify Token is a custom secret key that is used to verify against the API 245 | * the server is indeed the one that is supposed to receive the incoming requests. 246 | * 247 | * Your server will receive a GET request with the `hub.verify_token` and `hub.challenge` parameters. 248 | * The verify token should be equal to the one you provided in the constructor. 249 | * Once validated, you should reply with the `hub.challenge` parameter. 250 | * 251 | * The verify token is manually generated while setting up the callback URL in the 252 | * Meta's app dashboard -\> WhatsApp -\> Configuration. 253 | * 254 | * @example 255 | * ```ts 256 | * new WhatsAppAPI({ 257 | * webhookVerifyToken: "your-verify-token", 258 | * // other options 259 | * }); 260 | * ``` 261 | * 262 | * @see https://whatsappapijs.web.app/types/types.TheBasicConstructorArguments.html 263 | * @see https://developers.facebook.com/docs/graph-api/webhooks/getting-started/#verification-requests 264 | */ 265 | export class WhatsAppAPIMissingVerifyTokenError extends WhatsAppAPIError { 266 | /** 267 | * @internal 268 | */ 269 | constructor() { 270 | super("Missing verify token", 500); 271 | } 272 | 273 | /** 274 | * @override 275 | */ 276 | get docs() { 277 | return this.url("WhatsAppAPIMissingVerifyTokenError"); 278 | } 279 | } 280 | 281 | /** 282 | * Thrown when the search parameters are missing in the request 283 | * 284 | * @description 285 | * In order to validate your server against the API, you need to provide the request params. 286 | * 287 | * If you are NOT using a middleware, make sure you are passing the parameters correctly. 288 | * Otherwise, feel free to open an issue on {@link https://github.com/Secreto31126/whatsapp-api-js/issues | GitHub}. 289 | * 290 | * @see https://whatsappapijs.web.app/types/types.GetParams.html 291 | * @see https://developers.facebook.com/docs/graph-api/webhooks/getting-started/#verification-requests 292 | */ 293 | export class WhatsAppAPIMissingSearchParamsError extends WhatsAppAPIError { 294 | /** 295 | * @internal 296 | */ 297 | constructor() { 298 | super("Missing search params", 400); 299 | } 300 | 301 | /** 302 | * @override 303 | */ 304 | get docs() { 305 | return this.url("WhatsAppAPIMissingSearchParamsError"); 306 | } 307 | } 308 | 309 | /** 310 | * Thrown when the verification token doesn't match from the request 311 | * 312 | * @description 313 | * The verify_token in the request doesn't match the one provided on initialization. 314 | * Either they are hacking you, or your Webhook Verify Token is invalid. 315 | * 316 | * Make sure you provided the correct verify token on initialization. 317 | * It is manually generated while setting up the callback URL in the 318 | * Meta's app dashboard -\> WhatsApp -\> Configuration. 319 | * 320 | * @example 321 | * ```ts 322 | * new WhatsAppAPI({ 323 | * webhookVerifyToken: "your-verify-token", 324 | * // other options 325 | * }); 326 | * ``` 327 | * 328 | * @description 329 | * It might also be possible you didn't provide the correct request params 330 | * for the verification token (hub.verify_token). If you are using a middleware and 331 | * this seems to be the case, consider opening an issue on 332 | * {@link https://github.com/Secreto31126/whatsapp-api-js/issues | GitHub}. 333 | * 334 | * @see https://whatsappapijs.web.app/classes/WhatsAppAPI.WhatsAppAPI.html#get 335 | * @see https://whatsappapijs.web.app/types/types.TheBasicConstructorArguments.html 336 | * @see https://whatsappapijs.web.app/types/types.GetParams.html 337 | * @see https://developers.facebook.com/docs/graph-api/webhooks/getting-started/#verification-requests 338 | */ 339 | export class WhatsAppAPIFailedToVerifyTokenError extends WhatsAppAPIError { 340 | /** 341 | * @internal 342 | */ 343 | constructor() { 344 | super("Invalid token verification", 403); 345 | } 346 | 347 | /** 348 | * @override 349 | */ 350 | get docs() { 351 | return this.url("WhatsAppAPIFailedToVerifyTokenError"); 352 | } 353 | } 354 | 355 | /** 356 | * Thrown in unusual cases, such as on empty or unknown payloads from the API side. 357 | * 358 | * It 100% should never happen, and if it does, feel free to open an issue on 359 | * {@link https://github.com/Secreto31126/whatsapp-api-js/issues | GitHub} so we can 360 | * investigate these impossibles scenarios. 361 | */ 362 | export class WhatsAppAPIUnexpectedError extends WhatsAppAPIError { 363 | /** 364 | * @internal 365 | */ 366 | constructor(message: string, httpStatus: number) { 367 | super(message + " ¯\\_(ツ)_/¯", httpStatus); 368 | } 369 | 370 | get docs() { 371 | return this.url("WhatsAppAPIUnexpectedError"); 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /src/messages/contacts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClientMessage, 3 | type ContactComponent, 4 | ContactUniqueComponent, 5 | ContactMultipleComponent 6 | } from "../types.js"; 7 | import type { AtLeastOne } from "../utils.d.ts"; 8 | 9 | /** 10 | * @group Contacts 11 | */ 12 | export type BuiltContact = { 13 | name: Name; 14 | } & Partial<{ 15 | birthday: string; 16 | org: Organization; 17 | addresses: Address[]; 18 | phones: Phone[]; 19 | emails: Email[]; 20 | urls: Url[]; 21 | }>; 22 | 23 | /** 24 | * Contacts API object 25 | * 26 | * @group Contacts 27 | */ 28 | export class Contacts extends ClientMessage { 29 | /** 30 | * The contacts of the message 31 | */ 32 | readonly component: BuiltContact[]; 33 | 34 | /** 35 | * @override 36 | * @internal 37 | */ 38 | get _type(): "contacts" { 39 | return "contacts"; 40 | } 41 | 42 | /** 43 | * Create a Contacts object for the API 44 | * 45 | * @example 46 | * ```ts 47 | * import { Contacts, Name, Address, Phone } from "whatsapp-api-js/messages"; 48 | * 49 | * const contact_message = new Contacts([ 50 | * new Name("John Doe", "John", "Doe", undefined, "Mr.", "Jr."), 51 | * new Address( 52 | * "United States", 53 | * "US", 54 | * "FL", 55 | * "Miami", 56 | * "221B Baker Street", 57 | * "33101", 58 | * "Mystery" 59 | * ), 60 | * new Phone("+123456789", "Mystery", "123456789") 61 | * ]); 62 | * ``` 63 | * 64 | * @param contact - Array of contact's components 65 | * @throws If contact contains multiple of the same components and _many is set to false (for example, Name, Birthday and Organization) 66 | */ 67 | constructor( 68 | ...contact: AtLeastOne< 69 | Array< 70 | | Address 71 | | Birthday 72 | | Email 73 | | Name 74 | | Organization 75 | | Phone 76 | | Url 77 | | ContactComponent 78 | > 79 | > 80 | ) { 81 | super(); 82 | 83 | this.component = []; 84 | 85 | for (const components of contact) { 86 | const contact = {} as BuiltContact; 87 | 88 | for (const component of components) { 89 | const name = component._type as keyof typeof contact; 90 | 91 | if (component._many) { 92 | if (!contact[name]) { 93 | Object.defineProperty(contact, name, { 94 | value: [] as Address[] | Email[] | Phone[] | Url[], 95 | enumerable: true 96 | }); 97 | } 98 | 99 | const pointer = contact[name] as (typeof component)[]; 100 | pointer.push(component._build() as ContactComponent); 101 | } else { 102 | if (contact[name]) 103 | throw new Error( 104 | `Contact already has a ${name} component and _many is set to false` 105 | ); 106 | 107 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 108 | // @ts-ignore - TS doesn't know that contact[name] will match the type of the built component 109 | contact[name] = 110 | // reduce ts-ignore impact 111 | component._build() as Exclude< 112 | (typeof contact)[keyof typeof contact], 113 | undefined 114 | >; 115 | } 116 | } 117 | 118 | if (!contact.name) 119 | throw new Error("Contact must have a name component"); 120 | 121 | this.component.push(contact); 122 | } 123 | } 124 | 125 | /** 126 | * @internal 127 | */ 128 | toJSON() { 129 | return JSON.stringify(this.component); 130 | } 131 | } 132 | 133 | /** 134 | * Address API object 135 | * 136 | * @group Contacts 137 | */ 138 | export class Address extends ContactMultipleComponent { 139 | /** 140 | * The country of the address 141 | */ 142 | readonly country?: string; 143 | /** 144 | * The country code of the address 145 | */ 146 | readonly country_code?: string; 147 | /** 148 | * The state of the address 149 | */ 150 | readonly state?: string; 151 | /** 152 | * The city of the address 153 | */ 154 | readonly city?: string; 155 | /** 156 | * The street of the address 157 | */ 158 | readonly street?: string; 159 | /** 160 | * The zip code of the address 161 | */ 162 | readonly zip?: string; 163 | /** 164 | * The type of the address 165 | */ 166 | readonly type?: string; 167 | 168 | /** 169 | * @override 170 | * @internal 171 | */ 172 | get _type(): "addresses" { 173 | return "addresses"; 174 | } 175 | 176 | /** 177 | * Builds an address object for a contact. 178 | * A contact can contain multiple addresses objects. 179 | * 180 | * @param country - Full country name 181 | * @param country_code - Two-letter country abbreviation 182 | * @param state - State abbreviation 183 | * @param city - City name 184 | * @param street - Street number and name 185 | * @param zip - ZIP code 186 | * @param type - Address type. Standard Values: HOME, WORK 187 | */ 188 | constructor( 189 | country?: string, 190 | country_code?: string, 191 | state?: string, 192 | city?: string, 193 | street?: string, 194 | zip?: string, 195 | type?: string 196 | ) { 197 | super(); 198 | if (country) this.country = country; 199 | if (country_code) this.country_code = country_code; 200 | if (state) this.state = state; 201 | if (city) this.city = city; 202 | if (street) this.street = street; 203 | if (zip) this.zip = zip; 204 | if (type) this.type = type; 205 | } 206 | } 207 | 208 | /** 209 | * Birthday API object 210 | * 211 | * @group Contacts 212 | */ 213 | export class Birthday extends ContactUniqueComponent { 214 | /** 215 | * The birthday of the contact 216 | */ 217 | readonly birthday: string; 218 | 219 | /** 220 | * @override 221 | * @internal 222 | */ 223 | get _type(): "birthday" { 224 | return "birthday"; 225 | } 226 | 227 | /** 228 | * Builds a birthday object for a contact 229 | * 230 | * @param year - Year of birth (YYYY) 231 | * @param month - Month of birth (MM) 232 | * @param day - Day of birth (DD) 233 | * @throws If the year, month, or day doesn't have a valid length 234 | */ 235 | constructor(year: string, month: string, day: string) { 236 | super(); 237 | if (year.length !== 4) throw new Error("Year must be 4 digits"); 238 | if (month.length !== 2) throw new Error("Month must be 2 digits"); 239 | if (day.length !== 2) throw new Error("Day must be 2 digits"); 240 | this.birthday = `${year}-${month}-${day}`; 241 | } 242 | 243 | /** 244 | * @override 245 | * @internal 246 | */ 247 | _build(): string { 248 | return this.birthday; 249 | } 250 | } 251 | 252 | /** 253 | * Email API object 254 | * 255 | * @group Contacts 256 | */ 257 | export class Email extends ContactMultipleComponent { 258 | /** 259 | * The email of the contact 260 | */ 261 | readonly email?: string; 262 | /** 263 | * The type of the email 264 | */ 265 | readonly type?: string; 266 | 267 | /** 268 | * @override 269 | * @internal 270 | */ 271 | get _type(): "emails" { 272 | return "emails"; 273 | } 274 | 275 | /** 276 | * Builds an email object for a contact. 277 | * A contact can contain multiple emails objects. 278 | * 279 | * @param email - Email address 280 | * @param type - Email type. Standard Values: HOME, WORK 281 | */ 282 | constructor(email?: string, type?: string) { 283 | super(); 284 | if (email) this.email = email; 285 | if (type) this.type = type; 286 | } 287 | } 288 | 289 | /** 290 | * Name API object 291 | * 292 | * @group Contacts 293 | */ 294 | export class Name extends ContactUniqueComponent { 295 | /** 296 | * The formatted name of the contact 297 | */ 298 | readonly formatted_name: string; 299 | /** 300 | * The first name of the contact 301 | */ 302 | readonly first_name?: string; 303 | /** 304 | * The last name of the contact 305 | */ 306 | readonly last_name?: string; 307 | /** 308 | * The middle name of the contact 309 | */ 310 | readonly middle_name?: string; 311 | /** 312 | * The suffix of the contact 313 | */ 314 | readonly suffix?: string; 315 | /** 316 | * The prefix of the contact 317 | */ 318 | readonly prefix?: string; 319 | 320 | /** 321 | * @override 322 | * @internal 323 | */ 324 | get _type(): "name" { 325 | return "name"; 326 | } 327 | 328 | /** 329 | * Builds a name object for a contact, required for contacts. 330 | * The object requires a formatted_name and at least another property. 331 | * 332 | * @param formatted_name - Full name, as it normally appears 333 | * @param first_name - First name 334 | * @param last_name - Last name 335 | * @param middle_name - Middle name 336 | * @param suffix - Name suffix 337 | * @param prefix - Name prefix 338 | * @throws If no other component apart from formatted_name is defined 339 | */ 340 | constructor( 341 | formatted_name: string, 342 | first_name?: string, 343 | last_name?: string, 344 | middle_name?: string, 345 | suffix?: string, 346 | prefix?: string 347 | ) { 348 | super(); 349 | 350 | this.formatted_name = formatted_name; 351 | if (first_name) this.first_name = first_name; 352 | if (last_name) this.last_name = last_name; 353 | if (middle_name) this.middle_name = middle_name; 354 | if (suffix) this.suffix = suffix; 355 | if (prefix) this.prefix = prefix; 356 | 357 | if (Object.keys(this).length < 2) { 358 | throw new Error( 359 | "Name must have at least one of the following: first_name, last_name, middle_name, prefix, suffix" 360 | ); 361 | } 362 | } 363 | } 364 | 365 | /** 366 | * Organization API object 367 | * 368 | * @group Contacts 369 | */ 370 | export class Organization extends ContactUniqueComponent { 371 | /** 372 | * The company of the contact 373 | */ 374 | readonly company?: string; 375 | /** 376 | * The department of the contact 377 | */ 378 | readonly department?: string; 379 | /** 380 | * The title of the contact 381 | */ 382 | readonly title?: string; 383 | 384 | /** 385 | * @override 386 | * @internal 387 | */ 388 | get _type(): "org" { 389 | return "org"; 390 | } 391 | 392 | /** 393 | * Builds an organization object for a contact 394 | * 395 | * @param company - Name of the contact's company 396 | * @param department - Name of the contact's department 397 | * @param title - Contact's business title 398 | */ 399 | constructor(company?: string, department?: string, title?: string) { 400 | super(); 401 | if (company) this.company = company; 402 | if (department) this.department = department; 403 | if (title) this.title = title; 404 | } 405 | } 406 | 407 | /** 408 | * Phone API object 409 | * 410 | * @group Contacts 411 | */ 412 | export class Phone extends ContactMultipleComponent { 413 | /** 414 | * The phone number of the contact 415 | */ 416 | readonly phone?: string; 417 | /** 418 | * The type of the phone number 419 | */ 420 | readonly type?: string; 421 | /** 422 | * The WhatsApp ID of the contact 423 | */ 424 | readonly wa_id?: string; 425 | 426 | /** 427 | * @override 428 | * @internal 429 | */ 430 | get _type(): "phones" { 431 | return "phones"; 432 | } 433 | 434 | /** 435 | * Builds a phone object for a contact. 436 | * A contact can contain multiple phones objects. 437 | * 438 | * @param phone - Phone number, automatically populated with the wa_id value as a formatted phone number 439 | * @param type - Phone type. Standard Values: CELL, MAIN, IPHONE, HOME, WORK 440 | * @param wa_id - WhatsApp ID 441 | */ 442 | constructor(phone?: string, type?: string, wa_id?: string) { 443 | super(); 444 | if (phone) this.phone = phone; 445 | if (type) this.type = type; 446 | if (wa_id) this.wa_id = wa_id; 447 | } 448 | } 449 | 450 | /** 451 | * Url API object 452 | * 453 | * @group Contacts 454 | */ 455 | export class Url extends ContactMultipleComponent { 456 | /** 457 | * The URL of the contact 458 | */ 459 | readonly url?: string; 460 | /** 461 | * The type of the URL 462 | */ 463 | readonly type?: string; 464 | 465 | /** 466 | * @override 467 | * @internal 468 | */ 469 | get _type(): "urls" { 470 | return "urls"; 471 | } 472 | 473 | /** 474 | * Builds an url object for a contact. 475 | * A contact can contain multiple urls objects. 476 | * 477 | * @param url - URL 478 | * @param type - URL type. Standard Values: HOME, WORK 479 | */ 480 | constructor(url?: string, type?: string) { 481 | super(); 482 | if (url) this.url = url; 483 | if (type) this.type = type; 484 | } 485 | } 486 | -------------------------------------------------------------------------------- /src/messages/globals.ts: -------------------------------------------------------------------------------- 1 | import { Section, type ClientTypedMessageComponent } from "../types.js"; 2 | import type { AtLeastOne } from "../utils.d.ts"; 3 | 4 | /** 5 | * TS knowledge intensifies 6 | * @internal 7 | * @deprecated - Unused with the release of ActionProductList 8 | */ 9 | export function isProductSections(obj: unknown[]): obj is ProductSection[] { 10 | return obj[0] instanceof ProductSection; 11 | } 12 | 13 | /** 14 | * Section API object 15 | * 16 | * @group Globals 17 | */ 18 | export class ProductSection extends Section { 19 | /** 20 | * The products of the section 21 | */ 22 | readonly product_items: Product[]; 23 | 24 | /** 25 | * Builds a product section component 26 | * 27 | * @param title - The title of the product section 28 | * @param products - The products to add to the product section 29 | * @throws If title is over 24 characters if provided 30 | * @throws If more than 30 products are provided 31 | */ 32 | constructor(title: string | undefined, ...products: AtLeastOne) { 33 | super("ProductSection", "products", products, 30, title); 34 | this.product_items = products.map(Product.create); 35 | } 36 | } 37 | 38 | /** 39 | * Product API object 40 | * 41 | * @group Globals 42 | */ 43 | export class Product implements ClientTypedMessageComponent { 44 | /** 45 | * The id of the product 46 | */ 47 | readonly product_retailer_id: string; 48 | 49 | /** 50 | * @override 51 | * @internal 52 | */ 53 | get _type(): "product" { 54 | return "product"; 55 | } 56 | 57 | /** 58 | * Builds a product component 59 | * 60 | * @param product_retailer_id - The id of the product 61 | */ 62 | constructor(product_retailer_id: string) { 63 | this.product_retailer_id = product_retailer_id; 64 | } 65 | 66 | /** 67 | * Clone a product object (useful for lambdas and scoping down {@link CatalogProduct}) 68 | * 69 | * @param product - The product to create a new object from 70 | * @returns A new product object 71 | */ 72 | static create(product: Product): Product { 73 | return new Product(product.product_retailer_id); 74 | } 75 | } 76 | 77 | /** 78 | * Product API object 79 | * 80 | * @group Globals 81 | */ 82 | export class CatalogProduct extends Product { 83 | /** 84 | * Builds a cataloged product component 85 | * 86 | * @param product_retailer_id - The id of the product 87 | * @param catalog_id - The id of the catalog the product belongs to 88 | */ 89 | constructor( 90 | product_retailer_id: string, 91 | readonly catalog_id: string 92 | ) { 93 | super(product_retailer_id); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/messages/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module messages 3 | * 4 | * @description 5 | * A collection of classes to construct WhatsApp API messages. 6 | * 7 | * There are 7 types of messages: 8 | * - {@link Text} 9 | * - {@link Media} 10 | * - {@link Template} 11 | * - {@link Interactive} 12 | * - {@link Location} 13 | * - {@link Contacts} 14 | * - {@link Reaction} 15 | */ 16 | 17 | export * from "./text.js"; 18 | export * from "./location.js"; 19 | export * from "./reaction.js"; 20 | export * from "./contacts.js"; 21 | export * from "./interactive.js"; 22 | export * from "./media.js"; 23 | export * from "./template.js"; 24 | export * from "./globals.js"; 25 | -------------------------------------------------------------------------------- /src/messages/location.ts: -------------------------------------------------------------------------------- 1 | import { ClientMessage } from "../types.js"; 2 | 3 | /** 4 | * Location API component 5 | * 6 | * @group Location 7 | */ 8 | export class Location extends ClientMessage { 9 | /** 10 | * The latitude of the location 11 | */ 12 | readonly longitude: number; 13 | /** 14 | * The longitude of the location 15 | */ 16 | readonly latitude: number; 17 | /** 18 | * The name of the location 19 | */ 20 | readonly name?: string; 21 | /** 22 | * The address of the location 23 | */ 24 | readonly address?: string; 25 | 26 | /** 27 | * @override 28 | * @internal 29 | */ 30 | get _type(): "location" { 31 | return "location"; 32 | } 33 | 34 | /** 35 | * Create a Location object for the API 36 | * 37 | * @example 38 | * ```ts 39 | * import { Location } from "whatsapp-api-js/messages"; 40 | * 41 | * const location_message = new Location(0, 0); 42 | * 43 | * const location_named_message = new Location(0, 0, "My Store", "Address"); 44 | * ``` 45 | * 46 | * @param longitude - Longitude of the location 47 | * @param latitude - Latitude of the location 48 | * @param name - Name of the location 49 | * @param address - Address of the location, only displayed if name is present 50 | */ 51 | constructor( 52 | longitude: number, 53 | latitude: number, 54 | name?: string, 55 | address?: string 56 | ) { 57 | super(); 58 | this.longitude = longitude; 59 | this.latitude = latitude; 60 | if (name) this.name = name; 61 | if (address) this.address = address; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/messages/media.ts: -------------------------------------------------------------------------------- 1 | import { ClientMessage } from "../types.js"; 2 | 3 | /** 4 | * Abstract class for all the media types 5 | * 6 | * @group Media 7 | */ 8 | export abstract class Media extends ClientMessage { 9 | /** 10 | * The id of the media 11 | */ 12 | readonly id?: string; 13 | /** 14 | * The link of the media 15 | */ 16 | readonly link?: string; 17 | 18 | /** 19 | * @param file - File to be sent 20 | * @param isItAnID - If the file is an ID (true) or an URL (false) 21 | */ 22 | constructor(file: string, isItAnID = false) { 23 | super(); 24 | this[isItAnID ? "id" : "link"] = file; 25 | } 26 | } 27 | 28 | /** 29 | * Audio API component 30 | * 31 | * @group Media 32 | */ 33 | export class Audio extends Media { 34 | /** 35 | * @override 36 | * @internal 37 | */ 38 | get _type(): "audio" { 39 | return "audio"; 40 | } 41 | 42 | /** 43 | * Create an Audio object for the API 44 | * 45 | * @example 46 | * ```ts 47 | * import { Audio } from "whatsapp-api-js/messages"; 48 | * 49 | * const audio_message = new Audio("https://www.example.com/audio.mp3"); 50 | * 51 | * const audio_id_message = new Audio("12345678", true); 52 | * ``` 53 | * 54 | * @param audio - The audio file's link or id 55 | * @param isItAnID - Whether audio is an id (true) or a link (false) 56 | */ 57 | constructor(audio: string, isItAnID = false) { 58 | super(audio, isItAnID); 59 | } 60 | } 61 | 62 | /** 63 | * Document API component 64 | * 65 | * @group Media 66 | */ 67 | export class Document extends Media { 68 | /** 69 | * The file's caption 70 | */ 71 | readonly caption?: string; 72 | /** 73 | * The file's filename 74 | */ 75 | readonly filename?: string; 76 | 77 | /** 78 | * @override 79 | * @internal 80 | */ 81 | get _type(): "document" { 82 | return "document"; 83 | } 84 | 85 | /** 86 | * Create a Document object for the API 87 | * 88 | * @example 89 | * ```ts 90 | * import { Document } from "whatsapp-api-js/messages"; 91 | * 92 | * const document_message = new Document("https://www.example.com/document.pdf"); 93 | * 94 | * const document_id_message = new Document("12345678", true); 95 | * 96 | * const document_caption_message = new Document( 97 | * "https://www.example.com/document.pdf", 98 | * false, 99 | * "Hello world!" 100 | * ); 101 | * 102 | * const document_filename_message = new Document( 103 | * "https://www.example.com/document.pdf", 104 | * false, 105 | * undefined, 106 | * "a weird filename.pdf" 107 | * ); 108 | * ``` 109 | * 110 | * @param document - The document file's link or id 111 | * @param isItAnID - Whether document is an id (true) or a link (false) 112 | * @param caption - Describes the specified document media 113 | * @param filename - Describes the filename for the specific document 114 | */ 115 | constructor( 116 | document: string, 117 | isItAnID = false, 118 | caption?: string, 119 | filename?: string 120 | ) { 121 | super(document, isItAnID); 122 | if (caption) this.caption = caption; 123 | if (filename) this.filename = filename; 124 | } 125 | } 126 | 127 | /** 128 | * Image API component 129 | * 130 | * @group Media 131 | */ 132 | export class Image extends Media { 133 | /** 134 | * The file's caption 135 | */ 136 | readonly caption?: string; 137 | 138 | /** 139 | * @override 140 | * @internal 141 | */ 142 | get _type(): "image" { 143 | return "image"; 144 | } 145 | 146 | /** 147 | * Create a Image object for the API 148 | * 149 | * @example 150 | * ```ts 151 | * import { Image } from "whatsapp-api-js/messages"; 152 | * 153 | * const image_message = new Image("https://i.imgur.com/4QfKuz1.png"); 154 | * 155 | * const image_id_message = new Image("12345678", true); 156 | * 157 | * const image_caption_message = new Image( 158 | * "https://i.imgur.com/4QfKuz1.png", 159 | * false, 160 | * "Hello world!" 161 | * ); 162 | * ``` 163 | * 164 | * @param image - The image file's link or id 165 | * @param isItAnID - Whether image is an id (true) or a link (false) 166 | * @param caption - Describes the specified image media 167 | */ 168 | constructor(image: string, isItAnID = false, caption?: string) { 169 | super(image, isItAnID); 170 | if (caption) this.caption = caption; 171 | } 172 | } 173 | 174 | /** 175 | * Sticker API component 176 | * 177 | * @group Media 178 | */ 179 | export class Sticker extends Media { 180 | /** 181 | * @override 182 | * @internal 183 | */ 184 | get _type(): "sticker" { 185 | return "sticker"; 186 | } 187 | 188 | /** 189 | * Create a Sticker object for the API 190 | * 191 | * @example 192 | * ```ts 193 | * import { Sticker } from "whatsapp-api-js/messages"; 194 | * 195 | * const sticker_message = new Sticker("https://www.example.com/sticker.webp"); 196 | * 197 | * const sticker_id_message = new Sticker("12345678", true); 198 | * ``` 199 | * 200 | * @param sticker - The sticker file's link 201 | * @param isItAnID - Whether sticker is an id (true) or a link (false) 202 | */ 203 | constructor(sticker: string, isItAnID = false) { 204 | super(sticker, isItAnID); 205 | } 206 | } 207 | 208 | /** 209 | * Video API component 210 | * 211 | * @group Media 212 | */ 213 | export class Video extends Media { 214 | /** 215 | * The file's caption 216 | */ 217 | readonly caption?: string; 218 | 219 | /** 220 | * @override 221 | * @internal 222 | */ 223 | get _type(): "video" { 224 | return "video"; 225 | } 226 | 227 | /** 228 | * Create a Video object for the API 229 | * 230 | * @example 231 | * ```ts 232 | * import { Video } from "whatsapp-api-js/messages"; 233 | * 234 | * const video_message = new Video("https://www.example.com/video.mp4"); 235 | * 236 | * const video_id_message = new Video("12345678", true); 237 | * 238 | * const video_caption_message = new Video( 239 | * "https://www.example.com/video.mp4", 240 | * false, 241 | * "Hello world!" 242 | * ); 243 | * ``` 244 | * 245 | * @param video - The video file's link 246 | * @param isItAnID - Whether video is an id (true) or a link (false) 247 | * @param caption - Describes the specified video media 248 | */ 249 | constructor(video: string, isItAnID = false, caption?: string) { 250 | super(video, isItAnID); 251 | if (caption) this.caption = caption; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/messages/reaction.ts: -------------------------------------------------------------------------------- 1 | import { ClientMessage } from "../types.js"; 2 | 3 | /** 4 | * Reaction API object 5 | * 6 | * @group Reaction 7 | */ 8 | export class Reaction extends ClientMessage { 9 | /** 10 | * The message's id to react to 11 | */ 12 | readonly message_id: string; 13 | /** 14 | * The reaction emoji 15 | */ 16 | readonly emoji: string; 17 | 18 | /** 19 | * @override 20 | * @internal 21 | */ 22 | get _type(): "reaction" { 23 | return "reaction"; 24 | } 25 | 26 | /** 27 | * Create a Reaction object for the API 28 | * 29 | * @example 30 | * ```ts 31 | * import { Reaction } from "whatsapp-api-js/messages"; 32 | * 33 | * const reaction_message = new Reaction("message_id", "👍"); 34 | * ``` 35 | * 36 | * @param message_id - The message's id (wamid) to react to 37 | * @param emoji - The emoji to react with, defaults to empty string to remove a reaction 38 | * @throws If a non-emoji or more than one emoji is provided 39 | */ 40 | constructor(message_id: string, emoji: string); 41 | /** 42 | * Create a _remove_ Reaction object for the API 43 | * 44 | * @example 45 | * ```ts 46 | * import { Reaction } from "whatsapp-api-js/messages"; 47 | * 48 | * const reaction_remove_message = new Reaction("message_id"); 49 | * ``` 50 | * 51 | * @param message_id - The message's id (wamid) to react to 52 | */ 53 | constructor(message_id: string); 54 | 55 | /** 56 | * Create a Reaction object for the API 57 | * 58 | * @param message_id - The message's id (wamid) to react to 59 | * @param emoji - The emoji to react with, defaults to empty string to remove a reaction 60 | * @throws If a non-emoji or more than one emoji is provided 61 | */ 62 | constructor(message_id: string, emoji = "") { 63 | super(); 64 | 65 | if (emoji && !/^\p{Extended_Pictographic}$/u.test(emoji)) 66 | throw new Error("Reaction emoji must be a single emoji"); 67 | 68 | this.message_id = message_id; 69 | this.emoji = emoji; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/messages/text.ts: -------------------------------------------------------------------------------- 1 | import { ClientMessage } from "../types.js"; 2 | 3 | /** 4 | * Text API object 5 | * 6 | * @group Text 7 | */ 8 | export class Text extends ClientMessage { 9 | /** 10 | * Body of the message. Maximum length: 4096 characters. 11 | */ 12 | readonly body: string; 13 | /** 14 | * Whether to enable preview for the text message 15 | */ 16 | readonly preview_url?: boolean; 17 | 18 | /** 19 | * @override 20 | * @internal 21 | */ 22 | get _type(): "text" { 23 | return "text"; 24 | } 25 | 26 | /** 27 | * Create a Text object for the API 28 | * 29 | * @example 30 | * ```ts 31 | * import { Text } from "whatsapp-api-js/messages"; 32 | * 33 | * const text_message = new Text("Hello world!"); 34 | * 35 | * const text_preview_message = new Text("Hello URL!", true); 36 | * ``` 37 | * 38 | * @param body - The content of the text message which can contain formatting and URLs which begin with http:// or https:// 39 | * @param preview_url - By default, WhatsApp recognizes URLs and makes them clickable, but you can also include a preview box with more information about the link. Set this field to true if you want to include a URL preview box. 40 | * @throws If body is over 4096 characters 41 | */ 42 | constructor(body: string, preview_url?: boolean) { 43 | super(); 44 | if (body.length > 4096) 45 | throw new Error("Text body must be less than 4096 characters"); 46 | this.body = body; 47 | if (preview_url) this.preview_url = preview_url; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/middleware/adonis.ts: -------------------------------------------------------------------------------- 1 | import { WhatsAppAPIMiddleware } from "./globals.js"; 2 | import { WhatsAppAPIError } from "../errors.js"; 3 | 4 | import type { Request } from "@adonisjs/http-server"; 5 | import type { GetParams, PostData } from "../types.d.ts"; 6 | 7 | /** 8 | * AdonisJS middleware for WhatsAppAPI 9 | */ 10 | export class WhatsAppAPI extends WhatsAppAPIMiddleware { 11 | /** 12 | * POST request handler for AdonisJS 13 | * 14 | * @example 15 | * ```ts 16 | * import Route from "@ioc:Adonis/Core/Route"; 17 | * import { WhatsAppAPI } from "whatsapp-api-js/middleware/adonis"; 18 | * 19 | * const Whatsapp = new WhatsAppAPI({ 20 | * token: "YOUR_TOKEN", 21 | * appSecret: "YOUR_APP_SECRET", 22 | * webhookVerifyToken: "YOUR_WEBHOOK_VERIFY_TOKEN" 23 | * }); 24 | * 25 | * Route.post('/', async ({ request, response }) => { 26 | * response.status(await Whatsapp.handle_post(request)); 27 | * }); 28 | * ``` 29 | * 30 | * @override 31 | * @param req - The request object from AdonisJS 32 | * @returns The status code to be sent to the client 33 | */ 34 | async handle_post(req: Request): Promise { 35 | try { 36 | await this.post( 37 | req.body() as PostData, 38 | req.raw() ?? "", 39 | req.header("x-hub-signature-256") ?? "" 40 | ); 41 | 42 | return 200; 43 | } catch (e) { 44 | // In case who knows what fails ¯\_(ツ)_/¯ 45 | return e instanceof WhatsAppAPIError ? e.httpStatus : 500; 46 | } 47 | } 48 | 49 | /** 50 | * GET request handler for AdonisJS 51 | * 52 | * @example 53 | * ```ts 54 | * import Route from "@ioc:Adonis/Core/Route"; 55 | * import { WhatsAppAPI } from "whatsapp-api-js/middleware/adonis"; 56 | * 57 | * const Whatsapp = new WhatsAppAPI({ 58 | * token: "YOUR_TOKEN", 59 | * appSecret: "YOUR_APP_SECRET", 60 | * webhookVerifyToken: "YOUR_WEBHOOK_VERIFY_TOKEN" 61 | * }); 62 | * 63 | * Route.get('/', ({ request, response }) => { 64 | * try { 65 | * return Whatsapp.handle_get(request); 66 | * } catch (e) { 67 | * response.status(e as number); 68 | * } 69 | * }); 70 | * ``` 71 | * 72 | * @override 73 | * @param req - The request object from AdonisJS 74 | * @returns The challenge string to be sent to the client 75 | * @throws The error code 76 | */ 77 | handle_get(req: Request): string { 78 | try { 79 | return this.get(req.qs() as GetParams); 80 | } catch (e) { 81 | // In case who knows what fails ¯\_(ツ)_/¯ 82 | throw e instanceof WhatsAppAPIError ? e.httpStatus : 500; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/middleware/azure.ts: -------------------------------------------------------------------------------- 1 | import { WhatsAppAPIMiddleware } from "./globals.js"; 2 | import { WhatsAppAPIError } from "../errors.js"; 3 | 4 | import type { HttpRequest } from "@azure/functions"; 5 | import type { GetParams } from "../types.d.ts"; 6 | 7 | /** 8 | * Azure Function middleware for WhatsAppAPI 9 | * 10 | * @remark This middleware is identical to the Web Standard API middleware, 11 | * but uses the Azure Function's HttpRequest object which is a _subset_ 12 | * of the Web Standard API's Request object. 13 | */ 14 | export class WhatsAppAPI extends WhatsAppAPIMiddleware { 15 | /** 16 | * POST request handler for Azure Function 17 | * 18 | * @example 19 | * ```ts 20 | * import { app } from "@azure/functions"; 21 | * import { WhatsAppAPI } from "whatsapp-api-js/middleware/azure"; 22 | * 23 | * const Whatsapp = new WhatsAppAPI({ 24 | * token: "YOUR_TOKEN", 25 | * appSecret: "YOUR_APP_SECRET", 26 | * webhookVerifyToken: "YOUR_WEBHOOK_VERIFY_TOKEN" 27 | * }); 28 | * 29 | * app.http('WhatsAppPOST', { 30 | * methods: ['POST'], 31 | * authLevel: 'anonymous', 32 | * route: 'message', 33 | * handler: async (req) => ({ 34 | * status: await Whatsapp.handle_post(req) 35 | * }) 36 | * }); 37 | * ``` 38 | * 39 | * @override 40 | * @param req - The request object from Azure Functions 41 | * @returns The status code to be sent to the client 42 | */ 43 | async handle_post(req: HttpRequest): Promise { 44 | try { 45 | const body = await req.text(); 46 | 47 | await this.post( 48 | JSON.parse(body || "{}"), 49 | body, 50 | req.headers.get("x-hub-signature-256") ?? "" 51 | ); 52 | 53 | return 200; 54 | } catch (e) { 55 | // In case the JSON.parse fails ¯\_(ツ)_/¯ 56 | return e instanceof WhatsAppAPIError ? e.httpStatus : 500; 57 | } 58 | } 59 | 60 | /** 61 | * GET request handler for Azure Function 62 | * 63 | * @example 64 | * ```ts 65 | * import { app } from "@azure/functions"; 66 | * import { WhatsAppAPI } from "whatsapp-api-js/middleware/azure"; 67 | * 68 | * const Whatsapp = new WhatsAppAPI({ 69 | * token: "YOUR_TOKEN", 70 | * appSecret: "YOUR_APP_SECRET", 71 | * webhookVerifyToken: "YOUR_WEBHOOK_VERIFY_TOKEN" 72 | * }); 73 | * 74 | * app.http('WhatsAppGET', { 75 | * methods: ['GET'], 76 | * authLevel: 'anonymous', 77 | * route: 'message', 78 | * handler: async (req) => { 79 | * try { 80 | * return { 81 | * body: Whatsapp.handle_get(req) 82 | * }; 83 | * } catch (e) { 84 | * return { 85 | * status: e as number 86 | * }; 87 | * } 88 | * } 89 | * }); 90 | * ``` 91 | * 92 | * @override 93 | * @param req - The request object from Azure Functions 94 | * @returns The challenge string to be sent to the client 95 | * @throws The error code 96 | */ 97 | handle_get(req: HttpRequest): string { 98 | try { 99 | return this.get( 100 | Object.fromEntries(new URL(req.url).searchParams) as GetParams 101 | ); 102 | } catch (e) { 103 | // In case who knows what fails ¯\_(ツ)_/¯ 104 | throw e instanceof WhatsAppAPIError ? e.httpStatus : 500; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/middleware/bun.ts: -------------------------------------------------------------------------------- 1 | import { WhatsAppAPI as WebStandardMiddleware } from "./web-standard.js"; 2 | 3 | /** 4 | * Bun server middleware for WhatsAppAPI 5 | */ 6 | export class WhatsAppAPI extends WebStandardMiddleware { 7 | /** 8 | * POST request handler for Bun server 9 | * 10 | * @example 11 | * ```ts 12 | * import { WhatsAppAPI } from "whatsapp-api-js/middleware/bun"; 13 | * 14 | * const Whatsapp = new WhatsAppAPI({ 15 | * token: "YOUR_TOKEN", 16 | * appSecret: "YOUR_APP_SECRET", 17 | * webhookVerifyToken: "YOUR_WEBHOOK_VERIFY_TOKEN" 18 | * }); 19 | * 20 | * Bun.serve({ 21 | * fetch: async (req) => { 22 | * if (req.url === "/message" && req.method === "POST") { 23 | * req.respond({ status: await Whatsapp.handle_post(req) }); 24 | * } 25 | * } 26 | * }); 27 | * ``` 28 | * 29 | * @param req - The request object 30 | * @returns The status code to be sent to the client 31 | */ 32 | async handle_post(req: Request): Promise { 33 | return super.handle_post(req); 34 | } 35 | 36 | /** 37 | * GET request handler for Bun server 38 | * 39 | * @example 40 | * ```ts 41 | * import { WhatsAppAPI } from "whatsapp-api-js/middleware/bun"; 42 | * 43 | * const Whatsapp = new WhatsAppAPI({ 44 | * token: "YOUR_TOKEN", 45 | * appSecret: "YOUR_APP_SECRET", 46 | * webhookVerifyToken: "YOUR_WEBHOOK_VERIFY_TOKEN" 47 | * }); 48 | * 49 | * Bun.serve({ 50 | * fetch: (req) => { 51 | * try { 52 | * return new Response(Whatsapp.handle_get(req)); 53 | * } catch (e) { 54 | * return new Response(null, { status: e as number }); 55 | * } 56 | * } 57 | * }); 58 | * ``` 59 | * 60 | * @param req - The request object 61 | * @returns The challenge string to be sent to the client 62 | * @throws The error code 63 | */ 64 | handle_get(req: Request): string { 65 | return super.handle_get(req); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/middleware/deno.ts: -------------------------------------------------------------------------------- 1 | import { WhatsAppAPI as WebStandardMiddleware } from "./web-standard.js"; 2 | 3 | /** 4 | * Deno server middleware for WhatsAppAPI 5 | */ 6 | export class WhatsAppAPI extends WebStandardMiddleware { 7 | /** 8 | * POST request handler for Deno server 9 | * 10 | * @example 11 | * ```ts 12 | * import { WhatsAppAPI } from "whatsapp-api-js/middleware/deno"; 13 | * 14 | * const Whatsapp = new WhatsAppAPI({ 15 | * token: "YOUR_TOKEN", 16 | * appSecret: "YOUR_APP_SECRET", 17 | * webhookVerifyToken: "YOUR_WEBHOOK_VERIFY_TOKEN" 18 | * }); 19 | * 20 | * Deno.serve(async (req) => { 21 | * if (req.url === "/message" && req.method === "POST") { 22 | * return new Response(null, { 23 | * status: await Whatsapp.handle_post(req) 24 | * }); 25 | * } 26 | * }); 27 | * ``` 28 | * 29 | * @param req - The request object 30 | * @returns The status code to be sent to the client 31 | */ 32 | async handle_post(req: Request): Promise { 33 | return super.handle_post(req); 34 | } 35 | 36 | /** 37 | * GET request handler for Deno server 38 | * 39 | * @example 40 | * ```ts 41 | * import { WhatsAppAPI } from "whatsapp-api-js/middleware/deno"; 42 | * 43 | * const Whatsapp = new WhatsAppAPI({ 44 | * token: "YOUR_TOKEN", 45 | * appSecret: "YOUR_APP_SECRET", 46 | * webhookVerifyToken: "YOUR_WEBHOOK_VERIFY_TOKEN" 47 | * }); 48 | * 49 | * Deno.serve((req) => { 50 | * if (req.url === "/message" && req.method === "GET") { 51 | * try { 52 | * return new Response(Whatsapp.handle_get(req)); 53 | * } catch (e) { 54 | * return new Response(null, { status: e as number }); 55 | * } 56 | * } 57 | * }); 58 | * ``` 59 | * 60 | * @param req - The request object 61 | * @returns The challenge string to be sent to the client 62 | * @throws The error code 63 | */ 64 | handle_get(req: Request): string { 65 | return super.handle_get(req); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/middleware/express.ts: -------------------------------------------------------------------------------- 1 | import { WhatsAppAPIMiddleware } from "./globals.js"; 2 | import { WhatsAppAPIError } from "../errors.js"; 3 | 4 | import type { Request } from "express"; 5 | import type { GetParams } from "../types.d.ts"; 6 | 7 | /** 8 | * Express.js middleware for WhatsAppAPI 9 | */ 10 | export class WhatsAppAPI extends WhatsAppAPIMiddleware { 11 | /** 12 | * POST request handler for Express.js 13 | * 14 | * @remarks This method expects the request body to be the original string, not a parsed body 15 | * @see https://expressjs.com/en/guide/using-middleware.html#middleware.router 16 | * @see https://expressjs.com/en/4x/api.html#express.text 17 | * 18 | * @example 19 | * ```ts 20 | * import express from "express"; 21 | * import { WhatsAppAPI } from "whatsapp-api-js/middleware/express"; 22 | * 23 | * const app = express(); 24 | * const Whatsapp = new WhatsAppAPI({ 25 | * token: "YOUR_TOKEN", 26 | * appSecret: "YOUR_APP_SECRET", 27 | * webhookVerifyToken: "YOUR_WEBHOOK_VERIFY_TOKEN" 28 | * }); 29 | * 30 | * // Your app shall use any express middleware, as long as the entry point where `handle_post` 31 | * // is called has the request body as a string, not a parsed body. 32 | * app.use(express.json()); 33 | * 34 | * // The `express.text({ type: '*\/*' })` is optional if you are NOT using `express.json()`. 35 | * app.post("/message", express.text({ type: '*\/*' }), async (req, res) => { 36 | * res.sendStatus(await Whatsapp.handle_post(req)); 37 | * }); 38 | * ``` 39 | * 40 | * @override 41 | * @param req - The request object from Express.js 42 | * @returns The status code to be sent to the client 43 | */ 44 | async handle_post(req: Request): Promise { 45 | try { 46 | await this.post( 47 | JSON.parse(req.body ?? "{}"), 48 | req.body, 49 | req.header("x-hub-signature-256") ?? "" 50 | ); 51 | 52 | return 200; 53 | } catch (e) { 54 | // In case the JSON.parse fails ¯\_(ツ)_/¯ 55 | return e instanceof WhatsAppAPIError ? e.httpStatus : 500; 56 | } 57 | } 58 | 59 | /** 60 | * GET request handler for Express.js 61 | * 62 | * @example 63 | * ```ts 64 | * import express from "express"; 65 | * import { WhatsAppAPI } from "whatsapp-api-js/middleware/express"; 66 | * 67 | * const app = express(); 68 | * const Whatsapp = new WhatsAppAPI({ 69 | * token: "YOUR_TOKEN", 70 | * appSecret: "YOUR_APP_SECRET", 71 | * webhookVerifyToken: "YOUR_WEBHOOK_VERIFY_TOKEN" 72 | * }); 73 | * 74 | * app.get("/message", (req, res) => { 75 | * try { 76 | * res.send(Whatsapp.handle_get(req)); 77 | * } catch (e) { 78 | * res.sendStatus(e as number); 79 | * } 80 | * }); 81 | * ``` 82 | * 83 | * @override 84 | * @param req - The request object from Express.js 85 | * @returns The challenge string to be sent to the client 86 | * @throws The error code 87 | */ 88 | handle_get(req: Request): string { 89 | try { 90 | return this.get(req.query as GetParams); 91 | } catch (e) { 92 | // In case who knows what fails ¯\_(ツ)_/¯ 93 | throw e instanceof WhatsAppAPIError ? e.httpStatus : 500; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/middleware/globals.ts: -------------------------------------------------------------------------------- 1 | import { WhatsAppAPI } from "../index.js"; 2 | 3 | /** 4 | * The abstract class for the middlewares, it extends the WhatsAppAPI class 5 | * and defines the handle_post and handle_get methods for its childs. 6 | */ 7 | export abstract class WhatsAppAPIMiddleware extends WhatsAppAPI { 8 | /** 9 | * This method should be called when the server receives a POST request. 10 | * Each child implements it differently depending on the framework. 11 | * 12 | * @returns The status code to be sent to the client 13 | */ 14 | abstract handle_post(...a: unknown[]): Promise; 15 | 16 | /** 17 | * This method should be called when the server receives a GET request. 18 | * Each child implements it differently depending on the framework. 19 | * 20 | * @returns The challenge string to be sent to the client 21 | * @throws The error code 22 | */ 23 | abstract handle_get(...a: unknown[]): string; 24 | } 25 | -------------------------------------------------------------------------------- /src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | // This file is a stud for the documentation. 2 | // You shouldn't import it as it might execute code for different frameworks. 3 | 4 | /** 5 | * @module middleware 6 | * 7 | * @description 8 | * Simplify the setup proccess of WhatsAppAPI for different frameworks. 9 | * 10 | * @example 11 | * ```ts 12 | * import { WhatsAppAPI } from "whatsapp-api-js/middleware/deno"; 13 | * 14 | * const Whatsapp = new WhatsAppAPI({ 15 | * token: "YOUR_TOKEN", 16 | * appSecret: "YOUR_APP_SECRET" 17 | * }); 18 | * 19 | * Deno.serve(async (req) => { 20 | * if (req.method === "POST") { 21 | * const status = await Whatsapp.handle_post(req); 22 | * return new Response(null, { status }); 23 | * } else if (req.method === "GET") { 24 | * const challenge = Whatsapp.handle_get(req); 25 | * return new Response(challenge); 26 | * } 27 | * }); 28 | * ``` 29 | * 30 | * @see {@link WhatsAppAPIMiddleware.handle_post} 31 | * @see {@link WhatsAppAPIMiddleware.handle_get} 32 | */ 33 | 34 | export { WhatsAppAPIMiddleware } from "./globals.js"; 35 | export { WhatsAppAPI as ExpressMiddleware } from "./express.js"; 36 | export { WhatsAppAPI as AdonisMiddleware } from "./adonis.js"; 37 | export { WhatsAppAPI as VercelMiddleware } from "./vercel.js"; 38 | export { WhatsAppAPI as DenoMiddleware } from "./deno.js"; 39 | export { WhatsAppAPI as BunMiddleware } from "./bun.js"; 40 | export { WhatsAppAPI as SvelteKitMiddleware } from "./sveltekit.js"; 41 | export { WhatsAppAPI as NextAppMiddleware } from "./next.js"; 42 | export { WhatsAppAPI as WebStandardMiddleware } from "./web-standard.js"; 43 | export { WhatsAppAPI as NodeHTTPMiddleware } from "./node-http.js"; 44 | -------------------------------------------------------------------------------- /src/middleware/next.ts: -------------------------------------------------------------------------------- 1 | import { WhatsAppAPI as WebStandardMiddleware } from "./web-standard.js"; 2 | 3 | /** 4 | * NextJS App Router Endpoints middleware for WhatsAppAPI 5 | */ 6 | export class WhatsAppAPI extends WebStandardMiddleware { 7 | /** 8 | * POST request handler for NextJS App Router 9 | * 10 | * @example 11 | * ```ts 12 | * import { WhatsAppAPI } from 'whatsapp-api-js/middleware/next'; 13 | * 14 | * const Whatsapp = new WhatsAppAPI({ 15 | * token: "YOUR_TOKEN", 16 | * appSecret: "YOUR_APP_SECRET", 17 | * webhookVerifyToken: "YOUR_WEBHOOK_VERIFY_TOKEN" 18 | * }); 19 | * 20 | * export async function POST(request: Request) { 21 | * return new Response(null, { 22 | * status: await Whatsapp.handle_post(request) 23 | * }); 24 | * } 25 | * ``` 26 | * 27 | * @param req - The request object 28 | * @returns The status code to be sent to the client 29 | */ 30 | async handle_post(req: Request): Promise { 31 | return super.handle_post(req); 32 | } 33 | 34 | /** 35 | * GET request handler for NextJS App Router 36 | * 37 | * @example 38 | * ```ts 39 | * import { WhatsAppAPI } from 'whatsapp-api-js/middleware/next'; 40 | * 41 | * const Whatsapp = new WhatsAppAPI({ 42 | * token: "YOUR_TOKEN", 43 | * appSecret: "YOUR_APP_SECRET", 44 | * webhookVerifyToken: "YOUR_WEBHOOK_VERIFY_TOKEN" 45 | * }); 46 | * 47 | * export async function GET(request: Request) { 48 | * try { 49 | * return new Response(Whatsapp.handle_get(request)); 50 | * } catch (e) { 51 | * return new Response(null, { status: e as number }); 52 | * } 53 | * } 54 | * ``` 55 | * 56 | * @param req - The request object 57 | * @returns The challenge string to be sent to the client 58 | * @throws The error code 59 | */ 60 | handle_get(req: Request): string { 61 | return super.handle_get(req); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/middleware/node-http.ts: -------------------------------------------------------------------------------- 1 | import { WhatsAppAPIMiddleware } from "./globals.js"; 2 | import { WhatsAppAPIError } from "../errors.js"; 3 | 4 | import type { IncomingMessage } from "node:http"; 5 | import type { Readable } from "node:stream"; 6 | 7 | import type { GetParams } from "../types.d.ts"; 8 | 9 | /** 10 | * node:http server middleware for WhatsAppAPI 11 | */ 12 | export class WhatsAppAPI extends WhatsAppAPIMiddleware { 13 | /** 14 | * POST request handler for node:http server 15 | * 16 | * @example 17 | * ```ts 18 | * import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'; 19 | * import { WhatsAppAPI } from "whatsapp-api-js/middleware/node-http"; 20 | * 21 | * const Whatsapp = new WhatsAppAPI({ 22 | * token: "YOUR_TOKEN", 23 | * appSecret: "YOUR_APP_SECRET", 24 | * webhookVerifyToken: "YOUR_WEBHOOK_VERIFY_TOKEN" 25 | * }); 26 | * 27 | * const server = createServer(async (request: IncomingMessage, response: ServerResponse) => { 28 | * if (request.url === "/message" && request.method === "POST") { 29 | * response.statusCode = await Whatsapp.handle_post(request); 30 | * response.end(); 31 | * } 32 | * }); 33 | * 34 | * server.listen(5000); 35 | * ``` 36 | * 37 | * @override 38 | * @param req - The request object 39 | * @returns The status code to be sent to the client 40 | */ 41 | async handle_post(req: IncomingMessage): Promise { 42 | /** 43 | * Copy pasted from an issue on Deno's repository :) 44 | * 45 | * @internal 46 | * @param readable - The readable stream 47 | * @returns The parsed body 48 | */ 49 | async function parseBody(readable: Readable) { 50 | const chunks = []; 51 | 52 | for await (const chunk of readable) { 53 | chunks.push( 54 | typeof chunk === "string" ? Buffer.from(chunk) : chunk 55 | ); 56 | } 57 | 58 | return Buffer.concat(chunks).toString("utf-8"); 59 | } 60 | 61 | try { 62 | const body = await parseBody(req); 63 | const signature = req.headers["x-hub-signature-256"]; 64 | 65 | if (typeof signature !== "string") throw 400; 66 | 67 | await this.post(JSON.parse(body || "{}"), body, signature); 68 | 69 | return 200; 70 | } catch (e) { 71 | // In case the JSON.parse fails ¯\_(ツ)_/¯ 72 | return e instanceof WhatsAppAPIError ? e.httpStatus : 500; 73 | } 74 | } 75 | 76 | /** 77 | * GET request handler for node:http server 78 | * 79 | * @example 80 | * ```ts 81 | * import { createServer, IncomingMessage, ServerResponse } from 'node:http'; 82 | * import { WhatsAppAPI } from "whatsapp-api-js/middleware/node-http"; 83 | * 84 | * const server = createServer((request: IncomingMessage, response: ServerResponse) => { 85 | * if (request.url === "/message" && request.method === "GET") { 86 | * try { 87 | * response.statusCode = 200; 88 | * response.end(Whatsapp.handle_get(request)); 89 | * } catch (e) { 90 | * response.statusCode = e as number; 91 | * response.end(); 92 | * } 93 | * } 94 | * }); 95 | * 96 | * server.listen(5000); 97 | * ``` 98 | * 99 | * @override 100 | * @param req - The request object 101 | * @returns The challenge string to be sent to the client 102 | * @throws The error code 103 | */ 104 | handle_get(req: IncomingMessage): string { 105 | try { 106 | return this.get( 107 | Object.fromEntries( 108 | new URL(req.url!, `http://${req.headers.host}`).searchParams 109 | ) as GetParams 110 | ); 111 | } catch (e) { 112 | // In case who knows what fails ¯\_(ツ)_/¯ 113 | throw e instanceof WhatsAppAPIError ? e.httpStatus : 500; 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/middleware/sveltekit.ts: -------------------------------------------------------------------------------- 1 | import { WhatsAppAPI as WebStandardMiddleware } from "./web-standard.js"; 2 | 3 | /** 4 | * SvelteKit Endpoints middleware for WhatsAppAPI 5 | * 6 | * @see https://kit.svelte.dev/docs/routing#server 7 | */ 8 | export class WhatsAppAPI extends WebStandardMiddleware { 9 | /** 10 | * POST request handler for SvelteKit RequestHandler 11 | * 12 | * @example 13 | * ```ts 14 | * import { WhatsAppAPI } from 'whatsapp-api-js/middleware/sveltekit'; 15 | * 16 | * import type { RequestHandler } from './$types'; 17 | * 18 | * const Whatsapp = new WhatsAppAPI({ 19 | * token: "YOUR_TOKEN", 20 | * appSecret: "YOUR_APP_SECRET", 21 | * webhookVerifyToken: "YOUR_WEBHOOK_VERIFY_TOKEN" 22 | * }); 23 | * 24 | * export const POST: RequestHandler = async ({ request }) => { 25 | * return new Response(null, { 26 | * status: await Whatsapp.handle_post(request) 27 | * }); 28 | * }; 29 | * ``` 30 | * 31 | * @param req - The request object 32 | * @returns The status code to be sent to the client 33 | */ 34 | async handle_post(req: Request): Promise { 35 | return super.handle_post(req); 36 | } 37 | 38 | /** 39 | * GET request handler for SvelteKit RequestHandler 40 | * 41 | * @example 42 | * ```ts 43 | * import { WhatsAppAPI } from 'whatsapp-api-js/middleware/sveltekit'; 44 | * 45 | * import type { RequestHandler } from './$types'; 46 | * 47 | * const Whatsapp = new WhatsAppAPI({ 48 | * token: "YOUR_TOKEN", 49 | * appSecret: "YOUR_APP_SECRET", 50 | * webhookVerifyToken: "YOUR_WEBHOOK_VERIFY_TOKEN" 51 | * }); 52 | * 53 | * export const GET: RequestHandler = ({ request, url }) => { 54 | * try { 55 | * return new Response(Whatsapp.handle_get(request)); 56 | * } catch (e) { 57 | * return new Response(null, { status: e as number }); 58 | * } 59 | * }; 60 | * ``` 61 | * 62 | * @param req - The request object 63 | * @returns The challenge string to be sent to the client 64 | * @throws The error code 65 | */ 66 | handle_get(req: Request): string { 67 | return super.handle_get(req); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/middleware/vercel.ts: -------------------------------------------------------------------------------- 1 | import { WhatsAppAPI as NodeHTTPMiddleware } from "./node-http.js"; 2 | import { WhatsAppAPIError } from "../errors.js"; 3 | 4 | import type { VercelRequest } from "@vercel/node"; 5 | import type { GetParams } from "../types.d.ts"; 6 | 7 | /** 8 | * Vercel serverless functions middleware for WhatsAppAPI (Node/Next.js) 9 | */ 10 | export class WhatsAppAPI extends NodeHTTPMiddleware { 11 | /** 12 | * POST request handler for Vercel serverless functions 13 | * 14 | * @remarks This method expects the request body to be the original string, not a parsed body 15 | * @see https://vercel.com/guides/how-do-i-get-the-raw-body-of-a-serverless-function 16 | * 17 | * @example 18 | * ```ts 19 | * import type { VercelRequest, VercelResponse } from "@vercel/node"; 20 | * import { WhatsAppAPI } from "whatsapp-api-js/middleware/vercel"; 21 | * 22 | * const Whatsapp = new WhatsAppAPI({ 23 | * token: "YOUR_TOKEN", 24 | * appSecret: "YOUR_APP_SECRET", 25 | * webhookVerifyToken: "YOUR_WEBHOOK_VERIFY_TOKEN" 26 | * }); 27 | * 28 | * // The `bodyParser: false` is required for the middleware to work 29 | * export const config = { 30 | * api: { 31 | * bodyParser: false 32 | * } 33 | * }; 34 | * 35 | * export default async (req: VercelRequest, res: VercelResponse) => { 36 | * if (req.method === "POST") { 37 | * res.status(await Whatsapp.handle_post(req)); 38 | * res.end(); 39 | * } 40 | * }; 41 | * ``` 42 | * 43 | * @param req - The request object 44 | * @returns The status code to be sent to the client 45 | */ 46 | handle_post(req: VercelRequest): Promise { 47 | return super.handle_post(req); 48 | } 49 | 50 | /** 51 | * GET request handler for Vercel serverless functions 52 | * 53 | * @example 54 | * ```ts 55 | * import type { VercelRequest, VercelResponse } from "@vercel/node"; 56 | * import { WhatsAppAPI } from "whatsapp-api-js/middleware/vercel"; 57 | * 58 | * const Whatsapp = new WhatsAppAPI({ 59 | * token: "YOUR_TOKEN", 60 | * appSecret: "YOUR_APP_SECRET", 61 | * webhookVerifyToken: "YOUR_WEBHOOK_VERIFY_TOKEN" 62 | * }); 63 | * 64 | * export default (req: VercelRequest, res: VercelResponse) => { 65 | * if (req.method === "GET") { 66 | * try { 67 | * res.end(Whatsapp.handle_get(req)); 68 | * res.status(200); 69 | * } catch (e) { 70 | * res.status(e as number).end(); 71 | * } 72 | * } 73 | * }; 74 | * ``` 75 | * 76 | * @override 77 | * @param req - The request object 78 | * @returns The challenge string to be sent to the client 79 | * @throws The error code 80 | */ 81 | handle_get(req: VercelRequest): string { 82 | try { 83 | return this.get(req.query as GetParams); 84 | } catch (e) { 85 | // In case who knows what fails ¯\_(ツ)_/¯ 86 | throw e instanceof WhatsAppAPIError ? e.httpStatus : 500; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/middleware/web-standard.ts: -------------------------------------------------------------------------------- 1 | import { WhatsAppAPIMiddleware } from "./globals.js"; 2 | import { WhatsAppAPIError } from "../errors.js"; 3 | 4 | import type { GetParams } from "../types.d.ts"; 5 | 6 | /** 7 | * Web Standard API http server middleware for WhatsAppAPI (deno/bun/SvelteKit/NextJS/Hono) 8 | */ 9 | export class WhatsAppAPI extends WhatsAppAPIMiddleware { 10 | /** 11 | * POST request handler for Web Standard API http server 12 | * 13 | * @override 14 | * @param req - The request object 15 | * @returns The status code to be sent to the client 16 | */ 17 | async handle_post(req: Request): Promise { 18 | try { 19 | const body = await req.text(); 20 | 21 | await this.post( 22 | JSON.parse(body || "{}"), 23 | body, 24 | req.headers.get("x-hub-signature-256") ?? "" 25 | ); 26 | 27 | return 200; 28 | } catch (e) { 29 | // In case the JSON.parse fails ¯\_(ツ)_/¯ 30 | return e instanceof WhatsAppAPIError ? e.httpStatus : 500; 31 | } 32 | } 33 | 34 | /** 35 | * GET request handler for Web Standard API http server 36 | * 37 | * @override 38 | * @param req - The request object 39 | * @returns The challenge string to be sent to the client 40 | * @throws The error code 41 | */ 42 | handle_get(req: Request): string { 43 | try { 44 | return this.get( 45 | Object.fromEntries(new URL(req.url).searchParams) as GetParams 46 | ); 47 | } catch (e) { 48 | // In case who knows what fails ¯\_(ツ)_/¯ 49 | throw e instanceof WhatsAppAPIError ? e.httpStatus : 500; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/setup/bun.ts: -------------------------------------------------------------------------------- 1 | import type { WhatsAppAPIConstructorArguments } from "../types.d.ts"; 2 | 3 | /** 4 | * A Bun quick setup for the WhatsAppAPI 5 | * 6 | * @remarks This method will return the same object as the one passed in 7 | * 8 | * @param settings - The WhatsAppAPI arguments 9 | * @returns A WhatsAppAPI arguments object for Bun 10 | */ 11 | export function Bun( 12 | settings: WhatsAppAPIConstructorArguments 13 | ): WhatsAppAPIConstructorArguments { 14 | return settings; 15 | } 16 | -------------------------------------------------------------------------------- /src/setup/deno.ts: -------------------------------------------------------------------------------- 1 | import type { WhatsAppAPIConstructorArguments } from "../types.d.ts"; 2 | 3 | /** 4 | * A Deno quick setup for the WhatsAppAPI 5 | * 6 | * @remarks This method will return the same object as the one passed in 7 | * 8 | * @param settings - The WhatsAppAPI arguments 9 | * @returns A WhatsAppAPI arguments object for Deno 10 | */ 11 | export function Deno( 12 | settings: WhatsAppAPIConstructorArguments 13 | ): WhatsAppAPIConstructorArguments { 14 | return settings; 15 | } 16 | -------------------------------------------------------------------------------- /src/setup/index.ts: -------------------------------------------------------------------------------- 1 | // This file is a stud for the documentation. 2 | // You shouldn't import it as it might execute code for different runtimes. 3 | 4 | /** 5 | * @module setup 6 | * 7 | * @description 8 | * Simplify the setup proccess of the WhatsAppAPI for different runtimes. 9 | * 10 | * @example 11 | * ```ts 12 | * import { WhatsAppAPI } from "whatsapp-api-js"; 13 | * import { NodeNext } from "whatsapp-api-js/setup/node"; 14 | * 15 | * const Whatsapp = new WhatsAppAPI(NodeNext({ 16 | * token: "YOUR_TOKEN", 17 | * appSecret: "YOUR_APP_SECRET" 18 | * })); 19 | * ``` 20 | */ 21 | 22 | export * from "./bun"; 23 | export * from "./deno"; 24 | export * from "./web"; 25 | export * from "./node"; 26 | -------------------------------------------------------------------------------- /src/setup/node.ts: -------------------------------------------------------------------------------- 1 | import type { WhatsAppAPIConstructorArguments } from "../types.d.ts"; 2 | 3 | // If this line of code didn't exist, 4 | // setup would be a single file rather than a folder 5 | import { webcrypto } from "node:crypto"; 6 | 7 | /** 8 | * A Node\@^19 quick setup for the WhatsAppAPI 9 | * 10 | * @remarks This method will return the same object as the one passed in 11 | * 12 | * @param settings - The WhatsAppAPI arguments 13 | * @returns A WhatsAppAPI arguments object for Node\@^19 14 | */ 15 | export function NodeNext( 16 | settings: WhatsAppAPIConstructorArguments 17 | ): WhatsAppAPIConstructorArguments { 18 | return settings; 19 | } 20 | 21 | /** 22 | * A Node\@18 quick setup for the WhatsAppAPI 23 | * 24 | * @remarks Assumes that the fetch function is available globally 25 | * 26 | * @param settings - The WhatsAppAPI arguments 27 | * @returns A WhatsAppAPI arguments object for Node\@^18 28 | */ 29 | export function Node18( 30 | settings: WhatsAppAPIConstructorArguments 31 | ): WhatsAppAPIConstructorArguments { 32 | return { 33 | ...settings, 34 | ponyfill: { 35 | subtle: webcrypto.subtle, 36 | ...settings.ponyfill 37 | } 38 | }; 39 | } 40 | 41 | /** 42 | * A Node 15 to 17 quick setup for the WhatsAppAPI 43 | * 44 | * @deprecated Node 15 to 17 reached EoL and are no longer supported by the library 45 | * @param settings - The WhatsAppAPI arguments 46 | * @param fetch_ponyfill - The fetch ponyfill function to use (e.g. node-fetch or undici) 47 | * @returns A WhatsAppAPI arguments object for Node 15 to 17 48 | */ 49 | export function Node15( 50 | settings: WhatsAppAPIConstructorArguments, 51 | fetch_ponyfill: typeof fetch 52 | ): WhatsAppAPIConstructorArguments { 53 | return { 54 | ...settings, 55 | ponyfill: { 56 | fetch: fetch_ponyfill, 57 | subtle: webcrypto.subtle, 58 | ...settings.ponyfill 59 | } 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /src/setup/web.ts: -------------------------------------------------------------------------------- 1 | import type { WhatsAppAPIConstructorArguments } from "../types.d.ts"; 2 | 3 | /** 4 | * A Web quick setup for the WhatsAppAPI 5 | * 6 | * @remarks 7 | * This method will return the same object as the one passed in, 8 | * as it assumes that fetch and crypto.subtle are available globally 9 | * 10 | * @param settings - The WhatsAppAPI arguments 11 | * @returns A WhatsAppAPI arguments object for the Web 12 | */ 13 | export function Web( 14 | settings: WhatsAppAPIConstructorArguments 15 | ): WhatsAppAPIConstructorArguments { 16 | return settings; 17 | } 18 | -------------------------------------------------------------------------------- /src/standalone.ts: -------------------------------------------------------------------------------- 1 | export * from "./utils.js"; 2 | export * from "./types.js"; 3 | export * from "./errors.js"; 4 | export * from "./emitters.js"; 5 | export * from "./messages/index.js"; 6 | export * from "./index.js"; 7 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module types 3 | * 4 | * @description 5 | * The types of the library. Mostly for internal use, 6 | * but if you want to "understand" the code under the hood, 7 | * feel free to read the docs :) 8 | */ 9 | 10 | import type { 11 | Text, 12 | Audio, 13 | Document, 14 | Image, 15 | Sticker, 16 | Video, 17 | Location, 18 | Interactive, 19 | Template, 20 | Reaction, 21 | Contacts 22 | } from "./messages/index.d.ts"; 23 | import type { AtLeastOne } from "./utils.d.ts"; 24 | 25 | export const DEFAULT_API_VERSION = "v22.0"; 26 | 27 | /** 28 | * The main constructor arguments for the API 29 | */ 30 | export type TheBasicConstructorArguments = { 31 | /** 32 | * The API token, given at setup. 33 | * You must provide an API token to use the framework. 34 | * 35 | * It can either be a temporal or permanent one. 36 | * 37 | * In order to create a permanent token, first make sure you have 38 | * linked your WhatsApp account to a Meta Commercial Account in 39 | * [Meta for Developers Dashboard](https://developers.facebook.com/apps). 40 | * 41 | * After that, head to [Bussiness Settings](https://business.facebook.com/settings/system-users), 42 | * select your app, create a new system user with `admin role`. 43 | * Then click "Add Actives", select Apps -\> Your App -\> App Administrator. 44 | * 45 | * And this was the point were Meta decided I was too sus because 46 | * I created a second bussiness to follow my own tutorial, 47 | * and as I didn't want to give them my ID, they banned my account. 48 | * 49 | * If you read until here, you probably will figure it out. 50 | * It's not that hard after getting in the right place. 51 | * 52 | * Really wish WhatsApp gets away from Meta soon... 53 | * 54 | * (Sorry for the rant, here's the [actual documentation](https://developers.facebook.com/docs/whatsapp/business-management-api/get-started) :) 55 | */ 56 | token: string; 57 | /** 58 | * The app secret, given at setup. 59 | * 60 | * The secret is used as a signature to validate payload's authenticity. 61 | * 62 | * To get your app secret, head to 63 | * [Meta for Developers Dashboard](https://developers.facebook.com/apps), 64 | * select your app and open Settings -\> Basic -\> App Secret -\> Show. 65 | * 66 | * If you want to skip the verification and remove the need to provide the secret, 67 | * set `secure` to `false`. 68 | */ 69 | appSecret?: string | never; 70 | /** 71 | * The webhook verify token, configured at setup. 72 | * Used exclusively to verify the server against WhatsApp's servers via the GET method. 73 | * 74 | * Not required by default, but calling this.get() without it will result in an error. 75 | */ 76 | webhookVerifyToken?: string; 77 | /** 78 | * The version of the API, defaults to {@link DEFAULT_API_VERSION}. 79 | */ 80 | v?: string; 81 | /** 82 | * Whether to return a pre-processed response from the API or the raw fetch response. 83 | * Intended for low level debugging. 84 | */ 85 | parsed?: boolean; 86 | /** 87 | * If set to false, none of the API checks will be performed, and it will be used in a less secure way. 88 | * 89 | * Defaults to true. 90 | */ 91 | secure?: boolean; 92 | /** 93 | * The ponyfills to use. 94 | * 95 | * This are meant to provide standard APIs implementations 96 | * on enviroments that don't have them. 97 | * 98 | * For example, if using Node 16, you will need to ponyfill 99 | * the fetch method with any spec complient fetch method. 100 | * 101 | * @remarks 102 | * With the additions of {@link setup} for the most common enviroments, 103 | * this parameter should no longer be configured manually. 104 | * 105 | * @example 106 | * ```ts 107 | * import { fetch } from "undici"; 108 | * import { subtle } from "node:crypto"; 109 | * 110 | * const api = new WhatsAppAPI({ 111 | * token: "my-token", 112 | * appSecret: "my-app-secret", 113 | * ponyfill: { 114 | * fetch, 115 | * subtle 116 | * } 117 | * }); 118 | * ``` 119 | */ 120 | ponyfill?: { 121 | /** 122 | * The fetch ponyfill to use for the requests. If not specified, it defaults to the fetch function from the enviroment. 123 | */ 124 | fetch?: typeof fetch; 125 | /** 126 | * The subtle ponyfill to use for the signatures. If not specified, it defaults to crypto.subtle from the enviroment. 127 | */ 128 | subtle?: Pick; 129 | }; 130 | }; 131 | 132 | /** 133 | * This switch allows TypeScript to cry if appSecret is not provided when secure is true. 134 | */ 135 | export type SecureLightSwitch = 136 | | { 137 | secure?: true; 138 | appSecret: string; 139 | } 140 | | { 141 | secure: false; 142 | appSecret?: never; 143 | }; 144 | 145 | /** 146 | * Created this type if in the future the constructor needs more complex types. 147 | */ 148 | export type ExtraTypesThatMakeTypescriptWork = SecureLightSwitch; 149 | 150 | /** 151 | * Monkey patching TypeDoc inability to handle complex types. 152 | * 153 | * You should absolutely read {@link TheBasicConstructorArguments} in order to use the framework. 154 | */ 155 | export type WhatsAppAPIConstructorArguments = TheBasicConstructorArguments & 156 | ExtraTypesThatMakeTypescriptWork; 157 | 158 | /** 159 | * The base class of all the library messages 160 | * 161 | * Polymorphism is cool :] 162 | */ 163 | export abstract class ClientMessage { 164 | /** 165 | * The message type 166 | * 167 | * @internal 168 | */ 169 | abstract get _type(): ClientMessageNames; 170 | } 171 | 172 | export interface ClientTypedMessageComponent { 173 | /** 174 | * The message's component type 175 | * 176 | * @internal 177 | */ 178 | get _type(): string; 179 | } 180 | 181 | export interface ClientBuildableMessageComponent { 182 | /** 183 | * The message's component builder method 184 | * 185 | * @internal 186 | */ 187 | _build(..._: unknown[]): unknown; 188 | } 189 | 190 | export abstract class ClientLimitedMessageComponent { 191 | /** 192 | * Throws an error if the array length is greater than the specified number. 193 | * 194 | * @param p - The parent component name 195 | * @param c - The component name 196 | * @param a - The array to check the length of 197 | * @param n - The maximum length 198 | * @throws `${p} can't have more than ${n} ${c}` 199 | */ 200 | constructor(p: string, c: string, a: Array, n: N) { 201 | if (a.length > n) { 202 | throw new Error(`${p} can't have more than ${n} ${c}`); 203 | } 204 | } 205 | } 206 | 207 | /** 208 | * All sections are structured the same way, so this abstract class is used to reduce code duplication 209 | * 210 | * @remarks 211 | * - All sections must have between 1 and N elements 212 | * - All sections must have a title if more than 1 section is provided 213 | * 214 | * @typeParam T - The type of the components of the section 215 | * @typeParam N - The maximum number of elements in the section 216 | */ 217 | export abstract class Section< 218 | T, 219 | N extends number 220 | > extends ClientLimitedMessageComponent { 221 | /** 222 | * The title of the section 223 | */ 224 | readonly title?: string; 225 | 226 | /** 227 | * Builds a section component 228 | * 229 | * @param name - The name of the section's type 230 | * @param keys_name - The name of the section's keys 231 | * @param elements - The elements of the section 232 | * @param max - The maximum number of elements in the section 233 | * @param title - The title of the section 234 | * @param title_length - The maximum length of the title 235 | * @throws If more than N elements are provided 236 | * @throws If title is over 24 characters if provided 237 | */ 238 | constructor( 239 | name: string, 240 | keys_name: string, 241 | elements: AtLeastOne, 242 | max: N, 243 | title?: string, 244 | title_length = 24 245 | ) { 246 | super(name, keys_name, elements, max); 247 | 248 | if (title) { 249 | if (title.length > title_length) { 250 | throw new Error( 251 | `${name} title must be ${title_length} characters or less` 252 | ); 253 | } 254 | 255 | this.title = title; 256 | } 257 | } 258 | } 259 | 260 | // Somehow, Contacts still manages to be annoying 261 | export abstract class ContactComponent 262 | implements ClientTypedMessageComponent, ClientBuildableMessageComponent 263 | { 264 | /** 265 | * @override 266 | * @internal 267 | */ 268 | _build(): unknown { 269 | return this; 270 | } 271 | 272 | /** 273 | * Whether the component can be repeated multiple times in a contact. 274 | * 275 | * @internal 276 | */ 277 | abstract get _many(): boolean; 278 | /** 279 | * @override 280 | * @internal 281 | */ 282 | abstract get _type(): string; 283 | } 284 | 285 | /** 286 | * A contact multiple component can be repeated multiple times in a contact. 287 | */ 288 | export abstract class ContactMultipleComponent extends ContactComponent { 289 | /** 290 | * @override 291 | * @internal 292 | */ 293 | get _many(): true { 294 | return true; 295 | } 296 | 297 | /** 298 | * @override 299 | * @internal 300 | */ 301 | abstract get _type(): string; 302 | } 303 | 304 | /** 305 | * A contact unique component can only be used once in a contact. 306 | */ 307 | export abstract class ContactUniqueComponent extends ContactComponent { 308 | /** 309 | * @override 310 | * @internal 311 | */ 312 | get _many(): false { 313 | return false; 314 | } 315 | 316 | /** 317 | * @override 318 | * @internal 319 | */ 320 | abstract get _type(): string; 321 | } 322 | 323 | /** 324 | * Polymorphism intensifies. Also helps with the _type typings :) 325 | */ 326 | export interface InteractiveAction extends ClientTypedMessageComponent { 327 | /** 328 | * @overload 329 | * @internal 330 | */ 331 | get _type(): 332 | | "list" 333 | | "button" 334 | | "catalog_message" 335 | | "product" 336 | | "product_list" 337 | | "cta_url" 338 | | "flow" 339 | | "location_request_message"; 340 | } 341 | 342 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 343 | export interface TemplateComponent extends ClientBuildableMessageComponent {} 344 | 345 | export type ClientMessageNames = 346 | | "text" 347 | | "audio" 348 | | "document" 349 | | "image" 350 | | "sticker" 351 | | "video" 352 | | "location" 353 | | "contacts" 354 | | "interactive" 355 | | "template" 356 | | "reaction"; 357 | 358 | export type ClientTypingIndicators = "text"; 359 | 360 | // #region Client Message Request 361 | 362 | export type ClientMessageRequest = { 363 | /** 364 | * The messaging product 365 | */ 366 | messaging_product: "whatsapp"; 367 | /** 368 | * The user's phone number 369 | */ 370 | to: string; 371 | /** 372 | * Currently you can only send messages to individuals 373 | */ 374 | recipient_type: "individual"; 375 | /** 376 | * The message to reply to 377 | */ 378 | context?: { 379 | /** 380 | * The message id to reply to 381 | */ 382 | message_id: string; 383 | }; 384 | /** 385 | * An arbitrary 512B string, useful for tracking. 386 | * 387 | * Any app subscribed to the messages webhook field on the WhatsApp Business Account can get this string, 388 | * as it is included in statuses object within webhook payloads. 389 | * 390 | * Cloud API does not process this field, it just returns it as part of sent/delivered/read message webhooks. 391 | */ 392 | biz_opaque_callback_data?: string; 393 | } & { 394 | [Type in ClientMessageNames]?: ClientMessage; 395 | } & ( 396 | | { 397 | type: "text"; 398 | text: Text; 399 | } 400 | | { 401 | type: "audio"; 402 | audio: Audio; 403 | } 404 | | { 405 | type: "document"; 406 | document: Document; 407 | } 408 | | { 409 | type: "image"; 410 | image: Image; 411 | } 412 | | { 413 | type: "sticker"; 414 | sticker: Sticker; 415 | } 416 | | { 417 | type: "video"; 418 | video: Video; 419 | } 420 | | { 421 | type: "location"; 422 | location: Location; 423 | } 424 | | { 425 | type: "contacts"; 426 | contacts: Contacts; 427 | } 428 | | { 429 | type: "interactive"; 430 | interactive: Interactive; 431 | } 432 | | { 433 | type: "template"; 434 | template: Template; 435 | } 436 | | { 437 | type: "reaction"; 438 | reaction: Reaction; 439 | } 440 | ); 441 | 442 | // #endregion 443 | 444 | export type ServerTextMessage = { 445 | type: "text"; 446 | text: { 447 | body: string; 448 | }; 449 | }; 450 | 451 | export type ServerAudioMessage = { 452 | type: "audio"; 453 | audio: { 454 | mime_type: string; 455 | sha256: string; 456 | id: string; 457 | }; 458 | }; 459 | 460 | export type ServerDocumentMessage = { 461 | type: "document"; 462 | document: { 463 | caption?: string; 464 | filename: string; 465 | mime_type: string; 466 | sha256: string; 467 | id: string; 468 | }; 469 | }; 470 | 471 | export type ServerImageMessage = { 472 | type: "image"; 473 | image: { 474 | caption?: string; 475 | mime_type: string; 476 | sha256: string; 477 | id: string; 478 | }; 479 | }; 480 | 481 | export type ServerStickerMessage = { 482 | type: "sticker"; 483 | sticker: { 484 | id: string; 485 | animated: boolean; 486 | mime_type: "image/webp"; 487 | sha256: string; 488 | }; 489 | }; 490 | 491 | export type ServerVideoMessage = { 492 | type: "video"; 493 | video: { 494 | mime_type: string; 495 | sha256: string; 496 | id: string; 497 | }; 498 | }; 499 | 500 | export type ServerLocationMessage = { 501 | type: "location"; 502 | location: { 503 | latitude: string; 504 | longitude: string; 505 | name?: string; 506 | address?: string; 507 | }; 508 | }; 509 | 510 | export type ServerContactsMessage = { 511 | type: "contacts"; 512 | contacts: [ 513 | { 514 | addresses?: [ 515 | { 516 | city?: string; 517 | country?: string; 518 | country_code?: string; 519 | state?: string; 520 | street?: string; 521 | type?: string; 522 | zip?: string; 523 | } 524 | ]; 525 | birthday?: string; 526 | emails?: [ 527 | { 528 | email?: string; 529 | type?: string; 530 | } 531 | ]; 532 | name: { 533 | formatted_name: string; 534 | first_name?: string; 535 | last_name?: string; 536 | middle_name?: string; 537 | suffix?: string; 538 | prefix?: string; 539 | }; 540 | org?: { 541 | company?: string; 542 | department?: string; 543 | title?: string; 544 | }; 545 | phones?: [ 546 | { 547 | phone?: string; 548 | wa_id?: string; 549 | type?: string; 550 | } 551 | ]; 552 | urls?: [ 553 | { 554 | url?: string; 555 | type?: string; 556 | } 557 | ]; 558 | } 559 | ]; 560 | }; 561 | 562 | export type ServerInteractiveMessage = { 563 | type: "interactive"; 564 | interactive: 565 | | { 566 | type: "button_reply"; 567 | button_reply: { 568 | id: string; 569 | title: string; 570 | }; 571 | list_reply: never; 572 | nfm_reply: never; 573 | } 574 | | { 575 | type: "list_reply"; 576 | list_reply: { 577 | id: string; 578 | title: string; 579 | description: string; 580 | }; 581 | button_reply: never; 582 | nfm_reply: never; 583 | } 584 | | { 585 | type: "nfm_reply"; 586 | nfm_reply: 587 | | { 588 | name: "address_message"; 589 | body?: string; 590 | response_json: string; 591 | } 592 | | { 593 | name: "flow"; 594 | body: "Sent"; 595 | response_json: string; 596 | } 597 | | { 598 | name?: string; 599 | body?: string; 600 | response_json: string; 601 | }; 602 | button_reply: never; 603 | list_reply: never; 604 | }; 605 | }; 606 | 607 | export type ServerButtonMessage = { 608 | type: "button"; 609 | button: { 610 | text: string; 611 | payload: string; 612 | }; 613 | }; 614 | 615 | export type ServerReactionMessage = { 616 | type: "reaction"; 617 | reaction: { 618 | emoji: string; 619 | message_id: string; 620 | }; 621 | }; 622 | 623 | export type ServerOrderMessage = { 624 | type: "order"; 625 | order: { 626 | catalog_id: string; 627 | product_items: [ 628 | { 629 | product_retailer_id: string; 630 | quantity: string; 631 | item_price: string; 632 | currency: string; 633 | } 634 | ]; 635 | text?: string; 636 | }; 637 | }; 638 | 639 | export type ServerSystemMessage = { 640 | type: "system"; 641 | system: { 642 | body: string; 643 | identity: string; 644 | /** 645 | * @deprecated Since v12.0 it is undefined, use `wa_id` instead. 646 | * 647 | * I'm actually stunned this exists, since I started the library with v13 or 14. 648 | */ 649 | new_wa_id: number | string; 650 | wa_id: string; 651 | type: 652 | | "customer_changed_number" 653 | | "customer_identity_changed" 654 | | string /* Backwards compatibility */; 655 | customer: string; 656 | }; 657 | }; 658 | 659 | export type ServerRequestWelcomeMessage = { 660 | type: "request_welcome"; 661 | }; 662 | 663 | export type ServerUnknownMessage = { 664 | type: "unknown"; 665 | errors: [ 666 | { 667 | code: number; 668 | details: "Message type is not currently supported"; 669 | title: "Unsupported message type"; 670 | } 671 | ]; 672 | }; 673 | 674 | export type ServerMessageTypes = 675 | | ServerTextMessage 676 | | ServerAudioMessage 677 | | ServerDocumentMessage 678 | | ServerImageMessage 679 | | ServerStickerMessage 680 | | ServerVideoMessage 681 | | ServerLocationMessage 682 | | ServerContactsMessage 683 | | ServerInteractiveMessage 684 | | ServerButtonMessage 685 | | ServerReactionMessage 686 | | ServerOrderMessage 687 | | ServerSystemMessage 688 | | ServerRequestWelcomeMessage 689 | | ServerUnknownMessage; 690 | 691 | export type ServerMessage = { 692 | from: string; 693 | id: string; 694 | timestamp: string; 695 | context?: { 696 | forwarded?: boolean; 697 | frequently_forwarded?: boolean; 698 | from?: string; 699 | id?: string; 700 | referred_product?: { 701 | catalog_id: string; 702 | product_retailer_id: string; 703 | }; 704 | }; 705 | identity?: { 706 | acknowledged: boolean; 707 | created_timestamp: string; 708 | hash: string; 709 | }; 710 | /** 711 | * Never saw this property on the wild, but it's documented 712 | */ 713 | errors?: [ServerError]; 714 | referral?: { 715 | source_url: string; 716 | source_id: string; 717 | source_type: "ad" | "post"; 718 | headline: string; 719 | body: string; 720 | ctwa_clid: string; 721 | media_type: "image" | "video"; 722 | } & ( 723 | | { 724 | media_type: "image"; 725 | image_url: string; 726 | } 727 | | { 728 | media_type: "video"; 729 | video_url: string; 730 | thumbnail_url: string; 731 | } 732 | ); 733 | } & ServerMessageTypes; 734 | 735 | export type ServerContacts = { 736 | profile: { 737 | name?: string; 738 | }; 739 | wa_id: string; 740 | }; 741 | 742 | export type ServerInitiation = 743 | | "authentication" 744 | | "marketing" 745 | | "utility" 746 | | "service" 747 | | "referral_conversion"; 748 | 749 | export type ServerStatus = "sent" | "delivered" | "read" | "failed"; 750 | 751 | export type ServerPricing = { 752 | pricing_model: "CBP"; 753 | /** 754 | * @deprecated Since v16 with the release of the new pricing model 755 | */ 756 | billable?: boolean; 757 | category: ServerInitiation | "authentication-international"; 758 | }; 759 | 760 | export type ServerConversation = { 761 | id: string; 762 | expiration_timestamp: number; 763 | origin: { 764 | type: ServerInitiation; 765 | }; 766 | }; 767 | 768 | export type ServerError = { 769 | code: number; 770 | title: string; 771 | message: string; 772 | error_data: { 773 | details: string; 774 | }; 775 | }; 776 | 777 | export type GetParams = { 778 | "hub.mode": "subscribe"; 779 | "hub.verify_token": string; 780 | "hub.challenge": string; 781 | }; 782 | 783 | export type PostData = { 784 | object: "whatsapp_business_account"; 785 | entry: { 786 | id: string; 787 | changes: { 788 | value: { 789 | messaging_product: "whatsapp"; 790 | metadata: { 791 | display_phone_number: string; 792 | phone_number_id: string; 793 | }; 794 | } & ( 795 | | { 796 | contacts?: [ServerContacts]; 797 | messages: [ServerMessage]; 798 | } 799 | | { 800 | statuses: [ 801 | { 802 | id: string; 803 | status: ServerStatus; 804 | timestamp: string; 805 | recipient_id: string; 806 | biz_opaque_callback_data?: string; 807 | } & ( 808 | | { 809 | conversation: ServerConversation; 810 | pricing: ServerPricing; 811 | errors: undefined; 812 | } 813 | | { 814 | conversation: undefined; 815 | pricing: undefined; 816 | errors: [ServerError]; 817 | } 818 | ) 819 | ]; 820 | } 821 | ); 822 | field: "messages"; 823 | }[]; 824 | }[]; 825 | }; 826 | 827 | /** 828 | * @see https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes 829 | */ 830 | export type ServerErrorResponse = { 831 | error: { 832 | message: string; 833 | type: string; 834 | code: number; 835 | error_data: { 836 | messaging_product: "whatsapp"; 837 | details: string; 838 | }; 839 | error_subcode: number; 840 | fbtrace_id: string; 841 | }; 842 | }; 843 | 844 | export type NoServerError = { 845 | error?: never; 846 | }; 847 | 848 | export type ServerSuccessResponse = { 849 | success: true; 850 | } & NoServerError; 851 | 852 | export type ServerSentMessageResponse = { 853 | messaging_product: "whatsapp"; 854 | contacts: [ 855 | { 856 | input: string; 857 | wa_id: string; 858 | } 859 | ]; 860 | messages: [ 861 | { 862 | id: string; 863 | message_status?: "accepted" | "held_for_quality_assessment"; 864 | } 865 | ]; 866 | }; 867 | 868 | export type ServerMessageResponse = 869 | | (ServerSentMessageResponse & NoServerError) 870 | | ServerErrorResponse; 871 | 872 | export type ServerMarkAsReadResponse = 873 | | ServerSuccessResponse 874 | | ServerErrorResponse; 875 | 876 | export type ServerQR = { 877 | code: string; 878 | prefilled_message: string; 879 | deep_link_url: string; 880 | qr_image_url?: string; 881 | }; 882 | 883 | export type ServerCreateQRResponse = 884 | | (ServerQR & NoServerError) 885 | | ServerErrorResponse; 886 | 887 | export type ServerRetrieveQRResponse = 888 | | ({ data: ServerQR[] } & NoServerError) 889 | | ServerErrorResponse; 890 | 891 | export type ServerUpdateQRResponse = 892 | | (ServerQR & NoServerError) 893 | | ServerErrorResponse; 894 | 895 | export type ServerDeleteQRResponse = 896 | | ServerSuccessResponse 897 | | ServerErrorResponse; 898 | 899 | export type ServerMedia = { 900 | id: string; 901 | }; 902 | 903 | export type ServerMediaUploadResponse = 904 | | (ServerMedia & NoServerError) 905 | | ServerErrorResponse; 906 | 907 | export type ValidMimeTypes = 908 | | "audio/aac" 909 | | "audio/mp4" 910 | | "audio/mpeg" 911 | | "audio/amr" 912 | | "audio/ogg" 913 | | "text/plain" 914 | | "application/pdf" 915 | | "application/vnd.ms-powerpoint" 916 | | "application/msword" 917 | | "application/vnd.ms-excel" 918 | | "application/vnd.openxmlformats-officedocument.wordprocessingml.document" 919 | | "application/vnd.openxmlformats-officedocument.presentationml.presentation" 920 | | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" 921 | | "image/jpeg" 922 | | "image/png" 923 | | "video/mp4" 924 | | "video/3gp" 925 | | "image/webp"; 926 | 927 | export type ServerMediaRetrieveResponse = 928 | | ({ 929 | messaging_product: "whatsapp"; 930 | url: string; 931 | mime_type: ValidMimeTypes; 932 | sha256: string; 933 | file_size: string; 934 | } & ServerMedia & 935 | NoServerError) 936 | | ServerErrorResponse; 937 | 938 | export type ServerMediaDeleteResponse = 939 | | ServerSuccessResponse 940 | | ServerErrorResponse; 941 | 942 | export type ServerBlockedError = Pick< 943 | ServerErrorResponse["error"], 944 | "message" | "type" | "code" 945 | > & { 946 | error_data: { 947 | details: string; 948 | }; 949 | }; 950 | 951 | export type ServerBlockedUser = { 952 | input: string; 953 | wa_id: string; 954 | }; 955 | 956 | export type ServerBlockFailedUser = { 957 | input: string; 958 | errors: Omit[]; 959 | }; 960 | 961 | export type ServerBlockResponse = 962 | | { 963 | messaging_product: "whatsapp"; 964 | block_users: { 965 | added_users: ServerBlockedUser[]; 966 | failed_users?: ServerBlockFailedUser[]; 967 | }; 968 | errors?: ServerBlockedError; 969 | } 970 | | ServerErrorResponse; 971 | 972 | export type ServerUnblockResponse = 973 | | { 974 | messaging_product: "whatsapp"; 975 | block_users: { 976 | removed_users: ServerBlockedUser[]; 977 | // Not sure if this actually exists... 978 | failed_users?: ServerBlockFailedUser[]; 979 | }; 980 | errors?: ServerBlockedError; 981 | } 982 | | ServerErrorResponse; 983 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export type AtLeastOne = [T, ...T[]]; 2 | 3 | export type MaybePromise = T | Promise | PromiseLike; 4 | 5 | export function escapeUnicode(str: string) { 6 | // https://stackoverflow.com/a/40558081 7 | return str.replace(/[^\0-~]/g, (ch) => { 8 | return "\\u" + ("000" + ch.charCodeAt(0).toString(16)).slice(-4); 9 | }); 10 | } 11 | 12 | type Without = { [P in Exclude]?: never }; 13 | export type XOR = T | U extends object 14 | ? (Without & U) | (Without & T) 15 | : T | U; 16 | -------------------------------------------------------------------------------- /test/server.mocks.js: -------------------------------------------------------------------------------- 1 | import { MockAgent } from "undici"; 2 | 3 | export const agent = new MockAgent({ 4 | keepAliveTimeout: 10, 5 | keepAliveMaxTimeout: 10 6 | }); 7 | agent.disableNetConnect(); 8 | 9 | export const clientFacebook = agent.get("https://graph.facebook.com"); 10 | export const clientExample = agent.get("https://example.com"); 11 | -------------------------------------------------------------------------------- /test/webhooks.mocks.js: -------------------------------------------------------------------------------- 1 | export class MessageWebhookMock { 2 | /** 3 | * Helper class to test the messages post request, conditionally creating the object based on the available data 4 | */ 5 | constructor(phoneID, phone, message, name) { 6 | this.object = "whatsapp_business_account"; 7 | this.entry = [ 8 | { 9 | id: "WHATSAPP_BUSINESS_ACCOUNT_ID", 10 | changes: [ 11 | { 12 | field: "messages", 13 | value: { 14 | messaging_product: "whatsapp", 15 | /** @type {Array} */ 16 | messages: [] 17 | } 18 | } 19 | ] 20 | } 21 | ]; 22 | 23 | if (phoneID) { 24 | this.entry[0].changes[0].value.metadata = { 25 | display_phone_number: phoneID, 26 | phone_number_id: phoneID 27 | }; 28 | } 29 | 30 | if (phone) { 31 | this.entry[0].changes[0].value.contacts = [ 32 | { 33 | wa_id: phone 34 | } 35 | ]; 36 | } 37 | 38 | if (message) { 39 | this.entry[0].changes[0].value.messages = [message]; 40 | } 41 | 42 | if (name) { 43 | if (!this.entry[0].changes[0].value.contacts) 44 | this.entry[0].changes[0].value.contacts = [{}]; 45 | this.entry[0].changes[0].value.contacts[0].profile = { name }; 46 | } 47 | } 48 | } 49 | 50 | export class StatusWebhookMock { 51 | /** 52 | * Helper class to test the status post request, conditionally creating the object based on the available data 53 | */ 54 | constructor( 55 | phoneID, 56 | phone, 57 | status, 58 | messageID, 59 | conversation, 60 | pricing, 61 | biz_opaque_callback_data 62 | ) { 63 | this.object = "whatsapp_business_account"; 64 | this.entry = [ 65 | { 66 | id: "WHATSAPP_BUSINESS_ACCOUNT_ID", 67 | changes: [ 68 | { 69 | field: "messages", 70 | value: { 71 | messaging_product: "whatsapp", 72 | statuses: [{}] 73 | } 74 | } 75 | ] 76 | } 77 | ]; 78 | 79 | if (phoneID) { 80 | this.entry[0].changes[0].value.metadata = { 81 | display_phone_number: phoneID, 82 | phone_number_id: phoneID 83 | }; 84 | } 85 | 86 | if (phone) { 87 | this.entry[0].changes[0].value.statuses[0].recipient_id = phone; 88 | } 89 | 90 | if (status) { 91 | this.entry[0].changes[0].value.statuses[0].status = status; 92 | } 93 | 94 | if (messageID) { 95 | this.entry[0].changes[0].value.statuses[0].id = messageID; 96 | } 97 | 98 | if (conversation) { 99 | this.entry[0].changes[0].value.statuses[0].conversation = 100 | conversation; 101 | } 102 | 103 | if (pricing) { 104 | this.entry[0].changes[0].value.statuses[0].pricing = pricing; 105 | } 106 | 107 | if (biz_opaque_callback_data) { 108 | this.entry[0].changes[0].value.statuses[0].biz_opaque_callback_data = 109 | biz_opaque_callback_data; 110 | } 111 | 112 | if ( 113 | Object.keys(this.entry[0].changes[0].value.statuses[0]).length === 0 114 | ) { 115 | this.entry[0].changes[0].value.statuses = []; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*"], 3 | "exclude": ["src/standalone.ts"], 4 | "compilerOptions": { 5 | "target": "ESNext", 6 | "module": "Preserve", 7 | "moduleResolution": "Bundler", 8 | "outDir": "lib", 9 | "declaration": true, 10 | "declarationMap": true, 11 | "emitDeclarationOnly": true, 12 | "isolatedModules": true, 13 | "esModuleInterop": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "skipLibCheck": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", 3 | "tagDefinitions": [ 4 | { 5 | "tagName": "@module", 6 | "syntaxKind": "block" 7 | }, 8 | { 9 | "tagName": "@group", 10 | "syntaxKind": "block" 11 | }, 12 | { 13 | "tagName": "@description", 14 | "syntaxKind": "block" 15 | }, 16 | { 17 | "tagName": "@overload", 18 | "syntaxKind": "block" 19 | }, 20 | { 21 | "tagName": "@template", 22 | "syntaxKind": "block", 23 | "allowMultiple": true 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | // Comments are supported, like tsconfig.json 3 | "entryPoints": [ 4 | "src/index.ts", 5 | "src/types.ts", 6 | "src/utils.ts", 7 | "src/errors.ts", 8 | "src/emitters.ts", 9 | "src/messages/index.ts", 10 | "src/setup/index.ts", 11 | "src/middleware/index.ts" 12 | ], 13 | "exclude": ["**/node_modules/**"], 14 | "out": "docs", 15 | "includeVersion": true, 16 | "excludeInternal": true 17 | } 18 | --------------------------------------------------------------------------------