Skip to content

Latest commit

 

History

History
235 lines (184 loc) · 7.98 KB

File metadata and controls

235 lines (184 loc) · 7.98 KB

WhatsApp Flows with @kapso/whatsapp-cloud-api

This guide walks through the full developer workflow for WhatsApp Flows using the SDK — from authoring Flow JSON in camelCase to deploying, previewing, and serving Data Endpoint callbacks.

Why code-first flows?

  • CamelCase authoring: Write JSON in idiomatic TS/JS; the SDK converts to Meta’s mixed casing automatically.
  • Lifecycle helpers: client.flows.create/updateAsset/publish/deploy/preview/get/list mirror the Graph API.
  • DX-focused validation: Meta errors come back camelized with hints when casing mistakes occur.
  • Server utilities: receiveFlowEvent, respondToFlow, and downloadFlowMedia handle encryption/HMAC, PhotoPicker/DocumentPicker payloads, and camelCase normalization.

1. Author Flow JSON (camelCase)

Create a file such as flows/csat.flow.ts:

export default {
  version: "7.2",
  screens: [
    {
      id: "CSAT",
      title: "How was your experience?",
      terminal: true,
      layout: {
        type: "SingleColumnLayout",
        children: [
          {
            type: "RadioButtonsGroup",
            name: "rating",
            label: "Rate us",
            required: true,
            dataSource: [
              { id: "up", title: "👍 Great" },
              { id: "down", title: "👎 Not great" }
            ]
          },
          {
            type: "Footer",
            label: "Submit",
            onClickAction: {
              name: "complete",
              payload: { rating: "${form.rating}" }
            }
          }
        ]
      }
    }
  ]
} as const;

✅ Use camelCase everywhere. The SDK maps keys like onClickAction, dataSource, maxUploadedPhotos, etc., to Meta’s on-click-action, data-source, max-uploaded-photos on upload.

Validation tips

  • client.flows.create/updateAsset/deploy surface Meta validation errors with camelCase pointers and DX hints when common casing mistakes occur.
  • toFlowJsonWireCase(flow, { strictCamel: false }) is available if you need to ingest legacy snake-case JSON temporarily.

2. Deploy & preview

Use the client to upload Flow JSON, optionally publish, and fetch a preview URL. The deploy helper is idempotent: it skips asset uploads when the content hash hasn’t changed.

import { WhatsAppClient } from "@kapso/whatsapp-cloud-api";
import csatFlow from "./flows/csat.flow";

const client = new WhatsAppClient({ accessToken: process.env.WHATSAPP_TOKEN! });

const result = await client.flows.deploy(csatFlow, {
  wabaId: process.env.WABA_ID!,
  name: "csat-flow",
  publish: true,
  preview: true, // or { interactive: true, params: { flowAction: "navigate" } }
});

console.log("Flow ID:", result.flowId);
console.log("Preview URL:", result.previewUrl);
console.log("Validation errors:", result.validationErrors);

3. Send Flow messages

Use client.messages.sendInteractiveFlow (typed with FlowInteractiveInput) to trigger your flow. flowCta is required; flowMessageVersion defaults to "3" when omitted.

import { WhatsAppClient, type FlowInteractiveInput } from "@kapso/whatsapp-cloud-api";

const client = new WhatsAppClient({ accessToken: process.env.WHATSAPP_TOKEN! });

const message: FlowInteractiveInput = {
  phoneNumberId: "1234567890",
  to: "+15551234567",
  bodyText: "Check out our new experience",
  parameters: {
    flowId: "1234567890",
    flowCta: "Open",
    flowToken: "token123",
    flowAction: "navigate",
    flowActionPayload: { screen: "WELCOME" }
  }
};

await client.messages.sendInteractiveFlow(message);

Other lifecycle helpers:

await client.flows.create({ wabaId, name, flowJson, publish: false });
await client.flows.updateAsset({ flowId, json: flowJson });
await client.flows.publish({ flowId });
await client.flows.list({ wabaId, limit: 20 });

4. Handle Data Endpoint requests

