Entrar Criar conta
Audiência

Formulários

Formulários de inscrição hospedados ou no seu site, com double opt-in.

Formulários de inscrição colocam pessoas na sua audiência: cada envio cria (ou atualiza) um contato, coloca-o no segmento do formulário e o inscreve nos tópicos escolhidos. Você usa a página pronta, hospedada em https://forms.postal.so/{slug}, ou envia do seu próprio site (HTML ou JavaScript), como mostra Envio público. Com double opt-in, a pessoa recebe um e-mail de confirmação e só entra na audiência depois de clicar no link.

Um formulário é feito de páginas e blocos (perguntas, com lógica entre as páginas), tem configurações de encerramento, avisos por e-mail a cada envio, estatísticas de visitas e de conversão e uma lista de envios de todos os formulários. Todos os endpoints desta página usam uma chave de API de acesso total; o envio público não usa chave.

#Páginas e blocos

pages é a lista de páginas do formulário (de 1 a 10), cada uma com id próprio (até 32 letras minúsculas, números, - ou _; submit é reservado), title, description, media, blocks (de 1 a 30 por página e 60 no formulário) e logic. O id de cada bloco (letra minúscula seguida de até 63 letras minúsculas, números ou _) é também a chave da resposta no envio. Todo formulário tem um bloco email, sempre obrigatório, sempre visível e numa página por onde toda resposta passa.

Tipo (type)O que é
heading, paragraph, imageConteúdo: aparece, mas não recebe resposta. image exige media.
email, first_name, last_nameCampos do próprio contato.
short_text, long_text, phoneTexto (até 255, 5.000 e 32 caracteres).
number, dateNúmero (com min e max opcionais) e data AAAA-MM-DD.
single_choice, multiple_choice, dropdownEscolha: exigem de 1 a 50 options (value, label, e image_url ou illustration nas duas primeiras). Aceitam randomize e other (a opção "Outro", cuja resposta vira other:texto); as de imagem aceitam columns (2, 3 ou 4).
yes_no, consentSim ou não, e a caixa de consentimento (true quando marcada; o envio guarda a data e o IP do consentimento).
rating, npsNota de 1 até max (de 3 a 10; padrão 5) e NPS de 0 a 10.
hiddenValor que não aparece: na página hospedada, vem da query string (por exemplo, ?utm_source=blog).

Todo bloco aceita label (obrigatório, menos em conteúdo e hidden), description, required, media e visible_if. placeholder vale para os campos de texto, e-mail, número e dropdown. property grava a resposta numa propriedade de contato (que precisa existir, ter um tipo compatível com o bloco e ser usada por um bloco só). Os campos email, first_name e last_name já gravam no contato.

Condições e lógica entre páginas

visible_if (num bloco) esconde o bloco enquanto a condição for falsa, e logic (numa página) manda a pessoa para outra página depois de responder. Uma condição é {"field", "operator", "value"}, em que field é o id de um bloco que vem antes e operator é equals, not_equals, contains, not_contains, is_empty, is_not_empty, greater_than ou less_than. Cada regra de logic é {"when": condição, "go_to": "id da página"}; vale a primeira que combinar, e go_to aponta para uma página mais adiante ou para "submit" (até 20 regras por página). No envio, só as respostas dos blocos visíveis no caminho que a pessoa percorreu são validadas e guardadas.

Lógica: quem não quer promoções vai direto ao fim
{
  "id": "p2",
  "blocks": [
    { "id": "interest", "type": "single_choice", "label": "...", "options": [{ "value": "news", "label": "..." }, { "value": "offers", "label": "..." }] },
    { "id": "budget", "type": "number", "label": "...", "visible_if": { "field": "interest", "operator": "equals", "value": "offers" } }
  ],
  "logic": [{ "when": { "field": "interest", "operator": "equals", "value": "news" }, "go_to": "submit" }]
}

Os erros de estrutura vêm como 422 validation_error com message (o primeiro problema) e errors: uma lista de {"page_id", "block_id", "message"}, em inglês. Mídias (media) são {"type": "image|illustration|video", "url", "illustration", "alt", "layout": "above|split_left|split_right|background"}: imagens precisam de URL https (envie o arquivo com POST /assets) e de alt; ilustrações são as de GET /forms/illustrations; vídeos são links do YouTube ou do Vimeo.

#Configurações e encerramento

O objeto settings controla o fluxo e o encerramento do formulário. Tudo é opcional; o que você não envia assume o padrão. Ao atualizar, o objeto enviado substitui o anterior.

CampoPadrãoO que faz
displaypagespages mostra os blocos de uma página juntos; one_by_one mostra um bloco por vez.
show_progresstrueBarra de progresso.
next_label, back_labelnullTextos dos botões de avançar e voltar (até 60 caracteres).
welcomedesligadaTela de boas-vindas: enabled, title, description (até 2.000), button_label e media.
keyboard_hintstrueDicas de teclado na página hospedada.
transitionslideslide, fade ou none.
validate_emailfalseConfere o e-mail antes de aceitar o envio: recusa endereços descartáveis e domínios que não recebem e-mail, e aponta uma provável troca de letras (gmial.com) em suggestion uma vez (o mesmo endereço enviado de novo em 15 minutos passa). Não gasta créditos de validação.
closes_atnullData e hora (ISO 8601) em que o formulário para de receber envios.
max_submissionsnullNúmero de envios (de 1 a 10.000.000) a partir do qual o formulário encerra.
closed_messagenullTexto (até 2.000 caracteres) mostrado a quem chega depois do encerramento.
prevent_duplicatesfalseRecusa (422) um e-mail que já respondeu ao formulário; envios que falharam não contam.
show_brandingnullfalse esconde o selo "Enviado com Postal" na página pública. Só planos que não incluem o selo permitem (403 plan_limit_reached).
languageautoIdioma do que a Postal escreve para quem preenche o formulário: auto, pt-BR ou en (null vale auto). Com auto, segue o Accept-Language do navegador da pessoa (pt-BR ou en; sem o cabeçalho ou com outro idioma, pt-BR). Vale para a página hospedada, as mensagens (validação, duplicidade, formulário encerrado ou indisponível), os textos padrão de sucesso e do botão e o e-mail de confirmação do double opt-in. Os textos que você escreveu (títulos, perguntas, mensagem de sucesso, closed_message) nunca são traduzidos. Veja Envio público.

O formulário devolve closed_reason: null enquanto aberto, manual (status: "paused"), scheduled (passou de closes_at) ou limit (chegou a max_submissions). Um formulário encerrado responde 410 com o nome form_closed aos envios em JSON (e mostra a mensagem na página hospedada); um formulário pausado à mão sem closed_message, ou pausado pelo plano, responde 404 not_found.

Em notifications, o formulário avisa por e-mail a cada envio: enabled, recipients (até 10 endereços), include_answers (padrão true), reply_to_respondent (padrão true), subject (até 200 caracteres) e frequency (instant ou daily, um resumo às 8h no horário de Brasília). Um endereço que não é de um membro do time recebe um convite e só passa a receber depois de confirmar (status: pending ou confirmed); você só envia endereços, e o status é controlado pela Postal. Esses avisos não contam na cota de envio.

#Listar formulários

GET/forms

Chave de acesso total

Cada formulário da lista traz também os números dos últimos 30 dias: views_30d, submissions_30d, conversion_30d (envios sobre visitas, de 0 a 1) e last_submission_at. paused_by_plan é true quando o plano deixou de comportar o formulário: ele continua listado e pode ser lido ou excluído, mas não pode ser alterado (403 paused_by_plan).

Parâmetros de consulta

  • limitinteger
    De 1 a 100. Padrão: 20.
  • afterstring
    Cursor da próxima página.
  • beforestring
    Cursor da página anterior.
  • searchstring
    Busca no nome.
GET /forms

cURL

