{"openapi":"3.0.3","info":{"title":"Memberfy Developers API","version":"0.1.0","description":"F-101: webhooks e integrações prontas da comunidade (owner e admin). Chamada pelo app e pelo MCP com a sessão da memberfy-api; nunca com chave de API."},"servers":[{"url":"https://developers.memberfy.net"}],"paths":{"security":{"0":{"bearerAuth":[]}},"/api/webhooks/events":{"get":{"tags":["Webhooks"],"summary":"Eventos que um webhook pode assinar, por família","operationId":"listWebhookEvents","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"}],"responses":{"200":{"description":"As famílias, os 97 eventos (os 13 da v1, `form.submitted`, os dois de check-in e os 77 da F-141, com a volta do membro, a mensagem direta recebida pela equipe (sem o texto), o ciclo de vida de cada família (publicado, editado, movido, excluído, transmissão, gravação, remarcado, encerrado, reaberto) e os fatos de aula, quiz, comentário, reação, post, tags, grupos e campos do membro, checkout, espaço, ingresso e lista de espera, assinatura, reserva, cupom, pesquisa, formulário, proposta, certificado, votação de ideias, downloads, classificados, encomendas, visitantes, presença e chamados, o badge recebido e retirado da F-148, e o nível e os pontos da pessoa da F-152) e o `ping` (o `ping` não se assina; vai no Enviar teste). Para assinar a família inteira, use `familia.*`.","content":{"application/json":{"example":{"success":true,"message":"Eventos disponíveis","data":{"families":[{"family":"member","label":"Membros","events":[{"type":"member.joined","description":"Um membro entrou na comunidade"}]}],"ping":{"type":"ping","description":"Teste enviado pelo botão Enviar teste"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/webhooks/fixed-ips":{"get":{"tags":["Webhooks"],"summary":"IPs fixos da Memberfy para entrega de webhooks","operationId":"getWebhookFixedIps","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"}],"responses":{"200":{"description":"Se a opção está disponível e os IPs publicados, para mostrar ao lado da opção.","content":{"application/json":{"example":{"success":true,"message":"IPs fixos","data":{"available":true,"ips":["198.51.100.10","198.51.100.11"]}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/webhooks/history":{"get":{"tags":["Webhooks"],"summary":"Histórico de alterações da aba Webhooks","operationId":"listWebhooksHistory","description":"Quem fez o quê e quando (criar, editar, testar, rotacionar, desligar, religar, reenviar, apagar e o desligamento automático, com `memberId` nulo). Na edição (`updated`), `changes` traz o antes e o depois de cada campo que mudou (`url` mascarada, `events`, `description`, `fixedIps`); nas outras ações, `changes` é nulo. Guardado por 30 dias.","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"},{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Página do histórico, mais recente primeiro.","content":{"application/json":{"example":{"success":true,"message":"Histórico obtido","data":{"history":[{"id":"0b9f5c1e-2d3a-4b5c-8d7e-9f0a1b2c3d4e","action":"created","label":"Criou o webhook","memberId":"7b1e2c3d-4f5a-4b6c-8d7e-9f0a1b2c3d4e","bySystem":false,"targetType":"endpoint","targetId":"5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a","deliveryId":null,"changes":null,"createdAt":"2026-10-19T12:00:00.000Z"},{"id":"1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f","action":"updated","label":"Editou o webhook","memberId":"7b1e2c3d-4f5a-4b6c-8d7e-9f0a1b2c3d4e","bySystem":false,"targetType":"endpoint","targetId":"5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a","deliveryId":null,"changes":{"url":{"before":"https://hooks.memberfy.net/crm","after":"https://hooks.memberfy.net/…m-v2"},"events":{"before":["member.joined"],"after":["member.*"]}},"createdAt":"2026-10-19T12:05:00.000Z"}]},"pagination":{"page":1,"limit":20,"total":1,"totalPages":1}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/webhooks":{"get":{"tags":["Webhooks"],"summary":"Webhooks da comunidade","operationId":"listWebhooks","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"}],"responses":{"200":{"description":"Os webhooks (URL mascarada, sem segredo) com a última entrega, e o teto por comunidade.","content":{"application/json":{"example":{"success":true,"message":"Webhooks obtidos","data":{"limit":10,"webhooks":[{"id":"5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a","urlMasked":"https://hooks.memberfy.net/…c123","description":"CRM","events":["member.joined","purchase.completed"],"status":"ACTIVE","fixedIps":false,"fixedIpAddresses":[],"lastSuccessAt":null,"failureCount":0,"lastDelivery":null}]}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Webhooks"],"summary":"Cria um webhook","operationId":"createWebhook","description":"Só `https`, porta 443, endereço público (conferido aqui e em cada entrega). O segredo\n(`whsec_…`, Standard Webhooks) vem **só nesta resposta**. Teto de 10 webhooks por\ncomunidade; criar, testar, religar, reenviar e trocar a URL somam 10 por minuto por comunidade.\n","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url","events"],"properties":{"url":{"type":"string","example":"https://hooks.memberfy.net/crm"},"description":{"type":"string","maxLength":200,"example":"CRM"},"events":{"type":"array","items":{"type":"string"},"example":["member.joined","purchase.completed"]},"fixedIps":{"type":"boolean","default":false}}}}}},"responses":{"201":{"description":"Criado. Guarde o `secret`; ele não volta.","content":{"application/json":{"example":{"success":true,"message":"Webhook criado","data":{"secret":"whsec_MfKQ9r1rXk2TzQ2a0b6p5p0m7y3v4Z8n2Q1s8t5u6w0=","webhook":{"id":"5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a","url":"https://hooks.memberfy.net/crm","urlMasked":"https://hooks.memberfy.net/…/crm","events":["member.joined","purchase.completed"],"status":"ACTIVE"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"URL recusada (campo `url`), eventos inválidos (`events`), IPs fixos indisponíveis (`fixedIps`) ou teto de 10 atingido."},"429":{"description":"Mais de 10 ações que enviam requisição ao destino no minuto (criar, testar, religar, reenviar, trocar a URL), com `Retry-After`."}}}},"/api/webhooks/{id}":{"get":{"tags":["Webhooks"],"summary":"Detalhe de um webhook","operationId":"getWebhook","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"},{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"O webhook, com a URL completa (nunca o segredo).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"webhook":{"$ref":"#/components/schemas/Webhook"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"tags":["Webhooks"],"summary":"Edita URL, descrição, eventos ou a opção de IPs fixos","operationId":"updateWebhook","description":"Mande só o que muda; campo ausente fica como está. A configuração nova vale para os\n**eventos que acontecerem depois** da edição. O que já estava a caminho (entregas\npendentes, em retentativa ou reenviadas depois) segue exatamente como foi criado:\nmesma URL, mesmo uso de IP fixo e mesmo payload. A assinatura usa sempre o segredo atual.\n\n- **Trocar a URL:** a URL nova passa pela mesma checagem da criação (só `https`, porta 443,\n  endereço público), entra no limite de 10 por minuto por comunidade e zera o contador de\n  falhas (`failureCount`), porque é outro destino.\n- **Webhook desligado** (`DISABLED` ou `DISABLED_FAILING`): editar, inclusive trocar a URL,\n  **não religa**. Religar continua sendo `POST /api/webhooks/{id}/enable`, que manda o teste e\n  só religa com resposta 2xx.\n- Cada edição que muda algo vira uma linha `updated` no histórico, com o antes e o depois\n  de cada campo em `changes` (URLs mascaradas). Uma edição sem mudança não gera linha.\n","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"},{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","maxLength":2048,"example":"https://hooks.memberfy.net/crm-v2"},"description":{"type":"string","nullable":true,"maxLength":200,"example":"CRM"},"events":{"type":"array","minItems":1,"maxItems":50,"items":{"type":"string"},"example":["member.*","purchase.completed"]},"fixedIps":{"type":"boolean"}}},"example":{"url":"https://hooks.memberfy.net/crm-v2","events":["member.*","purchase.completed"],"fixedIps":true}}}},"responses":{"200":{"description":"O webhook atualizado, com a URL completa (nunca o segredo).","content":{"application/json":{"example":{"success":true,"message":"Webhook atualizado","data":{"webhook":{"id":"5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a","url":"https://hooks.memberfy.net/crm-v2","urlMasked":"https://hooks.memberfy.net/…m-v2","description":"CRM","events":["member.*","purchase.completed"],"status":"ACTIVE","fixedIps":true,"fixedIpAddresses":["203.0.113.10","203.0.113.11"],"lastSuccessAt":null,"failureCount":0,"lastDelivery":null}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"URL recusada (campo `url`), eventos inválidos (`events`), descrição longa (`description`) ou IPs fixos indisponíveis (`fixedIps`)."},"429":{"description":"Com `url` no corpo: mais de 10 ações que enviam requisição ao destino no minuto (criar, testar, religar, reenviar, trocar a URL), com `Retry-After`."}}},"delete":{"tags":["Webhooks"],"summary":"Apaga o webhook","operationId":"deleteWebhook","description":"As tentativas pendentes são canceladas na hora.","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"},{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Apagado."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/webhooks/{id}/test":{"post":{"tags":["Webhooks"],"summary":"Enviar teste (ping)","operationId":"testWebhook","description":"Manda um `ping` assinado agora e devolve a entrega com o status HTTP e a duração. Não é retentado nem conta no uso. Entra no limite de 10 por minuto com a criação.","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"},{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"A entrega do ping (SUCCEEDED ou FAILED).","content":{"application/json":{"example":{"success":true,"message":"Teste enviado","data":{"delivery":{"id":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","eventType":"ping","status":"SUCCEEDED","attemptCount":1,"lastAttempt":{"at":"2026-10-19T12:00:00.000Z","httpStatus":200,"durationMs":182,"error":null}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"O webhook está desligado."},"429":{"description":"Mais de 10 ações que enviam requisição ao destino no minuto (criar, testar, religar, reenviar, trocar a URL), com `Retry-After`."}}}},"/api/webhooks/{id}/rotate-secret":{"post":{"tags":["Webhooks"],"summary":"Rotaciona o segredo","operationId":"rotateWebhookSecret","description":"O novo segredo vale na hora (a próxima tentativa já sai assinada só com ele) e o antigo deixa de valer. Vem só nesta resposta.","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"},{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"O webhook e o novo `secret`."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/webhooks/{id}/disable":{"post":{"tags":["Webhooks"],"summary":"Desliga o webhook","operationId":"disableWebhook","description":"Para as entregas na hora (tentativas pendentes canceladas); nenhuma entrega nova é criada.","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"},{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Desligado."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/webhooks/{id}/enable":{"post":{"tags":["Webhooks"],"summary":"Religa o webhook","operationId":"enableWebhook","description":"Manda um `ping` e só religa com resposta `2xx` (entra no limite de 10 por minuto). Sem `2xx`, responde 422 com a entrega do ping em `data.delivery`.","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"},{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Religado: `data: { webhook, delivery, enabled: true }`."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"O ping não teve `2xx`: `data: { webhook, delivery, enabled: false }`."},"429":{"description":"Mais de 10 ações que enviam requisição ao destino no minuto (criar, testar, religar, reenviar, trocar a URL), com `Retry-After`."}}}},"/api/webhooks/{id}/history":{"get":{"tags":["Webhooks"],"summary":"Histórico de alterações de um webhook","operationId":"getWebhookHistory","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"},{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Igual a `GET /api/webhooks/history`, só deste webhook."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/webhooks/{id}/deliveries":{"get":{"tags":["Webhooks"],"summary":"Histórico de entregas","operationId":"listWebhookDeliveries","description":"Mais recentes primeiro, guardadas por 30 dias. Filtros por status e por evento.","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"},{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"query","name":"status","schema":{"type":"string","enum":["PENDING","SUCCEEDED","FAILED","CANCELED"]}},{"in":"query","name":"eventType","schema":{"type":"string"},"example":"member.joined"},{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Página de entregas (sem o corpo): `data.deliveries` e `pagination`.","content":{"application/json":{"example":{"success":true,"message":"Entregas obtidas","data":{"deliveries":[{"id":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","eventId":"evt_01JAB3XK9Q2W7E5R4T6Y8U0I1O","eventType":"member.joined","status":"FAILED","attemptCount":8,"lastAttempt":{"at":"2026-10-21T09:00:00.000Z","httpStatus":500,"durationMs":120,"error":null},"nextAttemptAt":null}]},"pagination":{"page":1,"limit":20,"total":1,"totalPages":1}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/webhooks/{id}/deliveries/{deliveryId}":{"get":{"tags":["Webhooks"],"summary":"Detalhe de uma entrega","operationId":"getWebhookDelivery","description":"O corpo enviado e cada tentativa (status, duração, erro e até 1 KB da resposta).","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"},{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"path","name":"deliveryId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"A entrega.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"delivery":{"$ref":"#/components/schemas/Delivery"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/webhooks/{id}/deliveries/{deliveryId}/resend":{"post":{"tags":["Webhooks"],"summary":"Reenvia uma entrega","operationId":"resendWebhookDelivery","description":"Mesmo `webhook-id`, novo ciclo de 8 tentativas; a primeira sai agora e a resposta já traz o resultado dela.","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"},{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"path","name":"deliveryId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"A entrega depois da primeira tentativa."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Webhook desligado","ou a entrega ainda está pendente.":null},"429":{"description":"Mais de 10 ações que enviam requisição ao destino no minuto (criar, testar, religar, reenviar, trocar a URL), com `Retry-After`."}}}},"/api/apps":{"get":{"tags":["Apps"],"summary":"Catálogo de integrações prontas","operationId":"listApps","description":"Os apps feitos pela Memberfy, com o que cada um recebe (em linguagem simples), os eventos que\npode receber, os campos de configuração e a instalação da comunidade (ou `null`).\n\n- `kind: handler` (Discord, Slack, Telegram, RD Station, HubSpot): liga-se com `PUT /api/apps/{slug}/installation`.\n  Não têm IP fixo (`fixedIps: false`).\n- `kind: webhook` (Zapier, Make, n8n): atalhos. A tela abre o formulário de webhook (`POST /api/webhooks`)\n  pré-preenchido, com o guia (`guide`) e um exemplo de URL (`urlExample`); podem usar os IPs fixos.\n- HubSpot traz `scopes`: os escopos que o Private App precisa ter.\n","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"}],"responses":{"200":{"description":"O catálogo.","content":{"application/json":{"example":{"success":true,"message":"Integrações obtidas","data":{"apps":[{"slug":"discord","name":"Discord","kind":"handler","available":true,"description":"Publica uma mensagem curta num canal do Discord a cada evento escolhido.","receives":"O primeiro nome da pessoa e o nome do produto, do evento ou do curso. Nunca o e-mail.","guide":"No Discord, abra Configurações do canal › Integrações › Webhooks › Novo webhook e copie a URL.","fixedIps":false,"events":[{"type":"member.joined","description":"Uma pessoa entrou na comunidade."}],"fields":[{"key":"webhookUrl","type":"url","secret":true,"required":true,"label":"URL do webhook do canal","help":"Começa com https://discord.com/api/webhooks/."}],"installation":null},{"slug":"zapier","name":"Zapier","kind":"webhook","available":true,"description":"Leve os eventos da comunidade para mais de 6.000 apps com um Zap.","receives":"O evento completo, assinado, como em qualquer webhook.","guide":"No Zapier, crie um Zap com o gatilho Webhooks by Zapier › Catch Hook e cole aqui a URL gerada.","urlExample":"https://hooks.zapier.com/hooks/catch/123456/abcdef/","fixedIps":true}]}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api/apps/{slug}/installation":{"get":{"tags":["Apps"],"summary":"A instalação do app na comunidade","operationId":"getAppInstallation","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"},{"in":"path","name":"slug","required":true,"schema":{"type":"string","enum":["discord","slack","telegram","rdstation","hubspot"]}}],"responses":{"200":{"description":"A instalação, com a configuração mascarada e a última entrega.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"installation":{"$ref":"#/components/schemas/AppInstallation"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"App inexistente ou não instalado na comunidade."},"422":{"description":"Atalho (Zapier, Make, n8n): não se instala, é um webhook."}}},"put":{"tags":["Apps"],"summary":"Liga o app ou salva a configuração","operationId":"putAppInstallation","description":"**Ligar** e **editar** são a mesma chamada. Sem instalação: cria e liga (`201`). Com instalação: salva\ne, se estava desligada (por alguém ou por falhas), religa a mesma instalação, com a configuração salva (`200`).\n\n- `config`: os campos do app (ver `fields` em `GET /api/apps`). Campo secreto ausente, vazio ou\n  devolvido mascarado mantém o valor salvo; assim dá para trocar só os eventos.\n- Hosts conferidos ao salvar: Discord só `discord.com` e `discordapp.com` (`/api/webhooks/…`),\n  Slack só `hooks.slack.com` (`/services/…`). Telegram, RD Station e HubSpot falam só com os hosts\n  fixos do provedor (`api.telegram.org`, `api.rd.services`, `api.hubapi.com`).\n- `events`: subconjunto dos eventos do app; ausente na criação = todos.\n- Trocar a configuração vale para os eventos seguintes; o que já estava a caminho segue com a\n  configuração com que foi criado. Também zera o contador de falhas.\n- Histórico da aba: `app_installed`, `app_updated` (com `changes`, segredos mascarados), `app_enabled`.\n","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"},{"in":"path","name":"slug","required":true,"schema":{"type":"string","enum":["discord","slack","telegram","rdstation","hubspot"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"config":{"type":"object","additionalProperties":{"type":"string"}},"events":{"type":"array","minItems":1,"items":{"type":"string"}}}},"examples":{"discord":{"value":{"config":{"webhookUrl":"https://discord.com/api/webhooks/123456789012345678/AbCdEf"},"events":["member.joined","purchase.completed"]}},"slack":{"value":{"config":{"webhookUrl":"https://hooks.slack.com/services/T000/B000/XXXX"}}},"telegram":{"value":{"config":{"botToken":"123456789:AAH-exemplo-do-botfather-0123456789ab","chatId":"-1001234567890"}}},"rdstation":{"value":{"config":{"apiKey":"chave-de-api-do-rd-station-0123456789"}}},"hubspot":{"value":{"config":{"accessToken":"pat-na1-00000000-0000-0000-0000-000000000000"}}}}}}},"responses":{"200":{"description":"Salvo (e religado, se estava desligado).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"installation":{"$ref":"#/components/schemas/AppInstallation"}}}}}}}},"201":{"description":"Ligado pela primeira vez."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"App inexistente."},"422":{"description":"Campo inválido em `config.<campo>` (host não permitido, token em formato errado, endereço privado) ou `events` fora dos eventos do app; atalho não se instala."}}},"delete":{"tags":["Apps"],"summary":"Desliga ou remove o app","operationId":"disableAppInstallation","description":"Sem `remove`: desliga. As tentativas pendentes são canceladas na hora; a instalação e a configuração ficam guardadas e `PUT` religa.\n\nCom `remove=true`: remove a integração. A configuração selada (URL, token, chave) é apagada, o destino é desligado, as tentativas pendentes param e a configuração guardada em cada entrega é zerada: nenhuma entrega daquela instalação pode ser reenviada, e elas saem do histórico de entregas do app. O histórico de alterações registra `app_removed` com `changes.deliveriesCleared`. Depois disso, `GET` responde 404 e `PUT` liga do zero, pedindo os campos obrigatórios de novo.\n","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"},{"in":"path","name":"slug","required":true,"schema":{"type":"string"}},{"in":"query","name":"remove","required":false,"schema":{"type":"string","enum":["true","false"]},"description":"`true` remove a integração e apaga a configuração."}],"responses":{"200":{"description":"Desligado (`data.installation` com `status: DISABLED`) ou removido (`data.installation: null`).","content":{"application/json":{"examples":{"desligado":{"value":{"success":true,"message":"Integração desligada","data":{"installation":{"app":"telegram","status":"DISABLED"}}}},"removido":{"value":{"success":true,"message":"Integração removida","data":{"installation":null}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"App inexistente ou não instalado."}}}},"/api/apps/{slug}/installation/test":{"post":{"tags":["Apps"],"summary":"Enviar teste","operationId":"testAppInstallation","description":"Manda agora a mensagem de teste pelo app (Discord, Slack, Telegram: uma mensagem no canal; HubSpot: confere o\ntoken lendo um contato; RD Station: confere a chave sem criar lead). Uma tentativa, sem retentativa, não conta\nno uso. Responde `200` mesmo quando o provedor recusa: `ok: false` e `error` traduzido\n(token inválido, canal ou grupo não encontrado, limite do provedor). Entra no limite de 10 por minuto.\n","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"},{"in":"path","name":"slug","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"O resultado do teste.","content":{"application/json":{"example":{"success":true,"message":"O provedor recusou o teste","data":{"ok":false,"error":"O provedor recusou as credenciais. Confira o token ou a chave e salve de novo.","delivery":{"eventType":"ping","status":"FAILED","attemptCount":1,"lastAttempt":{"httpStatus":401}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"App inexistente ou não instalado."},"409":{"description":"App desligado."},"429":{"description":"Mais de 10 ações que enviam requisição para fora no minuto, com `Retry-After`."}}}},"/api/apps/telegram/chats":{"post":{"tags":["Apps"],"summary":"Encontrar meu grupo (Telegram)","operationId":"findTelegramChats","description":"Lista os grupos e canais em que o bot recebeu mensagem ou foi adicionado recentemente (`getUpdates` do Telegram),\npara escolher o `chatId`. Use o `botToken` digitado na tela ou, sem ele, o já salvo. Adicione o bot ao grupo e mande\numa mensagem lá antes. Entra no limite de 10 por minuto.\n","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"}],"requestBody":{"content":{"application/json":{"example":{"botToken":"123456789:AAH-exemplo-do-botfather-0123456789ab"}}}},"responses":{"200":{"description":"Os grupos encontrados (pode vir vazio).","content":{"application/json":{"example":{"success":true,"message":"Grupos encontrados","data":{"chats":[{"id":"-1001234567890","title":"Comunidade APICON","type":"supergroup"}]}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"O bot tem um webhook configurado em outro lugar, e o Telegram não entrega as atualizações."},"422":{"description":"Token em formato errado ou recusado pelo Telegram (campo `botToken`)."},"429":{"description":"Limite de 10 por minuto."},"502":{"description":"Telegram fora do ar."}}}},"/api/apps/{slug}/installation/deliveries":{"get":{"tags":["Apps"],"summary":"Histórico de entregas do app","operationId":"listAppDeliveries","description":"O mesmo histórico dos webhooks (30 dias), com filtro por status e tipo de evento.","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"},{"in":"path","name":"slug","required":true,"schema":{"type":"string"}},{"in":"query","name":"status","schema":{"type":"string","enum":["PENDING","SUCCEEDED","FAILED","CANCELED"]}},{"in":"query","name":"eventType","schema":{"type":"string"}},{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Página de entregas, mais recente primeiro."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"App inexistente ou não instalado."}}}},"/api/apps/{slug}/installation/deliveries/{deliveryId}":{"get":{"tags":["Apps"],"summary":"Detalhe de uma entrega do app","operationId":"getAppDelivery","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"},{"in":"path","name":"slug","required":true,"schema":{"type":"string"}},{"in":"path","name":"deliveryId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"A entrega, com o evento e as tentativas."},"404":{"description":"Entrega inexistente."}}}},"/api/apps/{slug}/installation/deliveries/{deliveryId}/resend":{"post":{"tags":["Apps"],"summary":"Reenviar uma entrega do app","operationId":"resendAppDelivery","description":"Novo ciclo de tentativas com a configuração com que a entrega foi criada; a primeira roda agora. Entra no limite de 10 por minuto.","parameters":[{"$ref":"#/components/parameters/CommunityIdHeader"},{"in":"path","name":"slug","required":true,"schema":{"type":"string"}},{"in":"path","name":"deliveryId","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"A entrega depois da primeira tentativa."},"404":{"description":"Entrega inexistente."},"409":{"description":"App desligado ou entrega ainda em andamento."},"429":{"description":"Limite de 10 por minuto."}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"parameters":{"CommunityIdHeader":{"in":"header","name":"X-CommunityId","required":true,"schema":{"type":"string","format":"uuid"},"description":"Comunidade da sessão; conferida em `GET /api/integrations/session` da memberfy-api."}},"schemas":{"Webhook":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"url":{"type":"string","description":"URL completa (só no detalhe, na criação e na edição)."},"urlMasked":{"type":"string","example":"https://hooks.memberfy.net/…c123"},"description":{"type":"string","nullable":true},"events":{"type":"array","items":{"type":"string"},"example":["member.joined","purchase.*"]},"status":{"type":"string","enum":["ACTIVE","DISABLED","DISABLED_FAILING"]},"fixedIps":{"type":"boolean"},"fixedIpAddresses":{"type":"array","items":{"type":"string"},"description":"IPs publicados, quando `fixedIps` está ligado."},"lastSuccessAt":{"type":"string","format":"date-time","nullable":true},"failureCount":{"type":"integer"},"createdById":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"lastDelivery":{"$ref":"#/components/schemas/Delivery"}}},"Delivery":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"webhookId":{"type":"string","format":"uuid"},"eventId":{"type":"string","example":"evt_01JAB3XK9Q2W7E5R4T6Y8U0I1O","description":"É o `webhook-id`: igual em toda tentativa e no reenvio."},"eventType":{"type":"string","example":"member.joined"},"status":{"type":"string","enum":["PENDING","SUCCEEDED","FAILED","CANCELED"]},"attemptCount":{"type":"integer"},"lastAttempt":{"type":"object","nullable":true,"properties":{"at":{"type":"string","format":"date-time"},"httpStatus":{"type":"integer","nullable":true},"durationMs":{"type":"integer"},"error":{"type":"string","nullable":true,"description":"timeout, network_error, blocked_address, proxy_quota…"}}},"nextAttemptAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"body":{"type":"object","description":"Só no detalhe: o corpo enviado."},"attempts":{"type":"array","description":"Só no detalhe: cada tentativa, com até 1 KB da resposta.","items":{"type":"object","properties":{"n":{"type":"integer"},"at":{"type":"string","format":"date-time"},"durationMs":{"type":"integer"},"httpStatus":{"type":"integer","nullable":true},"error":{"type":"string","nullable":true},"responseSnippet":{"type":"string","nullable":true},"counted":{"type":"boolean","description":"Conta para o desligamento automático (429 e cota do proxy não contam)."}}}}}},"Error":{"type":"object","properties":{"success":{"type":"boolean","example":false},"message":{"type":"string"},"errors":{"type":"array","items":{"type":"object","properties":{"param":{"type":"string"},"message":{"type":"string"}}}}}},"AppInstallation":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"app":{"type":"string","example":"discord"},"status":{"type":"string","enum":["ACTIVE","DISABLED","DISABLED_FAILING"],"description":"`DISABLED` = desligado por alguém; `DISABLED_FAILING` = desligado por falhas (72 h sem sucesso ou 410)."},"events":{"type":"array","items":{"type":"string"},"example":["member.joined","purchase.completed"]},"config":{"type":"object","description":"A configuração com os campos secretos mascarados (URL com o meio escondido, token com os 4 últimos caracteres). Nunca volta inteira.","example":{"webhookUrl":"https://discord.com/…AbCd"}},"lastSuccessAt":{"type":"string","format":"date-time","nullable":true},"failureCount":{"type":"integer"},"needsAttention":{"type":"object","nullable":true,"description":"Algo para a comunidade resolver, sem desligar o app. Hoje só `channel_gone` (o canal do Slack de uma mensagem de workflow sumiu). Some na próxima entrega com sucesso ou ao salvar a integração.","properties":{"reason":{"type":"string","enum":["channel_gone"]},"since":{"type":"string","format":"date-time"}},"example":{"reason":"channel_gone","since":"2026-10-25T12:00:00.000Z"}},"installedById":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"lastDelivery":{"$ref":"#/components/schemas/Delivery"}}}},"responses":{"Unauthorized":{"description":"Sem sessão, sessão inválida, `X-CommunityId` de outra comunidade ou chamada com `X-Api-Key`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"Papel sem acesso (só owner e admin).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Webhook (ou entrega) inexistente ou de outra comunidade.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"tags":[]}