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, image | Conteúdo: aparece, mas não recebe resposta. image exige media. |
email, first_name, last_name | Campos do próprio contato. |
short_text, long_text, phone | Texto (até 255, 5.000 e 32 caracteres). |
number, date | Número (com min e max opcionais) e data AAAA-MM-DD. |
single_choice, multiple_choice, dropdown | Escolha: 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, consent | Sim ou não, e a caixa de consentimento (true quando marcada; o envio guarda a data e o IP do consentimento). |
rating, nps | Nota de 1 até max (de 3 a 10; padrão 5) e NPS de 0 a 10. |
hidden | Valor 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.
{
"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.
| Campo | Padrão | O que faz |
|---|---|---|
display | pages | pages mostra os blocos de uma página juntos; one_by_one mostra um bloco por vez. |
show_progress | true | Barra de progresso. |
next_label, back_label | null | Textos dos botões de avançar e voltar (até 60 caracteres). |
welcome | desligada | Tela de boas-vindas: enabled, title, description (até 2.000), button_label e media. |
keyboard_hints | true | Dicas de teclado na página hospedada. |
transition | slide | slide, fade ou none. |
validate_email | false | Confere 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_at | null | Data e hora (ISO 8601) em que o formulário para de receber envios. |
max_submissions | null | Número de envios (de 1 a 10.000.000) a partir do qual o formulário encerra. |
closed_message | null | Texto (até 2.000 caracteres) mostrado a quem chega depois do encerramento. |
prevent_duplicates | false | Recusa (422) um e-mail que já respondeu ao formulário; envios que falharam não contam. |
show_branding | null | false esconde o selo "Enviado com Postal" na página pública. Só planos que não incluem o selo permitem (403 plan_limit_reached). |
language | auto | Idioma 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
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
limitintegerDe 1 a 100. Padrão: 20.afterstringCursor da próxima página.beforestringCursor da página anterior.searchstringBusca no nome.
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);
{
"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
Devolve o formulário completo, com o public_url e os valores padrão aplicados.
Corpo da requisição
namestringobrigatórioNome interno, até 255 caracteres.segment_idstringobrigatórioSegmento estático que recebe os contatos.slugstringEndereç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, comonewsletter-k3x9p2qa.statusstringactive(padrão) oupaused(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). Quandopagesefieldsvêm juntos, valepages.settingsobjectConfigurações do fluxo e do encerramento.fieldsobject[]De 1 a 20 campos, na ordem de exibição. O campoemailé obrigatório. Padrão: só o e-mail. É o formato simples: vira uma página só, e para perguntas, lógica e mídia usepages.keystringobrigatórioemail,first_name,last_nameou a chave de uma propriedade. O tipo do campo segue o tipo da propriedade.labelstringRótulo, até 255 caracteres.placeholderstringTexto de exemplo, até 255 caracteres.requiredbooleanObrigatório. O e-mail é sempre obrigatório.
double_opt_inbooleanPede confirmação por e-mail antes de incluir o contato.contentobjectTextos da página hospedada:title,description(até 2.000 caracteres) elogo_url.successobjectTela de sucesso:logo_url,title,message,button_label,button_urlemedia. Semtitleemessage, 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 | nullURL para onde mandar a pessoa depois do envio (ou da confirmação, com double opt-in), em vez da tela de sucesso.styleobjectAparência:layout(stackedouinline),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 deGET /forms/themes),logo_url(https) ebackground({"type": "color|gradient|image", …}, comcolor,gradient{from, to, angle}ouimage_url, maisoverlayde 0 a 0,85 eoverlay_color).allowed_domainsstring[]Até 50 domínios que podem enviar o formulário pelo navegador, comolojaexemplo.com.br(inclui subdomínios). Vazio: qualquer site.notificationsobjectAvisos por e-mail a cada envio, veja Configurações e encerramento.
Erros
- 422
validation_errorCampo inválido, slug em uso, páginas ou blocos fora das regras (vejaerrors), formulário sem o blocoemailou campo que não é contato nem propriedade. - 404
not_foundO segmento ou algum tópico não existe. - 403
plan_limit_reachedO plano não permite mais formulários, ou o plano mostra o selo da Postal e o formulário pediusettings.show_branding: false.
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);
{
"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}
Parâmetros de caminho
idstringobrigatórioID do formulário.
Erros
- 404
not_foundO recurso não existe ou é de outro time.
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);
{
"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}
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órioID do formulário.
Erros
- 422
validation_errorAlgum campo é inválido. A mensagem diz qual. - 403
paused_by_planO formulário está pausado porque passa do limite do plano. - 403
plan_limit_reachedsettings.show_branding: falsenum plano que mostra o selo da Postal. - 404
not_foundO recurso não existe ou é de outro time.
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);
{
"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}
O endereço público deixa de funcionar. Os contatos já inscritos continuam na audiência.
Parâmetros de caminho
idstringobrigatórioID do formulário.
Erros
- 404
not_foundO recurso não existe ou é de outro time.
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);
{
"object": "form",
"id": "019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d",
"deleted": true
}
#Temas prontos
GET/forms/themes
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).
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);
{
"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
A biblioteca de ilustrações para usar em media ("type": "illustration"), nas opções de escolha e no fundo do formulário.
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);
{
"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
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órioID do formulário.
Parâmetros de consulta
limitintegerDe 1 a 100. Padrão: 20.afterstringCursor da próxima página.beforestringCursor da página anterior.searchstringBusca no e-mail, no nome e no texto das respostas.statusstringUm ou mais status, separados por vírgula:pending,pending_confirmation,subscribed,failed.start_datestringInício do período, inclusive: uma data (2026-10-01) ou um horário ISO 8601.end_datestringFim do período, inclusive. Uma data sozinha cobre o dia inteiro (em UTC).422se vier antes destart_date.
Erros
- 422
validation_errorAlgum campo é inválido. A mensagem diz qual. - 404
not_foundO recurso não existe ou é de outro time.
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);
{
"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}
Parâmetros de caminho
idstringobrigatórioID do formulário.submission_idstringobrigatórioID do envio.
Erros
- 404
not_foundO formulário ou o envio não existe.
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);
{
"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}
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órioID do formulário.submission_idstringobrigatórioID do envio.
Erros
- 404
not_foundO formulário ou o envio não existe.
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);
{
"object": "form_submission",
"id": "019a1e20-8091-7ca2-9d1e-3f4a5b6c7d8e",
"deleted": true
}
#Exportar os envios de um formulário
GET/forms/{id}/submissions/export
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órioID do formulário.
Parâmetros de consulta
searchstringBusca no e-mail, no nome e no texto das respostas.statusstringUm ou mais status, separados por vírgula:pending,pending_confirmation,subscribed,failed.start_datestringInício do período, inclusive: uma data (2026-10-01) ou um horário ISO 8601.end_datestringFim do período, inclusive. Uma data sozinha cobre o dia inteiro (em UTC).422se vier antes destart_date.
Erros
- 422
validation_errorAlgum campo é inválido. A mensagem diz qual. - 403
plan_limit_reachedO plano não inclui exportações. - 404
not_foundO recurso não existe ou é de outro time.
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);
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
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
limitintegerDe 1 a 100. Padrão: 20.afterstringCursor da próxima página.beforestringCursor da página anterior.form_idstringSó os envios deste formulário.searchstringBusca no e-mail, no nome e no texto das respostas.statusstringUm ou mais status, separados por vírgula:pending,pending_confirmation,subscribed,failed.start_datestringInício do período, inclusive: uma data (2026-10-01) ou um horário ISO 8601.end_datestringFim do período, inclusive. Uma data sozinha cobre o dia inteiro (em UTC).422se vier antes destart_date.
Erros
- 422
validation_errorAlgum campo é inválido. A mensagem diz qual.
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);
{
"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
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_idstringSó os envios deste formulário.searchstringBusca no e-mail, no nome e no texto das respostas.statusstringUm ou mais status, separados por vírgula:pending,pending_confirmation,subscribed,failed.start_datestringInício do período, inclusive: uma data (2026-10-01) ou um horário ISO 8601.end_datestringFim do período, inclusive. Uma data sozinha cobre o dia inteiro (em UTC).422se vier antes destart_date.
Erros
- 422
validation_errorAlgum campo é inválido. A mensagem diz qual. - 403
plan_limit_reachedO plano não inclui exportações.
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);
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
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
fromstringInício do período. Padrão: 30 dias atrás.tostringFim do período. Padrão: agora.tzstringFuso horário IANA. Padrão:UTC.intervalstringday(padrão) ouhour.
Erros
- 422
validation_errorAlgum campo é inválido. A mensagem diz qual. - 403
plan_limit_reachedO plano não inclui a análise de formulários.
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);
{
"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
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órioID do formulário.
Parâmetros de consulta
fromstringInício do período. Padrão: 30 dias atrás.tostringFim do período. Padrão: agora.tzstringFuso horário IANA. Padrão:UTC.intervalstringday(padrão) ouhour.
Erros
- 422
validation_errorAlgum campo é inválido. A mensagem diz qual. - 403
plan_limit_reachedO plano não inclui a análise de formulários. - 404
not_foundO recurso não existe ou é de outro time.
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);
{
"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
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órioID do formulário.
Parâmetros de consulta
fromstringInício do período. Padrão: 30 dias atrás.tostringFim do período. Padrão: agora.tzstringFuso horário IANA. Padrão:UTC.
Erros
- 422
validation_errorAlgum campo é inválido. A mensagem diz qual. - 403
plan_limit_reachedO plano não inclui a análise de formulários. - 404
not_foundO recurso não existe ou é de outro time.
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);
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
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órioID do formulário.questionstringobrigatórioiddo bloco da pergunta.
Parâmetros de consulta
limitintegerDe 1 a 100. Padrão: 20.afterstringCursor da próxima página.beforestringCursor da página anterior.fromstringInício do período. Padrão: 30 dias atrás.tostringFim do período. Padrão: agora.tzstringFuso horário IANA. Padrão:UTC.
Erros
- 422
validation_errorA pergunta não é de texto, ou o período é inválido. - 403
plan_limit_reachedO plano não inclui a análise de formulários. - 404
not_foundO formulário ou a pergunta não existe.
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);
{
"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
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órioID do formulário.emailstringobrigatórioEndereço do destinatário, já listado emnotifications.recipients.
Erros
- 422
validation_errorO destinatário já confirmou. - 429
rate_limit_exceededConvites demais para este endereço. Tente amanhã. - 404
not_foundO formulário ou o destinatário não existe.
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);
{
"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
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órioID do formulário.
Erros
- 422
validation_errorNenhum destinatário confirmou ainda. - 429
rate_limit_exceededTestes demais. Tente em alguns minutos. - 404
not_foundO recurso não existe ou é de outro time.
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);
{
"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[]).
| Regra | Como funciona |
|---|---|
| Robôs | O campo _website é uma armadilha: deixe-o escondido e vazio. Se ele vier preenchido, a resposta é de sucesso, mas nada é guardado. |
| Limite | 5 envios por minuto por IP em cada formulário. Depois disso, 429. |
| Origem | Com 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. |
| CORS | Responde 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. |
| Idioma | A página hospedada, as mensagens e o e-mail de confirmação seguem o idioma do formulário. |
| Formulário pausado ou inexistente | 404. |
| Formulário encerrado | 410 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álidas | 422: 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ão | A 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.
<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.
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
}
{
"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
}
{
"statusCode": 422,
"name": "validation_error",
"message": "E-mail: Informe um e-mail válido.",
"errors": {
"email": "Informe um e-mail válido."
},
"suggestion": null
}
{
"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:
settings.languagedo formulário, quando épt-BRouen;- o
Accept-Languagede quem abre ou envia o formulário (o primeiro entrept-BReen, na ordem do cabeçalho); 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.