curl https://api.postal.so/forms \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/forms', {
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.get(
    "https://api.postal.so/forms",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->get('forms', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "form",
      "id": "019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d",
      "paused_by_plan": false,
      "name": "Newsletter",
      "slug": "newsletter-k3x9p2qa",
      "status": "active",
      "public_url": "https://forms.postal.so/newsletter-k3x9p2qa",
      "segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
      "topic_ids": ["019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b"],
      "pages": [
        {
          "id": "p1",
          "title": null,
          "description": null,
          "media": null,
          "logic": [],
          "blocks": [
            {
              "id": "email",
              "type": "email",
              "label": "E-mail",
              "description": null,
              "placeholder": "[email protected]",
              "required": true,
              "options": null,
              "min": null,
              "max": null,
              "property": null,
              "visible_if": null,
              "media": null,
              "columns": null,
              "randomize": false,
              "other": false
            },
            {
              "id": "first_name",
              "type": "first_name",
              "label": "Nome",
              "description": null,
              "placeholder": null,
              "required": false,
              "options": null,
              "min": null,
              "max": null,
              "property": null,
              "visible_if": null,
              "media": null,
              "columns": null,
              "randomize": false,
              "other": false
            }
          ]
        },
        {
          "id": "p2",
          "title": "Sobre você",
          "description": null,
          "media": null,
          "logic": [],
          "blocks": [
            {
              "id": "interest",
              "type": "single_choice",
              "label": "O que você quer receber?",
              "description": null,
              "placeholder": null,
              "required": true,
              "options": [
                {
                  "value": "news",
                  "label": "Novidades",
                  "image_url": null,
                  "illustration": null
                },
                {
                  "value": "offers",
                  "label": "Promoções",
                  "image_url": null,
                  "illustration": null
                }
              ],
              "min": null,
              "max": null,
              "property": null,
              "visible_if": null,
              "media": null,
              "columns": null,
              "randomize": false,
              "other": true
            }
          ]
        }
      ],
      "settings": {
        "display": "pages",
        "show_progress": true,
        "next_label": null,
        "back_label": null,
        "welcome": {
          "enabled": false,
          "title": null,
          "description": null,
          "button_label": null,
          "media": null
        },
        "keyboard_hints": true,
        "transition": "slide",
        "validate_email": false,
        "closes_at": null,
        "max_submissions": null,
        "closed_message": null,
        "prevent_duplicates": false,
        "show_branding": null,
        "language": "auto"
      },
      "fields": [
        {
          "key": "email",
          "label": "E-mail",
          "placeholder": "[email protected]",
          "required": true,
          "type": "email"
        },
        {
          "key": "first_name",
          "label": "Nome",
          "placeholder": null,
          "required": false,
          "type": "text"
        }
      ],
      "double_opt_in": true,
      "content": {
        "title": "Receba nossas novidades",
        "description": "Lançamentos e promoções, uma vez por semana.",
        "logo_url": null
      },
      "success": {
        "logo_url": null,
        "title": "Inscrição confirmada!",
        "message": "Obrigado por se inscrever. Em breve você recebe novidades no seu e-mail.",
        "button_label": null,
        "button_url": null,
        "media": null
      },
      "redirect_url": null,
      "style": {
        "layout": "stacked",
        "preset": null,
        "font": "system",
        "accent_color": "#4f46e5",
        "text_color": "#18181b",
        "background_color": "#ffffff",
        "answer_color": null,
        "button_text": "Inscrever-se",
        "button_color": "#4f46e5",
        "button_text_color": "#ffffff",
        "rounded": true,
        "radius": "md",
        "background": null,
        "logo_url": null
      },
      "allowed_domains": ["lojaexemplo.com.br"],
      "notifications": {
        "enabled": true,
        "recipients": [
          {
            "email": "[email protected]",
            "status": "confirmed",
            "confirmed_at": "2026-10-21T12:30:00.000000Z",
            "invited_at": "2026-10-21T12:25:00.000000Z"
          }
        ],
        "include_answers": true,
        "reply_to_respondent": true,
        "subject": null,
        "frequency": "instant"
      },
      "submissions_count": 412,
      "shows_postal_branding": false,
      "closed_reason": null,
      "created_at": "2026-10-21T12:00:00.000000Z",
      "updated_at": "2026-10-21T12:00:00.000000Z",
      "views_30d": 1820,
      "submissions_30d": 96,
      "conversion_30d": 0.0527,
      "last_submission_at": "2026-10-24T15:00:00Z"
    }
  ]
}

#Criar um formulário

POST/forms

Chave de acesso total

Devolve o formulário completo, com o public_url e os valores padrão aplicados.

Corpo da requisição

  • namestringobrigatório
    Nome interno, até 255 caracteres.
  • segment_idstringobrigatório
    Segmento estático que recebe os contatos.
  • slugstring
    Endereço público, de 3 a 64 caracteres (letras minúsculas, números e hífens), único na Postal. Padrão: gerado a partir do nome, como newsletter-k3x9p2qa.
  • statusstring
    active (padrão) ou paused (não aceita envios).
  • topic_idsstring[]
    Até 20 tópicos em que o contato é inscrito (opt_in).
  • pagesobject[]
    As páginas e os blocos do formulário. Um formulário novo começa com uma página só com o bloco de e-mail (rótulo e texto de exemplo no idioma da requisição). Quando pages e fields vêm juntos, vale pages.
  • settingsobject
    Configurações do fluxo e do encerramento.
  • fieldsobject[]
    De 1 a 20 campos, na ordem de exibição. O campo email é obrigatório. Padrão: só o e-mail. É o formato simples: vira uma página só, e para perguntas, lógica e mídia use pages.
    • keystringobrigatório
      email, first_name, last_name ou a chave de uma propriedade. O tipo do campo segue o tipo da propriedade.
    • labelstring
      Rótulo, até 255 caracteres.
    • placeholderstring
      Texto de exemplo, até 255 caracteres.
    • requiredboolean
      Obrigatório. O e-mail é sempre obrigatório.
  • double_opt_inboolean
    Pede confirmação por e-mail antes de incluir o contato.
  • contentobject
    Textos da página hospedada: title, description (até 2.000 caracteres) e logo_url.
  • successobject
    Tela de sucesso: logo_url, title, message, button_label, button_url e media. Sem title e message, a página usa um texto padrão no idioma do formulário (nas respostas da API, o padrão aparece no idioma da requisição).
  • redirect_urlstring | null
    URL para onde mandar a pessoa depois do envio (ou da confirmação, com double opt-in), em vez da tela de sucesso.
  • styleobject
    Aparência: layout (stacked ou inline), font (system, serif, mono, rounded, inter, plus_jakarta_sans, dm_sans, space_grotesk, fraunces, playfair_display, ibm_plex_mono), cores em hexadecimal (accent_color, text_color, background_color, button_color, button_text_color, answer_color), button_text, radius (none, sm, md, lg, full; rounded é o formato antigo), preset (um tema de GET /forms/themes), logo_url (https) e background ({"type": "color|gradient|image", …}, com color, gradient {from, to, angle} ou image_url, mais overlay de 0 a 0,85 e overlay_color).
  • allowed_domainsstring[]
    Até 50 domínios que podem enviar o formulário pelo navegador, como lojaexemplo.com.br (inclui subdomínios). Vazio: qualquer site.
  • notificationsobject
    Avisos por e-mail a cada envio, veja Configurações e encerramento.

Erros

  • 422validation_error
    Campo inválido, slug em uso, páginas ou blocos fora das regras (veja errors), formulário sem o bloco email ou campo que não é contato nem propriedade.
  • 404not_found
    O segmento ou algum tópico não existe.
  • 403plan_limit_reached
    O plano não permite mais formulários, ou o plano mostra o selo da Postal e o formulário pediu settings.show_branding: false.
POST /forms

cURL

curl -X POST https://api.postal.so/forms \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Newsletter",
    "segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
    "topic_ids": ["019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b"],
    "double_opt_in": true,
    "pages": [
      {
        "id": "p1",
        "blocks": [
          {
            "id": "email",
            "type": "email",
            "label": "E-mail",
            "placeholder": "[email protected]"
          },
          {
            "id": "first_name",
            "type": "first_name",
            "label": "Nome"
          }
        ]
      },
      {
        "id": "p2",
        "title": "Sobre você",
        "blocks": [
          {
            "id": "interest",
            "type": "single_choice",
            "label": "O que você quer receber?",
            "required": true,
            "other": true,
            "options": [
              {
                "value": "news",
                "label": "Novidades"
              },
              {
                "value": "offers",
                "label": "Promoções"
              }
            ]
          }
        ]
      }
    ],
    "settings": {
      "max_submissions": 5000,
      "prevent_duplicates": true
    },
    "content": {
      "title": "Receba nossas novidades"
    },
    "allowed_domains": ["lojaexemplo.com.br"]
  }'

Node.js

