npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bigbinary/neeto-whatsapp-frontend

v1.2.1

Published

This is the frontend package for the neeto-whatsapp-nano project.

Readme

neeto-whatsapp-nano

The neeto-whatsapp-nano manages WhatsApp messaging within neeto applications. The nano exports the @bigbinary/neeto-whatsapp-frontend NPM package and neeto-whatsapp-engine Rails engine for development.

It also meters WhatsApp messages per workspace. Each workspace gets free messages every month, and admins can buy prepaid message packs from NeetoAuth Billing. See WhatsApp message packs.

Contents

  1. Development with Host Application
  2. Instructions for Publishing

Development with Host Application

Engine

The engine handles WhatsApp Business API integration, including sending and receiving messages, processing webhooks for message status updates, and managing WhatsApp message templates.

Installation

  1. Add this line to your application's Gemfile:

    source "NEETO_GEM_SERVER_URL" do
       # ..existing gems
    
       gem 'neeto-whatsapp-engine'
    
       # Use this for live development:
       # gem 'neeto-whatsapp-engine', path: "../neeto-whatsapp-nano"
    end
  2. And then execute:

    bundle install
  3. Add this line to your application's config/routes.rb file:

    mount NeetoWhatsappEngine::Engine, at: "/neeto_whatsapp"
  4. Run the following command to copy the migrations from the engine to the host application:

    bundle exec rails neeto_whatsapp_engine:install:migrations
  5. Add the migrations to the database:

    bundle exec rails db:migrate
  6. Add the following environment variables:

WHATSAPP_NANO_WEBHOOK_VERIFY_TOKEN=abc123
WHATSAPP_WEBHOOK_APP_SECRET=abc123
WHATSAPP_ENABLED_ORGANIZATIONS="spinkart"
WHATSAPP_DISABLE_INBOUND_MESSAGES=true   <--- To disable processing incoming messages. Defaults to false.
  1. Add the following association to the entity to which you want to associate the whatsapp messages
has_many :whatsapp_messages, class_name: "NeetoWhatsappEngine::Message", as: :entity, dependent: :destroy
  1. Override the fetch_entity! method for NeetoWhatsappEngine::Api::V1::MessagesController
# app/overrides/controllers/neeto_whatsapp_engine/api/v1/messages_controller_override.rb

# frozen_string_literal: true

NeetoWhatsappEngine::Api::V1::MessagesController.class_eval do

  private
    def fetch_entity!
      # Fetch entity here
      # example:
      # @entity = @organization.bookings.find_by!(id: params[:entity_id])
    end
end
  1. The backend exposes 2 services for sending whatsapp messages:
  • For sending template messages
NeetoWhatsappEngine::TemplateMessageService.new(account).process(
  recipient_phone:,
  template_name:,
  language_code:,
  entity: booking,
  # Required. The workspace the message is metered against. See "WhatsApp message packs".
  organization: booking.organization,
  # Optional. Shown as the recipient in WhatsApp logs.
  recipient_name: booking.name,
  template_params: [
    # This is an example on how to send params.
    # Check the params required based on the template(created on meta's dashboard) which you are going to use.
    { name: "client_name", type: "text", text: booking.name },
    { name: "meeting_name", type: "text", text: booking.meeting.name },
    { name: "host_meeting_time", type: "text", text: meeting_time_in_host_time_zone },
    { name: "attendee_list", type: "text", text: attendee_list },
    { name: "meeting_location", type: "text", text: meeting_location },
    { name: "booking_id", type: "text", text: booking.sid }
  ],
  # Optional. Only needed if the template has buttons with dynamic values.
  # See the "Template buttons" section below for the accepted shape.
  template_buttons: [
    { index: 0, sub_type: "url", text: booking.sid },
    { index: 1, sub_type: "quick_reply", text: "unsubscribe" },
    { index: 2, sub_type: "copy_code", text: "25OFF" }
  ]
)
  • For sending free-form text message, you can use the frontend component WhatsAppMessages which handles it internally. But if you want to call it manually, then
NeetoWhatsappEngine::TextMessageService.new(account).process(
  recipient_phone:,
  entity:,
  message_text: "Hello",
  organization:, # Required. The workspace the message is metered against.
  recipient_name: # Optional. Shown as the recipient in WhatsApp logs.
)

