Iniciar sesión Crear cuenta
Audiencia

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, imageContenido: se muestra, pero no recibe respuesta. image exige media.
email, first_name, last_nameCampos del propio contacto.
short_text, long_text, phoneTexto (hasta 255, 5.000 y 32 caracteres).
number, dateNúmero (con min y max opcionales) y fecha AAAA-MM-DD.
single_choice, multiple_choice, dropdownElecció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, consentSí o no, y la casilla de consentimiento (true cuando está marcada; el envío guarda la fecha y la IP del consentimiento).
rating, npsNota de 1 hasta max (de 3 a 10; predeterminado 5) y NPS de 0 a 10.
hiddenValor 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.

Lógica: quien no quiere promociones va directo al final
{
  "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.

CampoPredeterminadoQué hace
displaypagespages muestra los bloques de una página juntos; one_by_one muestra un bloque cada vez.
show_progresstrueBarra de progreso.
next_label, back_labelnullTextos de los botones de avanzar y volver (hasta 60 caracteres).
welcomedesactivadaPantalla de bienvenida: enabled, title, description (hasta 2.000), button_label y media.
keyboard_hintstrueAtajos de teclado en la página alojada.
transitionslideslide, fade o none.
validate_emailfalseComprueba 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_atnullFecha y hora (ISO 8601) en que el formulario deja de recibir envíos.
max_submissionsnullNúmero de envíos (de 1 a 10.000.000) a partir del cual el formulario se cierra.
closed_messagenullTexto (hasta 2.000 caracteres) que se muestra a quien llega después del cierre.
prevent_duplicatesfalseRechaza (422) un correo electrónico que ya respondió al formulario; los envíos que fallaron no cuentan.
show_brandingnullfalse 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).
languageautoIdioma 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

Clave de acceso total

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

  • limitinteger
    De 1 a 100. Predeterminado: 20.
  • afterstring
    Cursor de la página siguiente.
  • beforestring
    Cursor de la página anterior.
  • searchstring
    Búsqueda por nombre.
GET /forms

cURL

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

Node.js

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

const data = await response.json();

Python

import os
import requests

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

data = response.json()

PHP

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

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

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "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

Clave de acceso total

Devuelve el formulario completo, con el public_url y los valores predeterminados aplicados.

Cuerpo de la solicitud

  • namestringobligatorio
    Nombre interno, hasta 255 caracteres.
  • segment_idstringobligatorio
    Segmento estático que recibe los contactos.
  • slugstring
    Dirección pública, de 3 a 64 caracteres (letras minúsculas, números y guiones), única en Postal. Predeterminado: generado a partir del nombre, como newsletter-k3x9p2qa.
  • statusstring
    active (predeterminado) o paused (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). Cuando pages y fields llegan juntos, vale pages.
  • settingsobject
    Configuración del flujo y del cierre.
  • fieldsobject[]
    De 1 a 20 campos, en el orden de visualización. El campo email es 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 usa pages.
    • keystringobligatorio
      email, first_name, last_name o la clave de una propiedad. El tipo del campo sigue el tipo de la propiedad.
    • labelstring
      Etiqueta, hasta 255 caracteres.
    • placeholderstring
      Texto de ejemplo, hasta 255 caracteres.
    • requiredboolean
      Obligatorio. El correo electrónico siempre es obligatorio.
  • double_opt_inboolean
    Pide confirmación por correo electrónico antes de incluir al contacto.
  • contentobject
    Textos de la página alojada: title, description (hasta 2.000 caracteres) y logo_url.
  • successobject
    Pantalla de éxito: logo_url, title, message, button_label, button_url y media. Sin title y message, 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 | null
    URL 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.
  • styleobject
    Apariencia: layout (stacked o inline), 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; rounded es el formato antiguo), preset (un tema de GET /forms/themes), logo_url (https) y background ({"type": "color|gradient|image", …}, con color, gradient {from, to, angle} o image_url, más overlay de 0 a 0,85 y overlay_color).
  • allowed_domainsstring[]
    Hasta 50 dominios que pueden enviar el formulario desde el navegador, como lojaexemplo.com.br (incluye subdominios). Vacío: cualquier sitio.
  • notificationsobject
    Avisos por correo electrónico a cada envío, consulta Configuración y cierre.

