# Respostas e erros

> Entenda os dois formatos de resposta de sucesso da API e aprenda a tratar cada erro (400, 401, 404, 429) lendo as mensagens em português do backend.

- URL canônica: https://docs.growthstation.app/docs/api/conceitos/respostas-e-erros
- Idioma: pt-BR
- Última atualização: 2026-09-20T08:21:20.450Z
- Produto: GS Engage
- Mantido por: Produto GS Engage



<PageHero emoji="📬" title="Respostas e erros" description="Como a API do GS Engage responde quando dá certo — e o que fazer, passo a passo, quando dá errado." gradient="brand" />

Toda vez que você chama a API, ela devolve duas coisas: um **código de status** (um número que resume o que aconteceu) e um **corpo** (os dados ou a explicação do problema). Saber ler esses dois é o que separa uma integração que "às vezes falha misteriosamente" de uma que se recupera sozinha.

Esta página responde três perguntas práticas: como vem uma resposta de sucesso, como vem um erro, e o que fazer diante de cada código. As mensagens de erro do GS Engage já chegam **em português** — a ideia é você reaproveitá-las direto para quem for corrigir.

<Callout type="tip" title="A regra de ouro para tratar qualquer erro">
  Sempre siga o mesmo raciocínio: **o que houve → por quê → como corrigir**. O código de status responde "o que houve", o corpo (`error.message` e, no 400, `error.errors`) responde "por quê", e este guia te dá o "como corrigir".
</Callout>