const response = await fetch('https://api.postal.so/forms', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Newsletter',
    segment_id: '019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a',
    topic_ids: ['019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b'],
    double_opt_in: true,
    pages: [
      {
        id: 'p1',
        blocks: [
          {
            id: 'email',
            type: 'email',
            label: 'E-mail',
            placeholder: '[email protected]',
          },
          {
            id: 'first_name',
            type: 'first_name',
            label: 'Nome',
          },
        ],
      },
      {
        id: 'p2',
        title: 'Sobre você',
        blocks: [
          {
            id: 'interest',
            type: 'single_choice',
            label: 'O que você quer receber?',
            required: true,
            other: true,
            options: [
              {
                value: 'news',
                label: 'Novidades',
              },
              {
                value: 'offers',
                label: 'Promoções',
              },
            ],
          },
        ],
      },
    ],
    settings: {
      max_submissions: 5000,
      prevent_duplicates: true,
    },
    content: {
      title: 'Receba nossas novidades',
    },
    allowed_domains: ['lojaexemplo.com.br'],
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/forms",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    json={
        "name": "Newsletter",
        "segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
        "topic_ids": ["019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b"],
        "double_opt_in": True,
        "pages": [
            {
                "id": "p1",
                "blocks": [
                    {
                        "id": "email",
                        "type": "email",
                        "label": "E-mail",
                        "placeholder": "[email protected]",
                    },
                    {
                        "id": "first_name",
                        "type": "first_name",
                        "label": "Nome",
                    },
                ],
            },
            {
                "id": "p2",
                "title": "Sobre você",
                "blocks": [
                    {
                        "id": "interest",
                        "type": "single_choice",
                        "label": "O que você quer receber?",
                        "required": True,
                        "other": True,
                        "options": [
                            {
                                "value": "news",
                                "label": "Novidades",
                            },
                            {
                                "value": "offers",
                                "label": "Promoções",
                            },
                        ],
                    },
                ],
            },
        ],
        "settings": {
            "max_submissions": 5000,
            "prevent_duplicates": True,
        },
        "content": {
            "title": "Receba nossas novidades",
        },
        "allowed_domains": ["lojaexemplo.com.br"],
    },
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->post('forms', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'json' => [
        'name' => 'Newsletter',
        'segment_id' => '019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a',
        'topic_ids' => ['019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b'],
        'double_opt_in' => true,
        'pages' => [
            [
                'id' => 'p1',
                'blocks' => [
                    [
                        'id' => 'email',
                        'type' => 'email',
                        'label' => 'E-mail',
                        'placeholder' => '[email protected]',
                    ],
                    [
                        'id' => 'first_name',
                        'type' => 'first_name',
                        'label' => 'Nome',
                    ],
                ],
            ],
            [
                'id' => 'p2',
                'title' => 'Sobre você',
                'blocks' => [
                    [
                        'id' => 'interest',
                        'type' => 'single_choice',
                        'label' => 'O que você quer receber?',
                        'required' => true,
                        'other' => true,
                        'options' => [
                            [
                                'value' => 'news',
                                'label' => 'Novidades',
                            ],
                            [
                                'value' => 'offers',
                                'label' => 'Promoções',
                            ],
                        ],
                    ],
                ],
            ],
        ],
        'settings' => [
            'max_submissions' => 5000,
            'prevent_duplicates' => true,
        ],
        'content' => [
            'title' => 'Receba nossas novidades',
        ],
        'allowed_domains' => ['lojaexemplo.com.br'],
    ],
]);

$data = json_decode($response->getBody(), true);
201Resposta
{
  "object": "form",
  "id": "019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d",
  "paused_by_plan": false,
  "name": "Newsletter",
  "slug": "newsletter-k3x9p2qa",
  "status": "active",
  "public_url": "https://forms.postal.so/newsletter-k3x9p2qa",
  "segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
  "topic_ids": ["019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b"],
  "pages": [
    {
      "id": "p1",
      "title": null,
      "description": null,
      "media": null,
      "logic": [],
      "blocks": [
        {
          "id": "email",
          "type": "email",
          "label": "E-mail",
          "description": null,
          "placeholder": "[email protected]",
          "required": true,
          "options": null,
          "min": null,
          "max": null,
          "property": null,
          "visible_if": null,
          "media": null,
          "columns": null,
          "randomize": false,
          "other": false
        },
        {
          "id": "first_name",
          "type": "first_name",
          "label": "Nome",
          "description": null,
          "placeholder": null,
          "required": false,
          "options": null,
          "min": null,
          "max": null,
          "property": null,
          "visible_if": null,
          "media": null,
          "columns": null,
          "randomize": false,
          "other": false
        }
      ]
    },
    {
      "id": "p2",
      "title": "Sobre você",
      "description": null,
      "media": null,
      "logic": [],
      "blocks": [
        {
          "id": "interest",
          "type": "single_choice",
          "label": "O que você quer receber?",
          "description": null,
          "placeholder": null,
          "required": true,
          "options": [
            {
              "value": "news",
              "label": "Novidades",
              "image_url": null,
              "illustration": null
            },
            {
              "value": "offers",
              "label": "Promoções",
              "image_url": null,
              "illustration": null
            }
          ],
          "min": null,
          "max": null,
          "property": null,
          "visible_if": null,
          "media": null,
          "columns": null,
          "randomize": false,
          "other": true
        }
      ]
    }
  ],
  "settings": {
    "display": "pages",
    "show_progress": true,
    "next_label": null,
    "back_label": null,
    "welcome": {
      "enabled": false,
      "title": null,
      "description": null,
      "button_label": null,
      "media": null
    },
    "keyboard_hints": true,
    "transition": "slide",
    "validate_email": false,
    "closes_at": null,
    "max_submissions": null,
    "closed_message": null,
    "prevent_duplicates": false,
    "show_branding": null,
    "language": "auto"
  },
  "fields": [
    {
      "key": "email",
      "label": "E-mail",
      "placeholder": "[email protected]",
      "required": true,
      "type": "email"
    },
    {
      "key": "first_name",
      "label": "Nome",
      "placeholder": null,
      "required": false,
      "type": "text"
    }
  ],
  "double_opt_in": true,
  "content": {
    "title": "Receba nossas novidades",
    "description": "Lançamentos e promoções, uma vez por semana.",
    "logo_url": null
  },
  "success": {
    "logo_url": null,
    "title": "Inscrição confirmada!",
    "message": "Obrigado por se inscrever. Em breve você recebe novidades no seu e-mail.",
    "button_label": null,
    "button_url": null,
    "media": null
  },
  "redirect_url": null,
  "style": {
    "layout": "stacked",
    "preset": null,
    "font": "system",
    "accent_color": "#4f46e5",
    "text_color": "#18181b",
    "background_color": "#ffffff",
    "answer_color": null,
    "button_text": "Inscrever-se",
    "button_color": "#4f46e5",
    "button_text_color": "#ffffff",
    "rounded": true,
    "radius": "md",
    "background": null,
    "logo_url": null
  },
  "allowed_domains": ["lojaexemplo.com.br"],
  "notifications": {
    "enabled": true,
    "recipients": [
      {
        "email": "[email protected]",
        "status": "confirmed",
        "confirmed_at": "2026-10-21T12:30:00.000000Z",
        "invited_at": "2026-10-21T12:25:00.000000Z"
      }
    ],
    "include_answers": true,
    "reply_to_respondent": true,
    "subject": null,
    "frequency": "instant"
  },
  "submissions_count": 0,
  "shows_postal_branding": false,
  "closed_reason": null,
  "created_at": "2026-10-21T12:00:00.000000Z",
  "updated_at": "2026-10-21T12:00:00.000000Z"
}

#Obter um formulário

GET/forms/{id}

Chave de acesso total

Parâmetros de caminho

  • idstringobrigatório
    ID do formulário.

Erros

  • 404not_found
    O recurso não existe ou é de outro time.
GET /forms/{id}

cURL

curl https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d', {
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.get(
    "https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->get('forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "form",
  "id": "019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d",
  "paused_by_plan": false,
  "name": "Newsletter",
  "slug": "newsletter-k3x9p2qa",
  "status": "active",
  "public_url": "https://forms.postal.so/newsletter-k3x9p2qa",
  "segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
  "topic_ids": ["019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b"],
  "pages": [
    {
      "id": "p1",
      "title": null,
      "description": null,
      "media": null,
      "logic": [],
      "blocks": [
        {
          "id": "email",
          "type": "email",
          "label": "E-mail",
          "description": null,
          "placeholder": "[email protected]",
          "required": true,
          "options": null,
          "min": null,
          "max": null,
          "property": null,
          "visible_if": null,
          "media": null,
          "columns": null,
          "randomize": false,
          "other": false
        },
        {
          "id": "first_name",
          "type": "first_name",
          "label": "Nome",
          "description": null,
          "placeholder": null,
          "required": false,
          "options": null,
          "min": null,
          "max": null,
          "property": null,
          "visible_if": null,
          "media": null,
          "columns": null,
          "randomize": false,
          "other": false
        }
      ]
    },
    {
      "id": "p2",
      "title": "Sobre você",
      "description": null,
      "media": null,
      "logic": [],
      "blocks": [
        {
          "id": "interest",
          "type": "single_choice",
          "label": "O que você quer receber?",
          "description": null,
          "placeholder": null,
          "required": true,
          "options": [
            {
              "value": "news",
              "label": "Novidades",
              "image_url": null,
              "illustration": null
            },
            {
              "value": "offers",
              "label": "Promoções",
              "image_url": null,
              "illustration": null
            }
          ],
          "min": null,
          "max": null,
          "property": null,
          "visible_if": null,
          "media": null,
          "columns": null,
          "randomize": false,
          "other": true
        }
      ]
    }
  ],
  "settings": {
    "display": "pages",
    "show_progress": true,
    "next_label": null,
    "back_label": null,
    "welcome": {
      "enabled": false,
      "title": null,
      "description": null,
      "button_label": null,
      "media": null
    },
    "keyboard_hints": true,
    "transition": "slide",
    "validate_email": false,
    "closes_at": null,
    "max_submissions": null,
    "closed_message": null,
    "prevent_duplicates": false,
    "show_branding": null,
    "language": "auto"
  },
  "fields": [
    {
      "key": "email",
      "label": "E-mail",
      "placeholder": "[email protected]",
      "required": true,
      "type": "email"
    },
    {
      "key": "first_name",
      "label": "Nome",
      "placeholder": null,
      "required": false,
      "type": "text"
    }
  ],
  "double_opt_in": true,
  "content": {
    "title": "Receba nossas novidades",
    "description": "Lançamentos e promoções, uma vez por semana.",
    "logo_url": null
  },
  "success": {
    "logo_url": null,
    "title": "Inscrição confirmada!",
    "message": "Obrigado por se inscrever. Em breve você recebe novidades no seu e-mail.",
    "button_label": null,
    "button_url": null,
    "media": null
  },
  "redirect_url": null,
  "style": {
    "layout": "stacked",
    "preset": null,
    "font": "system",
    "accent_color": "#4f46e5",
    "text_color": "#18181b",
    "background_color": "#ffffff",
    "answer_color": null,
    "button_text": "Inscrever-se",
    "button_color": "#4f46e5",
    "button_text_color": "#ffffff",
    "rounded": true,
    "radius": "md",
    "background": null,
    "logo_url": null
  },
  "allowed_domains": ["lojaexemplo.com.br"],
  "notifications": {
    "enabled": true,
    "recipients": [
      {
        "email": "[email protected]",
        "status": "confirmed",
        "confirmed_at": "2026-10-21T12:30:00.000000Z",
        "invited_at": "2026-10-21T12:25:00.000000Z"
      }
    ],
    "include_answers": true,
    "reply_to_respondent": true,
    "subject": null,
    "frequency": "instant"
  },
  "submissions_count": 412,
  "shows_postal_branding": false,
  "closed_reason": null,
  "created_at": "2026-10-21T12:00:00.000000Z",
  "updated_at": "2026-10-21T12:00:00.000000Z"
}

