@yaotoshi/wa-followup-sdk
v0.1.2
Published
Typed consumer SDK for the WhatsApp follow-up service
Maintainers
Readme
@yaotoshi/wa-followup-sdk
A small, fully-typed client for the WhatsApp follow-up service.
Create follow-ups and read/cancel/refresh them, and build the standard source-contract
response for your source_check_url — all with inferred types.
Native fetch (Node 18+). The only runtime dependency is @yaotoshi/wa-followup-shared.
Install
npm install @yaotoshi/wa-followup-sdkUsage
import { createClient, buildStatus } from '@yaotoshi/wa-followup-sdk';
const wa = createClient({
baseUrl: 'https://followup.example.com',
apiToken: process.env.FOLLOWUP_API_TOKEN!,
});
// Create a follow-up (idempotent per active tenant+source_type+external_ref)
const { followup, deduped } = await wa.createFollowup({
tenant: 'acme',
source_type: 'order',
external_ref: '123',
external_label: 'Order #123',
wa_to: '+15555550100',
source_check_url: 'https://your-consumer.example.com/api/orders/123/followup-status',
});
await wa.getFollowup(followup.id);
await wa.listFollowups({ tenant: 'acme', state: 'processing' });
await wa.cancelFollowup(followup.id);
await wa.refreshFollowup(followup.id);Stopping a follow-up
To stop a follow-up, call cancelFollowup(id) — there is no separate "delete".
Cancelling transitions the follow-up to the terminal cancelled state and the
service guarantees no further WhatsApp messages are sent for it.
// The id comes from createFollowup — store it when you create the follow-up.
const { followup } = await wa.createFollowup({ /* … */ });
await wa.cancelFollowup(followup.id);Didn't keep the id? Because create is idempotent, there is at most one active
follow-up per tenant + source_type + external_ref, so you can look it up:
// Don't filter by state here: the active one may be `scheduled` (root not sent
// yet) OR `processing`. Match on external_ref and let cancel 409 if it's terminal.
const list = await wa.listFollowups({ tenant: 'acme', source_type: 'order' });
const mine = list.find((f) => f.external_ref === '123');
if (mine) await wa.cancelFollowup(mine.id);Cancel is safe to call more than once: if the follow-up is already finished
(done / failed / cancelled), the service responds 409 and the SDK throws
a WaFollowupClientError with status: 409 — treat that as "already stopped".
try {
await wa.cancelFollowup(id);
} catch (err) {
if (err instanceof WaFollowupClientError && err.status === 409) {
// already done/failed/cancelled — nothing left to stop
} else {
throw err;
}
}Note: a follow-up that is
scheduled(root message not sent yet) orprocessingis still active and cancellable. Only the four terminal states above return 409.
Re-creating after cancel
The idempotency key (tenant + source_type + external_ref) only locks the
active follow-up — it does not reserve the ref forever. Once a follow-up is
cancelled (or otherwise terminal), creating again with the same key starts a
brand-new follow-up with a new id (deduped: false); the old cancelled row
stays as history. Store the new id if you'll want to cancel it later.
const a = await wa.createFollowup({ tenant: 'acme', source_type: 'order', external_ref: '123', /* … */ });
await wa.cancelFollowup(a.followup.id); // a.followup.id = 1 → cancelled
const b = await wa.createFollowup({ tenant: 'acme', source_type: 'order', external_ref: '123', /* … */ });
// b.followup.id = 2, b.deduped = false → a fresh follow-up, not the cancelled oneThen, in the source_check_url handler that the service polls:
import { buildStatus } from '@yaotoshi/wa-followup-sdk';
res.json(buildStatus({ status: 'pending', progress: { done: 2, total: 5 } }));
// => { status: 'pending', progress: { done: 2, total: 5 } }Errors
Any non-2xx response throws a WaFollowupClientError carrying the HTTP status and the
service's error message:
import { WaFollowupClientError } from '@yaotoshi/wa-followup-sdk';
try {
await wa.getFollowup(9999);
} catch (err) {
if (err instanceof WaFollowupClientError) console.error(err.status, err.message);
}Requirements
Node 18+ (native fetch).
License
MIT