Errores

  • 422validation_error
    Campo no válido, slug en uso, páginas o bloques fuera de las reglas (consulta errors), formulario sin el bloque email o campo que no es de contacto ni de propiedad.
  • 404not_found
    El segmento o algún tema no existe.
  • 403plan_limit_reached
    El plan ya no permite más formularios, o el plan muestra el sello de Postal y el formulario pidió settings.show_branding: false.
POST /forms

cURL

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

Node.js

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

const data = await response.json();

Python

import os
import requests

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

data = response.json()

PHP

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

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

$data = json_decode($response->getBody(), true);
201Respuesta
{
  "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}

Clave de acceso total

Parámetros de ruta

  • idstringobligatorio
    ID del formulario.

Errores

  • 404not_found
    El recurso no existe o es de otro equipo.
GET /forms/{id}

cURL

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

Node.js

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

const data = await response.json();

Python

import os
import requests

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

data = response.json()

PHP

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

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

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "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}

Clave de acceso total

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

  • idstringobligatorio
    ID del formulario.

Errores

  • 422validation_error
    Algún campo no es válido. El mensaje indica cuál.
  • 403paused_by_plan
    El formulario está pausado porque supera el límite del plan.
  • 403plan_limit_reached
    settings.show_branding: false en un plan que muestra el sello de Postal.
  • 404not_found
    El recurso no existe o es de otro equipo.
PATCH /forms/{id}

cURL

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

Node.js

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

const data = await response.json();

Python

import os
import requests

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

data = response.json()

PHP

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

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

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "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}

Clave de acceso total

La dirección pública deja de funcionar. Los contactos ya suscritos siguen en la audiencia.

Parámetros de ruta

  • idstringobligatorio
    ID del formulario.

Errores

  • 404not_found
    El recurso no existe o es de otro equipo.
DELETE /forms/{id}

cURL

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

Node.js

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

const data = await response.json();

Python

import os
import requests

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

data = response.json()

PHP

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

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

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

#Temas listos

GET/forms/themes

Clave de acceso total

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).

GET /forms/themes

cURL

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

Node.js

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

const data = await response.json();

Python

import os
import requests

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

data = response.json()

PHP

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

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

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "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

Clave de acceso total

La biblioteca de ilustraciones para usar en media ("type": "illustration"), en las opciones de elección y en el fondo del formulario.

GET /forms/illustrations

cURL

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

Node.js

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

const data = await response.json();

Python

import os
import requests

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

data = response.json()

PHP

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

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

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "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

Clave de acceso total

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

  • idstringobligatorio
    ID del formulario.

Parámetros de consulta

  • limitinteger
    De 1 a 100. Predeterminado: 20.
  • afterstring
    Cursor de la página siguiente.
  • beforestring
    Cursor de la página anterior.
  • searchstring
    Busca en el correo electrónico, en el nombre y en el texto de las respuestas.
  • statusstring
    Uno o más estados, separados por comas: pending, pending_confirmation, subscribed, failed.
  • start_datestring
    Inicio del período, inclusive: una fecha (2026-10-01) o una hora ISO 8601.
  • end_datestring
    Fin del período, inclusive. Una fecha sola cubre el día completo (en UTC). 422 si es anterior a start_date.

Errores

  • 422validation_error
    Algún campo no es válido. El mensaje indica cuál.
  • 404not_found
    El recurso no existe o es de otro equipo.
GET /forms/{id}/submissions

cURL

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

Node.js

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

const data = await response.json();

Python

import os
import requests

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

data = response.json()

PHP

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

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

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "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}

Clave de acceso total

Parámetros de ruta

  • idstringobligatorio
    ID del formulario.
  • submission_idstringobligatorio
    ID del envío.

Errores

  • 404not_found
    El formulario o el envío no existe.
GET /forms/{id}/submissions/{submission_id}

cURL

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

Node.js

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

const data = await response.json();

Python

import os
import requests

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

data = response.json()

PHP

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

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

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "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}

Clave de acceso total

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

  • idstringobligatorio
    ID del formulario.
  • submission_idstringobligatorio
    ID del envío.

Errores

  • 404not_found
    El formulario o el envío no existe.