#Atualizar um formulário

PATCH/forms/{id}

Chave de acesso total

Todos os campos de criação são aceitos e opcionais. Em pages, content, success, style e settings, o objeto enviado substitui o anterior. Devolve o formulário atualizado.

Parâmetros de caminho

  • idstringobrigatório
    ID do formulário.

Erros

  • 422validation_error
    Algum campo é inválido. A mensagem diz qual.
  • 403paused_by_plan
    O formulário está pausado porque passa do limite do plano.
  • 403plan_limit_reached
    settings.show_branding: false num plano que mostra o selo da Postal.
  • 404not_found
    O recurso não existe ou é de outro time.
PATCH /forms/{id}

cURL

curl -X PATCH https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "paused"
  }'

Node.js

const response = await fetch('https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    status: 'paused',
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.patch(
    "https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    json={
        "status": "paused",
    },
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->patch('forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'json' => [
        'status' => 'paused',
    ],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "form",
  "id": "019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d",
  "paused_by_plan": false,
  "name": "Newsletter",
  "slug": "newsletter-k3x9p2qa",
  "status": "paused",
  "public_url": "https://forms.postal.so/newsletter-k3x9p2qa",
  "segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
  "topic_ids": ["019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b"],
  "pages": [
    {
      "id": "p1",
      "title": null,
      "description": null,
      "media": null,
      "logic": [],
      "blocks": [
        {
          "id": "email",
          "type": "email",
          "label": "E-mail",
          "description": null,
          "placeholder": "[email protected]",
          "required": true,
          "options": null,
          "min": null,
          "max": null,
          "property": null,
          "visible_if": null,
          "media": null,
          "columns": null,
          "randomize": false,
          "other": false
        },
        {
          "id": "first_name",
          "type": "first_name",
          "label": "Nome",
          "description": null,
          "placeholder": null,
          "required": false,
          "options": null,
          "min": null,
          "max": null,
          "property": null,
          "visible_if": null,
          "media": null,
          "columns": null,
          "randomize": false,
          "other": false
        }
      ]
    },
    {
      "id": "p2",
      "title": "Sobre você",
      "description": null,
      "media": null,
      "logic": [],
      "blocks": [
        {
          "id": "interest",
          "type": "single_choice",
          "label": "O que você quer receber?",
          "description": null,
          "placeholder": null,
          "required": true,
          "options": [
            {
              "value": "news",
              "label": "Novidades",
              "image_url": null,
              "illustration": null
            },
            {
              "value": "offers",
              "label": "Promoções",
              "image_url": null,
              "illustration": null
            }
          ],
          "min": null,
          "max": null,
          "property": null,
          "visible_if": null,
          "media": null,
          "columns": null,
          "randomize": false,
          "other": true
        }
      ]
    }
  ],
  "settings": {
    "display": "pages",
    "show_progress": true,
    "next_label": null,
    "back_label": null,
    "welcome": {
      "enabled": false,
      "title": null,
      "description": null,
      "button_label": null,
      "media": null
    },
    "keyboard_hints": true,
    "transition": "slide",
    "validate_email": false,
    "closes_at": null,
    "max_submissions": null,
    "closed_message": null,
    "prevent_duplicates": false,
    "show_branding": null,
    "language": "auto"
  },
  "fields": [
    {
      "key": "email",
      "label": "E-mail",
      "placeholder": "[email protected]",
      "required": true,
      "type": "email"
    },
    {
      "key": "first_name",
      "label": "Nome",
      "placeholder": null,
      "required": false,
      "type": "text"
    }
  ],
  "double_opt_in": true,
  "content": {
    "title": "Receba nossas novidades",
    "description": "Lançamentos e promoções, uma vez por semana.",
    "logo_url": null
  },
  "success": {
    "logo_url": null,
    "title": "Inscrição confirmada!",
    "message": "Obrigado por se inscrever. Em breve você recebe novidades no seu e-mail.",
    "button_label": null,
    "button_url": null,
    "media": null
  },
  "redirect_url": null,
  "style": {
    "layout": "stacked",
    "preset": null,
    "font": "system",
    "accent_color": "#4f46e5",
    "text_color": "#18181b",
    "background_color": "#ffffff",
    "answer_color": null,
    "button_text": "Inscrever-se",
    "button_color": "#4f46e5",
    "button_text_color": "#ffffff",
    "rounded": true,
    "radius": "md",
    "background": null,
    "logo_url": null
  },
  "allowed_domains": ["lojaexemplo.com.br"],
  "notifications": {
    "enabled": true,
    "recipients": [
      {
        "email": "[email protected]",
        "status": "confirmed",
        "confirmed_at": "2026-10-21T12:30:00.000000Z",
        "invited_at": "2026-10-21T12:25:00.000000Z"
      }
    ],
    "include_answers": true,
    "reply_to_respondent": true,
    "subject": null,
    "frequency": "instant"
  },
  "submissions_count": 412,
  "shows_postal_branding": false,
  "closed_reason": "manual",
  "created_at": "2026-10-21T12:00:00.000000Z",
  "updated_at": "2026-10-21T12:00:00.000000Z"
}

#Excluir um formulário

DELETE/forms/{id}

Chave de acesso total

O endereço público deixa de funcionar. Os contatos já inscritos continuam na audiência.

Parâmetros de caminho

  • idstringobrigatório
    ID do formulário.

Erros

  • 404not_found
    O recurso não existe ou é de outro time.
DELETE /forms/{id}

cURL

curl -X DELETE https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.delete(
    "https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->delete('forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "form",
  "id": "019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d",
  "deleted": true
}

#Temas prontos

GET/forms/themes

Chave de acesso total

Os temas prontos do editor. Cada um traz um style completo para usar em style ao criar ou atualizar um formulário (preset: postal, minimal, midnight, sunset, forest, ocean, paper ou neon).

GET /forms/themes

cURL