Template buttons

If a template (created on meta's dashboard) has buttons with dynamic values, pass them via template_buttons. This is separate from template_params: params fill the body text, buttons fill the button components. Each entry fills one button, matched by index to the button's position in the template.

template_buttons: [
  { index: 0, sub_type: "url", text: booking.sid },
  { index: 1, sub_type: "quick_reply", text: "unsubscribe" },
  { index: 2, sub_type: "copy_code", text: "25OFF" }
]

| Key | Required | Description | | --- | --- | --- | | index | No | 0-based position of the button in the template. Defaults to the entry's position in the array. | | sub_type | No | "url" (default), "quick_reply", or "copy_code". | | text | Yes | The dynamic value. Meaning depends on sub_type (see below). | | name | No | For url buttons whose template uses a named variable (e.g. {{promo_code}} instead of {{1}}). Omit for positional variables. |

text maps to different things per button type:

  • url — the value substituted into the template URL's {{1}} variable. The base URL lives in the template; you only pass the variable value here (e.g. a booking id). Percent-encode any special characters.
  • quick_reply — the payload returned in the webhook when the user taps the button.
  • copy_code — the code copied to the user's clipboard when tapped.

Note: template_buttons is stored under a reserved "template_buttons" key in the message's metadata. If you also pass that key in metadata, it is overwritten.

WhatsApp message packs

How metering works

  • Each workspace gets 100 free messages a month (UTC). Unused free messages don't carry over.
  • After the free messages run out, sends use prepaid messages. Admins buy these as packs from NeetoAuth (Billing › WhatsApp packs), and they never expire.
  • A message is reserved when it's sent, charged when WhatsApp reports it delivered or read, and released if it fails. NeetoWhatsappEngine::ReleaseStaleReservationsJob releases any message that is still pending after 30 days.
  • Once both free and prepaid messages have run out, TemplateMessageService and TextMessageService raise NeetoWhatsappEngine::Errors::BalanceExhaustedError instead of sending. The message is logged with the status "Not sent".
  • Both services take a required organization:, the workspace the message is metered against and shown under in WhatsApp logs.

Architecture

Components and network calls

flowchart TB
  subgraph Host["Host application, e.g. NeetoCal"]
    Caller["Host code, e.g. a booking confirmation"]
    subgraph Engine["neeto-whatsapp-engine"]
      Services["TemplateMessageService<br/>TextMessageService"]
      Billing["Billing::ReserveService<br/>Billing::ChargeService<br/>Billing::ReleaseService"]
      Webhooks["WebhooksController<br/>ProcessWebhookPayloadJob"]
      ServerAPI["Server API<br/>POST credits, GET balances"]
      Jobs["ThresholdCheckJob<br/>ReleaseStaleReservationsJob"]
      DB[("messages<br/>balances<br/>credits")]
    end
    Pages["WhatsApp logs and Usage pages"]
  end

  Meta["Meta WhatsApp Cloud API"]
  Auth["NeetoAuth Billing"]
  LS["Lemon Squeezy"]
  Admins(["Workspace admins"])

  Caller -->|"process(organization:, ...)"| Services
  Services -->|"reserve"| Billing
  Services -->|"1. send message"| Meta
  Meta -->|"2. status webhook: delivered, read, failed"| Webhooks
  Webhooks -->|"charge or release"| Billing
  Billing --> DB
  Billing -->|"low or ran out"| Jobs
  Jobs -->|"low-balance and ran-out emails"| Admins
  Admins -->|"Buy messages"| Auth
  Auth -->|"3. hosted checkout"| LS
  LS -->|"4. order_created webhook"| Auth
  Auth -->|"5. POST credits and GET balances, server token"| ServerAPI
  ServerAPI --> DB
  Pages --> DB

Sending a message

sequenceDiagram
  autonumber
  participant Host as Host code
  participant Svc as Message service
  participant Bal as Balance (row lock)
  participant Meta as Meta Cloud API
  participant Job as ProcessWebhookPayloadJob

  Host->>Svc: process(organization:, recipient_phone:, ...)
  Svc->>Bal: ReserveService: roll the month if needed
  alt free messages left this month
    Bal-->>Svc: free_reserved += 1, billed_from: free
  else prepaid messages left
    Bal-->>Svc: prepaid_available -= 1, prepaid_reserved += 1, billed_from: prepaid
  else nothing left
    Bal-->>Svc: BalanceExhaustedError
    Svc->>Svc: log the message as "Not sent"
    Svc-->>Host: raise BalanceExhaustedError
    Note over Svc: ThresholdCheckJob sends the ran-out email once
  end
  Svc->>Meta: send the message
  alt Meta accepts it
    Svc->>Svc: create the message, billing_state: reserved
  else Meta returns an error
    Svc->>Svc: create a failed message, then ReleaseService
  end
  Meta-->>Job: status webhook, processed in the background
  alt delivered or read
    Job->>Bal: ChargeService
    Note over Job: ThresholdCheckJob sends the low-balance email once total_remaining drops to low_balance_threshold, 50 by default
  else failed
    Job->>Bal: ReleaseService
  end

ReleaseStaleReservationsJob runs daily in the host (for example at 04:04 UTC in NeetoCal) and releases messages still reserved after 30 days, when Meta never sent a final status.

Buying a pack

sequenceDiagram
  autonumber
  actor Admin as Workspace admin
  participant Auth as NeetoAuth
  participant LS as Lemon Squeezy
  participant Eng as Engine server API

  Admin->>Auth: open Buy messages, from an email or the Usage page
  Auth->>Eng: GET /neeto_whatsapp/api/v1/server/balances
  Eng-->>Auth: free and prepaid balances, usage_url
  Admin->>Auth: choose a pack and buy
  Auth->>LS: create a checkout tagged kind: whatsapp_pack
  Auth-->>Admin: redirect to the hosted checkout
  Admin->>LS: pay
  LS-->>Admin: redirect back to the buy page
  LS->>Auth: order_created webhook
  Auth->>Auth: record WhatsappPackPurchase, unique on the order id
  Auth->>Eng: POST /neeto_whatsapp/api/v1/server/credits (CreditJob, retried until it succeeds)
  Eng->>Eng: create Credit, unique on purchase_identifier, then prepaid_available += messages
  Eng-->>Auth: 200, the same credit again on a repeat
  Auth-->>Admin: the buy page polls for the purchase, then shows the new balance

NeetoAuth takes the message count from its own pack configuration, matched on the Lemon Squeezy variant, never from the checkout data.

Balance counters

Each workspace has one neeto_whatsapp_engine_balances row. Every write locks it, so concurrent sends can't spend the same message twice.

| Counter | Meaning | | --- | --- | | free_used | Free messages charged this month | | free_reserved | Free messages sent this month and not yet delivered or failed | | prepaid_available | Prepaid messages left to spend | | prepaid_reserved | Prepaid messages sent and not yet delivered or failed | | period_starts_on | The month the free counters belong to |

free_remaining  = max(free_monthly_allowance - free_used - free_reserved, 0)
                  (the full allowance when period_starts_on is an earlier month)
total_remaining = free_remaining + prepaid_available

How each step moves the counters:

| Step | Free message | Prepaid message | | --- | --- | --- | | Reserve, on send | free_reserved + 1 | prepaid_available - 1, prepaid_reserved + 1 | | Charge, on delivered or read | free_reserved - 1, free_used + 1 | prepaid_reserved - 1 | | Release, on failure or after 30 days | free_reserved - 1 | prepaid_reserved - 1, prepaid_available + 1 | | Credit, on a pack purchase | n/a | prepaid_available + messages |

  • Month rollover: the first write in a new month resets free_used, free_reserved and both email flags. A free message reserved last month and delivered this month moves no counter, because last month's counters are already gone. Prepaid messages are never reset.
  • Emails: low_balance_notified_at and exhausted_notified_at make each email go out once per crossing. A credit re-arms the ran-out email, and the low-balance email once the balance is back above the threshold.
stateDiagram-v2
  [*] --> reserved: sent
  [*] --> not_metered: no messages left, logged as Not sent
  reserved --> charged: delivered or read
  reserved --> released: failed, or still reserved after 30 days
  charged --> [*]
  released --> [*]
  not_metered --> [*]

billing_state on a message. A message refused for having no messages left has no billing state and reserves nothing.

Each move out of reserved happens once: the service claims the message with a conditional update on billing_state = 'reserved', so duplicate or out-of-order webhooks don't charge twice.

Integrating message packs in a host application

<entity> below is the record your WhatsApp messages belong to, for example a booking.

  1. Run the metering migration. Copy and run the engine's migrations (see Installation). They add balances, credits, and billing fields on messages.

  2. Pass the workspace when sending, and handle a refused send. organization: is required. Also pass recipient_name: so WhatsApp logs show who the message went to.

    def send_whatsapp_message(entity:, recipient_phone:, recipient_name:, template_name:, language_code:,
      template_params:)
      NeetoWhatsappEngine::TemplateMessageService.new(account).process(
        recipient_phone:,
        recipient_name:,
        organization: entity.organization,
        template_name:,
        language_code:,
        entity:,
        template_params:
      )
    rescue NeetoWhatsappEngine::Errors::BalanceExhaustedError
      # No WhatsApp messages left. The message is already logged as "Not sent" and admins are emailed.
      Rails.logger.info("[WhatsApp] Skipped #{template_name}: no WhatsApp messages left")
    end
  3. Decide who can see WhatsApp logs and usage. The admin APIs call authorize_whatsapp_admin!, which raises until the host implements it:

    # app/overrides/controllers/neeto_whatsapp_engine/api/v1/admin_base_controller_override.rb
    NeetoWhatsappEngine::Api::V1::AdminBaseController.class_eval do
      private
    
        def authorize_whatsapp_admin!
          authorize(:settings, :can_view_whatsapp?) # use the host's own policy and permission
        end
    end
  4. Link messages back to their entity (optional). The message details pane in WhatsApp logs shows a link when the entity responds to whatsapp_log_link:

    # app/models/<entity>.rb
    def whatsapp_log_link
      { label: I18n.t("<entity>.whatsapp_log_link_label"), value: sid, url: show_url }
    end
  5. Name your templates. The Usage report and the logs show each template by its label under neeto_whatsapp_engine.template_labels, falling back to the humanized template name:

    # config/locales/en.yml
    en:
      neeto_whatsapp_engine:
        template_labels:
          <template_name>: "Readable name of the message"
  6. Tell the engine where the Usage page is. Set usage_path in every environment's config/constants.yml. The emails link to it, and NeetoAuth links back to it:

    neeto_whatsapp:
      usage_path: "<path of the host's WhatsApp usage page>"
  7. Schedule the cleanup job in config/scheduled_jobs.yml:

    whatsapp_release_stale_reservations:
      cron: "4 4 * * *"
      class: "NeetoWhatsappEngine::ReleaseStaleReservationsJob"
      queue: "low"
      description: "Runs daily at 04:04 UTC. Releases WhatsApp message reservations still unresolved after 30 days"
  8. Add the WhatsApp logs and usage pages to the host's admin panel with the WhatsAppLogs and WhatsAppUsage components. Put the usage page at usage_path, and guard both pages with the same permission as step 3.

  9. Enable the product in NeetoAuth. Packs are sold from <auth_app_url>/admin/billing/whatsapp-packs/<product slug>. The slug is the host's application_name lowercased (NeetoCal becomes neetocal), and it must match the product's ClientApplication#slug in NeetoAuth. Then add the product's ClientApplication name to whatsapp_packs.client_applications in NeetoAuth's config/constants.yml.

Configuration

Set these under neeto_whatsapp: in the host's config/constants.yml:

| Key | Default | Description | | --- | --- | --- | | usage_path | none | Path of the host's WhatsApp Usage page. Used in the emails, and returned to NeetoAuth as usage_url. | | free_monthly_allowance | 100 | Free messages per workspace each month. | | low_balance_threshold | 50 | Admins are emailed when free plus prepaid messages drop to this number. |

The engine reads the product name from the host's application_name (for example NeetoCal), and the NeetoAuth URL from each organization's auth_app_url.

Emails to workspace admins

Both emails go to every admin of the workspace.

  • "Only 50 WhatsApp messages left" is sent when a delivered message brings the balance to low_balance_threshold.
  • "WhatsApp notifications have stopped" is sent on the first send refused for having no messages left.

Each is sent once. A credit or the monthly reset re-arms it. Both have a Buy messages button (the NeetoAuth WhatsApp packs page) and a View usage link (the host's Usage page).

Server API for NeetoAuth

NeetoAuth calls these endpoints with the server authorization token (Authorization: Token token=<server_authorization_token>).

  • POST /neeto_whatsapp/api/v1/server/credits adds prepaid messages after a pack is paid for.
    • Params: organization_subdomain, purchase_identifier (the Lemon Squeezy order id), messages, amount.
    • It's idempotent on purchase_identifier: a repeat returns 200 and adds nothing.
  • GET /neeto_whatsapp/api/v1/server/balances?organization_subdomains[]=<subdomain> returns each workspace's balance:
    • free used, free remaining and the reset date
    • prepaid available and total remaining
    • messages sent this month
    • the last credit
    • usage_url

Rake tasks for development

Run these from the host application.

# Add prepaid messages to a workspace without a purchase
bin/rails "neeto_whatsapp_engine:credit[spinkart,500]"

# Pretend WhatsApp sent a status update for the latest outbound message (development only)
bin/rails "neeto_whatsapp_engine:simulate_status[delivered]"   # charges the reserved message
bin/rails "neeto_whatsapp_engine:simulate_status[failed]"      # releases it

Frontend package

Installation

  1. Install the latest neeto-whatsapp-frontend package using the below command:
    yarn add @bigbinary/neeto-whatsapp-frontend

Instructions for development

Check the Frontend package development guide for step-by-step instructions to develop the frontend package.

Components

WhatsAppMessages (source code)

This component provides a chat interface for viewing and sending WhatsApp messages for a given entity. It displays a scrollable message list with WhatsApp-style formatting, status icons, and a sticky compose form at the bottom. Messages are polled every 10 seconds for real-time updates.

Props
  • entityId: The ID of the entity whose messages should be displayed.
  • entityType: The type of the entity (e.g., "Booking").
Usage
import React from "react";

import { WhatsAppMessages } from "@bigbinary/neeto-whatsapp-frontend";

const App = ({ bookingId }) => (
  <WhatsAppMessages
    entityId={bookingId}
    entityType="Booking"
    // TO enable/disable the compose form to send messages. Defaults to true.
    disableComposeForm={false}
  />
);

export default App;

WhatsAppLogs (source code)

The workspace's WhatsApp logs: every message sent, with its status, recipient and type. Click a message to see its details, including a link back to its entity when the host defines whatsapp_log_link.

Props
  • breadcrumbs: Breadcrumbs for the page header.
  • pageTitle: The browser tab title.
  • usagePath: Route of the host's WhatsAppUsage page, linked from the header.
  • helpUrl: Optional. A help article link.

WhatsAppUsage (source code)

The workspace's WhatsApp balance:

  • free and prepaid messages left, and what happens when they run out
  • this month's messages by type
  • a Buy messages button that opens the NeetoAuth WhatsApp packs page
Props
  • breadcrumbs: Breadcrumbs for the page header.
  • pageTitle: The browser tab title.
  • logsPath: Route of the host's WhatsAppLogs page.
  • helpUrl: Optional. A help article link.
Usage
import WhatsAppLogs from "@bigbinary/neeto-whatsapp-frontend/WhatsAppLogs";
import WhatsAppUsage from "@bigbinary/neeto-whatsapp-frontend/WhatsAppUsage";

// Guard both routes with the permission used in authorize_whatsapp_admin!.
<Route
  path={routes.whatsappLogs}
  render={() => (
    <WhatsAppLogs
      breadcrumbs={breadcrumbs}
      pageTitle="WhatsApp logs"
      usagePath={routes.whatsappUsage}
    />
  )}
/>
<Route
  path={routes.whatsappUsage}
  render={() => (
    <WhatsAppUsage
      breadcrumbs={breadcrumbs}
      logsPath={routes.whatsappLogs}
      pageTitle="WhatsApp usage"
    />
  )}
/>

Instructions for Publishing

Consult the building and releasing packages guide for details on how to publish.