DELETE /forms/{id}/submissions/{submission_id}

cURL

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

Node.js

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

const data = await response.json();

Python

import os
import requests

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

data = response.json()

PHP

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

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

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

#Exportar los envíos de un formulario

GET/forms/{id}/submissions/export

Clave de acceso total

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

  • idstringobligatorio
    ID del formulario.

Parámetros de consulta

  • searchstring
    Busca en el correo electrónico, en el nombre y en el texto de las respuestas.
  • statusstring
    Uno o más estados, separados por comas: pending, pending_confirmation, subscribed, failed.
  • start_datestring
    Inicio del período, inclusive: una fecha (2026-10-01) o una hora ISO 8601.
  • end_datestring
    Fin del período, inclusive. Una fecha sola cubre el día completo (en UTC). 422 si es anterior a start_date.

Errores

  • 422validation_error
    Algún campo no es válido. El mensaje indica cuál.
  • 403plan_limit_reached
    El plan no incluye exportaciones.
  • 404not_found
    El recurso no existe o es de otro equipo.
GET /forms/{id}/submissions/export

cURL

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

Node.js

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

const data = await response.json();

Python

import os
import requests

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

data = response.json()

PHP

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

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

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

#Listar los envíos de todos los formularios

GET/forms/submissions

Clave de acceso total

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

  • limitinteger
    De 1 a 100. Predeterminado: 20.
  • afterstring
    Cursor de la página siguiente.
  • beforestring
    Cursor de la página anterior.
  • form_idstring
    Solo los envíos de este formulario.
  • searchstring
    Busca en el correo electrónico, en el nombre y en el texto de las respuestas.
  • statusstring
    Uno o más estados, separados por comas: pending, pending_confirmation, subscribed, failed.
  • start_datestring
    Inicio del período, inclusive: una fecha (2026-10-01) o una hora ISO 8601.
  • end_datestring
    Fin del período, inclusive. Una fecha sola cubre el día completo (en UTC). 422 si es anterior a start_date.

Errores

  • 422validation_error
    Algún campo no es válido. El mensaje indica cuál.
GET /forms/submissions

cURL

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

Node.js

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

const data = await response.json();

Python

import os
import requests

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

data = response.json()

PHP

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

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

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "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

Clave de acceso total

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_idstring
    Solo los envíos de este formulario.
  • searchstring
    Busca en el correo electrónico, en el nombre y en el texto de las respuestas.
  • statusstring
    Uno o más estados, separados por comas: pending, pending_confirmation, subscribed, failed.
  • start_datestring
    Inicio del período, inclusive: una fecha (2026-10-01) o una hora ISO 8601.
  • end_datestring
    Fin del período, inclusive. Una fecha sola cubre el día completo (en UTC). 422 si es anterior a start_date.

Errores

  • 422validation_error
    Algún campo no es válido. El mensaje indica cuál.
  • 403plan_limit_reached
    El plan no incluye exportaciones.
GET /forms/submissions/export

cURL

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

Node.js

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

const data = await response.json();

Python

import os
import requests

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

data = response.json()

PHP

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

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

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

#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

Clave de acceso total

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

  • fromstring
    Inicio del período. Predeterminado: 30 días atrás.
  • tostring
    Fin del período. Predeterminado: ahora.
  • tzstring
    Zona horaria IANA. Predeterminado: UTC.
  • intervalstring
    day (predeterminado) o hour.

Errores

  • 422validation_error
    Algún campo no es válido. El mensaje indica cuál.
  • 403plan_limit_reached
    El plan no incluye el análisis de formularios.
GET /forms/overview

cURL

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

Node.js

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

const data = await response.json();

Python

import os
import requests

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

data = response.json()

PHP

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

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

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "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

Clave de acceso total

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

  • idstringobligatorio
    ID del formulario.

Parámetros de consulta

  • fromstring
    Inicio del período. Predeterminado: 30 días atrás.
  • tostring
    Fin del período. Predeterminado: ahora.
  • tzstring
    Zona horaria IANA. Predeterminado: UTC.
  • intervalstring
    day (predeterminado) o hour.

Errores

  • 422validation_error
    Algún campo no es válido. El mensaje indica cuál.
  • 403plan_limit_reached
    El plan no incluye el análisis de formularios.
  • 404not_found
    El recurso no existe o es de otro equipo.
