/** * SDK TypeScript minimalista do Vértice (sem dependências). * * const client = new Vertice({ baseUrl: 'https://api.seudominio.com', apiKey: 'vtc_live_...' }); * await client.sendText({ instanceId, to: '5511999999999', text: 'Olá!' }); * * Para rotas de painel, use o cookie de sessão + CSRF (não recomendado fora do browser). */ export interface VerticeOptions { baseUrl: string; apiKey?: string; fetch?: typeof fetch; } export interface Instance { id: string; tenantId: string | null; name: string; status: 'created' | 'connecting' | 'qr' | 'connected' | 'disconnected' | 'logged_out'; phone: string | null; webhookUrl: string | null; webhookSecret?: string; webhookEvents: string[] | null; webhookNotifyFromMe: boolean; createdAt: string; updatedAt: string; } export interface MessageResult { id: string | null; messageId: string | null; zaapId: string; status: 'sent' | 'queued'; to: string; timestamp?: number; createdAt?: string; } export interface ProtocolMessageResult { id: string; messageId: string; to: string; timestamp: number; experimental: true; } export interface CapabilityReport { instanceId?: string; provider: string; features: { id: string; status: string; description: string }[]; voice?: { configured: boolean; engineAvailable: boolean; maxCallsPerNumber?: 1; maxConcurrentCalls?: number | null; activeCalls?: number; provider: 'vertice'; transport: 'native'; experimental: true; outbound: boolean; audioFile: boolean; liveAudio: boolean; inbound: boolean; sip: false; validation: 'requires_live_validation'; }; } export type InteractiveMessageKind = | 'buttons' | 'buttons/list' | 'buttons/image' | 'buttons/video' | 'options-list' | 'pix' | 'otp' | 'carousel' | 'order' | 'reply-button' | 'reply-template-button' | 'reply-option-list' | 'newsletter-admin-invite'; export interface VoiceCall { id: string; state: 'initiating' | 'ringing' | 'connected' | 'ended' | 'failed' | 'interrupted'; stateSource: 'native_engine'; direction?: 'inbound' | 'outbound'; nativeCallId?: string; instanceId: string; to: string; provider: 'vertice'; transport: 'native'; /** 0 means no automatic duration limit. */ durationMs: number; reason?: string; createdAt: string; updatedAt: string; } export interface QueueJob { id: string; status: 'queued' | 'sending' | 'sent' | 'delivered' | 'read' | 'failed' | 'canceled'; toJid: string; attempts: number; createdAt: string; messageId: string | null; error: string | null; } export interface GroupSummary { id: string; subject: string; size: number; owner: string | null; announce: boolean; restrict: boolean; } export class VerticeError extends Error { constructor( message: string, public readonly status: number, public readonly code: string, ) { super(message); this.name = 'VerticeError'; } } export class Vertice { private readonly baseUrl: string; private readonly apiKey?: string; private readonly fetchImpl: typeof fetch; constructor(options: VerticeOptions) { this.baseUrl = options.baseUrl.replace(/\/+$/, ''); this.apiKey = options.apiKey; this.fetchImpl = options.fetch ?? fetch; } private async request( path: string, options: { method?: string; body?: unknown; idempotencyKey?: string } = {}, ): Promise { const headers: Record = {}; if (this.apiKey) headers['x-api-key'] = this.apiKey; if (options.body !== undefined) headers['content-type'] = 'application/json'; if (options.idempotencyKey) headers['idempotency-key'] = options.idempotencyKey; const response = await this.fetchImpl(`${this.baseUrl}${path}`, { method: options.method ?? 'GET', headers, body: options.body === undefined ? undefined : JSON.stringify(options.body), }); if (response.status === 204) return undefined as T; const payload = (await response.json().catch(() => null)) as { data?: T; error?: { message?: string; code?: string }; } | null; if (!response.ok) { throw new VerticeError( payload?.error?.message ?? `HTTP ${response.status}`, response.status, payload?.error?.code ?? '', ); } return (payload?.data ?? payload) as T; } /* instâncias */ listInstances(): Promise { return this.request('/instances'); } createInstance(input: { name: string; webhookUrl?: string }): Promise { return this.request('/instances', { method: 'POST', body: input }); } getInstance(id: string): Promise { return this.request(`/instances/${id}`); } connect(id: string): Promise { return this.request(`/instances/${id}/connect`, { method: 'POST' }); } disconnect(id: string): Promise { return this.request(`/instances/${id}/disconnect`, { method: 'POST' }); } logout(id: string): Promise { return this.request(`/instances/${id}/logout`, { method: 'POST' }); } qr(id: string): Promise<{ status: string; qr: string | null; dataUrl: string | null }> { return this.request(`/instances/${id}/qr`); } /* mensagens */ sendText(input: { instanceId: string; to: string; text: string; replyTo?: string; mentions?: string[]; queue?: boolean; delayMessage?: number; delayTyping?: number; idempotencyKey?: string; }): Promise { const { instanceId, ...body } = input; return this.request(`/instances/${instanceId}/messages`, { method: 'POST', body, idempotencyKey: input.idempotencyKey, }); } sendMedia(input: { instanceId: string; to: string; kind: 'image' | 'video' | 'audio' | 'document'; url?: string; base64?: string; caption?: string; fileName?: string; mimetype?: string; ptt?: boolean; }): Promise { const { instanceId, ...body } = input; return this.request(`/instances/${instanceId}/media`, { method: 'POST', body }); } react(instanceId: string, messageId: string, emoji: string, jid?: string): Promise { return this.request(`/instances/${instanceId}/messages/${messageId}/reaction`, { method: 'POST', body: { emoji, ...(jid ? { jid } : {}) }, }); } markRead(instanceId: string, jid: string, ids?: string[]): Promise { return this.request(`/instances/${instanceId}/chats/read`, { method: 'POST', body: { jid, ...(ids ? { ids } : {}) }, }); } typing( instanceId: string, jid: string, state: 'composing' | 'paused' = 'composing', ): Promise { return this.request(`/instances/${instanceId}/presence`, { method: 'POST', body: { jid, state }, }); } /* fila */ listQueue(instanceId: string, status?: QueueJob['status']): Promise { const query = status ? `?status=${encodeURIComponent(status)}` : ''; return this.request(`/instances/${instanceId}/queue${query}`); } cancelJob(instanceId: string, jobId: string): Promise { return this.request(`/instances/${instanceId}/queue/${jobId}`, { method: 'DELETE' }); } clearQueue(instanceId: string): Promise<{ cleared: number }> { return this.request(`/instances/${instanceId}/queue`, { method: 'DELETE' }); } /** Native protocol relay. Recipient rendering is experimental; inspect capabilities first. */ sendInteractive( instanceId: string, kind: InteractiveMessageKind, input: Record, ): Promise { return this.request(`/instances/${encodeURIComponent(instanceId)}/messages/${kind}`, { method: 'POST', body: input, }); } capabilities(instanceId?: string): Promise { return this.request( instanceId ? `/instances/${encodeURIComponent(instanceId)}/capabilities` : '/public/capabilities', ); } managedQuickReplies(instanceId: string): Promise { return this.request(`/instances/${encodeURIComponent(instanceId)}/business/quick-replies`); } /** Native beta: one call per number; server pool defaults to 2. Full pool returns 429 voice_capacity_reached. */ initiateCall( instanceId: string, to: string, idempotencyKey: string, options: { base64?: string; durationMs?: number } = {}, ): Promise { return this.request(`/instances/${encodeURIComponent(instanceId)}/calls`, { method: 'POST', body: { to, type: 'audio', ...options }, idempotencyKey, }); } getCall(instanceId: string, callId: string): Promise { return this.request( `/instances/${encodeURIComponent(instanceId)}/calls/${encodeURIComponent(callId)}`, ); } endCall(instanceId: string, callId: string): Promise { return this.request( `/instances/${encodeURIComponent(instanceId)}/calls/${encodeURIComponent(callId)}/end`, { method: 'POST' }, ); } playCallAudio( instanceId: string, callId: string, base64: string, ): Promise<{ id: string; acceptedByEngine: true }> { return this.request( `/instances/${encodeURIComponent(instanceId)}/calls/${encodeURIComponent(callId)}/audio`, { method: 'POST', body: { base64 } }, ); } /* grupos */ listGroups(instanceId: string): Promise { return this.request(`/instances/${instanceId}/groups`); } createGroup( instanceId: string, subject: string, participants: string[], ): Promise<{ id: string }> { return this.request(`/instances/${instanceId}/groups`, { method: 'POST', body: { subject, participants }, }); } groupMetadata(instanceId: string, jid: string): Promise> { return this.request(`/instances/${instanceId}/groups/${encodeURIComponent(jid)}`); } groupParticipants( instanceId: string, jid: string, participants: string[], action: 'add' | 'remove' | 'promote' | 'demote', ): Promise { return this.request(`/instances/${instanceId}/groups/${encodeURIComponent(jid)}/participants`, { method: 'POST', body: { participants, action }, }); } groupInvite(instanceId: string, jid: string): Promise<{ code: string; link: string }> { return this.request(`/instances/${instanceId}/groups/${encodeURIComponent(jid)}/invite`); } /* V2: canais oficiais e Telegram. Use capacidades; HTTP202 não confirma entrega. */ channelStatus(): Promise { return this.request('/v2/status'); } /** Explicit Cloud registration; never use this to force coexistence. */ registerCloudNumber(id: string, pin: string): Promise { return this.request(`/v2/connections/${encodeURIComponent(id)}/register`, { method: 'POST', body: { pin }, }); } listConnections(): Promise<{ items: CloudConnection[]; cloudConfigured: boolean }> { return this.request('/v2/connections'); } cloudConnection(connectionId: string): Promise { return this.request(`/v2/connections/${encodeURIComponent(connectionId)}`); } cloudMessages( connectionId: string, to: string, before?: string, ): Promise<{ items: CloudMessage[]; nextCursor: string | null }> { return this.request( `/v2/connections/${encodeURIComponent(connectionId)}/messages?to=${encodeURIComponent(to)}${before ? '&before=' + encodeURIComponent(before) : ''}`, ); } sendCloudMessage( connectionId: string, input: Record, idempotencyKey: string, ): Promise<{ message: CloudMessage; created: boolean }> { return this.request('/v2/messages', { method: 'POST', body: { ...input, connectionId }, idempotencyKey, }); } cloudMessage(messageId: string): Promise { return this.request(`/v2/messages/${encodeURIComponent(messageId)}`); } cloudTemplates( connectionId: string, after?: string, ): Promise<{ items: Record[]; nextCursor: string | null }> { return this.request( `/v2/connections/${encodeURIComponent(connectionId)}/templates${after ? '?after=' + encodeURIComponent(after) : ''}`, ); } cloudWindow( connectionId: string, to: string, ): Promise<{ open: boolean; expiresAt: string | null; lastInboundAt: string | null }> { return this.request( `/v2/connections/${encodeURIComponent(connectionId)}/window?to=${encodeURIComponent(to)}`, ); } connectionCapabilities(connectionId: string): Promise { return this.request(`/v2/connections/${encodeURIComponent(connectionId)}/capabilities`); } connectChannel(input: ChannelConnectionInput): Promise { return this.request('/v2/connections', { method: 'POST', body: input }); } disconnectChannel(connectionId: string): Promise { return this.request(`/v2/connections/${encodeURIComponent(connectionId)}`, { method: 'DELETE', }); } channelConversations(connectionId: string): Promise<{ items: Record[] }> { return this.request(`/v2/connections/${encodeURIComponent(connectionId)}/conversations`); } uploadChannelMedia( connectionId: string, input: { base64: string; mimetype: string; filename?: string }, ): Promise<{ id: string }> { return this.request(`/v2/connections/${encodeURIComponent(connectionId)}/media`, { method: 'POST', body: input, }); } readChannelMessage(connectionId: string, messageId: string): Promise { return this.request( `/v2/connections/${encodeURIComponent(connectionId)}/messages/${encodeURIComponent(messageId)}/read`, { method: 'POST' }, ); } /** OAuth callback is hosted by Vértice; provider credentials never go to the browser. */ startSocialOnboarding(input: { provider: 'instagram' | 'messenger'; name: string; }): Promise { return this.request('/v2/social/onboarding/start', { method: 'POST', body: input }); } socialOnboarding(id: string): Promise { return this.request(`/v2/social/onboarding/${encodeURIComponent(id)}`); } completeSocialOnboarding(id: string, assetId: string): Promise<{ connection: CloudConnection }> { return this.request(`/v2/social/onboarding/${encodeURIComponent(id)}/complete`, { method: 'POST', body: { assetId }, }); } cancelSocialOnboarding(id: string): Promise { return this.request(`/v2/social/onboarding/${encodeURIComponent(id)}`, { method: 'DELETE' }); } /** Server-side onboarding. Use one stable admin API key for the entire authorization. */ startTelegramOnboarding(input: { name: string; }): Promise { return this.request('/v2/telegram/onboarding/start', { method: 'POST', body: input }); } telegramOnboarding(id: string): Promise { return this.request(`/v2/telegram/onboarding/${encodeURIComponent(id)}`); } completeTelegramOnboarding(id: string): Promise<{ connection: CloudConnection }> { return this.request(`/v2/telegram/onboarding/${encodeURIComponent(id)}/complete`, { method: 'POST', body: {}, }); } cancelTelegramOnboarding(id: string): Promise { return this.request(`/v2/telegram/onboarding/${encodeURIComponent(id)}`, { method: 'DELETE' }); } startMetaOnboarding(): Promise { return this.request('/v2/onboarding/start', { method: 'POST', body: {} }); } completeMetaOnboarding(input: { state: string; code: string; wabaId: string; phoneNumberId: string; name: string; }): Promise { return this.request('/v2/onboarding/complete', { method: 'POST', body: input }); } updateChannel( id: string, input: { name?: string; webhookUrl?: string | null }, ): Promise { return this.request(`/v2/connections/${encodeURIComponent(id)}`, { method: 'PATCH', body: input, }); } revalidateChannel(id: string): Promise { return this.request(`/v2/connections/${encodeURIComponent(id)}/revalidate`, { method: 'POST', body: {}, }); } updateChannelCredentials( id: string, input: { accessToken: string } | { botToken: string }, ): Promise { return this.request(`/v2/connections/${encodeURIComponent(id)}/credentials`, { method: 'POST', body: input, }); } channelProfile(id: string): Promise> { return this.request(`/v2/connections/${encodeURIComponent(id)}/profile`); } rotateChannelWebhookSecret(id: string): Promise<{ secret: string }> { return this.request(`/v2/connections/${encodeURIComponent(id)}/webhook-secret/rotate`, { method: 'POST', body: {}, }); } createCloudTemplate( id: string, input: { name: string; language: string; category: 'UTILITY' | 'MARKETING' | 'AUTHENTICATION'; components: Record[]; allow_category_change?: boolean; }, ): Promise> { return this.request(`/v2/connections/${encodeURIComponent(id)}/templates`, { method: 'POST', body: input, }); } deleteCloudTemplate(id: string, name: string): Promise> { return this.request( `/v2/connections/${encodeURIComponent(id)}/templates/${encodeURIComponent(name)}`, { method: 'DELETE' }, ); } /* conversas e webhooks */ listChats(instanceId: string): Promise { return this.request(`/instances/${instanceId}/chats`); } chatMessages(instanceId: string, jid: string, limit = 200): Promise { return this.request( `/instances/${instanceId}/chats/messages?jid=${encodeURIComponent(jid)}&limit=${limit}`, ); } deliveries(instanceId: string, status?: 'pendente' | 'entregue' | 'falhou'): Promise { const query = status ? `?status=${status}` : ''; return this.request(`/instances/${instanceId}/webhook-deliveries${query}`); } retryDeliveries(instanceId: string, ids?: string[]): Promise<{ requeued: number }> { return this.request(`/instances/${instanceId}/webhook-deliveries/retry`, { method: 'POST', body: { ids: ids ?? [] }, }); } } export interface CloudConnection { id: string; name: string; provider: 'whatsapp_cloud' | 'whatsapp_web' | 'instagram' | 'messenger' | 'telegram_bot'; channel: 'whatsapp' | 'instagram' | 'messenger' | 'telegram'; status: string; phone: string | null; coexistence: boolean; createdAt: string; updatedAt: string; } export interface CloudMessage { id: string; connectionId: string; remoteId: string | null; to: string; kind: string; payload: Record; direction: 'inbound' | 'outbound'; status: | 'queued' | 'sending' | 'accepted' | 'sent' | 'delivered' | 'read' | 'failed' | 'unknown' | 'received'; errorCode: string | null; createdAt: string; cursor?: string; } export interface CloudWebhookEvent { version: '2'; eventId: string; event: string; workspaceId: string; connectionId: string; channel: 'whatsapp' | 'instagram' | 'messenger' | 'telegram'; provider: 'whatsapp_cloud' | 'instagram' | 'messenger' | 'telegram_bot'; occurredAt: string; data: Record; } export interface ChannelCapabilities { messaging: boolean; media: boolean; templates: boolean; voice: boolean; groups: boolean; automaticRecording: boolean; upload?: boolean; readReceipts?: boolean; messageTypes?: string[]; } export type ChannelConnectionInput = { name: string; webhookUrl?: string | null } & ( | { provider: 'whatsapp_cloud'; wabaId: string; phoneNumberId: string; accessToken: string } | { provider: 'instagram'; pageId: string; instagramAccountId?: string; accessToken: string } | { provider: 'messenger'; pageId: string; accessToken: string } | { provider: 'telegram_bot'; botToken: string } ); export interface TelegramOnboardingSession { id: string; status: 'pending' | 'claimed' | 'ready' | 'connecting' | 'connected' | 'cancelled' | 'expired'; expiresAt: string; bot?: { id: string; name: string; username: string }; connectionId?: string; errorCode?: string; } export interface MetaOnboardingSession { state: string; appId: string; configId: string; graphVersion: string; sessionInfoVersion: '4' | '5'; expiresAt: string; } export interface ChannelStatus { enabled: boolean; configured: boolean; liveValidated: boolean; missing: string[]; providers?: Record; socialSignup?: Record<'instagram' | 'messenger', { configured: boolean; missing: string[] }>; telegramManagedSignup?: { configured: boolean; missing: string[]; managerUsername: string | null; }; } export interface SocialOnboardingAsset { id: string; pageId: string; name: string; instagramAccountId?: string; username?: string; } export interface SocialOnboardingSession { id: string; provider: 'instagram' | 'messenger'; name: string; status: | 'pending' | 'authorizing' | 'ready' | 'connecting' | 'connected' | 'cancelled' | 'expired' | 'failed'; expiresAt: string; assets: SocialOnboardingAsset[]; selectedAssetId?: string; connectionId?: string; errorCode?: string; }