@whatsapi.sh/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,402 @@
1
+ // GENERATED FILE — do not edit. Run `bun run sdk:gen`.
2
+ // Source: packages/shared/src/http/index.ts, packages/shared/src/billing/catalog.ts, packages/shared/src/dto/index.ts, packages/shared/src/webhooks/index.ts
3
+ //
4
+ // The shapes the API speaks, lifted from the server's own contract.
5
+
6
+ // ── from packages/shared/src/http/index.ts ───────────────────────────
7
+
8
+ /** Canonical error codes. The web client can switch on these / map them to i18n. */
9
+ export type ErrorCode =
10
+ | "VALIDATION_ERROR" // 400 — zod failures; details: issues[]
11
+ | "UNAUTHORIZED" // 401 — missing/invalid token
12
+ | "INVALID_TOKEN" // 401
13
+ | "FORBIDDEN" // 403 — RBAC / suspended account
14
+ | "NOT_FOUND" // 404
15
+ | "CONFLICT" // 409 — e.g. number already paired elsewhere
16
+ // 402 — out of message credits. Distinct from 429 on purpose: a rate/quota limit
17
+ // clears by waiting (or the month rolling over), this one clears by upgrading or
18
+ // buying a pack, and the client must not auto-retry it.
19
+ | "PAYMENT_REQUIRED"
20
+ // 429 — burst limiter (per key/user/ip). Carries a Retry-After header, as does
21
+ // every other error that knows when it clears.
22
+ | "RATE_LIMIT_EXCEEDED"
23
+ | "QUOTA_EXCEEDED" // 429 — monthly/daily message quota spent; details carry resetAt
24
+ // 409/502 — the WhatsApp number exists but its session is not connected right now
25
+ // (pairing pending, logged out, or banned). The gateway call never happened.
26
+ | "NUMBER_NOT_CONNECTED"
27
+ // 429 — QR/pairing-code generation is on cooldown (QR_GENERATION_COOLDOWN=180s,
28
+ // PairPhone ~5/30min budget). Carries Retry-After + details.retryAfterSecs.
29
+ // See docs/QUIRKS.md.
30
+ | "PAIRING_COOLDOWN"
31
+ | "OTP_INVALID" // 400 — otp/check code mismatch or expired
32
+ | "GATEWAY_ERROR" // 502 — wuzapi upstream failed; details carry the remapped cause
33
+ // 504 — WhatsApp did not confirm in time. Deliberately NOT a 502: the send
34
+ // was accepted upstream and very likely DELIVERED, we just stopped waiting.
35
+ // `details.messageId` is the id it went out with, so the caller watches for
36
+ // it on the `message.status` webhook instead of sending it a second time.
37
+ | "GATEWAY_TIMEOUT"
38
+ | "INTERNAL_ERROR" // 500
39
+ | "SERVICE_UNAVAILABLE"; // 503 — readiness/health check failed, quota store down
40
+ // ── from packages/shared/src/billing/catalog.ts ──────────────────────
41
+
42
+ export type PlanId = "free" | "starter" | "pro" | "scale";
43
+ // ── from packages/shared/src/dto/index.ts ────────────────────────────
44
+
45
+ /** A number's lifecycle: created (no session) → connected (session live) →
46
+ * disconnected (dropped) / banned (restriction signal — see docs/QUIRKS.md). */
47
+ export type NumberStatus = "pairing" | "connected" | "disconnected" | "banned";
48
+
49
+ /** A WhatsApp number (a paired session), server-projected — never carries the
50
+ * gateway instance id or token. */
51
+ export interface NumberDto {
52
+ id: string;
53
+ name: string;
54
+ phone: string | null;
55
+ status: NumberStatus;
56
+ connectedAt: string | null;
57
+ historySync: boolean;
58
+ createdAt: string;
59
+ }
60
+
61
+ /** Answer to POST /numbers/:id/pair. `state` tells the client what to render:
62
+ * "pairing" (show the QR/linking code, or poll again when both are absent —
63
+ * wuzapi can take ~5s to mint a QR after connect), "connected" (already live). */
64
+ export interface PairNumberDto {
65
+ mode: "qr" | "code";
66
+ state: "pairing" | "connected";
67
+ /** Base64 QR (mode qr, when ready). */
68
+ qrCode?: string;
69
+ /** XXXX-XXXX linking code (mode code). */
70
+ linkingCode?: string;
71
+ /** True when the QR is the cached one from an earlier generation. */
72
+ isCached: boolean;
73
+ /** Epoch ms of generation — the client resumes its countdown from this. */
74
+ generatedAt?: number;
75
+ /** Seconds until a NEW QR may be generated (0 when fresh). */
76
+ remainingCooldown: number;
77
+ }
78
+
79
+ /** POST /v1/numbers/{id}/unpair — the slot stays, the WhatsApp session is gone. */
80
+ export interface UnpairedNumberDto {
81
+ id: string;
82
+ status: "disconnected";
83
+ }
84
+
85
+ /** DELETE /v1/numbers/{id} — the slot itself is gone, and the plan's seat is free. */
86
+ export interface DeletedNumberDto {
87
+ id: string;
88
+ deleted: true;
89
+ }
90
+
91
+ /** A message WhatsApp accepted. `sent` is the only success state the send call
92
+ * can report — delivered/read arrive later on the `message.status` webhook. */
93
+ export interface MessageDto {
94
+ /** WhatsApp's message id — the same id the status webhooks carry. */
95
+ id: string;
96
+ /** E.164 recipient the message actually went to. */
97
+ to: string;
98
+ /** Id of the number that sent it (useful when the account has several). */
99
+ from: string;
100
+ status: "sent";
101
+ timestamp: string;
102
+ /** Messages left in this month's PLAN quota after this send — so a client can
103
+ * show a meter without a second call. Hits 0 while sends keep working if the
104
+ * account has credits: the two pools are reported separately on purpose. */
105
+ quotaRemaining: number;
106
+ /** Purchased messages left (packs never expire). Sends spill here once
107
+ * `quotaRemaining` is 0, so a client that stops at `quotaRemaining === 0`
108
+ * and ignores this one stops earlier than it had to. */
109
+ creditRemaining: number;
110
+ }
111
+
112
+ /** One recipient's outcome inside a batch. `skipped` is not a failure of its
113
+ * own: something systemic (the session died, the meter refused) stopped the
114
+ * run, and these are the recipients never attempted — send them again once the
115
+ * reason on the batch is cleared. */
116
+ export interface BatchResultDto {
117
+ to: string;
118
+ status: "sent" | "failed" | "skipped";
119
+ /** Present on `sent`. */
120
+ id?: string;
121
+ /** Present on `failed` and `skipped` — the same code the single-send
122
+ * endpoint would have answered with. */
123
+ error?: { code: ErrorCode; message: string };
124
+ }
125
+
126
+ /** POST /v1/messages/batch. Always a 200 with per-recipient outcomes: a batch
127
+ * where 24 of 25 landed is neither a success nor a failure, and collapsing it
128
+ * into one status code would throw away the only part that matters. */
129
+ export interface BatchMessageDto {
130
+ sent: number;
131
+ failed: number;
132
+ skipped: number;
133
+ /** Why the run stopped early, when it did. */
134
+ stoppedBecause?: { code: ErrorCode; message: string };
135
+ results: BatchResultDto[];
136
+ /** Messages left in this month's plan quota after the batch. */
137
+ quotaRemaining: number;
138
+ /** Purchased messages left after the batch — see `MessageDto.creditRemaining`. */
139
+ creditRemaining: number;
140
+ }
141
+
142
+ /** POST /v1/chats/{phone}/typing. Free, so there is no quota to report — it
143
+ * echoes what it did, and `to` is the address the indicator went to. */
144
+ export interface TypingDto {
145
+ to: string;
146
+ state: "composing" | "paused";
147
+ }
148
+
149
+ /** POST /v1/chats/{phone}/read — the ids that now carry blue ticks. */
150
+ export interface MarkReadDto {
151
+ to: string;
152
+ messageIds: string[];
153
+ }
154
+
155
+ /** POST /v1/otp — the code is returned to the CALLER (that's the whole point of
156
+ * the endpoint: send it and hand it back so the app can verify however it
157
+ * likes). Only its hash is stored. */
158
+ export interface OtpDto {
159
+ code: string;
160
+ expiresAt: string;
161
+ to: string;
162
+ /** The WhatsApp message that carried the code. */
163
+ messageId: string;
164
+ }
165
+
166
+ /** POST /v1/otp/check succeeds or throws OTP_INVALID — never `{ verified: false }`.
167
+ * A verification primitive that answers 200 to a wrong code is one forgotten
168
+ * `if` away from letting anyone in. */
169
+ export interface OtpCheckDto {
170
+ verified: true;
171
+ to: string;
172
+ }
173
+
174
+ /** GET /v1/numbers/check/{phone} — is this phone on WhatsApp? */
175
+ export interface NumberCheckDto {
176
+ /** E.164 of the variant WhatsApp actually matched: a Brazilian mobile may be
177
+ * registered with or without the 9th digit, and we check both. */
178
+ e164: string;
179
+ hasWhatsApp: boolean;
180
+ /** The address to send to. Null when the number isn't on WhatsApp. */
181
+ jid: string | null;
182
+ avatarUrl: string | null;
183
+ /** False when WhatsApp couldn't be reached and `hasWhatsApp` is an optimistic
184
+ * assumption — a check outage must never block sends to real recipients
185
+ * (docs/QUIRKS.md — number-check fail-open). */
186
+ verified: boolean;
187
+ }
188
+
189
+ /** GET /v1/usage — this month's spend against the plan, plus the per-key split. */
190
+ export interface UsageDto {
191
+ plan: PlanId;
192
+ /** "2026-08" (America/Sao_Paulo). */
193
+ month: string;
194
+ monthUsed: number;
195
+ monthLimit: number;
196
+ dayUsed: number;
197
+ dayLimit: number;
198
+ sendsPerMinute: number;
199
+ creditBalance: number;
200
+ /** When the monthly counter rolls over. */
201
+ resetAt: string;
202
+ /** Per-key breakdown, newest first. Revoked keys stay listed — their usage
203
+ * still counts toward the account total, and a month's spend that vanished
204
+ * when a key was rotated would look like a billing error. `revoked` is what
205
+ * tells the two apart. */
206
+ keys: { id: string; name: string; prefix: string; used: number; revoked: boolean }[];
207
+ }
208
+ // ── from packages/shared/src/webhooks/index.ts ───────────────────────
209
+
210
+ /** Every event an endpoint can subscribe to.
211
+ *
212
+ * Deliberately FIVE, normalized out of wuzapi's ~50 provider events: an
213
+ * inbound message, a status change on one we sent, and the three moments a
214
+ * number's session changes hands. Everything else (presence, chat presence,
215
+ * history sync, group metadata) is either noise to an API customer or
216
+ * privacy-sensitive, and is dropped at the ingestion boundary. */
217
+ export const WEBHOOK_EVENT_TYPES = [
218
+ /** Someone messaged one of your numbers. The payload carries the text (or
219
+ * the media's metadata), never the media bytes. */
220
+ "message.created",
221
+ /** WhatsApp reported delivery/read/played for messages you sent. */
222
+ "message.status",
223
+ /** A number's WhatsApp session came up and can send again. */
224
+ "number.connected",
225
+ /** A number's session dropped and did not come back on its own — it needs
226
+ * re-pairing. */
227
+ "number.disconnected",
228
+ /** WhatsApp logged the number out or restricted it. Sends will fail until
229
+ * it is paired again; see the 24h restriction window in the docs. */
230
+ "number.logged_out",
231
+ ] as const;
232
+
233
+ export type WebhookEventType = (typeof WEBHOOK_EVENT_TYPES)[number];
234
+
235
+ /** The synthetic event `POST /v1/webhooks/test` fires. Not subscribable — it is
236
+ * always delivered, because its whole job is proving the wiring works. */
237
+ export const WEBHOOK_TEST_EVENT = "ping";
238
+
239
+ export const WEBHOOK_DELIVERY_STATUSES = ["pending", "delivered", "failed"] as const;
240
+
241
+ export type WebhookDeliveryStatus = (typeof WEBHOOK_DELIVERY_STATUSES)[number];
242
+
243
+ /** The body of every delivery. `id` is also the `x-whatsapi-delivery` header:
244
+ * retries reuse it, so a receiver that has seen an id can safely drop the
245
+ * repeat. */
246
+ export interface WebhookEnvelope<T = unknown> {
247
+ id: string;
248
+ type: WebhookEventType | typeof WEBHOOK_TEST_EVENT;
249
+ /** ISO 8601, when we built the event — not when we sent it. */
250
+ timestamp: string;
251
+ data: T;
252
+ }
253
+
254
+ /** The sending/receiving number, on every event. Enough to route without a
255
+ * lookup, never the gateway instance or token. */
256
+ export interface WebhookNumberRef {
257
+ id: string;
258
+ name: string;
259
+ phone: string | null;
260
+ }
261
+
262
+ export type InboundMessageType =
263
+ | "text"
264
+ | "image"
265
+ | "video"
266
+ | "audio"
267
+ | "document"
268
+ | "location"
269
+ | "contact"
270
+ | "sticker"
271
+ | "reaction"
272
+ | "poll"
273
+ | "other";
274
+
275
+ /** `message.created`. Media arrives as metadata only: the gateway runs with
276
+ * `-skipmedia`, so the bytes stay on WhatsApp's servers until asked for. */
277
+ export interface MessageCreatedEvent {
278
+ number: WebhookNumberRef;
279
+ message: {
280
+ id: string;
281
+ /** The conversation: a phone number for a 1:1 chat, a group JID for a group. */
282
+ chat: string;
283
+ /** Who wrote it — the same as `chat` outside groups. */
284
+ from: string;
285
+ /** True when the message was sent BY this number (from the linked phone,
286
+ * or by an earlier API call) rather than to it. */
287
+ fromMe: boolean;
288
+ isGroup: boolean;
289
+ /** The sender's WhatsApp display name, when they publish one. */
290
+ pushName?: string;
291
+ timestamp: string;
292
+ type: InboundMessageType;
293
+ /** Text body, for text messages. */
294
+ text?: string;
295
+ /** Caption, for media that carries one. */
296
+ caption?: string;
297
+ media?: {
298
+ mimeType?: string;
299
+ fileName?: string;
300
+ /** Audio/video length in seconds. */
301
+ seconds?: number;
302
+ bytes?: number;
303
+ /** True for a voice note rather than an audio file. */
304
+ voice?: boolean;
305
+ };
306
+ };
307
+ }
308
+
309
+ /** `message.status`. WhatsApp acks in batches, so one event can settle several
310
+ * message ids at once. */
311
+ export interface MessageStatusEvent {
312
+ number: WebhookNumberRef;
313
+ /** Ids as returned by the send call. */
314
+ messageIds: string[];
315
+ /** The chat the acks came from. */
316
+ chat: string;
317
+ status: "delivered" | "read" | "played";
318
+ timestamp: string;
319
+ }
320
+
321
+ /** `number.connected` · `number.disconnected` · `number.logged_out`. */
322
+ export interface NumberStatusEvent {
323
+ number: WebhookNumberRef;
324
+ /** Why the session ended, when the provider said — "server", "logged_out",
325
+ * "connection_failure", "ban". Absent on connect. */
326
+ reason?: string;
327
+ }
328
+
329
+ /** `ping` — what the test-fire delivers. Not subscribable; see WEBHOOK_TEST_EVENT. */
330
+ export interface PingEvent {
331
+ message: string;
332
+ }
333
+
334
+ /** Which payload rides with which event. The map is what makes `WebhookEvent`
335
+ * below a discriminated union — narrow on `type` and `data` narrows with it —
336
+ * and it is load-bearing on the producing side too: the emitter's input is
337
+ * typed from it, so an event can never be published with the wrong shape. */
338
+ export interface WebhookPayloads {
339
+ "message.created": MessageCreatedEvent;
340
+ "message.status": MessageStatusEvent;
341
+ "number.connected": NumberStatusEvent;
342
+ "number.disconnected": NumberStatusEvent;
343
+ "number.logged_out": NumberStatusEvent;
344
+ ping: PingEvent;
345
+ }
346
+
347
+ /** Every delivery, as the receiver sees it:
348
+ *
349
+ * if (event.type === "message.created") event.data.message.text
350
+ */
351
+ export type WebhookEvent = {
352
+ [K in keyof WebhookPayloads]: WebhookEnvelope<WebhookPayloads[K]> & { type: K };
353
+ }[keyof WebhookPayloads];
354
+
355
+ export interface WebhookEndpointDto {
356
+ url: string;
357
+ events: WebhookEventType[];
358
+ enabled: boolean;
359
+ /** Set when WE turned the endpoint off: "failures" after a long run of dead
360
+ * deliveries. null while it is healthy. */
361
+ disabledReason: string | null;
362
+ /** Failed deliveries in a row. Resets on the first success. */
363
+ consecutiveFailures: number;
364
+ createdAt: string;
365
+ updatedAt: string;
366
+ }
367
+
368
+ /** The answer to `PUT /v1/webhooks`. `secret` is present ONLY when one was just
369
+ * minted (first registration, or `rotateSecret: true`) — it is never
370
+ * retrievable afterwards, exactly like an API key. */
371
+ export interface SavedWebhookDto {
372
+ secret: string | null;
373
+ record: WebhookEndpointDto;
374
+ }
375
+
376
+ export interface WebhookDeliveryDto {
377
+ /** Also the `x-whatsapi-delivery` header of the POST. */
378
+ id: string;
379
+ event: string;
380
+ status: WebhookDeliveryStatus;
381
+ attempts: number;
382
+ /** The HTTP status the endpoint answered, when it answered at all. */
383
+ responseStatus: number | null;
384
+ error: string | null;
385
+ createdAt: string;
386
+ deliveredAt: string | null;
387
+ }
388
+
389
+ /** The synchronous outcome of `POST /v1/webhooks/test`. */
390
+ export interface WebhookTestResultDto {
391
+ ok: boolean;
392
+ status: number | null;
393
+ error: string | null;
394
+ /** The delivery it was recorded as — it shows up in the delivery list too. */
395
+ deliveryId: string;
396
+ }
397
+
398
+ /** DELETE /v1/webhooks. `false` when there was nothing registered to delete —
399
+ * the end state is the same either way, so it is not an error. */
400
+ export interface DeletedWebhookDto {
401
+ deleted: boolean;
402
+ }