/*! @fliok/sdk v0.1.0 — the Fliok API client for JavaScript and TypeScript. Docs: https://fliok.com/docs/api */ /** * Fliok SDK for JavaScript and TypeScript — the official client for Fliok's public REST API * (https://fliok.com/api/v1). Docs: https://fliok.com/docs/api * * One file, no dependencies: it runs anywhere there is a `fetch` — Node 18+, Bun, Deno, edge * runtimes and browsers. (Keep API keys on a server: a key in a web page can be read by anyone who * opens it.) * * What it does for you, so a caller does not have to: * - every write (POST, PATCH, DELETE) carries an `Idempotency-Key` — a fresh random UUID unless you * pass your own — and the SAME key on every retry, so a retried send is never sent twice; * - a 429 waits the `Retry-After` the API asked for; a 5xx or a dropped connection backs off and * tries again, up to 3 times — but only for reads and for writes that carry a key; * - a refusal becomes a `FliokError` with the API's machine `code` (branch on that, not on the * message, which is written for people and may change); * - the `RateLimit-*` headers of the last answer are kept in `client.rateLimit`; * - `Fliok.verifyWebhook()` checks a webhook delivery's signature in constant time. */ export declare const VERSION = "0.1.0"; export declare const DEFAULT_BASE_URL = "https://fliok.com/api/v1"; export type HttpMethod = "GET" | "POST" | "PATCH" | "PUT" | "DELETE"; /** A JSON object as the API returns it. Fields are described in the docs, per endpoint. */ export type Json = Record; export type QueryValue = string | number | boolean | null | undefined | ReadonlyArray; export type Query = Record; export interface RequestOptions { /** Added to the URL. `undefined` and `null` are left out; an array repeats the key. */ query?: Query; /** Sent as JSON. */ body?: unknown; /** * Your own key for this write (1–255 characters, remembered by the API for 24 hours). Leave it out * and the client makes one. Pass `false` to send none — the request is then never retried, since * a retry could do the write twice. */ idempotencyKey?: string | false; /** Abort the request from outside (it is not retried once aborted). */ signal?: AbortSignal; } export interface FliokOptions { /** An API key (`wa_…`) from Settings → API keys. */ apiKey: string; /** Default https://fliok.com/api/v1. */ baseUrl?: string; /** Retries after the first attempt. Default 3; 0 turns retrying off. */ maxRetries?: number; /** Per attempt, in milliseconds. Default 60000 (a 16 MB media upload takes a while). */ timeoutMs?: number; /** * The longest `Retry-After` the client will sit out, in seconds. A longer one is thrown as the * 429 at once, so a script is never parked for an hour without knowing why. Default 60. */ maxRetryAfterSeconds?: number; /** Your own `fetch` (a proxy agent, a test double). Default: the global one. */ fetch?: typeof fetch; /** How the client waits between attempts. Override to log retries, or in tests. */ sleep?: (ms: number) => Promise; } /** The caller's bucket, from the `RateLimit-*` headers of the last answer. */ export interface RateLimitInfo { /** Requests the bucket holds. */ limit: number; /** Requests left in it after the last one. */ remaining: number; /** Seconds until it is full again. */ reset: number; } export interface ResponseInfo { status: number; requestId: string | null; /** True when the API replayed the first answer for this Idempotency-Key instead of running again. */ replayed: boolean; rateLimit: RateLimitInfo | null; } /** A refusal from the API — or, with status 0, a request that never got an answer. */ export declare class FliokError extends Error { /** The HTTP status; 0 when no answer arrived (`code` is then "network_error" or "timeout"). */ readonly status: number; /** * The API's machine code — the `error` field of the body ("rate_limited", "outside_24h_window", * "insufficient_scope"…). null when the body had none, or carried validation details instead * (they are in `body`). */ readonly code: string | null; /** A reference for support, when the answer carried one. */ readonly requestId: string | null; /** Seconds the API asked us to wait (`Retry-After`), on a 429 or 503. */ readonly retryAfter: number | null; /** The parsed error body, as sent. */ readonly body: unknown; constructor(init: { status: number; code: string | null; message: string; requestId?: string | null; retryAfter?: number | null; body?: unknown; }); } /** Who to send to: a phone number (E.164, "+" optional), or a WhatsApp user id for a customer who shares no number. */ export type Recipient = string | { recipient: string; }; export interface SendOptions { /** Which of your numbers sends (GET /phone-numbers). Default: your first active number. */ phoneNumberId?: string; /** A wamid to reply to: the message shows as a reply to it. */ replyTo?: string; idempotencyKey?: string | false; } export interface SendBody { to?: string; recipient?: string; type: "text" | "template" | "image" | "video" | "audio" | "document" | "interactive" | "reaction" | "location" | "sticker" | "contacts"; text?: string; /** Meta's message object for every type but text: { type: "", "": { … } }. */ payload?: Json; phoneNumberId?: string; replyTo?: string; } export interface SendResult { ok: boolean; /** Fliok's id for the message; GET /messages/{id} or the message.status webhook tells what happened. */ messageId: string; } export interface TemplateSend { /** The approved template's name. */ name: string; /** Its language code ("en", "en_US"); sent as { code } the way a send expects it. */ language: string; /** The body's {{1}}, {{2}}… in order. Fliok builds Meta's components from the approved template. */ variables?: Array; /** A text header's variables. */ headerText?: Array; /** An image, video or document header: an uploaded media id, or a public link (plus filename for a document). */ headerMedia?: { id?: string; link?: string; filename?: string; }; /** A coupon-code button's code. */ couponCode?: string; /** A limited-time offer's end (ISO time). */ offerExpiresAt?: string | number; /** Meta's own component array, when you build it yourself (then leave the shortcuts above out). */ components?: Json[]; } export type MediaType = "image" | "video" | "audio" | "document" | "sticker"; export interface MediaSend { /** A media id from `media.upload()`… */ id?: string; /** …or a public https link. */ link?: string; /** Image, video and document only. */ caption?: string; /** Document only: the name the customer sees. */ filename?: string; } export interface MediaUpload { /** The file's bytes — raw (Uint8Array, Buffer, ArrayBuffer) or already base64-encoded (no "data:" prefix). */ data: Uint8Array | ArrayBuffer | string; /** For example application/pdf, image/jpeg, video/mp4. The API's default is application/pdf. */ mimeType?: string; /** Up to 200 characters. The API's default is document.pdf. */ filename?: string; /** The number that will send it. Default: your first active number. */ phoneNumberId?: string; idempotencyKey?: string | false; } type OptIn = "unknown" | "opted_in" | "opted_out"; export interface ContactCreate { /** The number (`phone` or `e164`, one of the two). */ phone?: string; e164?: string; displayName?: string; tags?: string[]; attributes?: Json; optInStatus?: OptIn; optInSource?: string; } export interface ContactUpdate { displayName?: string | null; tags?: string[]; attributes?: Json; optInStatus?: OptIn; optInSource?: string; } export interface ConversationStart { /** An existing contact… */ contactId?: string; /** …or a number in any format. */ to?: string; phoneNumberId?: string; /** Only find the existing chat — create and send nothing. */ lookup?: boolean; /** The approved template that opens the chat: { type: "template", template: { name, language: { code }, components } }. */ payload?: Json; } export interface ConversationUpdate { state?: "open" | "pending" | "resolved"; priority?: "low" | "normal" | "high" | "urgent"; isArchived?: boolean; /** A team member's user id, or null to unassign. */ assignedAgentId?: string | null; } export interface TemplateCreate { /** Default: the workspace's first WhatsApp Business Account. */ wabaConnectionId?: string; /** Lowercase letters, digits and underscores. */ name: string; /** A plain code here ("en"); only a send uses { code }. */ language: string; category: "MARKETING" | "UTILITY" | "AUTHENTICATION"; /** Meta's components; every variable needs an example. */ components: Json[]; } export interface BroadcastCreate { name: string; templateId: string; phoneNumberId: string; /** The audience — by tags, contact ids or a pasted list (see the docs); or send `segmentId`. One of the two. */ segment?: Json; /** A saved audience (GET /segments). */ segmentId?: string; variables?: Json; /** ISO time to send at. */ scheduledAt?: string; sendNow?: boolean; /** You confirm that recipients who have never messaged you agreed to get WhatsApp messages from you. */ confirmNewRecipients?: boolean; /** Send only to recipients who have messaged you (or have an opt-in recorded). */ knownOnly?: boolean; /** The id of the broadcast this one follows up. */ followUpOf?: string; } export interface WebhookCreate { url: string; /** Leave out (or empty) for every event. GET /webhooks/events lists them. */ events?: string[]; /** One event — the REST-hook style automation tools use. Combined with `events` when both are sent. */ event?: string; } export interface WebhookUpdate { active?: boolean; events?: string[]; url?: string; } type AttributeValue = string | number | boolean | null; export interface LeadCreate { phone: string; name?: string | null; email?: string | null; source?: string | null; stage?: string; stageId?: string | null; owner?: string; ownerId?: string | null; rating?: number | null; dealValue?: number | null; tags?: string[]; attributes?: Record; note?: string | null; form?: string | null; } export interface LeadUpdate { name?: string | null; displayName?: string | null; email?: string | null; stage?: string; stageId?: string; lostReason?: string | null; lostReasonId?: string | null; stageNote?: string | null; rating?: number; source?: string | null; owner?: string | null; ownerId?: string | null; dealValue?: number | null; tags?: string[]; addTags?: string[]; removeTags?: string[]; attributes?: Record; } export interface SalesInvoiceCreate { /** kind (invoice | proforma | quotation), contactId or customer, items, discount, notes, dates — see the docs. */ document: Json; /** Leave out to save a draft. */ issue?: SalesInvoiceIssue; } export interface SalesInvoiceIssue { /** Send it to the customer as soon as it is issued. */ send: boolean; /** Mark it paid in the same step. */ paid?: Json | null; } export interface PaymentRequestCreate { /** The chat to send it in — or the customer's number as `to`. */ conversationId?: string; to?: string; /** Your reference (letters, digits, - _ and ., up to 35); a repeat of the same one is not sent twice. */ referenceId?: string; items: Array<{ productId?: string | null; name?: string; quantity: number; unitPaise: number; }>; taxPaise?: number; shippingPaise?: number; discountPaise?: number; body?: string; footer?: string | null; /** Minutes the customer has to pay; null = no expiry. */ expiresInMinutes?: number | null; } export interface VerifyOptions { /** How far the delivery's timestamp may be from now, in seconds. Default 300 (the docs' five minutes). */ toleranceSeconds?: number; /** "Now" in unix seconds — for tests. */ now?: number; } /** Headers as any server hands them over: a Fetch `Headers`, or a plain object (Node, Express). */ export type HeaderSource = { get(name: string): string | null; } | Record; export declare class Fliok { static readonly VERSION = "0.1.0"; private readonly apiKey; private readonly baseUrl; private readonly maxRetries; private readonly timeoutMs; private readonly maxRetryAfterMs; private readonly fetchImpl; private readonly sleep; private last; constructor(options: FliokOptions); /** The `RateLimit-*` headers of the last answer, or null when it carried none (a 401, say). */ get rateLimit(): RateLimitInfo | null; /** Status, request id, replay flag and rate limit of the last answer. */ get lastResponse(): ResponseInfo | null; /** * Call any endpoint: `request("GET", "/contacts", { query: { limit: 50 } })`. The path is relative * to the base URL. Returns the parsed JSON body; throws a `FliokError` on a refusal. */ request(method: HttpMethod, path: string, options?: RequestOptions): Promise; /** How long to wait before retrying this refusal — or null for "don't". */ private retryDelayMs; private fetchOnce; private url; /** Who is calling: the workspace, and the key's label, role and expiry. A cheap connection test. */ me(): Promise; readonly messages: { /** POST /send with a body you built. */ send: (body: SendBody, options?: { idempotencyKey?: string | false; }) => Promise; /** A free-form text. Needs an open 24-hour window (the customer wrote in the last 24 hours). */ sendText: (to: Recipient, text: string, options?: SendOptions) => Promise; /** An approved template — the only way to open a new conversation. */ sendTemplate: (to: Recipient, template: TemplateSend, options?: SendOptions) => Promise; /** An image, video, audio, document or sticker, by media id or public link. Needs an open 24-hour window. */ sendMedia: (to: Recipient, type: MediaType, media: MediaSend, options?: SendOptions) => Promise; /** GET /messages — filters such as conversationId and status, plus paging. */ list: (query?: Query) => Promise; /** GET /messages/{id} — by Fliok's id or by the wamid webhooks carry. */ get: (id: string) => Promise; }; readonly media: { /** POST /media — upload up to 16 MB and get a media id, reusable for about 30 days. */ upload: (file: MediaUpload) => Promise<{ id: string; }>; }; readonly contacts: { /** GET /contacts — newest first. Filters: search, phone, channel; paging: limit, cursor. */ list: (query?: { search?: string; phone?: string; channel?: string; limit?: number; cursor?: string; } & Query) => Promise; get: (id: string) => Promise; /** POST /contacts — creates the contact, or updates the one with that number. */ create: (body: ContactCreate, options?: { idempotencyKey?: string | false; }) => Promise; update: (id: string, body: ContactUpdate, options?: { idempotencyKey?: string | false; }) => Promise; delete: (id: string, options?: { idempotencyKey?: string | false; }) => Promise; }; readonly conversations: { list: (query?: Query) => Promise; get: (id: string) => Promise; /** POST /conversations — find a chat, or open one with an approved template. */ start: (body: ConversationStart, options?: { idempotencyKey?: string | false; }) => Promise; update: (id: string, body: ConversationUpdate, options?: { idempotencyKey?: string | false; }) => Promise; markRead: (id: string, options?: { idempotencyKey?: string | false; }) => Promise; addNote: (id: string, body: { body: string; }, options?: { idempotencyKey?: string | false; }) => Promise; }; readonly templates: { list: (query?: Query) => Promise; get: (id: string) => Promise; /** POST /templates — submit a template to Meta for approval. */ create: (body: TemplateCreate, options?: { idempotencyKey?: string | false; }) => Promise; /** POST /templates/sync — fetch the latest statuses from Meta. */ sync: (options?: { idempotencyKey?: string | false; }) => Promise; }; readonly broadcasts: { list: (query?: Query) => Promise; get: (id: string) => Promise; create: (body: BroadcastCreate, options?: { idempotencyKey?: string | false; }) => Promise; /** DELETE /broadcasts/{id} — cancel one that has not gone out. */ cancel: (id: string, options?: { idempotencyKey?: string | false; }) => Promise; }; readonly webhooks: { list: () => Promise; /** GET /webhooks/events — every event you can subscribe to. */ events: () => Promise; /** POST /webhooks — the answer carries the signing secret (whsec_…), shown only this once. */ create: (body: WebhookCreate, options?: { idempotencyKey?: string | false; }) => Promise; update: (id: string, body: WebhookUpdate, options?: { idempotencyKey?: string | false; }) => Promise; /** POST /webhooks/{id}/rotate — a new secret; deliveries carry both signatures for 24 hours. */ rotate: (id: string, options?: { idempotencyKey?: string | false; }) => Promise; delete: (id: string, options?: { idempotencyKey?: string | false; }) => Promise; }; readonly leads: { list: (query?: Query) => Promise; get: (id: string) => Promise; create: (body: LeadCreate, options?: { idempotencyKey?: string | false; }) => Promise; update: (id: string, body: LeadUpdate, options?: { idempotencyKey?: string | false; }) => Promise; addNote: (id: string, body: { body: string; }, options?: { idempotencyKey?: string | false; }) => Promise; }; /** Invoices, proformas and quotations you send your customers (Fliok's own bills to you are GET /invoices). */ readonly salesInvoices: { list: (query?: Query) => Promise; get: (id: string) => Promise; /** POST /sales-invoices — a draft, or with `issue` an issued document. */ create: (body: SalesInvoiceCreate, options?: { idempotencyKey?: string | false; }) => Promise; /** Give a draft its number (and send it). */ issue: (id: string, body: SalesInvoiceIssue, options?: { idempotencyKey?: string | false; }) => Promise; /** Send an issued document again. Default channels: whichever of WhatsApp and email the customer has. */ send: (id: string, body?: { channels?: Array<"whatsapp" | "email">; }, options?: { idempotencyKey?: string | false; }) => Promise; }; /** WhatsApp Pay requests (India). */ readonly paymentRequests: { create: (body: PaymentRequestCreate, options?: { idempotencyKey?: string | false; }) => Promise; get: (id: string) => Promise; }; /** * Is this webhook delivery really from Fliok? Pass the RAW body (the bytes or text exactly as they * arrived — re-serialised JSON will not match), the request headers and the endpoint's secret. * * Fliok signs `${x-webhook-timestamp}.${body}` with HMAC-SHA256 and sends it as * `x-webhook-signature: sha256=`. For 24 hours after a secret is rotated the header carries * two, comma-separated (new, then old); one match is enough. A timestamp more than five minutes * from now is refused, so a captured delivery cannot be replayed later. */ static verifyWebhook(rawBody: string | Uint8Array, headers: HeaderSource, secret: string, options?: VerifyOptions): Promise; } export default Fliok; /** The headers every request carries. Exported for tests and for anyone signing requests themselves. */ export declare function buildHeaders(apiKey: string, idempotencyKey: string | undefined, hasBody: boolean): Record; /** `undefined` and `null` are left out, booleans become "true"/"false", an array repeats its key. */ export declare function encodeQuery(query?: Query): string;