{"openapi":"3.1.0","info":{"title":"Sócios AI Hub — API de Mensageria","version":"1.0.0","description":"API unificada de mensageria multicanal: WhatsApp Cloud API (oficial, com coexistência), Instagram Direct, Messenger e Telegram. Envio via REST, recebimento via webhooks assinados (HMAC-SHA256, padrão Svix, entrega at-least-once). Multi-tenant: sua conta (tenant) pode ter subcontas — uma por cliente final — isoladas entre si.","contact":{"email":"sysadmin@metamorph-ai.com"}},"servers":[{"url":"https://hub.sociosai.com"}],"x-contract-version":"1.4","x-contract-notes":"Toda resposta traz o header X-Hub-Contract-Version. Mudança aditiva (campo novo) não altera esse valor; ele só muda em quebra de contrato. Registre-o e alerte quando mudar. Convenção: TODO campo de request, resposta e evento é snake_case. 1.3 (01/09/2026): GET /v1/channels, GET /v1/messages/{id} e GET /v1/webhooks passaram de camelCase para snake_case (display_name, external_id, created_at, provider_message_id, conversation_id, channel_id, updated_at). Em eventos, `from`/`to` ganharam `external_id`; `externalId` ainda é emitido por compatibilidade e será removido. Aditivo (02/09/2026): keys por subconta (POST/GET/DELETE /v1/subaccounts/{id}/api-keys), expires_at em keys, status \"expired\"; /v1/api-keys/* e /v1/subaccounts* passam a recusar X-Subaccount-Id com 422. Aditivo (05/09/2026, ciclo de vida da integração): GET /v1/me; external_ref e status (active|suspended) em subcontas, GET /v1/subaccounts/{id} e filtros ?external_ref/?status; contas suspensas respondem 403 account_suspended; webhooks ganharam GET/PATCH /{id}, POST /{id}/test (ping assinado), POST /{id}/rotate-secret, GET /{id}/deliveries, GET/POST /{id}/deliveries/{delivery_id}[/redeliver]; GET /v1/events e /v1/events/{id} (os eventos entregues, consultáveis); GET /v1/audit (trilha de auditoria); evento novo webhook.test; erros novos account_suspended e event_expired. Assinaturas de webhook passam a recusar nomes de evento desconhecidos (422). Aditivo (12/09/2026, Google Agenda): a subconta conecta agendas Google pelo mesmo connect hospedado (POST /v1/connect-sessions {integration: \"google_calendar\"}); GET/DELETE /v1/calendar-accounts, GET/PATCH /v1/calendars, POST /v1/calendars/availability (slots livres), CRUD em /v1/calendars/{id}/events; eventos calendar.connected/reconnected/disconnected e calendar.event.created/updated/cancelled (inclusive mudanças feitas direto no Google); erros novos calendar_needs_reauth, calendar_scope_missing, calendar_conflict, calendar_rate_limited, calendar_not_found, calendar_limit_reached, integration_unavailable, invalid_state; max_calendars nas subcontas. Aditivo (12/09/2026, agendamento): recursos (/v1/calendar-resources), serviços (/v1/calendar-services, com lembretes por template), GET /v1/bookings/availability, POST /v1/bookings, POST /v1/bookings/{id}/reschedule e /cancel; eventos booking.created/rescheduled/cancelled/confirmed/reschedule_requested/reminder_sent/reminder_failed; erros booking_not_found, booking_slot_taken, booking_outside_hours. Ferramentas de agente: GET /v1/agent-tools/calendar (JSON-schema), POST /v1/agent-tools/calendar/{tool} e MCP remoto em POST /mcp/calendar.","security":[{"apiKey":[]}],"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"API key `shk_...` do tenant-mãe OU de uma subconta. A key de subconta age só nela: não aceita X-Subaccount-Id, não gerencia subcontas e a rotação da mãe não a atinge. Backend-only — nunca em browser ou app móvel."}},"parameters":{"subaccount":{"name":"X-Subaccount-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Age em nome da subconta indicada (deve pertencer ao seu tenant)."},"idempotency":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Repetir a chamada com a mesma key devolve o mesmo recurso, sem duplicar."}},"schemas":{"Error":{"type":"object","description":"Formato de erro da API. Ramifique SEMPRE em `code` — ele é estável e faz parte do contrato. `error` e `detail` são texto para humano e mudam sem aviso. O campo `error` (string) é legado, mantido durante toda a v1.x e removido na v2.","properties":{"error":{"type":"string","description":"Legado, texto para humano. Não ramifique aqui."},"detail":{"type":"string","description":"Texto para humano. Não ramifique aqui."},"code":{"type":"string","description":"Código estável. É aqui que a lógica do integrador ramifica.","enum":["window_closed","invalid_payload","channel_disconnected","channel_not_found","rate_limited","unauthorized","forbidden","not_found","conflict","quota_exceeded","provider_error","internal_error","session_expired","account_suspended","event_expired","calendar_needs_reauth","calendar_scope_missing","calendar_conflict","calendar_rate_limited","calendar_not_found","calendar_limit_reached","integration_unavailable","invalid_state","booking_not_found","booking_slot_taken","booking_outside_hours"]},"retryable":{"type":"boolean","description":"true quando repetir mais tarde pode dar certo."},"details":{"description":"Detalhamento estruturado (ex.: campos inválidos)."},"meta_code":{"type":"integer","description":"Código original da Meta, quando o erro nasceu lá."}}},"Channel":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","enum":["whatsapp","instagram","messenger","telegram"]},"status":{"type":"string","enum":["connected","disconnected","pending"]},"display_name":{"type":"string","nullable":true},"external_id":{"type":"string","description":"phone_number_id / ig user id / page id / bot id"},"meta":{"type":"object"},"created_at":{"type":"string","format":"date-time"}}},"CalendarAccount":{"type":"object","description":"Conta Google conectada a uma subconta. A credencial nunca aparece.","properties":{"id":{"type":"string"},"provider":{"type":"string","enum":["google_calendar"]},"email":{"type":"string"},"display_name":{"type":"string","nullable":true},"status":{"type":"string","enum":["connected","needs_reauth","disconnected"],"description":"needs_reauth: o refresh token morreu (revogado, 6 meses parado, teto do Google); gere uma connect session com reconnect_account_id."},"status_reason":{"type":"string","nullable":true,"description":"invalid_grant | user"},"scopes":{"type":"array","items":{"type":"string"}},"calendars_count":{"type":"integer"},"connected_at":{"type":"string","format":"date-time"},"disconnected_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"Calendar":{"type":"object","properties":{"id":{"type":"string"},"account_id":{"type":"string"},"external_id":{"type":"string","description":"calendarId no Google (a primária é o e-mail)."},"name":{"type":"string"},"timezone":{"type":"string","description":"IANA, ex.: America/Sao_Paulo. Padrão para horários e disponibilidade."},"primary":{"type":"boolean"},"access_role":{"type":"string","nullable":true,"description":"owner | writer | reader"},"use_for_availability":{"type":"boolean","description":"Entra no cálculo de disponibilidade e recebe notificações de mudança."},"watch_active":{"type":"boolean","description":"true enquanto o hub recebe push do Google para esta agenda."},"last_synced_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"CalendarEvent":{"type":"object","description":"Compromisso. Título, descrição e link vêm do Google a cada leitura e não ficam no hub (LGPD: em clínica são dado de saúde); id, contato e origem são do hub. `id` é null para eventos que só existem no Google e ainda não foram sincronizados.","properties":{"id":{"type":"string","nullable":true},"calendar_id":{"type":"string"},"external_id":{"type":"string","description":"Id do evento no Google."},"title":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"location":{"type":"string","nullable":true},"start":{"type":"string","description":"ISO 8601 com offset do `tz` (ou YYYY-MM-DD se all_day)."},"end":{"type":"string"},"tz":{"type":"string"},"all_day":{"type":"boolean"},"status":{"type":"string","enum":["confirmed","tentative","cancelled"]},"meet_url":{"type":"string","nullable":true},"html_link":{"type":"string","nullable":true},"attendees":{"type":"array","items":{"type":"object","properties":{"email":{"type":"string"},"name":{"type":"string","nullable":true},"response":{"type":"string","nullable":true}}}},"contact_id":{"type":"string","nullable":true},"source":{"type":"string","enum":["hub","external"],"description":"hub = criado pela API; external = apareceu pelo Google."},"etag":{"type":"string","nullable":true}}},"CalendarEventInput":{"type":"object","required":["start","end","title"],"properties":{"start":{"type":"string","format":"date-time","description":"ISO 8601 com offset."},"end":{"type":"string","format":"date-time"},"tz":{"type":"string","description":"IANA; padrão = fuso da agenda."},"title":{"type":"string","maxLength":1024},"description":{"type":"string","maxLength":8000},"location":{"type":"string"},"contact_id":{"type":"string","description":"Contato do hub (da mesma subconta) vinculado ao compromisso."},"attendees":{"type":"array","maxItems":50,"items":{"type":"object","required":["email"],"properties":{"email":{"type":"string"},"name":{"type":"string"}}},"description":"Com convidados o Google envia o convite por e-mail."},"meet":{"type":"boolean","default":false,"description":"Cria link do Google Meet (meet_url na resposta)."},"reminders":{"type":"array","maxItems":5,"items":{"type":"object","properties":{"method":{"type":"string","enum":["popup","email"]},"minutes":{"type":"integer"}}}},"allow_overlap":{"type":"boolean","default":false,"description":"Pula a checagem de conflito (sem isto, slot ocupado → 409 calendar_conflict)."}}},"Availability":{"type":"object","properties":{"tz":{"type":"string"},"from":{"type":"string"},"to":{"type":"string"},"duration_min":{"type":"integer"},"slots":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string"},"end":{"type":"string"}}},"description":"Slots livres em ISO com offset do tz, já respeitando ocupações, buffer, horário de trabalho e antecedência."},"calendars_considered":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"account_id":{"type":"string"}}}}}},"CalendarResource":{"type":"object","description":"Quem ou o que atende (profissional, sala). Aponta para uma agenda da subconta.","properties":{"id":{"type":"string"},"name":{"type":"string"},"calendar_id":{"type":"string"},"calendar_name":{"type":"string"},"timezone":{"type":"string"},"working_hours":{"type":"object","nullable":true,"description":"Por dia da semana (sun..sat): janelas HH:MM. null = 24h."},"active":{"type":"boolean"},"meta":{"type":"object"},"created_at":{"type":"string","format":"date-time"}}},"CalendarService":{"type":"object","description":"Tipo de atendimento: duração, folga, antecedência, Meet, preço e lembretes.","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"duration_min":{"type":"integer"},"buffer_min":{"type":"integer"},"min_notice_min":{"type":"integer"},"max_days_ahead":{"type":"integer"},"meet":{"type":"boolean"},"price_cents":{"type":"integer","nullable":true},"reminders":{"type":"array","description":"Lembretes por WhatsApp (template utility aprovado). O payload dos botões volta como <ação>:<booking_id> em message.received e o hub age na reserva.","items":{"type":"object","properties":{"offset_min":{"type":"integer","description":"Minutos antes do início (1440 = 24h)."},"template":{"type":"string"},"language":{"type":"string","default":"pt_BR"},"channel_id":{"type":"string"},"params":{"type":"array","items":{"type":"string","enum":["contact_name","date","time","resource_name","service_name","meet_url"]}},"buttons":{"type":"array","items":{"type":"string","enum":["confirm","reschedule","cancel"]}}}}},"active":{"type":"boolean"},"meta":{"type":"object"},"created_at":{"type":"string","format":"date-time"}}},"Booking":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["confirmed","confirmed_by_contact","cancelled","cancelled_externally"]},"resource_id":{"type":"string"},"resource_name":{"type":"string"},"service_id":{"type":"string"},"service_name":{"type":"string"},"contact_id":{"type":"string","nullable":true},"contact_name":{"type":"string","nullable":true},"calendar_id":{"type":"string"},"event_id":{"type":"string","nullable":true},"start":{"type":"string"},"end":{"type":"string"},"tz":{"type":"string"},"meet_url":{"type":"string","nullable":true},"html_link":{"type":"string","nullable":true},"notes":{"type":"string","nullable":true,"description":"Cifradas em repouso."},"external_ref":{"type":"string","nullable":true},"source":{"type":"string","enum":["api","contact","calendar"],"description":"Origem da última mudança."},"cancel_reason":{"type":"string","nullable":true},"cancelled_at":{"type":"string","nullable":true},"confirmed_at":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"AgentTool":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"input_schema":{"type":"object","description":"JSON Schema dos argumentos."}}},"Message":{"type":"object","properties":{"id":{"type":"string"},"direction":{"type":"string","enum":["inbound","outbound"]},"type":{"type":"string"},"status":{"type":"string","enum":["queued","sent","delivered","read","failed"]},"error":{"type":"string","nullable":true},"provider_message_id":{"type":"string","nullable":true,"description":"Id da mensagem no provider (wamid, mid...)."},"conversation_id":{"type":"string","nullable":true},"channel_id":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"ApiKey":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string","enum":["active","expiring","revoked","expired"]},"last_used_at":{"type":"string","format":"date-time","nullable":true},"revoked_at":{"type":"string","format":"date-time","nullable":true},"expires_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"},"subaccount_id":{"type":"string","description":"Presente quando a key é de uma subconta."}}},"WebhookSubscription":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"WebhookDelivery":{"type":"object","description":"Uma tentativa de entrega de um evento a uma assinatura. `id` = `{subscription_id}:{event_id}`.","properties":{"id":{"type":"string"},"subscription_id":{"type":"string"},"event_id":{"type":"string"},"event":{"type":"string","nullable":true},"status":{"type":"string","enum":["pending","delivered","failed","dead"],"description":"dead = esgotou as 8 tentativas; reentregue com POST .../redeliver."},"attempts":{"type":"integer"},"last_error":{"type":"string","nullable":true},"delivered_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"Event":{"type":"object","description":"Um evento da conta, como foi entregue por webhook. `data` é o mesmo objeto do envelope; após a retenção (90 dias) vem null e `expired` = true.","properties":{"id":{"type":"string"},"event":{"type":"string"},"channel_id":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"data":{"type":"object","nullable":true},"expired":{"type":"boolean"}}},"AuditEntry":{"type":"object","description":"Ação sensível registrada na conta (keys, webhooks, subcontas, suspensão). Não é expurgada pela retenção.","properties":{"id":{"type":"string"},"type":{"type":"string","enum":["api_key.created","api_key.revoked","api_key.rotated","webhook.created","webhook.updated","webhook.deleted","webhook.secret_rotated","webhook.tested","webhook.redelivered","subaccount.created","subaccount.updated","subaccount.suspended","subaccount.reactivated","tenant.created","tenant.updated","tenant.suspended","tenant.reactivated"]},"actor":{"type":"object","nullable":true,"description":"{api_key_id, key_tenant_id, via: api|admin}"},"subaccount_id":{"type":"string","nullable":true},"ip":{"type":"string","nullable":true},"target":{"type":"object","description":"Campos específicos da ação (subscription_id, key_id, changes...)."},"created_at":{"type":"string","format":"date-time"}}},"Subaccount":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string","description":"Prefixado com o slug da mãe (`mae--cliente`). Use o id como chave."},"name":{"type":"string"},"max_channels":{"type":"integer","nullable":true},"external_ref":{"type":"string","nullable":true,"description":"Seu id para esta conta. Único por tenant."},"status":{"type":"string","enum":["active","suspended"]},"suspended_at":{"type":"string","format":"date-time","nullable":true},"connected_channels":{"type":"integer"},"created_at":{"type":"string","format":"date-time"}}},"Me":{"type":"object","properties":{"account":{"type":"object","description":"A conta desta chamada (a subconta, quando X-Subaccount-Id é usado).","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"kind":{"type":"string","enum":["tenant","subaccount"]},"parent_id":{"type":"string","nullable":true},"external_ref":{"type":"string","nullable":true},"status":{"type":"string","enum":["active","suspended"]},"max_channels":{"type":"integer","nullable":true},"rate_limit_rps":{"type":"integer","description":"Limite efetivo de requisições/s desta conta."},"plan":{"type":"object","nullable":true,"properties":{"slug":{"type":"string"},"name":{"type":"string"}}},"created_at":{"type":"string","format":"date-time"}}},"key_owner":{"type":"object","description":"Dono da API key usada.","properties":{"id":{"type":"string"},"slug":{"type":"string"},"kind":{"type":"string","enum":["tenant","subaccount"]}}},"api_key":{"type":"object","nullable":true,"properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"expires_at":{"type":"string","nullable":true}}},"acting_as_subaccount":{"type":"boolean"},"contract_version":{"type":"string"},"events":{"type":"array","items":{"type":"string"},"description":"Catálogo de eventos que esta versão entrega."}}},"EventParty":{"type":"object","description":"`from` em message.received, `to` em message.echo.","properties":{"external_id":{"type":"string","description":"wa_id (WhatsApp), PSID/IGSID (Messenger/Instagram), chat id (Telegram). É o `to` do envio."},"name":{"type":"string","nullable":true},"externalId":{"type":"string","deprecated":true,"description":"Alias de external_id; será removido."}}},"SendMessage":{"type":"object","required":["channel_id","to","type"],"properties":{"channel_id":{"type":"string"},"to":{"type":"string","description":"WhatsApp: E.164 sem \"+\" (5511999990000). Instagram/Messenger: o from.external_id recebido no message.received."},"type":{"type":"string","enum":["text","template","media"]},"text":{"type":"object","properties":{"body":{"type":"string","maxLength":4096}}},"template":{"type":"object","description":"Só WhatsApp. Única forma de iniciar conversa fora da janela de 24h.","properties":{"name":{"type":"string"},"language":{"type":"string","default":"pt_BR"},"params":{"type":"object","additionalProperties":{"type":"string"}},"components":{"type":"array","description":"Componentes de execução da Meta para cabeçalho, corpo e botões dinâmicos.","items":{"type":"object"}}}},"media":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"URL pública; o provider baixa."},"caption":{"type":"string"},"kind":{"type":"string","enum":["image","document","audio","video"]}}}}},"WebhookEnvelope":{"type":"object","description":"Corpo entregue no seu endpoint. Headers: webhook-id, webhook-timestamp, webhook-signature (\"v1,\" + base64(HMAC-SHA256(secret, `${id}.${timestamp}.${corpo bruto}`))). Valide antes de processar; processe com idempotência pelo webhook-id (entrega at-least-once). O envelope traz `subaccount_id` e `channel_id` para você conferir que a URL de entrega e o conteúdo concordam — erro de configuração vira erro detectado no primeiro evento.","properties":{"subaccount_id":{"type":"string","description":"Tenant/subconta dono do evento."},"channel_id":{"type":"string","description":"Canal a que o evento se refere, quando houver."},"id":{"type":"string"},"event":{"type":"string","enum":["message.received","message.status","message.echo","channel.connected","channel.reconnected","channel.disconnected","channel.health_changed","template.status","template.category_changed","history.completed","lead.received","calendar.connected","calendar.reconnected","calendar.disconnected","calendar.event.created","calendar.event.updated","calendar.event.cancelled","booking.created","booking.rescheduled","booking.cancelled","booking.confirmed","booking.reschedule_requested","booking.reminder_sent","booking.reminder_failed","webhook.test"],"description":"Trate evento desconhecido como no-op (responda 2xx): novos tipos são mudança aditiva e não sobem a versão do contrato."},"created_at":{"type":"integer","description":"epoch em segundos"},"data":{"type":"object"}}}}},"paths":{"/v1/me":{"get":{"summary":"Quem sou (conta, key, limites, contrato)","description":"Primeira chamada de qualquer integração: confirma que a key vale, que X-Subaccount-Id foi aceito e quais limites e plano se aplicam. Traz o catálogo de eventos e a versão do contrato.","parameters":[{"$ref":"#/components/parameters/subaccount"}],"responses":{"200":{"description":"Identidade da conta.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"403":{"description":"account_suspended: a conta (ou a mãe dela) está suspensa."}}}},"/v1/subaccounts":{"post":{"summary":"Criar subconta (um cliente final seu)","description":"Com `external_ref` (seu id da empresa) a criação fica idempotente: repetir com o mesmo valor devolve 409 conflict com `details.existing` (a subconta já criada). O slug devolvido vem prefixado pelo slug da mãe.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"slug":{"type":"string","description":"Opcional; derivado do nome."},"max_channels":{"type":"integer","nullable":true,"description":"Entitlement; o hub bloqueia o excedente. null = sem limite."},"external_ref":{"type":"string","maxLength":120,"description":"Seu identificador desta conta. Único por tenant."}}}}}},"responses":{"201":{"description":"Subconta criada (guarde o id).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subaccount"}}}},"409":{"description":"slug ou external_ref já em uso; `details.existing` traz a subconta existente."}}},"get":{"summary":"Listar subcontas com canais conectados","parameters":[{"name":"external_ref","in":"query","required":false,"schema":{"type":"string"},"description":"Filtra pela sua referência."},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["active","suspended"]}}],"responses":{"200":{"description":"Visão da conta-mãe.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Subaccount"}}}}}}}}}},"/v1/subaccounts/{id}":{"get":{"summary":"Detalhe de uma subconta","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Subconta.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subaccount"}}}},"404":{"description":"Não é sua."}}},"patch":{"summary":"Atualizar subconta (nome, max_channels, external_ref, status)","description":"`status: \"suspended\"` suspende o cliente final (inadimplência): a API responde 403 account_suspended para ele, os webhooks param, os dados e eventos ficam. `status: \"active\"` reativa.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"max_channels":{"type":"integer","nullable":true},"external_ref":{"type":"string","nullable":true},"status":{"type":"string","enum":["active","suspended"]}}}}}},"responses":{"200":{"description":"Subconta atualizada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subaccount"}}}},"409":{"description":"external_ref já usado por outra subconta."}}}},"/v1/subaccounts/{id}/api-keys":{"post":{"summary":"Criar API key para uma subconta (só a mãe)","description":"A key autentica COMO a subconta: enxerga só ela, não aceita X-Subaccount-Id, não cria subcontas. Rotação da key da mãe não a atinge. É a credencial certa para uma instalação on-premise ou para um cliente que não pode receber a key da mãe. A key aparece só nesta resposta.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":80},"expires_at":{"type":"string","format":"date-time","description":"Opcional; no futuro."}}}}}},"responses":{"201":{"description":"{key, id, name, status, expires_at, created_at, subaccount_id}"},"404":{"description":"Subconta não é sua."},"422":{"description":"Corpo inválido, ou X-Subaccount-Id enviado."}}},"get":{"summary":"Listar keys de uma subconta","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{data: ApiKey[]} (sem o valor da key)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}}}}}}}}}},"/v1/subaccounts/{id}/api-keys/{key_id}":{"delete":{"summary":"Revogar key de subconta (imediato, sem graça)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"key_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{id, status: \"revoked\", revoked_at}"}}}},"/v1/connect-sessions":{"post":{"summary":"Gerar link de conexão de canal (wizard hospedado)","description":"Devolve uma URL de uso único (30 min). Redirecione o cliente: ele autoriza na Meta e volta ao return_url com o canal ativo. Nenhuma credencial da Meta passa pelo seu sistema.","parameters":[{"$ref":"#/components/parameters/subaccount"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"channel_type":{"type":"string","enum":["whatsapp","messenger","instagram"],"default":"whatsapp","description":"Canal a conectar. Exclusivo com `integration`."},"integration":{"type":"string","enum":["google_calendar"],"description":"Em vez de canal, conecta uma integração da subconta: o cliente autoriza a Google Agenda (OAuth) e volta ao return_url com `calendar_account_id`. Exclusivo com `channel_type`. Exige max_calendars livre (422 calendar_limit_reached)."},"reconnect_account_id":{"type":"string","description":"Google: reautorizar uma conta já conectada (status needs_reauth) sem contar na quota. 404 calendar_not_found se não for da subconta."},"coexistence":{"type":"boolean","default":false,"description":"WhatsApp: conectar número que JÁ está no app WhatsApp Business (QR no wizard; o app continua funcionando)."},"return_url":{"type":"string","format":"uri"}}}}}},"responses":{"201":{"description":"{id, url, expires_at, channel_type | integration}"},"422":{"description":"Limite de canais (quota_exceeded) ou de agendas (calendar_limit_reached) atingido."},"503":{"description":"integration_unavailable: Google não configurado neste hub."}}}},"/v1/channels":{"get":{"summary":"Listar canais","parameters":[{"$ref":"#/components/parameters/subaccount"}],"responses":{"200":{"description":"Canais do tenant/subconta.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Channel"}}}}}}}}},"post":{"summary":"Criar canal com credencial própria (uso avançado)","description":"Para quem já tem token: WhatsApp {phone_number_id, waba_id, access_token}, Messenger/Instagram {page_id, access_token} (page token), Telegram {bot_token}. O caminho normal para cliente final é a connect session.","parameters":[{"$ref":"#/components/parameters/subaccount"}],"responses":{"201":{"description":"Canal conectado."}}}},"/v1/channels/{id}":{"get":{"summary":"Detalhe do canal, com saúde","description":"Traz `health` (qualidade, limite de mensagens, billing_ok, name_status). `?refresh=1` relê da Graph.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"refresh","in":"query","required":false,"schema":{"type":"integer","enum":[1]}}],"responses":{"200":{"description":"Channel + health","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Channel"}}}}}},"delete":{"summary":"Desconectar canal","description":"Desinscreve os webhooks no provider (quando aplicável) e marca disconnected. Histórico preservado.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{id, status, provider_cleanup}"}}}},"/v1/messages":{"post":{"summary":"Enviar mensagem","parameters":[{"$ref":"#/components/parameters/subaccount"},{"$ref":"#/components/parameters/idempotency"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessage"}}}},"responses":{"202":{"description":"Enfileirada: {message_id, status: \"queued\"}. Progresso via webhook message.status."},"409":{"description":"Canal desconectado."},"422":{"description":"Janela de 24h fechada (WhatsApp: use template; IG/Messenger: aguarde o cliente) ou payload inválido."}}}},"/v1/messages/{id}":{"get":{"summary":"Consultar status de uma mensagem","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Timeline: queued → sent → delivered → read | failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}}}}},"/v1/conversations":{"get":{"summary":"Listar conversas (para montar uma caixa de entrada)","description":"Ordenadas pela última mensagem. Traz contato, canal, prévia da última mensagem e `window_open` — use este campo para habilitar ou não o campo de resposta livre.","parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"channel_id","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":100}}],"responses":{"200":{"description":"Lista de conversas do tenant (ou da subconta)."}}}},"/v1/conversations/{id}/messages":{"get":{"summary":"Ler a thread de uma conversa","description":"Em ordem cronológica. Permite recuperar o histórico sem depender de ter recebido todos os webhooks. Cada item inclui provider_message_id e created_at.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/subaccount"},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"maximum":200}}],"responses":{"200":{"description":"Conversa (contato, canal, janela) + mensagens."},"404":{"description":"Conversa inexistente ou de outro tenant."}}}},"/v1/conversations/{id}/sync":{"post":{"summary":"Sincronizar o histórico de uma conversa do Instagram ou Messenger","description":"Recupera pela Conversations API respostas feitas no aplicativo do Instagram, Facebook ou Messenger. É idempotente pelo ID da mensagem na Meta.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/subaccount"}],"responses":{"200":{"description":"{conversation_id, scanned, imported}"},"404":{"description":"Conversa inexistente ou de outro tenant."},"422":{"description":"A conversa não é do Instagram nem do Messenger."}}}},"/v1/channels/whatsapp/embedded-signup":{"get":{"summary":"app_id e config_id para embutir o FB.login() do Embedded Signup","description":"Para quem já tem o popup da Meta na própria UI. O caminho normal é a connect session hospedada.","responses":{"200":{"description":"{app_id, config_id, graph_version}"}}}},"/v1/channels/whatsapp/connect":{"post":{"summary":"Concluir Embedded Signup por API","description":"Recebe {code, waba_id, phone_number_id} do popup, troca por token, registra o número, assina os webhooks e ativa o canal.","parameters":[{"$ref":"#/components/parameters/subaccount"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["code","waba_id","phone_number_id"],"properties":{"code":{"type":"string"},"waba_id":{"type":"string"},"phone_number_id":{"type":"string"},"display_name":{"type":"string"}}}}}},"responses":{"201":{"description":"Channel conectado."},"409":{"description":"Número já conectado por outra conta."},"422":{"description":"quota_exceeded ou payload inválido."},"502":{"description":"provider_error (Meta recusou)."}}}},"/v1/media/{mediaId}":{"get":{"summary":"Baixar mídia pelo id estável do hub (med_...)","description":"Serve do storage do hub; cai para o provider enquanto o download não terminou. 410 quando expirou pela retenção.","parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"mediaId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Bytes da mídia (content-type do original)."},"410":{"description":"Expirou."}}}},"/v1/media/{channelId}/{mediaId}":{"get":{"summary":"Baixar mídia recebida (proxy legado pelo id do provider)","description":"WhatsApp (media_id) e Telegram (file_id): o hub baixa do provider com o token do canal e devolve em streaming. Messenger/Instagram: a URL do CDN já vem no payload do message.received.","parameters":[{"name":"channelId","in":"path","required":true,"schema":{"type":"string"}},{"name":"mediaId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Bytes da mídia (content-type do original)."}}}},"/v1/templates":{"get":{"summary":"Listar templates WhatsApp da WABA","responses":{"200":{"description":"Com ?status, ?language, ?name, ?limit, ?after."}}},"post":{"summary":"Criar template e submeter à aprovação","responses":{"201":{"description":"{status: \"PENDING\"} — acompanhe via evento template.status."}}}},"/v1/templates/{name}":{"get":{"summary":"Detalhar template (todas as línguas)","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Status e motivo de rejeição por língua."}}},"patch":{"summary":"Editar template (volta a PENDING)","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Atualizado."}}},"delete":{"summary":"Apagar template","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Sem ?language apaga todas as línguas."}}}},"/v1/webhooks":{"get":{"summary":"Listar assinaturas de webhook","responses":{"200":{"description":"Assinaturas ativas do tenant/subconta.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookSubscription"}}}}}}}}},"post":{"summary":"Registrar endpoint de webhook","description":"O secret aparece SÓ na resposta desta chamada. Eventos são entregues por tenant: registre uma assinatura para cada subconta (com X-Subaccount-Id) apontando para sua URL.","parameters":[{"$ref":"#/components/parameters/subaccount"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"https:// obrigatório"},"events":{"type":"array","items":{"type":"string"},"default":["*"],"description":"\"*\" ou nomes do catálogo (GET /v1/me → events). Nome desconhecido → 422."}}}}}},"responses":{"201":{"description":"WebhookSubscription + `secret` (só aqui)."},"422":{"description":"URL não https ou evento desconhecido (details.unknown_events)."}}}},"/v1/webhooks/{id}":{"get":{"summary":"Detalhe da assinatura","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Sem o secret.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSubscription"}}}}}},"patch":{"summary":"Editar assinatura (url, events, active)","description":"`active: false` pausa as entregas (as pendentes ficam registradas como falhas até reativar e reentregar).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Assinatura atualizada.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSubscription"}}}}}},"delete":{"summary":"Desativar assinatura","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Desativada."}}}},"/v1/webhooks/{id}/rotate-secret":{"post":{"summary":"Trocar o secret da assinatura","description":"O secret novo aparece só nesta resposta. Entregas já na fila saem assinadas com o secret vigente na hora do envio: guarde o novo e aceite os dois por alguns minutos antes de descartar o antigo.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"WebhookSubscription + `secret`."}}}},"/v1/webhooks/{id}/test":{"post":{"summary":"Ping assinado (evento webhook.test)","description":"Entrega um evento `webhook.test` pela fila real, assinado, a esta assinatura mesmo que ela não liste o evento. Prova URL, HMAC e 2xx sem esperar tráfego. Acompanhe pelo `delivery_id` em GET .../deliveries/{delivery_id}.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"{event_id, delivery_id, event: \"webhook.test\"}"},"409":{"description":"Assinatura inativa."}}}},"/v1/webhooks/{id}/deliveries":{"get":{"summary":"Entregas desta assinatura","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["pending","delivered","failed","dead"]}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"maximum":200}},{"name":"before","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Paginação: valor de `next_before` da página anterior."}],"responses":{"200":{"description":"{data: WebhookDelivery[], has_more, next_before}","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}},"has_more":{"type":"boolean"},"next_before":{"type":"string","nullable":true}}}}}}}}},"/v1/webhooks/{id}/deliveries/{delivery_id}":{"get":{"summary":"Estado de uma entrega","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"delivery_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"WebhookDelivery","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDelivery"}}}}}}},"/v1/webhooks/{id}/deliveries/{delivery_id}/redeliver":{"post":{"summary":"Reentregar um evento (inclusive dead)","description":"Reenvia o mesmo evento (mesmo `id`, mesmo `data`) pela fila, à assinatura indicada. Receptor idempotente pelo webhook-id trata como duplicata. 410 event_expired quando o conteúdo já saiu pela retenção.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"delivery_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"{delivery_id, event_id, event, status: \"pending\"}"},"409":{"description":"Assinatura inativa."},"410":{"description":"event_expired."}}}},"/v1/events":{"get":{"summary":"Eventos da conta (os mesmos dos webhooks), consultáveis","description":"Recuperação de quem ficou fora do ar ou ainda não tinha webhook: lista os eventos entregues, mais recentes primeiro. Mesmo `data` do envelope. Retenção de 90 dias (depois `data` = null e `expired` = true).","parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"event","in":"query","required":false,"schema":{"type":"string"}},{"name":"channel_id","in":"query","required":false,"schema":{"type":"string"}},{"name":"since","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"until","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"maximum":200}},{"name":"before","in":"query","required":false,"schema":{"type":"string"},"description":"Paginação: id devolvido em `next_before`."}],"responses":{"200":{"description":"{data: Event[], has_more, next_before}","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Event"}},"has_more":{"type":"boolean"},"next_before":{"type":"string","nullable":true}}}}}}}}},"/v1/events/{id}":{"get":{"summary":"Um evento, com o estado das entregas dele","parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Event + deliveries[] {id, subscription_id, status, attempts, last_error, delivered_at}"}}}},"/v1/audit":{"get":{"summary":"Trilha de auditoria da conta","description":"Quem criou/revogou key, mexeu em webhook, suspendeu subconta. Sobrevive à retenção. Com X-Subaccount-Id, a trilha da subconta.","parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"type","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"maximum":200}},{"name":"before","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"{data: AuditEntry[], has_more, next_before}","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AuditEntry"}},"has_more":{"type":"boolean"},"next_before":{"type":"string","nullable":true}}}}}}}}},"/v1/usage":{"get":{"summary":"Uso do mês (canais + mensagens)","parameters":[{"$ref":"#/components/parameters/subaccount"}],"responses":{"200":{"description":"Base para o seu billing por cliente."}}}},"/v1/calendar-accounts":{"get":{"summary":"Listar contas de agenda (Google) da subconta","tags":["Agenda"],"parameters":[{"$ref":"#/components/parameters/subaccount"}],"responses":{"200":{"description":"{data: CalendarAccount[]}. Contas desconectadas continuam listadas com status.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CalendarAccount"}}}}}}}}}},"/v1/calendar-accounts/{id}":{"get":{"summary":"Detalhe da conta com as agendas","tags":["Agenda"],"parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"CalendarAccount + calendars[]"},"404":{"description":"calendar_not_found (inclusive se for de outra subconta)."}}},"delete":{"summary":"Desconectar (ou expurgar) a conta","tags":["Agenda"],"description":"Revoga o token no Google, apaga a credencial e para as notificações. A conta fica listada como `disconnected` e as agendas ficam (eventos já entregues referenciam os ids). Com `?purge=true` apaga conta, agendas e eventos de vez (pedido de eliminação do titular, LGPD art. 18) e registra em /v1/audit.","parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"purge","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"CalendarAccount (ou {id, purged: true, calendars, events})"},"404":{"description":"Não encontrada ou já desconectada."}}}},"/v1/calendars":{"get":{"summary":"Listar agendas da subconta","tags":["Agenda"],"parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"account_id","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"{data: Calendar[]}","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Calendar"}}}}}}}}}},"/v1/calendars/{id}":{"patch":{"summary":"Escolher se a agenda entra na disponibilidade; fuso","tags":["Agenda"],"description":"Ligar use_for_availability cria a notificação de mudanças (watch) no Google; desligar a para.","parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"use_for_availability":{"type":"boolean"},"timezone":{"type":"string"}}}}}},"responses":{"200":{"description":"Calendar","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Calendar"}}}},"422":{"description":"timezone inválida."}}}},"/v1/calendars/availability":{"post":{"summary":"Horários livres (freebusy + regras de negócio)","tags":["Agenda"],"description":"Consulta o freebusy de TODAS as agendas marcadas com use_for_availability (ou só `calendar_ids`), mesmo de contas diferentes, e devolve os slots que cabem. É o que o bot chama antes de oferecer horários. Janela máxima de 62 dias.","parameters":[{"$ref":"#/components/parameters/subaccount"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["from","to","duration_min"],"properties":{"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"duration_min":{"type":"integer","minimum":5},"buffer_min":{"type":"integer","default":0,"description":"Folga antes e depois de cada ocupação."},"min_notice_min":{"type":"integer","default":0,"description":"Não oferece slots que começam antes de agora + este valor."},"step_min":{"type":"integer","description":"Passo entre candidatos; padrão = duration_min."},"tz":{"type":"string","description":"IANA; padrão = fuso da agenda primária."},"working_hours":{"type":"object","description":"Por dia da semana (sun..sat): lista de janelas HH:MM. Dia ausente = fechado. Sem o campo = 24h.","additionalProperties":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string","example":"09:00"},"end":{"type":"string","example":"18:00"}}}}},"calendar_ids":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Availability","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Availability"}}}},"403":{"description":"calendar_needs_reauth (details.calendar_account_id diz qual conta reconectar)."},"422":{"description":"Janela inválida ou nenhuma agenda marcada."},"429":{"description":"calendar_rate_limited (quota do Google; retryable)."}}}},"/v1/calendars/{id}/events":{"get":{"summary":"Listar compromissos (lidos do Google, cache 60 s)","tags":["Agenda"],"parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Padrão: agora."},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Padrão: from + 30 dias (máx. 62)."},{"name":"tz","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"{data: CalendarEvent[]}","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CalendarEvent"}}}}}}}}},"post":{"summary":"Criar compromisso","tags":["Agenda"],"description":"Cria no Google (opcionalmente com Meet e convidados) e devolve o compromisso. Antes de criar, confere o freebusy do intervalo: ocupado → 409 calendar_conflict com details.busy (use allow_overlap para encaixar mesmo assim). Idempotency-Key suportado. Emite calendar.event.created (changed_by: hub).","parameters":[{"$ref":"#/components/parameters/subaccount"},{"$ref":"#/components/parameters/idempotency"},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CalendarEventInput"}}}},"responses":{"200":{"description":"Idempotency-Key repetida: o compromisso já criado."},"201":{"description":"CalendarEvent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CalendarEvent"}}}},"404":{"description":"calendar_not_found | not_found (contact_id de outra subconta)."},"409":{"description":"calendar_conflict: slot ocupado."}}}},"/v1/calendars/{id}/events/{event_id}":{"get":{"summary":"Ler um compromisso","tags":["Agenda"],"parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"event_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"CalendarEvent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CalendarEvent"}}}}}},"patch":{"summary":"Remarcar ou editar","tags":["Agenda"],"description":"start e end vão juntos (remarcação, com checagem de conflito). Usa If-Match com o etag conhecido: se o evento mudou no Google desde a última leitura, 409 calendar_conflict retryable (releia e tente de novo). Emite calendar.event.updated.","parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"event_id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CalendarEventInput"}}}},"responses":{"200":{"description":"CalendarEvent"},"409":{"description":"calendar_conflict (slot ocupado, ou etag mudou → retryable)."}}},"delete":{"summary":"Cancelar","tags":["Agenda"],"description":"Cancela no Google (convidados são avisados) e no hub. Emite calendar.event.cancelled. Segundo DELETE → 404.","parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"event_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{id, external_id, status: cancelled, ...}"}}}},"/v1/calendar-resources":{"get":{"summary":"Listar recursos (profissionais/salas)","tags":["Agendamento"],"parameters":[{"$ref":"#/components/parameters/subaccount"}],"responses":{"200":{"description":"{data: CalendarResource[]}"}}},"post":{"summary":"Criar recurso","tags":["Agendamento"],"parameters":[{"$ref":"#/components/parameters/subaccount"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","calendar_id"],"properties":{"name":{"type":"string"},"calendar_id":{"type":"string"},"timezone":{"type":"string"},"working_hours":{"type":"object"},"meta":{"type":"object"}}}}}},"responses":{"201":{"description":"CalendarResource","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CalendarResource"}}}},"404":{"description":"calendar_not_found"}}}},"/v1/calendar-resources/{id}":{"get":{"summary":"Detalhe do recurso","tags":["Agendamento"],"parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"CalendarResource"}}},"patch":{"summary":"Editar recurso (nome, agenda, horário de trabalho, active)","tags":["Agendamento"],"parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"CalendarResource"}}},"delete":{"summary":"Desativar recurso (reservas existentes ficam)","tags":["Agendamento"],"parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"CalendarResource com active=false"}}}},"/v1/calendar-services":{"get":{"summary":"Listar serviços","tags":["Agendamento"],"parameters":[{"$ref":"#/components/parameters/subaccount"}],"responses":{"200":{"description":"{data: CalendarService[]}"}}},"post":{"summary":"Criar serviço","tags":["Agendamento"],"parameters":[{"$ref":"#/components/parameters/subaccount"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","duration_min"],"properties":{"name":{"type":"string"},"description":{"type":"string"},"duration_min":{"type":"integer","minimum":5},"buffer_min":{"type":"integer","default":0},"min_notice_min":{"type":"integer","default":0},"max_days_ahead":{"type":"integer","default":60},"meet":{"type":"boolean","default":false},"price_cents":{"type":"integer"},"reminders":{"type":"array","items":{"type":"object"}},"meta":{"type":"object"}}}}}},"responses":{"201":{"description":"CalendarService","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CalendarService"}}}}}}},"/v1/calendar-services/{id}":{"get":{"summary":"Detalhe do serviço","tags":["Agendamento"],"parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"CalendarService"}}},"patch":{"summary":"Editar serviço (inclusive reminders e active)","tags":["Agendamento"],"parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"CalendarService"}}},"delete":{"summary":"Desativar serviço","tags":["Agendamento"],"parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"CalendarService com active=false"}}}},"/v1/bookings/availability":{"get":{"summary":"Horários livres de um recurso para um serviço","tags":["Agendamento"],"description":"Aplica o horário de trabalho do recurso, a duração, a folga e a antecedência do serviço às ocupações da agenda. É o que o bot chama antes de oferecer opções.","parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"resource_id","in":"query","required":true,"schema":{"type":"string"}},{"name":"service_id","in":"query","required":true,"schema":{"type":"string"}},{"name":"from","in":"query","required":true,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":true,"schema":{"type":"string","format":"date-time"}},{"name":"tz","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"{resource_id, service_id, tz, duration_min, slots[]}"},"403":{"description":"calendar_needs_reauth"}}}},"/v1/bookings":{"get":{"summary":"Listar reservas","tags":["Agendamento"],"parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"from","in":"query","schema":{"type":"string"}},{"name":"to","in":"query","schema":{"type":"string"}},{"name":"resource_id","in":"query","schema":{"type":"string"}},{"name":"service_id","in":"query","schema":{"type":"string"}},{"name":"contact_id","in":"query","schema":{"type":"string"}},{"name":"status","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"{data: Booking[]}","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Booking"}}}}}}}}},"post":{"summary":"Reservar","tags":["Agendamento"],"description":"Cria a reserva e o compromisso na agenda do recurso (título = serviço · contato; Meet se o serviço pede). Checa sobreposição com outras reservas e com a agenda (409 booking_slot_taken), horário de trabalho, antecedência e janela máxima (422 booking_outside_hours). Agenda os lembretes configurados no serviço. Idempotency-Key suportado. Emite booking.created.","parameters":[{"$ref":"#/components/parameters/subaccount"},{"$ref":"#/components/parameters/idempotency"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["resource_id","service_id","start"],"properties":{"resource_id":{"type":"string"},"service_id":{"type":"string"},"start":{"type":"string","format":"date-time"},"contact_id":{"type":"string","description":"Contato do hub: liga a reserva ao WhatsApp para lembretes e botões."},"notes":{"type":"string","description":"Cifradas em repouso; vão na descrição do compromisso."},"external_ref":{"type":"string"},"attendee_email":{"type":"string"},"title":{"type":"string"}}}}}},"responses":{"201":{"description":"Booking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"409":{"description":"booking_slot_taken"},"422":{"description":"booking_outside_hours | recurso/serviço inativo"}}}},"/v1/bookings/{id}":{"get":{"summary":"Detalhe da reserva","tags":["Agendamento"],"parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Booking"},"404":{"description":"booking_not_found"}}}},"/v1/bookings/{id}/reschedule":{"post":{"summary":"Remarcar","tags":["Agendamento"],"description":"Mesmas checagens da criação; move o compromisso; reagenda os lembretes. Emite booking.rescheduled com previous_start.","parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["start"],"properties":{"start":{"type":"string","format":"date-time"}}}}}},"responses":{"200":{"description":"Booking"},"409":{"description":"booking_slot_taken | reserva cancelada"}}}},"/v1/bookings/{id}/cancel":{"post":{"summary":"Cancelar","tags":["Agendamento"],"description":"Cancela o compromisso e remove os lembretes. Emite booking.cancelled (source: api). Já cancelada → 409.","parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Booking"}}}},"/v1/agent-tools/calendar":{"get":{"summary":"Catálogo de ferramentas de agenda para agentes","tags":["Agentes"],"description":"Seis ferramentas com JSON-schema (calendar_list_offerings, calendar_get_availability, calendar_book, calendar_reschedule, calendar_cancel, calendar_list_bookings), prontas para virar tool definitions do seu LLM.","parameters":[{"$ref":"#/components/parameters/subaccount"}],"responses":{"200":{"description":"{tools: AgentTool[]}","content":{"application/json":{"schema":{"type":"object","properties":{"tools":{"type":"array","items":{"$ref":"#/components/schemas/AgentTool"}}}}}}}}}},"/v1/agent-tools/calendar/{tool}":{"post":{"summary":"Executar uma ferramenta","tags":["Agentes"],"description":"Erro de negócio vem como dado, com HTTP 200: {ok: false, error: {code, message, details}}. O agente lê o código e conduz a conversa (ex.: booking_slot_taken → oferecer outro horário). Ferramenta desconhecida → 404.","parameters":[{"$ref":"#/components/parameters/subaccount"},{"name":"tool","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","description":"Argumentos conforme input_schema."}}}},"responses":{"200":{"description":"{ok: true, result} | {ok: false, error}"}}}},"/mcp/calendar":{"post":{"summary":"MCP remoto (JSON-RPC 2.0 por HTTP)","tags":["Agentes"],"description":"Servidor MCP com as mesmas ferramentas, autenticado pela API key (Bearer) e X-Subaccount-Id. Métodos: initialize, ping, tools/list, tools/call, notifications/*. Erro de negócio vira isError com o código em structuredContent.error.","parameters":[{"$ref":"#/components/parameters/subaccount"}],"responses":{"200":{"description":"Resposta JSON-RPC"},"202":{"description":"Notificação aceita"}}}},"/v1/api-keys":{"get":{"summary":"Listar API keys do dono da key","description":"Age no dono da key (mãe ou subconta). X-Subaccount-Id → 422.","responses":{"200":{"description":"{data: ApiKey[]}","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}}}}}}}}}},"/v1/api-keys/rotate":{"post":{"summary":"Rotacionar API key","description":"Cria key nova e agenda a expiração das antigas do MESMO dono (grace_hours, default 24). A key nova aparece só na resposta. Keys de subcontas não são atingidas pela rotação da mãe. X-Subaccount-Id → 422.","responses":{"201":{"description":"{key, key_id, old_keys_valid_until}"}}}}}}