GET /forms/{id}/analytics

cURL

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

Node.js

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

const data = await response.json();

Python

import os
import requests

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

data = response.json()

PHP

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

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

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "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

Clave de acceso total

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

  • idstringobligatorio
    ID del formulario.

Parámetros de consulta

  • fromstring
    Inicio del período. Predeterminado: 30 días atrás.
  • tostring
    Fin del período. Predeterminado: ahora.
  • tzstring
    Zona horaria IANA. Predeterminado: UTC.

Errores

  • 422validation_error
    Algún campo no es válido. El mensaje indica cuál.
  • 403plan_limit_reached
    El plan no incluye el análisis de formularios.
  • 404not_found
    El recurso no existe o es de otro equipo.
GET /forms/{id}/analytics/export

cURL

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

Node.js

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

const data = await response.json();

Python

import os
import requests

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

data = response.json()

PHP

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

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

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

#Respuestas de una pregunta de texto

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

Clave de acceso total

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

  • idstringobligatorio
    ID del formulario.
  • questionstringobligatorio
    id del bloque de la pregunta.

Parámetros de consulta

  • limitinteger
    De 1 a 100. Predeterminado: 20.
  • afterstring
    Cursor de la página siguiente.
  • beforestring
    Cursor de la página anterior.
  • fromstring
    Inicio del período. Predeterminado: 30 días atrás.
  • tostring
    Fin del período. Predeterminado: ahora.
  • tzstring
    Zona horaria IANA. Predeterminado: UTC.

Errores

  • 422validation_error
    La pregunta no es de texto, o el período no es válido.
  • 403plan_limit_reached
    El plan no incluye el análisis de formularios.
  • 404not_found
    El formulario o la pregunta no existe.
GET /forms/{id}/analytics/questions/{question}/answers

cURL

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

Node.js

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

const data = await response.json();

Python

import os
import requests

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

data = response.json()

PHP

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

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

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "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

Clave de acceso total

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

  • idstringobligatorio
    ID del formulario.
  • emailstringobligatorio
    Dirección del destinatario, ya listada en notifications.recipients.

Errores

  • 422validation_error
    El destinatario ya confirmó.
  • 429rate_limit_exceeded
    Demasiadas invitaciones para esta dirección. Inténtalo mañana.
  • 404not_found
    El formulario o el destinatario no existe.
POST /forms/{id}/notifications/recipients/{email}/resend

cURL

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

Node.js

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

const data = await response.json();

Python

import os
import requests

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

data = response.json()

PHP

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

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

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "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

Clave de acceso total

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

  • idstringobligatorio
    ID del formulario.

Errores

  • 422validation_error
    Ningún destinatario ha confirmado todavía.
  • 429rate_limit_exceeded
    Demasiadas pruebas. Inténtalo en unos minutos.
  • 404not_found
    El recurso no existe o es de otro equipo.
POST /forms/{id}/notifications/test

cURL

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

Node.js

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

const data = await response.json();

Python

import os
import requests

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

data = response.json()

PHP

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

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

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "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[]).

ReglaCómo funciona
RobotsEl 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ímite5 envíos por minuto por IP en cada formulario. Pasado ese límite, 429.
OrigenCon 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.
CORSResponde a OPTIONS y permite POST con los encabezados Content-Type y Accept: para cualquier origen, o solo para los allowed_domains. No usa cookies.
IdiomaLa página alojada, los mensajes y el correo electrónico de confirmación siguen el idioma del formulario.
Formulario pausado o inexistente404.
Formulario cerrado410 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álidas422: 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ónLa 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.

HTML en tu sitio
<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.

JavaScript en tu sitio
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
}
201Respuesta
{
  "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
}
422Respuesta de error
{
  "statusCode": 422,
  "name": "validation_error",
  "message": "E-mail: Informe um e-mail válido.",
  "errors": {
    "email": "Informe um e-mail válido."
  },
  "suggestion": null
}
410Formulario cerrado
{
  "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:

  1. el settings.language del formulario, cuando es pt-BR o en;
  2. el Accept-Language de quien abre o envía el formulario (el primero entre pt-BR y en, en el orden del encabezado);
  3. 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.