Genuka WA docs

TypeScript library

@genuka/whatsapp — typed builders, validation and event normalization.

@genuka/whatsapp is not another HTTP client. It is the layer that prevents avoidable 400s, makes webhook handling exhaustive at compile time, and turns Meta's ~200 error codes into seven actionable decisions.

The library works standalone against Meta's Cloud API, with no Genuka account. It ships under the MIT license.

Installation

npm install @genuka/whatsapp

Building a message

Lengths, cardinalities and phone number format are checked before the network call.

import { messages } from "@genuka/whatsapp";

const choice = messages.buttons("+237 6 99 00 11 22", {
  body: "How can we help?",
  buttons: [
    { id: "order", title: "My order" },
    { id: "support", title: "A problem" },
  ],
});

A construction error is thrown immediately, with the offending field path:

ValidationError: interactive.buttons: must contain at most 3 items (got 4)

The 24-hour window

import { sendStrategy } from "@genuka/whatsapp";

sendStrategy(contact.lastInboundAt); // "free_form" | "template"

A function call, rather than a 131047 discovered after the attempt was already billed.

Handling a webhook

Meta nests everything in entry[].changes[].value, where the real discriminator is not a field but the presence of messages or statuses. The library flattens that once and for all.

import { parseWebhook, eventKey } from "@genuka/whatsapp/webhooks";

for (const event of parseWebhook(await request.json())) {
  if (await alreadyProcessed(eventKey(event))) continue;

  switch (event.kind) {
    case "message":         // inbound
    case "status":          // sent / delivered / read / failed / deleted
    case "template":        // approval, quality, recategorization
    case "account":         // number quality, limits, suspension
    case "user_preference": // marketing opt-out
    case "coexistence":     // history, echoes, contacts
    case "unknown":         // never lost, always forwarded
  }
}

The parser never throws: a malformed payload yields an empty list. An exception here would become a 500, and Meta would replay the whole batch.

Deciding what to do with an error

errorClass carries the decision, not the numeric code.

import { WhatsAppError } from "@genuka/whatsapp";

try {
  await send(payload);
} catch (error) {
  if (!(error instanceof WhatsAppError)) throw error;

  switch (error.errorClass) {
    case "needs_template":      return resendAsTemplate();      // 131047
    case "recipient_permanent": return markUnreachable();       // 131026, 131050
    case "recipient_throttled": return requeueLater();          // 131049
    case "media":               return reuploadAndRetryOnce();  // 131052
    case "retryable":           return backoff();               // 4, 130429, 5xx
    case "config":              return alertOperator();         // 190, 133010
    case "template":            return surfaceToCustomer();     // 132xxx
    case "validation":
    case "unknown":             throw error;
  }
}

Two transports, one core

The same built payload can travel two routes, with the same builders, validation and error taxonomy:

// With a Genuka WA key — no Meta token involved
import { GenukaTransport } from "@genuka/whatsapp";
const transport = new GenukaTransport({ apiKey: process.env.GENUKA_WA_API_KEY! });

// Straight to Meta, if you hold your own credentials
import { MetaTransport } from "@genuka/whatsapp";
const transport = new MetaTransport({ accessToken: process.env.META_TOKEN! });

Full reference

Every module has its own guide, sourced from the package itself — they live next to the code they describe, so they do not drift:

Graph API version

The library pins a Graph API version explicitly rather than following "latest". A silent version bump is the best way to discover a breaking change in production: changing DEFAULT_GRAPH_VERSION is a deliberate release.

On this page