/** * 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'; 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); 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`); } /* 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 ?? [] }, }); } }