When Meta calls your Data Endpoint, use receiveFlowEvent to decrypt the payload and normalize it to camelCase, and respondToFlow to send the next screen data.

Express example

import express from "express";
import { receiveFlowEvent, respondToFlow } from "@kapso/whatsapp-cloud-api/server";

const app = express();
const phoneNumberId = process.env.PHONE_ID!;
const flowKey = process.env.FLOW_PRIVATE_KEY_PEM!;

app.post("/flows/csat", express.raw({ type: "*/*" }), async (req, res) => {
  try {
    const ctx = await receiveFlowEvent({
      rawBody: req.body as Buffer,
      phoneNumberId,
      getPrivateKey: async () => flowKey
    });

    if (ctx.action === "COMPLETE") {
      console.log("CSAT rating:", ctx.form.rating);
      const reply = respondToFlow({ screen: ctx.screen, data: {} });
      return res.status(reply.status).set(reply.headers).send(reply.body);
    }

    const reply = respondToFlow({ screen: ctx.screen, data: {} });
    res.status(reply.status).set(reply.headers).send(reply.body);
  } catch (error) {
    if (error instanceof Error && "status" in error) {
      const flowError = error as any;
      return res.status(flowError.status).set(flowError.headers).send(flowError.body);
    }
    res.status(500).send({ error: "Unexpected error" });
  }
});

Edge / standard Fetch handler

import { receiveFlowEvent, respondToFlow } from "@kapso/whatsapp-cloud-api/server";

export default async function handler(request: Request) {
  const buffer = new Uint8Array(await request.arrayBuffer());
  const ctx = await receiveFlowEvent({
    rawBody: buffer,
    phoneNumberId: process.env.PHONE_ID!,
    getPrivateKey: async () => process.env.FLOW_PRIVATE_KEY_PEM!
  });

  const reply = respondToFlow({ screen: ctx.screen, data: {} });
  return new Response(reply.body, { status: reply.status, headers: reply.headers });
}

5. Decrypt PhotoPicker/DocumentPicker media

Use downloadFlowMedia (alias for downloadAndDecrypt) to pull media from the CDN once your webhook receives the descriptor.

import { downloadFlowMedia } from "@kapso/whatsapp-cloud-api/server";

const media = await downloadFlowMedia({
  cdnUrl: descriptor.cdnUrl,
  encryptionMetadata: descriptor.encryptionMetadata,
});

// media is an ArrayBuffer — persist to storage, run validations, etc.

6. Validation errors & hints

When Meta returns validation errors, the SDK camelizes field names and pointer paths, and adds a hint when the fix is a casing issue:

[
  {
    "error": "INVALID_PROPERTY_VALUE",
    "errorType": "FLOW_JSON_ERROR",
    "message": "Invalid value for property",
    "pointers": [
      {
        "path": "screens[0].layout.children[0].on-click-action",
        "lineStart": 10,
        "columnStart": 5
      }
    ],
    "hint": "Use onClickAction (camelCase). We map it to on-click-action."
  }
]

Use these hints to fix your camelCase authoring quickly.

7. Local testing & iteration

  • Unit tests: npm test covers case mapping, hashing, resource requests, and server helpers.
  • Manual integration (without publish):
    • npm run build
    • npm pack → install the tarball in your app (npm install ../@kapso-whatsapp-cloud-api-*.tgz), or
    • npm install file:../whatsapp-cloud-api-js (with build artifacts), or
    • npm link for rapid iteration.
  • Preview flows locally: use client.flows.preview({ flowId, interactive: true }) to generate a URL you can share with stakeholders.

8. Checklist before production

  • Flow JSON authored in camelCase (dataApiVersion, routingModel, onClickAction, etc.).
  • Data Endpoint responds within 10 seconds (Meta timeout).
  • Handle FlowServerError statuses: 421 (decrypt error), 432 (signature/HMAC), 427 (invalid flow token).
  • Refresh Flow private key when rotating phone numbers.
  • Log ctx.action, ctx.screen, and timing for observability (recommended).

Additional references

Have feedback? Open an issue or PR — the Flow DX is still evolving and we’d love your input.