@pilot-status/sdk
v0.5.0
Published
Official TypeScript SDK for the Pilot Status public API.
Downloads
366
Maintainers
Readme
@pilot-status/sdk
Official TypeScript SDK for the Pilot Status public API.
Installation
npm i @pilot-status/sdkQuickstart (Node.js / TypeScript)
Create an API key in the dashboard and use it only on the backend.
import { PilotStatusClient } from "@pilot-status/sdk";
const client = new PilotStatusClient({
apiKey: process.env.PILOT_STATUS_API_KEY!,
});
const accepted = await client.messages.send({
templateId: "onboarding-test",
destinationNumber: "+5511999999999",
variables: { name: "John" },
});
const message = await client.messages.get(accepted.id);
console.log(message.status);
// Conversation history (both directions, every provider), newest first.
// This is how you read old messages — the webhook does not replay history.
const history = await client.messages.history({
startDate: "2026-07-01T00:00:00Z",
endDate: "2026-07-31T23:59:59Z",
pageSize: 100,
});
console.log(history.total, history.messages[0]?.providerTimestamp);Management (projects, API keys, numbers)
These endpoints create resources within the scope (project + environment) of the current apiKey.
Projects
const project = await client.projects.create({
name: "My Project",
description: "Optional description",
});
const projects = await client.projects.list();API keys
// Regenerate the default key of one number (tenant-scoped). The new usable key
// is returned once — there is no "create another key" concept.
const regenerated = await client.apiKeys.regenerateNumber("wn_1");
console.log(regenerated.key); // shown only once
const keys = await client.apiKeys.list();
// With a number-scoped key this returns that number's masked keys (`ApiKeyListItem[]`).
// With a tenant-scoped key it returns a lean per-number list (`NumberApiKeyReveal[]`):
// { numberId, number, displayName, keyId, keyLast4, key, revealable } — `key` is the real usable value when `revealable` is true.Numbers (WhatsApp)
const created = await client.numbers.create({
name: "My WhatsApp",
number: "+5511999999999",
});
// created.qrcodeBase64 — QR image; created.pairingCode — letter code when available (else null)
const refreshed = await client.numbers.connect(created.instance.id);
// refreshed.qrcodeBase64, refreshed.pairingCode
const status = await client.numbers.getStatus(created.instance.id);
const detail = await client.numbers.get(created.instance.id);
console.log(detail.settings.appliesTo.advanced); // "evolution-go" | "none"Per-number settings
numbers.update() is a PARTIAL patch of the number: retention policy and/or the
settings block. There is no POST /v1/numbers/{id}/settings, and Evolution's
syncFullHistory has no equivalent here.
// stop replaying the device's old messages into the webhook on every reconnect
await client.numbers.updateSettings(id, { webhookHistoricalMessages: false });
await client.numbers.update(id, {
piiMode: "STORE_X_DAYS",
piiRetentionDays: 30,
settings: { rejectCall: true, msgRejectCall: "I don't take calls here" },
});historyImportEnabled / webhookHistoricalMessages apply to every provider. The
other six are the Evolution GO advancedSettings, in the GO dialect
(ignoreGroups, not the v2 groupsIgnore); passing null resets one to the
provider default. On a Meta number they are stored but never applied — which is
what settings.appliesTo.advanced === "none" tells you.
They are also pushed to the number's connected instances; settingsSync
({applied, failed, skipped}) reports that push. It is best-effort: a
disconnected instance still keeps the persisted value and picks it up on its next
provisioning.
Analytics
const stats = await client.analytics.getDashboardStats({ tz: "America/Sao_Paulo" });
console.log(stats.totalSent, stats.failureRate);Calls (WhatsApp Business Calling)
Voice calls over the /v1/calls* endpoints, on two kinds of numbers:
- Meta Cloud API numbers — signaling-only:
initiate/acceptcarry the SDP (RFC 8866) produced by your WebRTC client, and audio flows directly between the browser and WhatsApp. Settings/permissions/preAcceptare Meta-only. - Web (Pilot Status / unofficial) numbers — call media is handled
server-side, so there is no SDP (omit
sdponinitiate/accept). No call permission is required beforeinitiateand there is no Meta per-minute billing. Extra media controls:play(stream an audio file into the call) andrealtimeSession(full-duplex PCM16 WebSocket).
Numbers on any other provider get 400 FEATURE_NOT_SUPPORTED. callId
arguments accept the Pilot Status id (call_...) or the provider call id
(Meta wacid... / Evolution GO CallID).
Billing: on Meta numbers, business-initiated calls (BIC) are billed by Meta directly on your WABA — per minute, in 6-second pulses, only when answered; user-initiated calls (UIC) are free. Calls on web numbers have no Meta billing at all. Pilot Status does not charge for calls.
Web numbers are unofficial (QR-paired) WhatsApp sessions — call quality and availability depend on the paired device/session, and heavy automated calling carries the usual unofficial-number ban risk.
// 1. Permission first (required before calling a user)
const perm = await client.calls.getPermissions("+5511999999999");
if (perm.permission.status === "no_permission") {
await client.calls.requestPermission({
to: "+5511999999999",
text: "May we call you about your order?",
});
// the user's reply arrives as the call.permission_updated webhook
}
// 2. Start a business-initiated call (sdp = offer from your WebRTC client)
const call = await client.calls.initiate({
to: "+5511999999999",
sdp: offerSdp,
bizOpaqueCallbackData: "order-42",
});
// 3. Answer an inbound call (after the call.ringing webhook)
const inbound = await client.calls.get("wacid.ABGG...", { includeSdp: true });
// feed inbound.sdpOffer to your WebRTC client, produce the answer, then:
await client.calls.accept(inbound.id, answerSdp);
// Other controls
await client.calls.reject("wacid.ABGG...");
await client.calls.terminate("wacid.ABGG...");
// History + settings (settings are Meta-only)
const { calls } = await client.calls.list({ limit: 25 });
const settings = await client.calls.getSettings();
await client.calls.updateSettings({ status: "ENABLED" });On a web (Pilot Status) number the same flow needs no SDP and no permission step, and you get server-side media controls:
// Start a call (no sdp, no permission step)
const call = await client.calls.initiate({ to: "+5511999999999" });
// Answer an inbound call (after the call.ringing webhook) — no sdp
await client.calls.accept(call.id);
// Stream an audio file into the active call (.mp3/.wav/.opus by URL;
// queued and played on connect when the call is not active yet)
await client.calls.play(call.id, "https://cdn.example.com/ivr-greeting.mp3");
// Full-duplex realtime audio: returns { wsUrl, token, expiresInSeconds }.
// Connect a WebSocket to wsUrl and exchange RAW binary PCM16 LE frames
// (this is a plain WebSocket transport, NOT WebRTC; token is single-use, ~2 min)
const session = await client.calls.realtimeSession(call.id, "talk");Webhooks (parse / validation)
import { parseCustomerWebhook } from "@pilot-status/sdk";
export async function handler(req: Request) {
const payload = await req.json();
const event = parseCustomerWebhook(payload);
if (event.event === "message.failed") {
console.log(event.data.errorMessage);
}
return new Response("ok");
}Notes:
- Customer webhook payloads do not include:
projectSlug,lastMessageId. OptionalcorrelationId(same as HTTP 202 when present) may appear on outbound status events and onmessage.reply/message.receivedwhen correlated to a prior send. - For outbound status events (
message.sent,message.delivered,message.read,message.failed),messageIdis the WhatsApp provider message id (key.id) andinternalMessageIdis the Pilot Status message id. message.receivedincludesfromMe(boolean).message.groupis delivered for inbound group messages (includesgroupName).message.newsletteris delivered for inbound WhatsApp channel / newsletter messages (@newsletter, includesnewsletterId).- Supported events in the parser:
message.sent,message.delivered,message.read,message.failed,message.reply,message.received,message.group,message.newsletter,number.created,number.connected,number.disconnected,number.removed,call.ringing,call.connected,call.ended,call.missed,call.permission_updated. call.*payloads are flat (fields sit next toevent, nodatawrapper):{ event, callId, externalCallId?, direction, status, from, to, timestamp, duration? }.duration(seconds) appears oncall.endedonly when the call was answered;call.permission_updatedhascallId/directionnullandstatusNO_PERMISSION|TEMPORARY|PERMANENT.
Errors
For non-2xx responses, the SDK throws an HTTP error with status and, when available, body.
import { PilotStatusHttpError } from "@pilot-status/sdk";
try {
await client.messages.send({
templateId: "x",
destinationNumber: "+5511999999999",
variables: {},
});
} catch (err) {
if (err instanceof PilotStatusHttpError) {
console.log(err.status, err.body);
}
}