Formularios
Formularios de suscripción alojados o en tu sitio, con double opt-in.
Los formularios de suscripción incorporan personas a tu audiencia: cada envío crea (o actualiza) un contacto, lo agrega al segmento del formulario y lo suscribe a los temas elegidos. Puedes usar la página lista, alojada en https://forms.postal.so/{slug}, o enviar desde tu propio sitio (HTML o JavaScript), como muestra Envío público. Con double opt-in, la persona recibe un correo electrónico de confirmación y solo entra en la audiencia después de hacer clic en el enlace.
Un formulario se compone de páginas y bloques (preguntas, con lógica entre las páginas), tiene configuración de cierre, avisos por correo electrónico a cada envío, estadísticas de visitas y de conversión, y una lista de envíos de todos los formularios. Todos los endpoints de esta página usan una clave de API de acceso total; el envío público no usa clave.
#Páginas y bloques
pages es la lista de páginas del formulario (de 1 a 10), cada una con su propio id (hasta 32 letras minúsculas, números, - o _; submit está reservado), title, description, media, blocks (de 1 a 30 por página y 60 en el formulario) y logic. El id de cada bloque (letra minúscula seguida de hasta 63 letras minúsculas, números o _) es también la clave de la respuesta en el envío. Todo formulario tiene un bloque email, siempre obligatorio, siempre visible y en una página por la que pasa toda respuesta.
Tipo (type) | Qué es |
|---|---|
heading, paragraph, image | Contenido: se muestra, pero no recibe respuesta. image exige media. |
email, first_name, last_name | Campos del propio contacto. |
short_text, long_text, phone | Texto (hasta 255, 5.000 y 32 caracteres). |
number, date | Número (con min y max opcionales) y fecha AAAA-MM-DD. |
single_choice, multiple_choice, dropdown | Elección: exigen de 1 a 50 options (value, label, e image_url o illustration en las dos primeras). Aceptan randomize y other (la opción "Otro", cuya respuesta se convierte en other:texto); las de imagen aceptan columns (2, 3 o 4). |
yes_no, consent | Sí o no, y la casilla de consentimiento (true cuando está marcada; el envío guarda la fecha y la IP del consentimiento). |
rating, nps | Nota de 1 hasta max (de 3 a 10; predeterminado 5) y NPS de 0 a 10. |
hidden | Valor que no se muestra: en la página alojada, viene de la query string (por ejemplo, ?utm_source=blog). |
Todo bloque acepta label (obligatorio, salvo en contenido y hidden), description, required, media y visible_if. placeholder vale para los campos de texto, correo electrónico, número y dropdown. property guarda la respuesta en una propiedad de contacto (que debe existir, tener un tipo compatible con el bloque y ser usada por un solo bloque). Los campos email, first_name y last_name ya se guardan en el contacto.
Condiciones y lógica entre páginas
visible_if (en un bloque) oculta el bloque mientras la condición sea falsa, y logic (en una página) manda a la persona a otra página después de responder. Una condición es {"field", "operator", "value"}, donde field es el id de un bloque que viene antes y operator es equals, not_equals, contains, not_contains, is_empty, is_not_empty, greater_than o less_than. Cada regla de logic es {"when": condição, "go_to": "id da página"}; vale la primera que coincida, y go_to apunta a una página posterior o a "submit" (hasta 20 reglas por página). En el envío, solo se validan y se guardan las respuestas de los bloques visibles en el camino que recorrió la persona.
{
"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" }]
}
Los errores de estructura llegan como 422 validation_error con message (el primer problema) y errors: una lista de {"page_id", "block_id", "message"}, en inglés. Los medios (media) son {"type": "image|illustration|video", "url", "illustration", "alt", "layout": "above|split_left|split_right|background"}: las imágenes necesitan una URL https (sube el archivo con POST /assets) y un alt; las ilustraciones son las de GET /forms/illustrations; los videos son enlaces de YouTube o de Vimeo.
#Configuración y cierre
El objeto settings controla el flujo y el cierre del formulario. Todo es opcional; lo que no envíes toma el valor predeterminado. Al actualizar, el objeto enviado reemplaza al anterior.
| Campo | Predeterminado | Qué hace |
|---|---|---|
display | pages | pages muestra los bloques de una página juntos; one_by_one muestra un bloque cada vez. |
show_progress | true | Barra de progreso. |
next_label, back_label | null | Textos de los botones de avanzar y volver (hasta 60 caracteres). |
welcome | desactivada | Pantalla de bienvenida: enabled, title, description (hasta 2.000), button_label y media. |
keyboard_hints | true | Atajos de teclado en la página alojada. |
transition | slide | slide, fade o none. |
validate_email | false | Comprueba el correo electrónico antes de aceptar el envío: rechaza direcciones desechables y dominios que no reciben correo, y señala un probable cambio de letras (gmial.com) en suggestion una sola vez (la misma dirección enviada de nuevo en 15 minutos pasa). No gasta créditos de verificación. |
closes_at | null | Fecha y hora (ISO 8601) en que el formulario deja de recibir envíos. |
max_submissions | null | Número de envíos (de 1 a 10.000.000) a partir del cual el formulario se cierra. |
closed_message | null | Texto (hasta 2.000 caracteres) que se muestra a quien llega después del cierre. |
prevent_duplicates | false | Rechaza (422) un correo electrónico que ya respondió al formulario; los envíos que fallaron no cuentan. |
show_branding | null | false oculta el sello "Enviado con Postal" en la página pública. Solo lo permiten los planes que no incluyen el sello (403 plan_limit_reached). |
language | auto | Idioma de lo que Postal escribe a quien rellena el formulario: auto, pt-BR o en (null equivale a auto). Con auto, sigue el Accept-Language del navegador de la persona (pt-BR o en; sin el encabezado o con otro idioma, pt-BR). Vale para la página alojada, los mensajes (validación, duplicidad, formulario cerrado o no disponible), los textos predeterminados de éxito y del botón y el correo electrónico de confirmación del double opt-in. Los textos que tú escribiste (títulos, preguntas, mensaje de éxito, closed_message) nunca se traducen. Consulta Envío público. |
El formulario devuelve closed_reason: null mientras está abierto, manual (status: "paused"), scheduled (pasó closes_at) o limit (llegó a max_submissions). Un formulario cerrado responde 410 con el nombre form_closed a los envíos en JSON (y muestra el mensaje en la página alojada); un formulario pausado a mano sin closed_message, o pausado por el plan, responde 404 not_found.
En notifications, el formulario avisa por correo electrónico a cada envío: enabled, recipients (hasta 10 direcciones), include_answers (predeterminado true), reply_to_respondent (predeterminado true), subject (hasta 200 caracteres) y frequency (instant o daily, un resumen a las 8h en horario de Brasilia). Una dirección que no es de un miembro del equipo recibe una invitación y solo empieza a recibir avisos tras confirmar (status: pending o confirmed); tú solo envías direcciones, y el estado lo controla Postal. Estos avisos no cuentan en la cuota de envío.
#Listar formularios
GET/forms
Cada formulario de la lista trae también los números de los últimos 30 días: views_30d, submissions_30d, conversion_30d (envíos sobre visitas, de 0 a 1) y last_submission_at. paused_by_plan es true cuando el plan dejó de admitir el formulario: sigue en la lista y se puede leer o eliminar, pero no se puede modificar (403 paused_by_plan).
Parámetros de consulta
limitintegerDe 1 a 100. Predeterminado: 20.afterstringCursor de la página siguiente.beforestringCursor de la página anterior.searchstringBúsqueda por nombre.
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"
}
]
}
#Crear un formulario
POST/forms
Devuelve el formulario completo, con el public_url y los valores predeterminados aplicados.
Cuerpo de la solicitud
namestringobligatorioNombre interno, hasta 255 caracteres.segment_idstringobligatorioSegmento estático que recibe los contactos.slugstringDirección pública, de 3 a 64 caracteres (letras minúsculas, números y guiones), única en Postal. Predeterminado: generado a partir del nombre, comonewsletter-k3x9p2qa.statusstringactive(predeterminado) opaused(no acepta envíos).topic_idsstring[]Hasta 20 temas a los que se suscribe el contacto (opt_in).pagesobject[]Las páginas y los bloques del formulario. Un formulario nuevo empieza con una sola página con el bloque de correo electrónico (etiqueta y texto de ejemplo en el idioma de la solicitud). Cuandopagesyfieldsllegan juntos, valepages.settingsobjectConfiguración del flujo y del cierre.fieldsobject[]De 1 a 20 campos, en el orden de visualización. El campoemailes obligatorio. Predeterminado: solo el correo electrónico. Es el formato simple: se convierte en una sola página, y para preguntas, lógica y medios usapages.keystringobligatorioemail,first_name,last_nameo la clave de una propiedad. El tipo del campo sigue el tipo de la propiedad.labelstringEtiqueta, hasta 255 caracteres.placeholderstringTexto de ejemplo, hasta 255 caracteres.requiredbooleanObligatorio. El correo electrónico siempre es obligatorio.
double_opt_inbooleanPide confirmación por correo electrónico antes de incluir al contacto.contentobjectTextos de la página alojada:title,description(hasta 2.000 caracteres) ylogo_url.successobjectPantalla de éxito:logo_url,title,message,button_label,button_urlymedia. Sintitleymessage, la página usa un texto predeterminado en el idioma del formulario (en las respuestas de la API, el valor predeterminado aparece en el idioma de la solicitud).redirect_urlstring | nullURL a la que enviar a la persona después del envío (o de la confirmación, con double opt-in), en lugar de la pantalla de éxito.styleobjectApariencia:layout(stackedoinline),font(system,serif,mono,rounded,inter,plus_jakarta_sans,dm_sans,space_grotesk,fraunces,playfair_display,ibm_plex_mono), colores en hexadecimal (accent_color,text_color,background_color,button_color,button_text_color,answer_color),button_text,radius(none,sm,md,lg,full;roundedes el formato antiguo),preset(un tema deGET /forms/themes),logo_url(https) ybackground({"type": "color|gradient|image", …}, concolor,gradient{from, to, angle}oimage_url, másoverlayde 0 a 0,85 yoverlay_color).allowed_domainsstring[]Hasta 50 dominios que pueden enviar el formulario desde el navegador, comolojaexemplo.com.br(incluye subdominios). Vacío: cualquier sitio.notificationsobjectAvisos por correo electrónico a cada envío, consulta Configuración y cierre.
Errores
- 422
validation_errorCampo no válido, slug en uso, páginas o bloques fuera de las reglas (consultaerrors), formulario sin el bloqueemailo campo que no es de contacto ni de propiedad. - 404
not_foundEl segmento o algún tema no existe. - 403
plan_limit_reachedEl plan ya no permite más formularios, o el plan muestra el sello de Postal y el formulario pidiósettings.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"
}
#Obtener un formulario
GET/forms/{id}
Parámetros de ruta
idstringobligatorioID del formulario.
Errores
- 404
not_foundEl recurso no existe o es de otro equipo.
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"
}
#Actualizar un formulario
PATCH/forms/{id}
Se aceptan todos los campos de creación, todos opcionales. En pages, content, success, style y settings, el objeto enviado reemplaza al anterior. Devuelve el formulario actualizado.
Parámetros de ruta
idstringobligatorioID del formulario.
Errores
- 422
validation_errorAlgún campo no es válido. El mensaje indica cuál. - 403
paused_by_planEl formulario está pausado porque supera el límite del plan. - 403
plan_limit_reachedsettings.show_branding: falseen un plan que muestra el sello de Postal. - 404
not_foundEl recurso no existe o es de otro equipo.
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"
}
#Eliminar un formulario
DELETE/forms/{id}
La dirección pública deja de funcionar. Los contactos ya suscritos siguen en la audiencia.
Parámetros de ruta
idstringobligatorioID del formulario.
Errores
- 404
not_foundEl recurso no existe o es de otro equipo.
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 listos
GET/forms/themes
Los temas listos del editor. Cada uno trae un style completo para usar en style al crear o actualizar un formulario (preset: postal, minimal, midnight, sunset, forest, ocean, paper o 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"
}
}
]
}
#Ilustraciones listas
GET/forms/illustrations
La biblioteca de ilustraciones para usar en media ("type": "illustration"), en las opciones de elección y en el fondo del formulario.
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 envíos de un formulario
GET/forms/{id}/submissions
Cada envío recibido, del más reciente al más antiguo. status: pending (en cola), pending_confirmation (correo electrónico de confirmación enviado), subscribed (contacto en la audiencia) o failed (por ejemplo, se alcanzó el límite de contactos del plan o el correo electrónico está en la lista de supresión). answers trae las respuestas por el id de cada bloque, y consent la fecha y la IP del consentimiento, cuando lo hubo.
Parámetros de ruta
idstringobligatorioID del formulario.
Parámetros de consulta
limitintegerDe 1 a 100. Predeterminado: 20.afterstringCursor de la página siguiente.beforestringCursor de la página anterior.searchstringBusca en el correo electrónico, en el nombre y en el texto de las respuestas.statusstringUno o más estados, separados por comas:pending,pending_confirmation,subscribed,failed.start_datestringInicio del período, inclusive: una fecha (2026-10-01) o una hora ISO 8601.end_datestringFin del período, inclusive. Una fecha sola cubre el día completo (en UTC).422si es anterior astart_date.
Errores
- 422
validation_errorAlgún campo no es válido. El mensaje indica cuál. - 404
not_foundEl recurso no existe o es de otro equipo.
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"
}
]
}
#Obtener un envío
GET/forms/{id}/submissions/{submission_id}
Parámetros de ruta
idstringobligatorioID del formulario.submission_idstringobligatorioID del envío.
Errores
- 404
not_foundEl formulario o el envío no 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"
}
#Eliminar un envío
DELETE/forms/{id}/submissions/{submission_id}
Borra el registro del envío y descuenta uno de submissions_count. El contacto creado por él sigue en la audiencia; para quitarlo, elimina el contacto.
Parámetros de ruta
idstringobligatorioID del formulario.submission_idstringobligatorioID del envío.
Errores
- 404
not_foundEl formulario o el envío no 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 los envíos de un formulario
GET/forms/{id}/submissions/export
Devuelve un archivo CSV (text/csv, UTF-8 con BOM), al instante y sin paginación. Necesita un plan que incluya exportaciones (403 plan_limit_reached si no las incluye). Una columna por pregunta del formulario. Acepta los mismos filtros de la lista (search, status, start_date y end_date). Una respuesta escrita en la opción «Otro» sale como Outro: texto (o Other: texto, según el Accept-Language de la solicitud).
Parámetros de ruta
idstringobligatorioID del formulario.
Parámetros de consulta
searchstringBusca en el correo electrónico, en el nombre y en el texto de las respuestas.statusstringUno o más estados, separados por comas:pending,pending_confirmation,subscribed,failed.start_datestringInicio del período, inclusive: una fecha (2026-10-01) o una hora ISO 8601.end_datestringFin del período, inclusive. Una fecha sola cubre el día completo (en UTC).422si es anterior astart_date.
Errores
- 422
validation_errorAlgún campo no es válido. El mensaje indica cuál. - 403
plan_limit_reachedEl plan no incluye exportaciones. - 404
not_foundEl recurso no existe o es de otro equipo.
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 los envíos de todos los formularios
GET/forms/submissions
Los envíos de todos los formularios del equipo en una sola lista, del más reciente al más antiguo. Cada fila trae el formulario, quién envió, las primeras respuestas (answers_preview) y de dónde vino el visitante (utm_source, device, country, cuando se midió la visita).
Parámetros de consulta
limitintegerDe 1 a 100. Predeterminado: 20.afterstringCursor de la página siguiente.beforestringCursor de la página anterior.form_idstringSolo los envíos de este formulario.searchstringBusca en el correo electrónico, en el nombre y en el texto de las respuestas.statusstringUno o más estados, separados por comas:pending,pending_confirmation,subscribed,failed.start_datestringInicio del período, inclusive: una fecha (2026-10-01) o una hora ISO 8601.end_datestringFin del período, inclusive. Una fecha sola cubre el día completo (en UTC).422si es anterior astart_date.
Errores
- 422
validation_errorAlgún campo no es válido. El mensaje indica cuál.
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 los envíos de todos los formularios
GET/forms/submissions/export
Devuelve un archivo CSV (text/csv, UTF-8 con BOM), al instante y sin paginación. Necesita un plan que incluya exportaciones (403 plan_limit_reached si no las incluye). Una fila por envío, con las respuestas en answers como pares "pregunta: respuesta" y las columnas id, created_at, form_id, form_name, status, email, name, contact_id, utm_source, utm_medium, utm_campaign, source, device y country. Acepta los mismos filtros de la lista. Una respuesta escrita en la opción «Otro» sale como Outro: texto (o Other: texto, según el Accept-Language de la solicitud).
Parámetros de consulta
form_idstringSolo los envíos de este formulario.searchstringBusca en el correo electrónico, en el nombre y en el texto de las respuestas.statusstringUno o más estados, separados por comas:pending,pending_confirmation,subscribed,failed.start_datestringInicio del período, inclusive: una fecha (2026-10-01) o una hora ISO 8601.end_datestringFin del período, inclusive. Una fecha sola cubre el día completo (en UTC).422si es anterior astart_date.
Errores
- 422
validation_errorAlgún campo no es válido. El mensaje indica cuál. - 403
plan_limit_reachedEl plan no incluye exportaciones.
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
#Estadísticas de los formularios
Las estadísticas cuentan las visitas a la página alojada y a los formularios incrustados (Postal mide las visitas mediante una señal que envía la propia página; los envíos de otros sistemas aparecen en all_submissions, pero no en visitas), cuánto avanzó cada persona y los envíos. Necesitan un plan con análisis de formularios (403 plan_limit_reached si no lo tiene). Todas aceptan el período en from y to (fechas u horas ISO 8601; una fecha sola cubre el día completo; predeterminado: los últimos 30 días; hasta 400 días), en tz (zona horaria IANA, como America/Sao_Paulo; predeterminado UTC) e interval (day o hour). El fin nunca pasa de ahora; 422 si from es posterior a to, si la zona horaria no existe o si el período genera más de 2.000 intervalos. Los números más pesados se recalculan como máximo una vez por minuto.
#Resumen de todos los formularios
GET/forms/overview
Las estadísticas de todos los formularios del equipo sumadas, en el mismo período y con los mismos parámetros del análisis de un formulario. Cada número de totals llega como {"value", "previous"}, con el período anterior de igual duración; series trae los intervalos; top_forms, los 10 formularios con más visitas (con sparkline de envíos por día); breakdowns, los 8 principales orígenes, UTMs, dispositivos, navegadores, países y posiciones; hours_heatmap; recent_submissions; y list_quality de todos los formularios.
list_quality mide la calidad de la audiencia que trajeron los envíos del período: submissions recibidos y contacts distintos que se convirtieron en contacto; double_opt_in (confirmed y pending); failed (envíos que no se convirtieron en contacto) y suppressed_rejections (los que se rechazaron por estar en la lista de supresión); unsubscribed; suppressed (contactos cuya dirección entró en la lista de supresión, por origen: bounce, complaint, manual…); hard_bounce_rate (rebotes permanentes sobre contactos); y verification (valid, invalid, risky y unverified, de la verificación de correos).
Parámetros de consulta
fromstringInicio del período. Predeterminado: 30 días atrás.tostringFin del período. Predeterminado: ahora.tzstringZona horaria IANA. Predeterminado:UTC.intervalstringday(predeterminado) ohour.
Errores
- 422
validation_errorAlgún campo no es válido. El mensaje indica cuál. - 403
plan_limit_reachedEl plan no incluye el análisis de formularios.
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
}
}
}
#Estadísticas de un formulario
GET/forms/{id}/analytics
Las visitas, la conversión y las respuestas de un formulario en el período. Campos principales: totals (views, unique_visitors, starts, submissions, all_submissions, abandoned, bounced, las tasas start_rate, completion_rate y conversion_rate de 0 a 1, y el tiempo promedio y mediano en segundos); previous (los mismos totales en el período anterior de igual duración); funnel (cada etapa con cuántas visitas llegaron, dónde abandonaron las personas y el biggest_drop); timeseries; breakdowns (por origen, UTM, dispositivo, navegador, país y posición: page, iframe o inline); questions (un resumen por pregunta, con summary.kind: choice, yes_no, consent, rating, nps, number, date, text o values); heatmap (día de la semana por hora); recent_submitters y list_quality. sampled es true cuando el período tiene más de 20.000 envíos y las preguntas leen solo los más recientes.
list_quality mide la calidad de la audiencia que trajeron los envíos del período: submissions recibidos y contacts distintos que se convirtieron en contacto; double_opt_in (confirmed y pending); failed (envíos que no se convirtieron en contacto) y suppressed_rejections (los que se rechazaron por estar en la lista de supresión); unsubscribed; suppressed (contactos cuya dirección entró en la lista de supresión, por origen: bounce, complaint, manual…); hard_bounce_rate (rebotes permanentes sobre contactos); y verification (valid, invalid, risky y unverified, de la verificación de correos).
Parámetros de ruta
idstringobligatorioID del formulario.
Parámetros de consulta
fromstringInicio del período. Predeterminado: 30 días atrás.tostringFin del período. Predeterminado: ahora.tzstringZona horaria IANA. Predeterminado:UTC.intervalstringday(predeterminado) ohour.
Errores
- 422
validation_errorAlgún campo no es válido. El mensaje indica cuál. - 403
plan_limit_reachedEl plan no incluye el análisis de formularios. - 404
not_foundEl recurso no existe o es de otro equipo.
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 las visitas de un formulario
GET/forms/{id}/analytics/export
Un archivo CSV (text/csv) con una fila por visita del período: cuándo vio, empezó y envió, la etapa más lejana a la que llegó, el dispositivo y el origen. Sin datos personales, respuestas ni IDs de envío. Acepta from, to y tz.
Parámetros de ruta
idstringobligatorioID del formulario.
Parámetros de consulta
fromstringInicio del período. Predeterminado: 30 días atrás.tostringFin del período. Predeterminado: ahora.tzstringZona horaria IANA. Predeterminado:UTC.
Errores
- 422
validation_errorAlgún campo no es válido. El mensaje indica cuál. - 403
plan_limit_reachedEl plan no incluye el análisis de formularios. - 404
not_foundEl recurso no existe o es de otro equipo.
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
#Respuestas de una pregunta de texto
GET/forms/{id}/analytics/questions/{question}/answers
Lista las respuestas a una pregunta de texto (short_text, long_text, phone o hidden), de las más recientes a las más antiguas, sin decir quién respondió. Las preguntas de elección, nota y otras ya vienen resumidas en el análisis. Acepta el período (from, to, tz) y limit, after y before.
Parámetros de ruta
idstringobligatorioID del formulario.questionstringobligatorioiddel bloque de la pregunta.
Parámetros de consulta
limitintegerDe 1 a 100. Predeterminado: 20.afterstringCursor de la página siguiente.beforestringCursor de la página anterior.fromstringInicio del período. Predeterminado: 30 días atrás.tostringFin del período. Predeterminado: ahora.tzstringZona horaria IANA. Predeterminado:UTC.
Errores
- 422
validation_errorLa pregunta no es de texto, o el período no es válido. - 403
plan_limit_reachedEl plan no incluye el análisis de formularios. - 404
not_foundEl formulario o la pregunta no 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 la invitación de un aviso
POST/forms/{id}/notifications/recipients/{email}/resend
Vuelve a enviar la invitación a un destinatario de los avisos que aún no confirmó (incluso a quien la rechazó: solo su confirmación vuelve a activar los avisos). Devuelve el destinatario con el status y las fechas.
Parámetros de ruta
idstringobligatorioID del formulario.emailstringobligatorioDirección del destinatario, ya listada ennotifications.recipients.
Errores
- 422
validation_errorEl destinatario ya confirmó. - 429
rate_limit_exceededDemasiadas invitaciones para esta dirección. Inténtalo mañana. - 404
not_foundEl formulario o el destinatario no 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 un aviso de prueba
POST/forms/{id}/notifications/test
Envía un aviso con respuestas de ejemplo, con la configuración guardada, a los destinatarios que ya confirmaron. Hasta 5 pruebas por formulario cada 10 minutos.
Parámetros de ruta
idstringobligatorioID del formulario.
Errores
- 422
validation_errorNingún destinatario ha confirmado todavía. - 429
rate_limit_exceededDemasiadas pruebas. Inténtalo en unos minutos. - 404
not_foundEl recurso no existe o es de otro equipo.
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]"]
}
#Envío público
Los envíos van al host de los formularios, sin clave de API: POST https://forms.postal.so/{slug}. El cuerpo puede ser un formulario HTML común (application/x-www-form-urlencoded o multipart/form-data) o JSON. Los nombres de los campos son los id de los bloques de pages (email, first_name… y tus preguntas); las respuestas de lista, como multiple_choice, van como lista (JSON) o repitiendo el nombre (interest[]).
| Regla | Cómo funciona |
|---|---|
| Robots | El campo _website es una trampa: déjalo oculto y vacío. Si llega relleno, la respuesta es de éxito, pero no se guarda nada. |
| Límite | 5 envíos por minuto por IP en cada formulario. Pasado ese límite, 429. |
| Origen | Con allowed_domains, solo las páginas de esos dominios (y subdominios) pueden enviar desde el navegador; las demás reciben 403 origin_not_allowed. La página alojada siempre puede. |
| CORS | Responde a OPTIONS y permite POST con los encabezados Content-Type y Accept: para cualquier origen, o solo para los allowed_domains. No usa cookies. |
| Idioma | La página alojada, los mensajes y el correo electrónico de confirmación siguen el idioma del formulario. |
| Formulario pausado o inexistente | 404. |
| Formulario cerrado | 410 con form_closed (JSON) cuando el formulario pasó de closes_at, llegó a max_submissions o se pausó con un closed_message; el mensaje es el texto de closed_message (tal como lo escribiste) o, sin él, "Este formulário não está mais recebendo respostas." (o "This form is no longer accepting answers.", según el idioma del formulario). Consulta Configuración y cierre. |
| Respuestas no válidas | 422: obligatoria en blanco, valor fuera del tipo o de las opciones, correo electrónico rechazado por validate_email o repetido con prevent_duplicates. Solo se comprueban los bloques visibles en el camino recorrido. |
| Correo en la lista de supresión | La persona ve la misma respuesta de éxito, pero no se crea ningún contacto (y, con double opt-in, no se envía ningún correo electrónico de confirmación). El envío queda como failed en Listar envíos. |
Formulario HTML
Un envío de formulario común termina en una redirección 303: a la redirect_url, si existe (y sin double opt-in), o a la pantalla de éxito de Postal (/{slug}/obrigado). Si algún campo es incorrecto, aparece la página alojada con los errores.
<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)
Con Accept: application/json o un cuerpo JSON, la respuesta es JSON: 201 con los textos para mostrar en tu página, o un error en el formato estándar. En el 422, el campo extra errors trae los mensajes de cada campo (la clave es el id del bloque), en el idioma del formulario, y suggestion trae la corrección de un correo electrónico con un probable error de escritura, cuando validate_email está activado.
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 del formulario
La página alojada, los mensajes que escribe Postal (validación, duplicados, formulario cerrado o no disponible, límite de intentos), los textos predeterminados de éxito y del botón y el correo electrónico de confirmación del double opt-in salen en pt-BR o en. Postal elige en este orden:
- el
settings.languagedel formulario, cuando espt-BRoen; - el
Accept-Languagede quien abre o envía el formulario (el primero entrept-BRyen, en el orden del encabezado); pt-BR, cuando no hay ninguno de los dos.
El correo electrónico de confirmación sale en el idioma en que la persona vio el formulario al enviarlo. Lo que tú escribiste (títulos, preguntas, el mensaje de éxito, closed_message) nunca se traduce. Para fijar el idioma, envía settings.language al crear o actualizar el formulario.
Cuando el contacto entra en la audiencia (al instante o después de la confirmación), Postal dispara el webhook form.submitted, además de contact.created o contact.updated. Quien se suscribe mediante un formulario vuelve a recibir envíos de marketing aunque se haya dado de baja antes.