curl https://api.postal.so/forms/themes \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/forms/themes', {
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.get(
    "https://api.postal.so/forms/themes",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->get('forms/themes', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "list",
  "data": [
    {
      "key": "postal",
      "label": "Postal",
      "style": {
        "preset": "postal",
        "font": "plus_jakarta_sans",
        "radius": "md",
        "background_color": "#f5f2ff",
        "text_color": "#1e1846",
        "accent_color": "#5b3df5",
        "answer_color": "#ffffff"
      }
    }
  ]
}

#Ilustrações prontas

GET/forms/illustrations

Chave de acesso total

A biblioteca de ilustrações para usar em media ("type": "illustration"), nas opções de escolha e no fundo do formulário.

GET /forms/illustrations

cURL

curl https://api.postal.so/forms/illustrations \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/forms/illustrations', {
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.get(
    "https://api.postal.so/forms/illustrations",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->get('forms/illustrations', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "list",
  "data": [
    {
      "key": "rocket",
      "label": "Rocket",
      "url": "https://media.postal.so/form-illustrations/rocket.svg"
    }
  ]
}

#Listar envios de um formulário

GET/forms/{id}/submissions

Chave de acesso total

Cada envio recebido, do mais recente para o mais antigo. status: pending (na fila), pending_confirmation (e-mail de confirmação enviado), subscribed (contato na audiência) ou failed (por exemplo, o limite de contatos do plano foi atingido ou o e-mail está na lista de supressão). answers traz as respostas pelo id de cada bloco, e consent a data e o IP do consentimento, quando houve.

Parâmetros de caminho

  • idstringobrigatório
    ID do formulário.

Parâmetros de consulta

  • limitinteger
    De 1 a 100. Padrão: 20.
  • afterstring
    Cursor da próxima página.
  • beforestring
    Cursor da página anterior.
  • searchstring
    Busca no e-mail, no nome e no texto das respostas.
  • statusstring
    Um ou mais status, separados por vírgula: pending, pending_confirmation, subscribed, failed.
  • start_datestring
    Início do período, inclusive: uma data (2026-10-01) ou um horário ISO 8601.
  • end_datestring
    Fim do período, inclusive. Uma data sozinha cobre o dia inteiro (em UTC). 422 se vier antes de start_date.

Erros

  • 422validation_error
    Algum campo é inválido. A mensagem diz qual.
  • 404not_found
    O recurso não existe ou é de outro time.
GET /forms/{id}/submissions

cURL

curl 'https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/submissions?status=subscribed&start_date=2026-10-01&end_date=2026-10-24' \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/submissions?status=subscribed&start_date=2026-10-01&end_date=2026-10-24', {
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.get(
    "https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/submissions",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    params={
        "status": "subscribed",
        "start_date": "2026-10-01",
        "end_date": "2026-10-24",
    },
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->get('forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/submissions', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'query' => [
        'status' => 'subscribed',
        'start_date' => '2026-10-01',
        'end_date' => '2026-10-24',
    ],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "list",
  "has_more": true,
  "data": [
    {
      "object": "form_submission",
      "id": "019a1e20-8091-7ca2-9d1e-3f4a5b6c7d8e",
      "form_id": "019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d",
      "contact_id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
      "email": "[email protected]",
      "answers": {
        "email": "[email protected]",
        "first_name": "Ana",
        "interest": "news"
      },
      "consent": null,
      "data": {
        "email": "[email protected]",
        "first_name": "Ana",
        "properties": {}
      },
      "status": "subscribed",
      "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)",
      "referrer": "https://lojaexemplo.com.br/blog",
      "created_at": "2026-10-24T15:00:00.000000Z",
      "confirmed_at": "2026-10-24T15:03:12.000000Z"
    }
  ]
}

#Obter um envio

GET/forms/{id}/submissions/{submission_id}

Chave de acesso total

Parâmetros de caminho

  • idstringobrigatório
    ID do formulário.
  • submission_idstringobrigatório
    ID do envio.

Erros

  • 404not_found
    O formulário ou o envio não existe.
GET /forms/{id}/submissions/{submission_id}

cURL

curl https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/submissions/019a1e20-8091-7ca2-9d1e-3f4a5b6c7d8e \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/submissions/019a1e20-8091-7ca2-9d1e-3f4a5b6c7d8e', {
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.get(
    "https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/submissions/019a1e20-8091-7ca2-9d1e-3f4a5b6c7d8e",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->get('forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/submissions/019a1e20-8091-7ca2-9d1e-3f4a5b6c7d8e', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "form_submission",
  "id": "019a1e20-8091-7ca2-9d1e-3f4a5b6c7d8e",
  "form_id": "019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d",
  "contact_id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
  "email": "[email protected]",
  "answers": {
    "email": "[email protected]",
    "first_name": "Ana",
    "interest": "news"
  },
  "consent": null,
  "data": {
    "email": "[email protected]",
    "first_name": "Ana",
    "properties": {}
  },
  "status": "subscribed",
  "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)",
  "referrer": "https://lojaexemplo.com.br/blog",
  "created_at": "2026-10-24T15:00:00.000000Z",
  "confirmed_at": "2026-10-24T15:03:12.000000Z"
}

#Excluir um envio

DELETE/forms/{id}/submissions/{submission_id}

Chave de acesso total

Apaga o registro do envio e desconta um de submissions_count. O contato criado por ele continua na audiência; para removê-lo, exclua o contato.

Parâmetros de caminho

  • idstringobrigatório
    ID do formulário.
  • submission_idstringobrigatório
    ID do envio.

Erros

  • 404not_found
    O formulário ou o envio não existe.
DELETE /forms/{id}/submissions/{submission_id}

cURL

curl -X DELETE https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/submissions/019a1e20-8091-7ca2-9d1e-3f4a5b6c7d8e \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/submissions/019a1e20-8091-7ca2-9d1e-3f4a5b6c7d8e', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.delete(
    "https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/submissions/019a1e20-8091-7ca2-9d1e-3f4a5b6c7d8e",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->delete('forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/submissions/019a1e20-8091-7ca2-9d1e-3f4a5b6c7d8e', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "form_submission",
  "id": "019a1e20-8091-7ca2-9d1e-3f4a5b6c7d8e",
  "deleted": true
}

#Exportar os envios de um formulário

GET/forms/{id}/submissions/export

Chave de acesso total

olve um arquivo CSV (text/csv, UTF-8 com BOM), na hora e sem paginação. Precisa de um plano que inclua exportações (403 plan_limit_reached se não inclu Uma coluna por pergunta do formulário. Aceita os mesmos filtros da lista (search, status, start_date e end_date). Uma resposta escrita na opção “Outro” sai como Outro: texto (ou Other: texto, conforme o Accept-Language da requisição).

Parâmetros de caminho

  • idstringobrigatório
    ID do formulário.

Parâmetros de consulta

  • searchstring
    Busca no e-mail, no nome e no texto das respostas.
  • statusstring
    Um ou mais status, separados por vírgula: pending, pending_confirmation, subscribed, failed.
  • start_datestring
    Início do período, inclusive: uma data (2026-10-01) ou um horário ISO 8601.
  • end_datestring
    Fim do período, inclusive. Uma data sozinha cobre o dia inteiro (em UTC). 422 se vier antes de start_date.

Erros

  • 422validation_error
    Algum campo é inválido. A mensagem diz qual.
  • 403plan_limit_reached
    O plano não inclui exportações.
  • 404not_found
    O recurso não existe ou é de outro time.
GET /forms/{id}/submissions/export

cURL

curl 'https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/submissions/export?start_date=2026-10-01' \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/submissions/export?start_date=2026-10-01', {
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.get(
    "https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/submissions/export",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    params={
        "start_date": "2026-10-01",
    },
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->get('forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/submissions/export', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'query' => [
        'start_date' => '2026-10-01',
    ],
]);

$data = json_decode($response->getBody(), true);
200Resposta (text/csv)
id,created_at,status,E-mail,Nome,O que você quer receber?,consent_given_at
019a1e20-8091-7ca2-9d1e-3f4a5b6c7d8e,2026-10-24T15:00:00+00:00,subscribed,[email protected],Ana,Novidades,

#Listar os envios de todos os formulários

GET/forms/submissions

Chave de acesso total

Os envios de todos os formulários do time numa lista só, do mais recente para o mais antigo. Cada linha traz o formulário, quem enviou, as primeiras respostas (answers_preview) e de onde veio o visitante (utm_source, device, country, quando a visita foi medida).

Parâmetros de consulta

  • limitinteger
    De 1 a 100. Padrão: 20.
  • afterstring
    Cursor da próxima página.
  • beforestring
    Cursor da página anterior.
  • form_idstring
    Só os envios deste formulário.
  • searchstring
    Busca no e-mail, no nome e no texto das respostas.
  • statusstring
    Um ou mais status, separados por vírgula: pending, pending_confirmation, subscribed, failed.
  • start_datestring
    Início do período, inclusive: uma data (2026-10-01) ou um horário ISO 8601.
  • end_datestring
    Fim do período, inclusive. Uma data sozinha cobre o dia inteiro (em UTC). 422 se vier antes de start_date.

Erros

  • 422validation_error
    Algum campo é inválido. A mensagem diz qual.
GET /forms/submissions

cURL

curl 'https://api.postal.so/forms/submissions?status=subscribed&start_date=2026-10-01' \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/forms/submissions?status=subscribed&start_date=2026-10-01', {
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.get(
    "https://api.postal.so/forms/submissions",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    params={
        "status": "subscribed",
        "start_date": "2026-10-01",
    },
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->get('forms/submissions', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'query' => [
        'status' => 'subscribed',
        'start_date' => '2026-10-01',
    ],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "form_submission_row",
      "id": "019a1e20-8091-7ca2-9d1e-3f4a5b6c7d8e",
      "form_id": "019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d",
      "form_name": "Newsletter",
      "email": "[email protected]",
      "name": "Ana Souza",
      "status": "subscribed",
      "answers_preview": [
        {
          "id": "first_name",
          "question": "Nome",
          "value": "Ana"
        },
        {
          "id": "interest",
          "question": "O que você quer receber?",
          "value": "Novidades"
        }
      ],
      "contact_id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
      "utm_source": "newsletter",
      "device": "mobile",
      "country": "BR",
      "created_at": "2026-10-24T15:00:00.000000Z"
    }
  ]
}

#Exportar os envios de todos os formulários

GET/forms/submissions/export

Chave de acesso total

olve um arquivo CSV (text/csv, UTF-8 com BOM), na hora e sem paginação. Precisa de um plano que inclua exportações (403 plan_limit_reached se não inclu Uma linha por envio, com as respostas em answers como pares "pergunta: resposta" e as colunas id, created_at, form_id, form_name, status, email, name, contact_id, utm_source, utm_medium, utm_campaign, source, device e country. Aceita os mesmos filtros da lista. Uma resposta escrita na opção “Outro” sai como Outro: texto (ou Other: texto, conforme o Accept-Language da requisição).

Parâmetros de consulta

  • form_idstring
    Só os envios deste formulário.
  • searchstring
    Busca no e-mail, no nome e no texto das respostas.
  • statusstring
    Um ou mais status, separados por vírgula: pending, pending_confirmation, subscribed, failed.
  • start_datestring
    Início do período, inclusive: uma data (2026-10-01) ou um horário ISO 8601.
  • end_datestring
    Fim do período, inclusive. Uma data sozinha cobre o dia inteiro (em UTC). 422 se vier antes de start_date.

Erros

  • 422validation_error
    Algum campo é inválido. A mensagem diz qual.
  • 403plan_limit_reached
    O plano não inclui exportações.
GET /forms/submissions/export

cURL

curl 'https://api.postal.so/forms/submissions/export?start_date=2026-10-01' \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/forms/submissions/export?start_date=2026-10-01', {
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.get(
    "https://api.postal.so/forms/submissions/export",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    params={
        "start_date": "2026-10-01",
    },
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->get('forms/submissions/export', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'query' => [
        'start_date' => '2026-10-01',
    ],
]);

$data = json_decode($response->getBody(), true);
200Resposta (text/csv)
id,created_at,form_id,form_name,status,email,name,contact_id,answers,utm_source,utm_medium,utm_campaign,source,device,country
019a1e20-8091-7ca2-9d1e-3f4a5b6c7d8e,2026-10-24T15:00:00+00:00,019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d,Newsletter,subscribed,[email protected],Ana Souza,019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f,"Nome: Ana; O que você quer receber?: Novidades",newsletter,email,,,mobile,BR

#Estatísticas dos formulários

As estatísticas contam as visitas à página hospedada e aos formulários embutidos (a Postal mede as visitas por um sinal que a própria página envia; envios de outros sistemas aparecem em all_submissions, mas não em visitas), o quanto cada pessoa avançou e os envios. Precisam de um plano com análise de formulários (403 plan_limit_reached se não tiver). Todas aceitam o período em from e to (datas ou horários ISO 8601; uma data sozinha cobre o dia inteiro; padrão: os últimos 30 dias; até 400 dias), em tz (fuso IANA, como America/Sao_Paulo; padrão UTC) e interval (day ou hour). O fim nunca passa de agora; 422 se from vier depois de to, se o fuso não existir ou se o período gerar mais de 2.000 intervalos. Os números pesados são recalculados no máximo uma vez por minuto.

#Visão geral de todos os formulários

GET/forms/overview

Chave de acesso total

As estatísticas de todos os formulários do time somadas, no mesmo período e com os mesmos parâmetros da análise de um formulário. Cada número de totals vem como {"value", "previous"}, com o período anterior de mesma duração; series traz os intervalos; top_forms, os 10 formulários com mais visitas (com sparkline de envios por dia); breakdowns, as 8 principais origens, UTMs, aparelhos, navegadores, países e posições; hours_heatmap; recent_submissions; e list_quality de todos os formulários.

list_quality mede a qualidade da audiência que os envios do período trouxeram: submissions recebidos e contacts distintos que viraram contato; double_opt_in (confirmed e pending); failed (envios que não viraram contato) e suppressed_rejections (os que foram recusados por estarem na lista de supressão); unsubscribed; suppressed (contatos cujo endereço entrou na lista de supressão, por origem: bounce, complaint, manual…); hard_bounce_rate (bounces permanentes sobre contatos); e verification (valid, invalid, risky e unverified, da validação de e-mails).

Parâmetros de consulta

  • fromstring
    Início do período. Padrão: 30 dias atrás.
  • tostring
    Fim do período. Padrão: agora.
  • tzstring
    Fuso horário IANA. Padrão: UTC.
  • intervalstring
    day (padrão) ou hour.

Erros

  • 422validation_error
    Algum campo é inválido. A mensagem diz qual.
  • 403plan_limit_reached
    O plano não inclui a análise de formulários.
GET /forms/overview

cURL

curl 'https://api.postal.so/forms/overview?from=2026-10-01&to=2026-10-24&tz=America/Sao_Paulo' \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/forms/overview?from=2026-10-01&to=2026-10-24&tz=America/Sao_Paulo', {
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.get(
    "https://api.postal.so/forms/overview",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    params={
        "from": "2026-10-01",
        "to": "2026-10-24",
        "tz": "America/Sao_Paulo",
    },
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->get('forms/overview', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'query' => [
        'from' => '2026-10-01',
        'to' => '2026-10-24',
        'tz' => 'America/Sao_Paulo',
    ],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "forms_overview",
  "from": "2026-10-01T00:00:00-03:00",
  "to": "2026-10-24T23:59:59-03:00",
  "interval": "day",
  "timezone": "America/Sao_Paulo",
  "previous_period": {
    "from": "2026-09-07T00:00:00-03:00",
    "to": "2026-09-30T23:59:59-03:00"
  },
  "totals": {
    "forms": {
      "value": 6,
      "previous": 5
    },
    "active_forms": {
      "value": 5,
      "previous": null
    },
    "views": {
      "value": 1820,
      "previous": 1544
    },
    "submissions": {
      "value": 412,
      "previous": 377
    },
    "conversion_rate": {
      "value": 0.2264,
      "previous": 0.2442
    }
  },
  "series": [
    {
      "date": "2026-10-24T00:00:00-03:00",
      "views": 71,
      "starts": 25,
      "submissions": 17
    }
  ],
  "top_forms": [
    {
      "id": "019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d",
      "name": "Newsletter",
      "status": "active",
      "views": 1250,
      "submissions": 301,
      "conversion_rate": 0.2408,
      "sparkline": [9, 12, 10, 15, 17]
    }
  ],
  "breakdowns": {
    "sources": [
      {
        "value": "lojaexemplo.com.br",
        "views": 640,
        "starts": 280,
        "submissions": 190,
        "conversion_rate": 0.2969
      }
    ],
    "devices": [
      {
        "value": "mobile",
        "views": 1100,
        "starts": 380,
        "submissions": 240,
        "conversion_rate": 0.2182
      }
    ]
  },
  "hours_heatmap": {
    "max": 14,
    "cells": [
      [
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      [
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      [
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      [
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      [
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      [
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      [
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ]
    ]
  },
  "recent_submissions": [
    {
      "id": "019a1e20-8091-7ca2-9d1e-3f4a5b6c7d8e",
      "form_id": "019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d",
      "form_name": "Newsletter",
      "email": "[email protected]",
      "name": "Ana Souza",
      "created_at": "2026-10-24T15:00:00-03:00"
    }
  ],
  "list_quality": {
    "submissions": 412,
    "contacts": 371,
    "double_opt_in": {
      "confirmed": 340,
      "pending": 21
    },
    "failed": 9,
    "suppressed_rejections": 3,
    "unsubscribed": 4,
    "suppressed": {
      "total": 6,
      "bounce": 2,
      "complaint": 1,
      "manual": 3
    },
    "hard_bounce_rate": 0.0054,
    "verification": {
      "valid": 280,
      "invalid": 12,
      "risky": 18,
      "unverified": 61
    }
  }
}

#Estatísticas de um formulário

GET/forms/{id}/analytics

Chave de acesso total

As visitas, a conversão e as respostas de um formulário no período. Campos principais: totals (views, unique_visitors, starts, submissions, all_submissions, abandoned, bounced, as taxas start_rate, completion_rate e conversion_rate de 0 a 1, e o tempo médio e mediano em segundos); previous (os mesmos totais no período anterior de mesma duração); funnel (cada etapa com quantas visitas chegaram, onde as pessoas desistiram e a biggest_drop); timeseries; breakdowns (por origem, UTM, aparelho, navegador, país e posição: page, iframe ou inline); questions (um resumo por pergunta, com summary.kind: choice, yes_no, consent, rating, nps, number, date, text ou values); heatmap (dia da semana por hora); recent_submitters e list_quality. sampled é true quando o período tem mais de 20.000 envios e as perguntas leem só os mais recentes.

list_quality mede a qualidade da audiência que os envios do período trouxeram: submissions recebidos e contacts distintos que viraram contato; double_opt_in (confirmed e pending); failed (envios que não viraram contato) e suppressed_rejections (os que foram recusados por estarem na lista de supressão); unsubscribed; suppressed (contatos cujo endereço entrou na lista de supressão, por origem: bounce, complaint, manual…); hard_bounce_rate (bounces permanentes sobre contatos); e verification (valid, invalid, risky e unverified, da validação de e-mails).

Parâmetros de caminho

  • idstringobrigatório
    ID do formulário.

Parâmetros de consulta

  • fromstring
    Início do período. Padrão: 30 dias atrás.
  • tostring
    Fim do período. Padrão: agora.
  • tzstring
    Fuso horário IANA. Padrão: UTC.
  • intervalstring
    day (padrão) ou hour.

Erros

  • 422validation_error
    Algum campo é inválido. A mensagem diz qual.
  • 403plan_limit_reached
    O plano não inclui a análise de formulários.
  • 404not_found
    O recurso não existe ou é de outro time.
GET /forms/{id}/analytics

cURL

curl 'https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/analytics?from=2026-10-01&to=2026-10-24&tz=America/Sao_Paulo' \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/analytics?from=2026-10-01&to=2026-10-24&tz=America/Sao_Paulo', {
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.get(
    "https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/analytics",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    params={
        "from": "2026-10-01",
        "to": "2026-10-24",
        "tz": "America/Sao_Paulo",
    },
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->get('forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/analytics', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'query' => [
        'from' => '2026-10-01',
        'to' => '2026-10-24',
        'tz' => 'America/Sao_Paulo',
    ],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "form_analytics",
  "form_id": "019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d",
  "from": "2026-10-01T00:00:00-03:00",
  "to": "2026-10-24T23:59:59-03:00",
  "interval": "day",
  "timezone": "America/Sao_Paulo",
  "totals": {
    "views": 1820,
    "unique_visitors": 1410,
    "starts": 640,
    "submissions": 412,
    "all_submissions": 412,
    "abandoned": 228,
    "bounced": 1180,
    "start_rate": 0.3516,
    "completion_rate": 0.6438,
    "conversion_rate": 0.2264,
    "average_seconds": 48,
    "median_seconds": 37
  },
  "previous": {
    "from": "2026-09-07T00:00:00-03:00",
    "to": "2026-09-30T23:59:59-03:00",
    "views": 1544,
    "unique_visitors": 1410,
    "starts": 640,
    "submissions": 377,
    "all_submissions": 412,
    "abandoned": 228,
    "bounced": 1180,
    "start_rate": 0.3516,
    "completion_rate": 0.6438,
    "conversion_rate": 0.2264,
    "average_seconds": 48,
    "median_seconds": 37
  },
  "anonymous": {
    "views": 40,
    "starts": 12,
    "submissions": 9
  },
  "funnel": {
    "views": 1820,
    "starts": 640,
    "submissions": 412,
    "steps": [
      {
        "index": 0,
        "id": "p1",
        "kind": "page",
        "type": null,
        "label": "Página 1",
        "page_id": "p1",
        "reached": 1820,
        "reached_rate": 1.0,
        "drop_off": 0,
        "drop_off_rate": 0.0,
        "abandoned": 120,
        "abandoned_rate": 0.0659
      },
      {
        "index": 1,
        "id": "p2",
        "kind": "page",
        "type": null,
        "label": "Sobre você",
        "page_id": "p2",
        "reached": 520,
        "reached_rate": 0.2857,
        "drop_off": 1300,
        "drop_off_rate": 0.7143,
        "abandoned": 108,
        "abandoned_rate": 0.2077
      }
    ],
    "biggest_drop": {
      "id": "p2",
      "index": 1,
      "label": "Sobre você",
      "abandoned": 108,
      "abandoned_rate": 0.2077
    }
  },
  "timeseries": [
    {
      "period": "2026-10-23T00:00:00-03:00",
      "views": 64,
      "starts": 22,
      "submissions": 15
    },
    {
      "period": "2026-10-24T00:00:00-03:00",
      "views": 71,
      "starts": 25,
      "submissions": 17
    }
  ],
  "breakdowns": {
    "sources": [
      {
        "value": "lojaexemplo.com.br",
        "views": 640,
        "starts": 280,
        "submissions": 190,
        "conversion_rate": 0.2969
      }
    ],
    "utm_sources": [],
    "utm_mediums": [],
    "utm_campaigns": [],
    "devices": [
      {
        "value": "mobile",
        "views": 1100,
        "starts": 380,
        "submissions": 240,
        "conversion_rate": 0.2182
      }
    ],
    "browsers": [],
    "countries": [],
    "placements": []
  },
  "questions": [
    {
      "id": "interest",
      "type": "single_choice",
      "label": "O que você quer receber?",
      "page_id": "p2",
      "answered": 412,
      "answer_rate": 1.0,
      "summary": {
        "kind": "choice",
        "multiple": false,
        "options": [
          {
            "value": "news",
            "label": "Novidades",
            "count": 301,
            "rate": 0.7306
          },
          {
            "value": "offers",
            "label": "Promoções",
            "count": 111,
            "rate": 0.2694
          }
        ],
        "other": {
          "count": 0,
          "rate": 0.0,
          "latest": []
        }
      }
    }
  ],
  "heatmap": {
    "max": 14,
    "cells": [
      [
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      [
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      [
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      [
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      [
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      [
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      [
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ]
    ]
  },
  "recent_submitters": [
    {
      "id": "019a1e20-8091-7ca2-9d1e-3f4a5b6c7d8e",
      "email": "[email protected]",
      "name": "Ana Souza",
      "contact_id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
      "status": "subscribed",
      "created_at": "2026-10-24T15:00:00Z",
      "source": "lojaexemplo.com.br",
      "utm_source": "newsletter",
      "utm_campaign": null,
      "device": "mobile",
      "browser": "Safari",
      "country": "BR",
      "placement": "page",
      "duration_seconds": 41,
      "tracked": true
    }
  ],
  "list_quality": {
    "submissions": 412,
    "contacts": 371,
    "double_opt_in": {
      "confirmed": 340,
      "pending": 21
    },
    "failed": 9,
    "suppressed_rejections": 3,
    "unsubscribed": 4,
    "suppressed": {
      "total": 6,
      "bounce": 2,
      "complaint": 1,
      "manual": 3
    },
    "hard_bounce_rate": 0.0054,
    "verification": {
      "valid": 280,
      "invalid": 12,
      "risky": 18,
      "unverified": 61
    }
  },
  "sampled": false
}

#Exportar as visitas de um formulário

GET/forms/{id}/analytics/export

Chave de acesso total

Um arquivo CSV (text/csv) com uma linha por visita do período: quando viu, começou e enviou, a etapa mais longe que chegou, o aparelho e a origem. Sem dados pessoais, respostas nem IDs de envio. Aceita from, to e tz.

Parâmetros de caminho

  • idstringobrigatório
    ID do formulário.

Parâmetros de consulta

  • fromstring
    Início do período. Padrão: 30 dias atrás.
  • tostring
    Fim do período. Padrão: agora.
  • tzstring
    Fuso horário IANA. Padrão: UTC.

Erros

  • 422validation_error
    Algum campo é inválido. A mensagem diz qual.
  • 403plan_limit_reached
    O plano não inclui a análise de formulários.
  • 404not_found
    O recurso não existe ou é de outro time.
GET /forms/{id}/analytics/export

cURL

curl 'https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/analytics/export?from=2026-10-01&to=2026-10-24' \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/analytics/export?from=2026-10-01&to=2026-10-24', {
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.get(
    "https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/analytics/export",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    params={
        "from": "2026-10-01",
        "to": "2026-10-24",
    },
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->get('forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/analytics/export', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'query' => [
        'from' => '2026-10-01',
        'to' => '2026-10-24',
    ],
]);

$data = json_decode($response->getBody(), true);
200Resposta (text/csv)
visit_id,viewed_at,started_at,completed_at,submitted,duration_seconds,furthest_step_index,furthest_step,steps_reached,placement,device,browser,country,referrer_host,utm_source,utm_medium,utm_campaign
019a1e25-9102-7db3-8e2f-4a5b6c7d8e9f,2026-10-24T14:58:10+00:00,2026-10-24T14:58:19+00:00,2026-10-24T14:59:00+00:00,true,41,1,Sobre você,2,page,mobile,Safari,BR,lojaexemplo.com.br,newsletter,email,outubro

#Respostas de uma pergunta de texto

GET/forms/{id}/analytics/questions/{question}/answers

Chave de acesso total

Lista as respostas a uma pergunta de texto (short_text, long_text, phone ou hidden), das mais recentes para as mais antigas, sem dizer quem respondeu. As perguntas de escolha, nota e outras já vêm resumidas na análise. Aceita o período (from, to, tz) e limit, after e before.

Parâmetros de caminho

  • idstringobrigatório
    ID do formulário.
  • questionstringobrigatório
    id do bloco da pergunta.

Parâmetros de consulta

  • limitinteger
    De 1 a 100. Padrão: 20.
  • afterstring
    Cursor da próxima página.
  • beforestring
    Cursor da página anterior.
  • fromstring
    Início do período. Padrão: 30 dias atrás.
  • tostring
    Fim do período. Padrão: agora.
  • tzstring
    Fuso horário IANA. Padrão: UTC.

Erros

  • 422validation_error
    A pergunta não é de texto, ou o período é inválido.
  • 403plan_limit_reached
    O plano não inclui a análise de formulários.
  • 404not_found
    O formulário ou a pergunta não existe.
GET /forms/{id}/analytics/questions/{question}/answers

cURL

curl 'https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/analytics/questions/comment/answers?limit=20' \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/analytics/questions/comment/answers?limit=20', {
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.get(
    "https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/analytics/questions/comment/answers",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    params={
        "limit": 20,
    },
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->get('forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/analytics/questions/comment/answers', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'query' => [
        'limit' => 20,
    ],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "form_answer",
      "submission_id": "019a1e20-8091-7ca2-9d1e-3f4a5b6c7d8e",
      "question_id": "comment",
      "value": "Quero ver mais conteúdo sobre entregabilidade.",
      "created_at": "2026-10-24T15:00:00.000000Z"
    }
  ]
}

#Reenviar o convite de um aviso

POST/forms/{id}/notifications/recipients/{email}/resend

Chave de acesso total

Manda de novo o convite para um destinatário dos avisos que ainda não confirmou (inclusive quem recusou: só a confirmação dele volta a ligar os avisos). Devolve o destinatário com o status e as datas.

Parâmetros de caminho

  • idstringobrigatório
    ID do formulário.
  • emailstringobrigatório
    Endereço do destinatário, já listado em notifications.recipients.

Erros

  • 422validation_error
    O destinatário já confirmou.
  • 429rate_limit_exceeded
    Convites demais para este endereço. Tente amanhã.
  • 404not_found
    O formulário ou o destinatário não existe.
POST /forms/{id}/notifications/recipients/{email}/resend

cURL

curl -X POST https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/notifications/recipients/[email protected]/resend \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/notifications/recipients/[email protected]/resend', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/notifications/recipients/[email protected]/resend",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->post('forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/notifications/recipients/[email protected]/resend', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "form_notification_recipient",
  "email": "[email protected]",
  "status": "pending",
  "confirmed_at": null,
  "invited_at": "2026-10-24T15:10:00.000000Z"
}

#Enviar um aviso de teste

POST/forms/{id}/notifications/test

Chave de acesso total

Manda um aviso com respostas de exemplo, com as configurações salvas, para os destinatários que já confirmaram. Até 5 testes por formulário a cada 10 minutos.

Parâmetros de caminho

  • idstringobrigatório
    ID do formulário.

Erros

  • 422validation_error
    Nenhum destinatário confirmou ainda.
  • 429rate_limit_exceeded
    Testes demais. Tente em alguns minutos.
  • 404not_found
    O recurso não existe ou é de outro time.
POST /forms/{id}/notifications/test

cURL

curl -X POST https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/notifications/test \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/notifications/test', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
  },
});

const data = await response.json();

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/notifications/test",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    timeout=10,
)

data = response.json()

PHP

$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);

$response = $client->post('forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/notifications/test', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "form_notification_test",
  "recipients": ["[email protected]"]
}

#Envio público

Os envios vão para o host dos formulários, sem chave de API: POST https://forms.postal.so/{slug}. O corpo pode ser um formulário HTML comum (application/x-www-form-urlencoded ou multipart/form-data) ou JSON. Os nomes dos campos são os id dos blocos de pages (email, first_name… e as suas perguntas); respostas de lista, como multiple_choice, vão como lista (JSON) ou repetindo o nome (interest[]).

RegraComo funciona
RobôsO campo _website é uma armadilha: deixe-o escondido e vazio. Se ele vier preenchido, a resposta é de sucesso, mas nada é guardado.
Limite5 envios por minuto por IP em cada formulário. Depois disso, 429.
OrigemCom allowed_domains, só páginas desses domínios (e subdomínios) podem enviar pelo navegador; as demais recebem 403 origin_not_allowed. A página hospedada sempre pode.
CORSResponde a OPTIONS e libera POST com os cabeçalhos Content-Type e Accept: para qualquer origem, ou só para os allowed_domains. Não usa cookies.
IdiomaA página hospedada, as mensagens e o e-mail de confirmação seguem o idioma do formulário.
Formulário pausado ou inexistente404.
Formulário encerrado410 com form_closed (JSON) quando o formulário passou de closes_at, chegou a max_submissions ou foi pausado com uma closed_message; a mensagem é o texto de closed_message (como você o escreveu) ou, sem ele, "Este formulário não está mais recebendo respostas." (ou "This form is no longer accepting answers.", conforme o idioma do formulário). Veja Configurações e encerramento.
Respostas inválidas422: obrigatória em branco, valor fora do tipo ou das opções, e-mail recusado por validate_email ou repetido com prevent_duplicates. Só os blocos visíveis no caminho percorrido são conferidos.
E-mail na lista de supressãoA pessoa vê a mesma resposta de sucesso, mas nenhum contato é criado (e, com double opt-in, nenhum e-mail de confirmação é enviado). O envio fica como failed em Listar envios.

Formulário HTML

Um envio de formulário comum termina num redirecionamento 303: para o redirect_url, se houver (e sem double opt-in), ou para a tela de sucesso da Postal (/{slug}/obrigado). Se algum campo estiver errado, a página hospedada aparece com os erros.

HTML no seu site
<form action="https://forms.postal.so/newsletter-k3x9p2qa" method="post">
  <input type="email" name="email" placeholder="[email protected]" required>
  <input type="text" name="first_name" placeholder="Seu nome">

  <!-- Armadilha para robôs: fica escondida e precisa chegar vazia -->
  <input type="text" name="_website" tabindex="-1" autocomplete="off" hidden>

  <button type="submit">Inscrever-se</button>
</form>

JavaScript (JSON)

Com Accept: application/json ou corpo JSON, a resposta é JSON: 201 com os textos para mostrar na sua página, ou um erro no formato padrão. No 422, o campo extra errors traz as mensagens de cada campo (a chave é o id do bloco), no idioma do formulário, e suggestion traz a correção de um e-mail com provável erro de digitação, quando validate_email está ligado.

JavaScript no seu site
const response = await fetch('https://forms.postal.so/newsletter-k3x9p2qa', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
  body: JSON.stringify({ email: '[email protected]', first_name: 'Ana', interest: 'news', _website: '' }),
});

const body = await response.json();

if (response.ok) {
  showMessage(body.title, body.message); // "Confirme seu e-mail" com double opt-in
} else if (response.status === 410) {
  showMessage(body.message);             // form_closed: o formulário encerrou
} else {
  showError(body.message);              // 422: body.errors traz o erro de cada campo
}
201Resposta
{
  "object": "form_submission",
  "id": "019a1e20-8091-7ca2-9d1e-3f4a5b6c7d8e",
  "requires_confirmation": true,
  "title": "Confirme seu e-mail",
  "message": "Enviamos um link para o seu e-mail. Clique nele para confirmar a inscrição.",
  "redirect_url": null
}
422Resposta com erro
{
  "statusCode": 422,
  "name": "validation_error",
  "message": "E-mail: Informe um e-mail válido.",
  "errors": {
    "email": "Informe um e-mail válido."
  },
  "suggestion": null
}
410Formulário encerrado
{
  "statusCode": 410,
  "name": "form_closed",
  "message": "As inscrições terminaram em 31/10. Obrigado pelo interesse!"
}

Idioma do formulário

A página hospedada, as mensagens que a Postal escreve (validação, duplicidade, formulário encerrado ou indisponível, limite de tentativas), os textos padrão de sucesso e do botão e o e-mail de confirmação do double opt-in saem em pt-BR ou en. A Postal escolhe nesta ordem:

  1. settings.language do formulário, quando é pt-BR ou en;
  2. o Accept-Language de quem abre ou envia o formulário (o primeiro entre pt-BR e en, na ordem do cabeçalho);
  3. pt-BR, quando não há nenhum dos dois.

O e-mail de confirmação sai no idioma em que a pessoa viu o formulário ao enviar. O que você escreveu (títulos, perguntas, a mensagem de sucesso, closed_message) nunca é traduzido. Para fixar o idioma, envie settings.language ao criar ou atualizar o formulário.

Quando o contato entra na audiência (na hora ou depois da confirmação), a Postal dispara o webhook form.submitted, além de contact.created ou contact.updated. Quem se inscreve por um formulário volta a receber envios de marketing mesmo que tenha se descadastrado antes.