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.
#Listar formulários
GET/forms
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",
"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"],
"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
},
"redirect_url": null,
"style": {
"layout": "stacked",
"font": "system",
"accent_color": "#4f46e5",
"text_color": "#18181b",
"background_color": "#ffffff",
"button_text": "Inscrever-se",
"button_color": "#4f46e5",
"button_text_color": "#ffffff",
"rounded": true
},
"allowed_domains": ["lojaexemplo.com.br"],
"submissions_count": 412,
"shows_postal_branding": false,
"created_at": "2026-10-21T12:00:00.000000Z",
"updated_at": "2026-10-21T12:00:00.000000Z"
}
]
}
#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 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).fieldsobject[]De 1 a 20 campos, na ordem de exibição. O campoemailé obrigatório. Padrão: só o e-mail.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_labelebutton_url.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), cores em hexadecimal (accent_color,text_color,background_color,button_color,button_text_color),button_texterounded.allowed_domainsstring[]Até 50 domínios que podem enviar o formulário pelo navegador, comolojaexemplo.com.br(inclui subdomínios). Vazio: qualquer site.
Erros
- 422
validation_errorCampo inválido, slug em uso, formulário sem o campoemailou campo que não é contato nem propriedade. - 404
not_foundO segmento ou algum tópico não existe.
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,
"fields": [
{
"key": "email",
"label": "E-mail",
"placeholder": "[email protected]"
},
{
"key": "first_name",
"label": "Nome"
}
],
"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,
fields: [
{
key: 'email',
label: 'E-mail',
placeholder: '[email protected]',
},
{
key: 'first_name',
label: 'Nome',
},
],
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,
"fields": [
{
"key": "email",
"label": "E-mail",
"placeholder": "[email protected]",
},
{
"key": "first_name",
"label": "Nome",
},
],
"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,
'fields' => [
[
'key' => 'email',
'label' => 'E-mail',
'placeholder' => '[email protected]',
],
[
'key' => 'first_name',
'label' => 'Nome',
],
],
'content' => [
'title' => 'Receba nossas novidades',
],
'allowed_domains' => ['lojaexemplo.com.br'],
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "form",
"id": "019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d",
"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"],
"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
},
"redirect_url": null,
"style": {
"layout": "stacked",
"font": "system",
"accent_color": "#4f46e5",
"text_color": "#18181b",
"background_color": "#ffffff",
"button_text": "Inscrever-se",
"button_color": "#4f46e5",
"button_text_color": "#ffffff",
"rounded": true
},
"allowed_domains": ["lojaexemplo.com.br"],
"submissions_count": 0,
"shows_postal_branding": false,
"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",
"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"],
"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
},
"redirect_url": null,
"style": {
"layout": "stacked",
"font": "system",
"accent_color": "#4f46e5",
"text_color": "#18181b",
"background_color": "#ffffff",
"button_text": "Inscrever-se",
"button_color": "#4f46e5",
"button_text_color": "#ffffff",
"rounded": true
},
"allowed_domains": ["lojaexemplo.com.br"],
"submissions_count": 412,
"shows_postal_branding": false,
"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 content, success e style, 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. - 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",
"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"],
"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
},
"redirect_url": null,
"style": {
"layout": "stacked",
"font": "system",
"accent_color": "#4f46e5",
"text_color": "#18181b",
"background_color": "#ffffff",
"button_text": "Inscrever-se",
"button_color": "#4f46e5",
"button_text_color": "#ffffff",
"rounded": true
},
"allowed_domains": ["lojaexemplo.com.br"],
"submissions_count": 412,
"shows_postal_branding": false,
"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
}
#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).
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.
Erros
- 404
not_foundO recurso não existe ou é de outro time.
cURL
curl https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/submissions \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/forms/019a1da0-7f80-7b91-8c0d-2e3f4a5b6c7d/submissions', {
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']}"},
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')],
]);
$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]",
"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"
}
]
}
#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 as chaves de fields.
| 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. |
| Formulário pausado ou inexistente | 404. |
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, em português.
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', _website: '' }),
});
const body = await response.json();
if (response.ok) {
showMessage(body.title, body.message); // "Confirme seu e-mail" com double opt-in
} 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": "Informe um e-mail válido.",
"errors": {
"email": ["Informe um e-mail válido."]
}
}
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.