📦 Dois formatos de sucesso [#-dois-formatos-de-sucesso]

Aqui está a pegadinha que mais confunde quem começa: **nem toda listagem responde no mesmo formato**. Existem dois, e você precisa saber qual esperar de cada endpoint, senão seu código procura `data` onde só tem uma lista solta.

Formato 1 — paginado: { data, meta } [#formato-1--paginado--data-meta-]

É o formato da **maioria** das listagens. Os itens vêm dentro de `data`, e um bloco `meta` te conta quantos existem no total e quantas páginas há. Você usa isso para saber quando parar de pedir.

```json
{
  "data": [
    { "id": "507f1f77bcf86cd799439011", "name": "Maria Silva" },
    { "id": "507f1f77bcf86cd799439012", "name": "João Pereira" }
  ],
  "meta": {
    "limit": 20,
    "count": 20,
    "total": 137,
    "page": 1,
    "totalPages": 7
  }
}
```

O que isso significa para você: há **137 leads** no total (`total`), distribuídos em **7 páginas** (`totalPages`). Você está na página 1 e recebeu 20 itens. Peça a página 2 com `?page=2` e siga até chegar em `totalPages`.

Respondem neste formato paginado, por exemplo: `GET /api/v1/leads`, `GET /api/v1/routines`, `GET /api/v1/prospections`, `GET /api/v1/conversations/threads` e as demais listagens.

Formato 2 — data sem meta [#formato-2--data-sem-meta]

**Um** endpoint foge à paginação: `GET /api/v1/webhooks` devolve a lista em `data`, mas **sem** o bloco `meta` (retorna todos os webhooks do projeto de uma vez, sem paginar).

```json
{
  "data": [
    { "id": "507f1f77bcf86cd799439021", "name": "Webhook CRM", "url": "https://..." }
  ]
}
```

<Callout type="warning" title="custom-fields NÃO é exceção">
  `GET /api/v1/custom-fields` é paginado normal (`{ data, meta }`), como a maioria. O único que vem **sem `meta`** é o `GET /api/v1/webhooks`. Em todos os casos os itens ficam em `data` — **nunca** numa lista solta na raiz. Se o seu código lê a resposta como um array direto, ele quebra.
</Callout>

<Tabs items={['JavaScript', 'Python']}>
  <Tab value="JavaScript">
    ```js
    // Paginado: itens em .data, com .meta
    const leadsRes = await fetch(`${BASE}/leads?apiKey=${apiKey}`);
    const { data: leads, meta } = await leadsRes.json();
    console.log(`${leads.length} de ${meta.total} leads`);

    // Webhooks: itens em .data, mas SEM .meta (sem paginação)
    const whRes = await fetch(`${BASE}/webhooks?apiKey=${apiKey}`);
    const { data: webhooks } = await whRes.json();
    console.log(`${webhooks.length} webhooks`);
    ```
  </Tab>

  <Tab value="Python">
    ```python
    # Paginado: itens em ["data"], com ["meta"]
    leads_res = requests.get(f"{BASE}/leads", params={"apiKey": api_key})
    body = leads_res.json()
    leads, meta = body["data"], body["meta"]
    print(f"{len(leads)} de {meta['total']} leads")

    # Webhooks: itens em ["data"], mas SEM ["meta"]
    wh_res = requests.get(f"{BASE}/webhooks", params={"apiKey": api_key})
    webhooks = wh_res.json()["data"]
    print(f"{len(webhooks)} webhooks")
    ```
  </Tab>
</Tabs>

🚨 Como vem um erro [#-como-vem-um-erro]

Quando algo dá errado, o código de status **não** é 200. O corpo muda de cara: em vez de dados, você recebe uma explicação. O caso mais rico é o **400 (Bad Request)**, que detalha campo por campo o que precisa ser ajustado.

O envelope de erro [#o-envelope-de-erro]

Todo erro da API vem embrulhado num objeto **`error`** — nunca na raiz do corpo. Dentro dele há sempre um `message` (o resumo, em português) e, **no caso do 400**, uma lista `errors` detalhando campo por campo.

```json
{
  "error": {
    "message": "Não foi possível criar o lead.",
    "errors": [
      { "field": ["emails"], "message": "É obrigatório informar ao menos um contato." },
      { "field": ["customFields", "orcamento"], "message": "Campo personalizado não encontrado neste projeto." }
    ]
  }
}
```

<Callout type="warning" title="Entre em error primeiro">
  O `message` e o `errors` ficam **dentro de `error`** — ou seja, `resposta.error.message`, não `resposta.message`. Se o seu código lê `resposta.message` direto, vai achar `undefined`.
</Callout>

Cada item de `errors` tem duas partes:

<FieldInfoGroup>
  <FieldInfo title="field" required>
    O **caminho** do campo com problema, na forma de **lista** (ex.: `["emails"]`, `["content"]`, `["customFields", "orcamento"]`). Junte com ponto para exibir: `emails`, `customFields.orcamento`. É por ele que você sabe **onde** corrigir.
  </FieldInfo>

  <FieldInfo title="message" required>
    A explicação do problema, **já em português**, pronta para mostrar a quem for arrumar. É o "por quê" do erro.
  </FieldInfo>
</FieldInfoGroup>

<Callout type="tip" title="As mensagens já vêm em português — reaproveite">
  Você não precisa traduzir nem reescrever nada. O `message` de cada erro chega em pt-BR direto do GS Engage. Mostre-o como está no seu log, na sua tela ou no seu ticket.
</Callout>

📋 A tabela dos status: o que houve, por quê e como corrigir [#-a-tabela-dos-status-o-que-houve-por-quê-e-como-corrigir]

Estes são os quatro códigos de erro que você vai encontrar na prática. Para cada um: o que ele significa, a causa mais comum e o que fazer.

| Status                    | O que houve                                      | Por que costuma acontecer                                                                                                                          | Como corrigir                                                                                                                           |
| ------------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **400** Bad Request       | O pedido chegou, mas os dados estão inválidos.   | Falta um contato no lead, uma nota passou de 1000 caracteres, um `customField` não existe, um enum recebeu valor fora da lista.                    | Leia a lista `errors`: cada `field` diz o campo e cada `message` (em pt-BR) diz o ajuste. Corrija e reenvie.                            |
| **401** Unauthorized      | A API não reconheceu você.                       | A `apiKey` está **ausente**, errada ou foi revogada.                                                                                               | Confira o `?apiKey=` na URL. Gere/copie a chave em **Configurações > Configurações de API** e teste com um `GET /api/v1/custom-fields`. |
| **404** Not Found         | O recurso pedido não existe.                     | O `id` na URL está errado, aponta para um item de outro projeto, ou o item foi removido (ex.: `leadId`, `prospectionId`, `webhookId` inexistente). | Verifique o identificador. Liste o recurso antes (ex.: `GET /api/v1/leads`) para pegar um `id` válido.                                  |
| **429** Too Many Requests | Você passou do limite de requisições por minuto. | Muitas chamadas na janela de 60s: 200/min de leitura ou 100/min de escrita.                                                                        | Espere os segundos do header `Retry-After` e tente de novo, com **backoff exponencial**. Veja abaixo.                                   |

<Callout type="danger" title="401 e o alerta de segurança da apiKey">
  Um `401` costuma vir de uma chave errada — mas cuidado ao investigar: como a `apiKey` viaja **na URL**, ela aparece em logs, no histórico do navegador e em links copiados. Nunca cole a URL completa (com a chave) em chats ou tickets para "pedir ajuda". Trate a chave como senha.
</Callout>

O caso do 429: respeite o Retry-After [#o-caso-do-429-respeite-o-retry-after]

O `429` é o único erro que **não é culpa dos seus dados** — é só ritmo. O corpo segue o mesmo envelope (`{ "error": { "message": "Limite de requisições excedido..." } }`), mas o que importa aqui são os **cabeçalhos HTTP** que acompanham a resposta:

```http
Retry-After: 12
X-RateLimit-Limit: 200
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1700000000
```

O que fazer: espere os segundos indicados em `Retry-After` (aqui, 12s) antes de tentar de novo. Se falhar outra vez, dobre a espera — isso é **backoff exponencial** (espera crescente a cada tentativa). Nunca fique martelando a API em loop apertado; você só prolonga o bloqueio.

🧪 Lendo os erros de validação na prática [#-lendo-os-erros-de-validação-na-prática]

Vamos juntar tudo. O gatilho abaixo é o mais comum: tentar criar um lead **sem nenhum contato**. A API recusa com `400` e explica exatamente o que faltou. O código lê a lista `errors` e mostra cada problema já em português.

<Tabs items={['JavaScript', 'Python']}>
  <Tab value="JavaScript">
    ```js
    const res = await fetch(`${BASE}/leads?apiKey=${apiKey}`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name: "Lead sem contato" }), // falta emails/phones/mobiles
    });

    if (!res.ok) {
      const { error } = await res.json(); // tudo vem dentro de "error"

      if (res.status === 400) {
        console.error(`O que houve: ${error.message}`);
        // Percorre cada campo com problema — a message já vem em pt-BR.
        // field é uma lista (caminho) — junte com ponto para exibir.
        for (const e of error.errors ?? []) {
          console.error(`  • ${e.field.join(".")}: ${e.message}`);
        }
      } else if (res.status === 401) {
        console.error("apiKey ausente ou inválida — confira sua chave.");
      } else if (res.status === 429) {
        const wait = Number(res.headers.get("Retry-After") || 5);
        console.warn(`Limite atingido — aguarde ${wait}s e tente de novo.`);
      }
    }
    ```
  </Tab>

  <Tab value="Python">
    ```python
    res = requests.post(
        f"{BASE}/leads",
        params={"apiKey": api_key},
        json={"name": "Lead sem contato"},  # falta emails/phones/mobiles
    )

    if not res.ok:
        error = res.json().get("error", {})  # tudo vem dentro de "error"

        if res.status_code == 400:
            print(f"O que houve: {error.get('message')}")
            # Percorre cada campo com problema — a message já vem em pt-BR.
            # field é uma lista (caminho) — junte com ponto para exibir.
            for e in error.get("errors", []):
                print(f"  • {'.'.join(map(str, e['field']))}: {e['message']}")
        elif res.status_code == 401:
            print("apiKey ausente ou inválida — confira sua chave.")
        elif res.status_code == 429:
            wait = int(res.headers.get("Retry-After", "5"))
            print(f"Limite atingido — aguarde {wait}s e tente de novo.")
    ```
  </Tab>
</Tabs>

Rodando o exemplo acima, a saída seria algo assim:

```bash
O que houve: Não foi possível criar o lead.
  • emails: É obrigatório informar ao menos um contato.
```

O que isso significa para o negócio: você sabe na hora que o lead não entrou porque veio sem forma de contato — e a mensagem em português já pode ir direto para quem preencheu a origem dos dados.

<DoDont>
  <DoDontItem type="do">
    Cheque o 

    **código de status**

     antes de tratar o corpo — ele te diz qual caminho seguir.
  </DoDontItem>

  <DoDontItem type="do">
    Percorra a lista 

    `errors`

     do 

    `400`

     e mostre cada 

    `field`

     \+ 

    `message`

    .
  </DoDontItem>

  <DoDontItem type="do">
    Reaproveite as mensagens em pt-BR do backend como estão.
  </DoDontItem>

  <DoDontItem type="do">
    Respeite o 

    `Retry-After`

     no 

    `429`

    , com backoff exponencial.
  </DoDontItem>

  <DoDontItem type="dont">
    Não assuma que toda listagem tem 

    `meta`

     — 

    `GET /api/v1/webhooks`

     traz 

    `data`

     sem 

    `meta`

    . Mas os itens estão 

    **sempre**

     em 

    `data`

    .
  </DoDontItem>

  <DoDontItem type="dont">
    Não trate todo erro como "deu ruim" genérico — cada status pede uma ação diferente.
  </DoDontItem>

  <DoDontItem type="dont">
    Não repita a chamada em loop apertado após um 

    `429`

     — você prolonga o bloqueio.
  </DoDontItem>
</DoDont>

❓ Perguntas frequentes [#-perguntas-frequentes]

<FAQ>
  <FAQItem question="Alguma listagem vem sem meta?">
    Sim, uma: `GET /api/v1/webhooks` retorna `data` **sem** `meta` (lista completa, sem paginação). Todas as outras — inclusive `custom-fields` — usam o formato paginado `{ data, meta }`. Em todos os casos os itens ficam em `data`.
  </FAQItem>

  <FAQItem question="O 400 sempre traz a lista errors?">
    Sim — dentro de `error`. O corpo de um `400` sempre tem `error.message` (resumo) e `error.errors` (lista onde `field` é o **caminho** do campo, como lista, e `message` é a explicação). É de lá que você tira exatamente o que corrigir.
  </FAQItem>

  <FAQItem question="Recebi 401 mas tenho certeza de que a chave está certa. O que pode ser?">
    Confira se a chave está mesmo na URL como `?apiKey=SUA_CHAVE` (é query param, não header), se não há espaços ou quebras coladas junto, e se a chave não foi revogada. Teste isolado com `GET /api/v1/custom-fields`: um `200` confirma que a chave é válida.
  </FAQItem>

  <FAQItem question="Qual a diferença entre 404 e 400?">
    O `404` diz que o **recurso não existe** (o `id` da URL aponta para o nada). O `400` diz que o recurso até faz sentido, mas os **dados enviados** estão inválidos. Um é "não achei isso"; o outro é "o que você mandou está errado".
  </FAQItem>

  <FAQItem question="As mensagens de erro vêm em inglês?">
    Não. O GS Engage devolve as mensagens de erro já em **português**. Você pode exibi-las direto para a pessoa que vai corrigir, sem traduzir.
  </FAQItem>
</FAQ>

📚 Continue a leitura [#-continue-a-leitura]

Artigos Relacionados [#artigos-relacionados]

<RelatedArticles>
  <RelatedArticle href="/docs/api/conceitos/paginacao-filtros-ordenacao" title="Paginação e rate limit" description="Percorra listas grandes com data/meta e respeite o limite de requisições." />

  <RelatedArticle href="/docs/api/conceitos/autenticacao" title="Autenticação com apiKey" description="Como criar, usar e proteger sua chave — a origem da maioria dos 401." />

  <RelatedArticle href="/docs/api/conceitos/glossario" title="Glossário do GS Engage" description="Todos os termos técnicos traduzidos em uma frase." />

  <RelatedArticle href="/docs/api" title="Visão geral da API" description="Primeiros passos e o quickstart do GS Engage." />
</RelatedArticles>
