{
  "info": {
    "name": "CRPRO Hub API",
    "description": "API do CRPRO Hub para a API oficial do WhatsApp (Cloud API). Preencha apiKey com uma chave hub_pk_ criada em /painel/chaves e channelId com o id do canal. Referência completa: https://crprohub.com/docs/referencia/canais",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{apiKey}}",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://crprohub.com"
    },
    {
      "key": "apiKey",
      "value": "hub_pk_COLE_AQUI"
    },
    {
      "key": "channelId",
      "value": ""
    },
    {
      "key": "webhookId",
      "value": ""
    },
    {
      "key": "mediaId",
      "value": ""
    },
    {
      "key": "templateId",
      "value": ""
    },
    {
      "key": "deliveryId",
      "value": ""
    }
  ],
  "item": [
    {
      "name": "Canais",
      "item": [
        {
          "name": "Lista os canais da organização",
          "request": {
            "method": "GET",
            "description": "Devolve os canais ativos da organização, mais recentes primeiro. Canais arquivados não aparecem aqui nem em nenhuma outra rota de canal — depois do DELETE, o id passa a responder 404 em todo o resto da API.\n\nEscopo: channels:read.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/channels",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "channels"
              ]
            }
          }
        },
        {
          "name": "Cria um canal e emite o token do canal uma única vez",
          "request": {
            "method": "POST",
            "description": "O canal nasce em draft, sem WABA nem número — a conexão com a Meta acontece depois, pelo link de /connect-link. O channel_token (hub_ch_...) vem só nesta resposta: não é reemitido, e perdê-lo obriga a rotacionar com /regenerate-token. Responde 402 se a assinatura não permite criar canais e 403 se o limite de canais do plano já foi atingido.\n\nEscopo: channels:write.",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/channels",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "channels"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Atendimento\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Consulta um canal",
          "request": {
            "method": "GET",
            "description": "Um id de outro canal, de outra organização ou de um canal já arquivado responde 404 — não 403.\n\nEscopo: channels:read.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/channels/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "channels",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{channelId}}"
                }
              ]
            }
          }
        },
        {
          "name": "Atualiza o nome e as configurações do canal",
          "request": {
            "method": "PATCH",
            "description": "É preciso enviar name ou external_id — corpo vazio ({}) responde 400. Campos omitidos ficam como estavam; enviar external_id: null remove o valor atual.\n\nEscopo: channels:write.",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/channels/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "channels",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{channelId}}"
                }
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Atendimento — Loja 2\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Arquiva o canal e, opcionalmente, tira o número da Meta",
          "request": {
            "method": "DELETE",
            "description": "É um soft delete: o registro continua no banco, mas some de toda a API (GET, mensagens, mídia, templates passam a responder 404 para este id). Não há endpoint para desarquivar — é preciso criar um canal novo. A vaga do plano é liberada na hora. Em toda exclusão o Hub também revoga o token do canal, invalida links de conexão pendentes, desvincula o canal dos endpoints de webhook e apaga o token da Meta guardado para ele — isso acontece mesmo se o deregister falhar. Com deregister=true, antes de arquivar o Hub ainda chama o deregister do número na Cloud API: a chamada age só naquele número e não mexe no app da WABA, então os outros números da mesma conta continuam funcionando. Depois do deregister o cliente precisa registrar o número de novo, com o PIN da verificação em duas etapas. Se a Meta recusar (token expirado, número já desregistrado), a exclusão acontece do mesmo jeito e a falha fica gravada no log de auditoria como channel.deregister_failed. O mesmo vale quando a credencial da Meta do canal nao pode ser lida (deregistro: sem-credencial) — o número segue registrado na Meta e precisa ser resolvido no Business Manager.\n\nEscopo: channels:write.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/channels/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "channels",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{channelId}}"
                }
              ]
            }
          }
        },
        {
          "name": "Gera um link de conexão para o cliente final",
          "request": {
            "method": "POST",
            "description": "O link expira em 7 dias e serve para o cliente final autorizar o número dele direto na Meta, sem precisar de credencial do painel. Cada chamada gera um connect_token novo e invalida qualquer link anterior ainda não usado — não há como recuperar um link já gerado, só criar outro.\n\nEscopo: channels:write.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/channels/:id/connect-link",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "channels",
                ":id",
                "connect-link"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{channelId}}"
                }
              ]
            }
          }
        },
        {
          "name": "Valida token, número e assinatura de webhooks sem expor segredos",
          "request": {
            "method": "GET",
            "description": "Faz três checagens ao vivo na Meta (validade do token, número acessível, app inscrito na WABA) e nunca devolve o token em si, só checks booleanos e recommendations. Exige que o canal já esteja conectado (com waba_id e phone_number_id); senão responde 409 CHANNEL_NOT_CONNECTED. O token hub_ch_... do próprio canal também autentica esta chamada, mas nunca autoriza consultar outro canal.\n\nEscopo: channels:read.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/channels/:id/diagnostics",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "channels",
                ":id",
                "diagnostics"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{channelId}}"
                }
              ]
            }
          }
        },
        {
          "name": "Tira o número da plataforma da Meta sem excluir o canal",
          "request": {
            "method": "POST",
            "description": "Chama o deregister do número na Meta e rebaixa o canal para connected — vinculado, mas sem poder enviar. Diferente de DELETE ?deregister=true, o canal NÃO é arquivado: o token continua válido, os links seguem de pé e a vaga do plano não é liberada. Serve para tirar um número da API Local (On-Premises) e reconectá-lo em seguida com o PIN da verificação em duas etapas. Enquanto o número não for registrado de novo, os envios respondem 409 CHANNEL_NOT_CONNECTED — isso é esperado, porque o número realmente não envia. O status HTTP desta rota é sempre 200, para os quatro valores possíveis do campo deregistro — 200 NÃO significa que o número foi liberado da Meta; quem chama precisa olhar o campo deregistro, nunca só o status, para saber o que de fato aconteceu. Os quatro valores: \"feito\" — a Meta aceitou o deregister e o canal foi rebaixado para connected; \"falhou\" — a Meta recusou (token expirado, número já desregistrado, cota estourada etc.), nada mudou no canal, e o motivo da recusa vem no campo motivo e no log de auditoria como channel.deregister_failed; \"sem-numero\" — o canal não tem phone_number_id vinculado, não havia o que desregistrar; \"sem-credencial\" — a credencial da Meta deste canal não pôde ser lida, o número segue registrado e precisa ser resolvido no Business Manager. Depois de um \"falhou\", não fique retentando sem investigar o motivo: a Meta limita o register a 10 chamadas por número em janela móvel de 72h, e o deregister a outras 10, contadas à parte (erro #133016); cada tentativa — inclusive as que falham — consome uma dessas chamadas.\n\nEscopo: channels:write.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/channels/:id/deregister",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "channels",
                ":id",
                "deregister"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{channelId}}"
                }
              ]
            }
          }
        },
        {
          "name": "Conclui o registro do número na Cloud API com o PIN",
          "request": {
            "method": "POST",
            "description": "Para canais no status connected — número vinculado, recebendo mensagens, mas sem poder enviar porque o registro na Cloud API ficou pendente. O Hub chama o /register da Meta com a credencial que ele guarda para o canal e, se der certo, promove o canal para active e emite channel.connected, exatamente como o fluxo do link de conexão. É idempotente: se o canal já está active, ou se a Meta já reporta o número como registrado na Cloud API, a resposta é 200 com already_registered: true, sem nova chamada de register. Qualquer outro status responde 409 NOT_PENDING_REGISTRATION. O PIN precisa ser uma string de exatamente 6 dígitos (senão 422 INVALID_PIN_FORMAT, sem ir à Meta). Ele não é gravado nem aparece em log, auditoria ou resposta. Erros com código próprio: 400 INVALID_PIN (o PIN não confere; details.attempts_remaining), 400 PIN_REQUIRED_RECOVERY (terceiro PIN recusado na janela de 1 hora — a verificação em duas etapas já está ativa com outro PIN, e só a redefinição no WhatsApp Manager ou o link de conexão resolvem), 429 PIN_LOCKED (details.locked_by = hub quando o limite deste endpoint estourou, com retry_after_seconds e header Retry-After; = meta quando a Meta bloqueou, com retry_after_seconds null — a Meta informa o prazo no detalhe do erro, que o Hub ainda não repassa — e retry_window_hours: 72 no erro #133016), 502 META_AUTH_FAILED (credencial da Meta inválida, expirada ou ausente), 502/504 META_UNREACHABLE (Meta instável ou sem resposta) e 422 REGISTRATION_REJECTED (outra recusa da Meta; details.meta_code traz o código, e details.reason = on_premises quando o número está na API Local). Este endpoint é o atalho para quem sabe o PIN. Quem esqueceu o PIN continua precisando do link de conexão ou da recuperação da Meta. A Meta limita register e deregister a 10 chamadas por número em 72h.\n\nEscopo: channels:write.",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/channels/:id/register",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "channels",
                ":id",
                "register"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{channelId}}"
                }
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"pin\": \"000000\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Conecta o canal com as credenciais do app da Meta do próprio cliente",
          "request": {
            "method": "POST",
            "description": "Alternativa ao link de conexão para quem já tem um app na Meta, um usuário do sistema com token permanente e o número nesse app. Antes de gravar, o Hub confere o App Secret, confere o token (válido, do mesmo app, com whatsapp_business_management e whatsapp_business_messaging sobre a WABA), confere que o número pertence à WABA e assina o app nos webhooks da WABA. Em seguida aponta os webhooks do número para a URL do app no Hub (webhook.callback_url). Se esse passo falhar, a conexão continua e webhook_override vem false: nesse caso, configure a URL e o verify token devolvidos como URL padrão do app, em WhatsApp > Configuração. Mesmo com o override, eventos de conta (status de template, qualidade do número, account_update) só chegam pela URL padrão do app. Número já registrado na Cloud API fica active. Número ainda não registrado fica connected (registration_pending: true); com pin no corpo, o Hub registra na sequência pelo mesmo fluxo de /register, e uma recusa do PIN não desfaz a conexão (o erro vem em registro). Serve também para trocar um token vencido ou migrar um canal do Embedded Signup para o app próprio. App Secret, token e PIN não são devolvidos nem registrados em log ou auditoria. Erros com código próprio: 400 INVALID_BODY (details.campos lista os campos fora do formato), 422 INVALID_PIN_FORMAT, 409 CHANNEL_SUSPENDED, 422 META_APP_SECRET_INVALID, 422 META_TOKEN_INVALID, 422 META_TOKEN_APP_MISMATCH, 422 META_TOKEN_SCOPE_MISSING, 422 PHONE_NUMBER_NOT_IN_WABA, 502 META_SUBSCRIPTION_FAILED, 502 META_UNREACHABLE e 409 PHONE_ALREADY_CONNECTED.\n\nEscopo: channels:write.",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/channels/:id/credentials",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "channels",
                ":id",
                "credentials"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{channelId}}"
                }
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"phone_number_id\": \"123456789012345\",\n  \"waba_id\": \"234567890123456\",\n  \"app_id\": \"345678901234567\",\n  \"app_secret\": \"0123456789abcdef0123456789abcdef\",\n  \"access_token\": \"EAAEXEMPLONAOREAL\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Rotaciona o token do canal; o novo valor aparece uma única vez",
          "request": {
            "method": "POST",
            "description": "O token anterior é revogado imediatamente: qualquer integração ainda usando o hub_ch_... antigo passa a responder 401 no mesmo instante. O novo valor só aparece nesta resposta — se você perdê-lo, o único jeito de recuperar é rotacionar de novo.\n\nEscopo: channels:write.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/channels/:id/regenerate-token",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "channels",
                ":id",
                "regenerate-token"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{channelId}}"
                }
              ]
            }
          }
        }
      ]
    },
    {
      "name": "Mensagens",
      "item": [
        {
          "name": "Lista o histórico de mensagens do canal, com paginação por cursor",
          "request": {
            "method": "GET",
            "description": "A paginação é por cursor opaco, não por offset: passe o next_cursor da página anterior em cursor para avançar. next_cursor vem null quando não há mais páginas. Diferente do POST desta mesma rota, o token de canal (hub_ch_...) não funciona aqui — só sessão ou chave de API.\n\nEscopo: logs:read.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/channels/:id/messages",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "channels",
                ":id",
                "messages"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{channelId}}"
                }
              ]
            }
          }
        },
        {
          "name": "Envia uma mensagem pelo canal",
          "request": {
            "method": "POST",
            "description": "A resposta é 202: a mensagem foi aceita e enfileirada para a Meta, não entregue. O estado final chega pelo webhook e pelo histórico do canal. O header Idempotency-Key é obrigatório — reenviar a mesma chave com o mesmo corpo devolve a resposta original em vez de enviar de novo; reenviar a mesma chave com um corpo diferente responde 409 IDEMPOTENCY_KEY_REUSED. O token hub_ch_... do canal também autentica esta chamada, mas nunca autoriza enviar por outro canal.\n\nEscopo: messages:send.",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "Obrigatória. Use o mesmo valor ao repetir o MESMO envio; um valor novo manda de novo."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/channels/:id/messages",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "channels",
                ":id",
                "messages"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{channelId}}"
                }
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"to\": \"5521999999999\",\n  \"type\": \"text\",\n  \"text\": {\n    \"body\": \"Olá\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        }
      ]
    },
    {
      "name": "Mídia",
      "item": [
        {
          "name": "Envia um arquivo para a Meta e devolve o media ID",
          "request": {
            "method": "POST",
            "description": "O corpo é multipart/form-data, não JSON. O media_id devolvido é o mesmo que a Meta espera em mensagens do tipo image, audio, video, document e sticker — mas some de lá em poucos dias, então não vale guardar para uso posterior. O token hub_ch_... do canal também autentica esta chamada, mas nunca autoriza enviar por outro canal. Para arquivos grandes, prefira enviar uma URL https no payload da mensagem (ver POST .../messages): o upload multipart/form-data desta rota passa pelo host do CRPRO Hub e pode ter um limite de tamanho de requisição menor do que o aceito diretamente pela Meta.\n\nEscopo: messages:send.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/channels/:id/media",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "channels",
                ":id",
                "media"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{channelId}}"
                }
              ]
            }
          }
        },
        {
          "name": "Baixa o arquivo bruto de uma mídia",
          "request": {
            "method": "GET",
            "description": "Esta rota não devolve JSON: ela baixa o arquivo da Meta e repassa o conteúdo binário direto na resposta, com o Content-Type real da mídia. Não há envelope { \"data\": ... } aqui — é o arquivo puro. O token hub_ch_... do canal também autentica esta chamada, mas nunca autoriza ler mídia de outro canal.\n\nEscopo: channels:read.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/channels/:id/media/:mediaId",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "channels",
                ":id",
                "media",
                ":mediaId"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{channelId}}"
                },
                {
                  "key": "mediaId",
                  "value": "{{mediaId}}"
                }
              ]
            }
          }
        },
        {
          "name": "Remove uma mídia da Meta",
          "request": {
            "method": "DELETE",
            "description": "Remove o arquivo dos servidores da Meta — não é reversível, e mensagens antigas que referenciam esse media ID deixam de conseguir baixá-lo. O token hub_ch_... do canal também autentica esta chamada, mas nunca autoriza remover mídia de outro canal.\n\nEscopo: messages:send.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/channels/:id/media/:mediaId",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "channels",
                ":id",
                "media",
                ":mediaId"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{channelId}}"
                },
                {
                  "key": "mediaId",
                  "value": "{{mediaId}}"
                }
              ]
            }
          }
        }
      ]
    },
    {
      "name": "Templates",
      "item": [
        {
          "name": "Lista os templates do canal",
          "request": {
            "method": "GET",
            "description": "Repassa a lista da Meta (message_templates) com cache de até 60 segundos por WABA; criar ou excluir um template limpa o cache na hora, então a sua própria alteração aparece imediatamente. Exige que o canal tenha WABA conectada; senão responde 409 CHANNEL_NOT_CONNECTED. O token hub_ch_... do canal também autentica esta chamada, mas nunca autoriza ler templates de outro canal.\n\nEscopo: templates:read.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/channels/:id/templates",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "channels",
                ":id",
                "templates"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{channelId}}"
                }
              ]
            }
          }
        },
        {
          "name": "Cria um template na Meta",
          "request": {
            "method": "POST",
            "description": "Cria o template direto na Meta e espelha uma cópia local (por nome + idioma); o status inicial normalmente é PENDING — o template não pode ser usado em mensagens até a Meta aprovar. O corpo inteiro não pode passar de 64 KB, senão responde 400 INVALID_TEMPLATE. O token hub_ch_... do canal também autentica esta chamada, mas nunca autoriza criar template em outro canal.\n\nEscopo: templates:write.",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/channels/:id/templates",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "channels",
                ":id",
                "templates"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{channelId}}"
                }
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"boas_vindas\",\n  \"language\": \"pt_BR\",\n  \"category\": \"UTILITY\",\n  \"components\": [\n    {\n      \"type\": \"BODY\",\n      \"text\": \"Olá {{1}}, bem-vindo!\"\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Remove um template",
          "request": {
            "method": "DELETE",
            "description": "Só remove templates que foram criados por esta API (o POST /templates é o único que grava a cópia local): se não houver uma cópia local com esse templateId para este canal, responde 404 mesmo que o template exista na Meta. A remoção é direto na Meta e não é reversível. O token hub_ch_... do canal também autentica esta chamada, mas nunca autoriza remover template de outro canal.\n\nEscopo: templates:write.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/channels/:id/templates/:templateId",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "channels",
                ":id",
                "templates",
                ":templateId"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{channelId}}"
                },
                {
                  "key": "templateId",
                  "value": "{{templateId}}"
                }
              ]
            }
          }
        }
      ]
    },
    {
      "name": "Webhooks",
      "item": [
        {
          "name": "Lista os endpoints de webhook",
          "request": {
            "method": "GET",
            "description": "Devolve todos os endpoints cadastrados na organização, sem paginação. Nunca inclui o segredo HMAC — só a URL, os eventos assinados, o modo de payload, o formato de entrega (delivery_format) e o estado.\n\nEscopo: webhooks:read.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/webhooks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks"
              ]
            }
          }
        },
        {
          "name": "Cria um endpoint; o segredo HMAC aparece uma única vez",
          "request": {
            "method": "POST",
            "description": "A url precisa ser https e resolver para um endereço público — IPs privados, loopback e link-local respondem 400 WEBHOOK_URL_NOT_ALLOWED. O secret (whsec_...) usado para validar a assinatura HMAC das entregas vem só nesta resposta: não é reemitido, e perdê-lo obriga a rotacionar com /rotate-secret. all_channels: true exige channel_ids vazio; all_channels: false exige channel_ids não vazio — a combinação errada responde 400 INVALID_WEBHOOK. Se você está migrando do EvoHub e cria canal e webhook em chamadas separadas, mande delivery_format: \"evohub\" aqui: sem isso o endpoint nasce native e a verificação de assinatura falha em toda entrega.\n\nEscopo: webhooks:write.",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/webhooks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Endpoint principal\",\n  \"url\": \"https://exemplo.com/webhooks/crprohub\",\n  \"event_types\": [\n    \"message.received\",\n    \"message.status\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Consulta um endpoint",
          "request": {
            "method": "GET",
            "description": "Um id de outro endpoint ou de outra organização responde 404. Não devolve o segredo HMAC.\n\nEscopo: webhooks:read.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/webhooks/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{webhookId}}"
                }
              ]
            }
          }
        },
        {
          "name": "Atualiza URL, eventos ou canais do endpoint",
          "request": {
            "method": "PATCH",
            "description": "Corpo vazio ({}) responde 400 pelo .refine do schema — envie ao menos um campo. Campos omitidos ficam como estavam. Vale a mesma regra de all_channels/channel_ids do POST: a combinação inconsistente responde 400 INVALID_WEBHOOK. status também pode ser alterado aqui, para pausar (paused) ou desabilitar (disabled) o endpoint sem removê-lo.\n\nEscopo: webhooks:write.",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/webhooks/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{webhookId}}"
                }
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"status\": \"paused\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Remove o endpoint",
          "request": {
            "method": "DELETE",
            "description": "Remoção definitiva, não um soft delete: o registro some do banco. Não há como recuperar — crie um novo endpoint se precisar.\n\nEscopo: webhooks:write.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/webhooks/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{webhookId}}"
                }
              ]
            }
          }
        },
        {
          "name": "Lista tentativas de entrega e seus estados",
          "request": {
            "method": "GET",
            "description": "Traz até 50 entregas mais recentes deste endpoint, mais recente primeiro; a rota não aceita parâmetros de paginação. status é pending, processing, retry, succeeded, dead ou canceled. Só replay (succeeded, dead ou canceled) pode ser reenviada por /replay/{deliveryId}.\n\nEscopo: webhooks:read.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/webhooks/:id/deliveries",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                ":id",
                "deliveries"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{webhookId}}"
                }
              ]
            }
          }
        },
        {
          "name": "Reenvia uma entrega específica",
          "request": {
            "method": "POST",
            "description": "Só reenvia entregas já finalizadas (succeeded, dead ou canceled) de um endpoint ativo; uma entrega ainda pending, processing ou retry, ou um endpoint pausado, responde 404. O reenvio cria uma entrega nova, com delivery id novo e o mesmo corpo (o id do evento se mantém) — a resposta é 202, o resultado real chega depois em deliveries.\n\nEscopo: webhooks:write.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/webhooks/:id/replay/:deliveryId",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                ":id",
                "replay",
                ":deliveryId"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{webhookId}}"
                },
                {
                  "key": "deliveryId",
                  "value": "{{deliveryId}}"
                }
              ]
            }
          }
        },
        {
          "name": "Rotaciona o segredo HMAC; o novo valor aparece uma única vez",
          "request": {
            "method": "POST",
            "description": "O segredo anterior é invalidado imediatamente: assinaturas calculadas com ele deixam de bater a partir desta chamada. O novo secret (whsec_...) só aparece nesta resposta — não é reemitido, e se você perdê-lo o único jeito de recuperar é rotacionar de novo.\n\nEscopo: webhooks:write.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/webhooks/:id/rotate-secret",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                ":id",
                "rotate-secret"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{webhookId}}"
                }
              ]
            }
          }
        },
        {
          "name": "Enfileira um evento de teste para o endpoint",
          "request": {
            "method": "POST",
            "description": "Cria um evento sintético webhook.test e o enfileira como qualquer outra entrega, respeitando payload_mode do endpoint. Só funciona em endpoints com status active — endpoint pausado ou desabilitado responde 404. A resposta 202 confirma o enfileiramento, não a entrega; o resultado aparece depois em deliveries.\n\nEscopo: webhooks:write.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/webhooks/:id/test",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "webhooks",
                ":id",
                "test"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{webhookId}}"
                }
              ]
            }
          }
        }
      ]
    },
    {
      "name": "Assinatura",
      "item": [
        {
          "name": "Consulta plano, status, quotas e uso",
          "request": {
            "method": "GET",
            "description": "Responde 404 se a organização nunca teve entitlements provisionados (não deveria acontecer em uso normal). portal_available diz se já existe cliente de cobrança criado — quando true, POST /api/v1/subscription/portal abre o autoatendimento.\n\nEscopo: billing:read.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/subscription",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "subscription"
              ]
            }
          }
        }
      ]
    },
    {
      "name": "Uso",
      "item": [
        {
          "name": "Consulta o uso atual das quotas",
          "request": {
            "method": "GET",
            "description": "É um recorte de GET /subscription: só os campos usage e plan, sem os dados de cobrança. Útil para checagens de quota que não precisam do resto da assinatura.\n\nEscopo: billing:read.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/usage",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "usage"
              ]
            }
          }
        }
      ]
    },
    {
      "name": "Logs",
      "item": [
        {
          "name": "Lista os logs de auditoria da organização, com paginação por cursor",
          "request": {
            "method": "GET",
            "description": "A paginação é por cursor opaco, não por offset: passe o next_cursor da página anterior em cursor para avançar. Um cursor inválido responde 400 INVALID_CURSOR. Ações do tipo api_key.* (ex.: api_key.created) têm o metadata zerado quando o chamador é uma chave de API, mesmo com escopo logs:read — só uma sessão vê o name e os scopes gravados nesses eventos, para que uma chave vazada não consiga ler por aqui o que a guarda de sessão de /api-keys já bloqueia.\n\nEscopo: logs:read.",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/logs",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "logs"
              ]
            }
          }
        }
      ]
    }
  ]
}