Iniciar sesión Crear cuenta
Referencia de la API

Plantillas

Plantillas con variables, borrador, versiones publicadas, pruebas e imágenes.

Las plantillas guardan el contenido de los correos que envías con frecuencia, con variables. Editas un borrador y publicas cuando esté listo: los envíos usan siempre la última versión publicada, así que editar el borrador no cambia nada en producción. Para enviar con una plantilla, usa el campo template en POST /emails. En todos los endpoints de plantillas de abajo, {id} acepta el ID o el alias. Los layouts guardan diseños de correo para reutilizar, y el plan define cuántas plantillas y layouts tiene el equipo (consulta Límites del plan).

#Listar plantillas

GET/templates

Clave de acceso total

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 nombre y en el alias.
  • statusstring
    published o draft (nunca publicadas).
GET /templates

cURL

curl 'https://api.postal.so/templates?status=published' \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/templates?status=published', {
  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/templates",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    params={
        "status": "published",
    },
    timeout=10,
)

data = response.json()

PHP

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

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

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "template",
      "id": "0199f3a0-1b2c-7d3e-9f4a-5b6c7d8e9f0a",
      "paused_by_plan": false,
      "name": "Pedido enviado",
      "alias": "order-shipped",
      "status": "published",
      "published_at": "2026-10-10T15:00:00.000000Z",
      "created_at": "2026-10-08T11:30:00.000000Z",
      "updated_at": "2026-10-10T14:58:12.000000Z",
      "has_unpublished_versions": false,
      "current_version_id": "0199f3a0-8c9d-7e0f-a1b2-c3d4e5f6a7b8"
    }
  ]
}

#Crear una plantilla

POST/templates

Clave de acceso total

Crea la plantilla como borrador. Publícala con POST /templates/{id}/publish antes de usarla en los envíos.

Cuerpo de la solicitud

  • namestringobligatorio
    Nombre interno, hasta 255 caracteres. Obligatorio al crear.
  • htmlstringobligatorio
    HTML del correo, hasta 2 MB, con variables en el formato {{{NAME}}}. Obligatorio al crear.
  • aliasstring | null
    Apodo único para usar en lugar del ID, como order-shipped: letras, números, ., - y _, hasta 100 caracteres. Debe empezar con una letra o un número y no puede parecerse a un ID.
  • fromstring | null
    Remitente predeterminado.
  • subjectstring | null
    Asunto predeterminado; acepta variables.
  • reply_tostring | string[] | null
    Direcciones de respuesta predeterminadas.
  • textstring | null
    Versión en texto. Si se omite, Postal genera el texto a partir del HTML al enviar.
  • variablesobject[] | null
    Hasta 50 variables declaradas.
    • keystringobligatorio
      Nombre de la variable: letras, números y _, hasta 50 caracteres.
    • typestring
      string (predeterminado) o number.
    • fallback_valuestring | number | null
      Valor usado cuando el envío no indica la variable. Si se omite, la variable es obligatoria.
  • editorobject | null
    Estado del editor visual del panel. Puedes ignorarlo.

Errores

  • 422validation_error
    Campo no válido, alias ya usado o variable repetida.
  • 403plan_limit_reached
    El plan no permite más plantillas.
POST /templates

cURL

curl -X POST https://api.postal.so/templates \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Pedido enviado",
    "alias": "order-shipped",
    "from": "Loja Exemplo <[email protected]>",
    "subject": "Seu pedido #{{{ORDER}}} saiu para entrega",
    "html": "<p>Olá, {{{NAME}}}! O pedido <b>#{{{ORDER}}}</b> está a caminho.</p>",
    "variables": [
      {
        "key": "NAME",
        "fallback_value": "cliente"
      },
      {
        "key": "ORDER",
        "type": "number"
      }
    ]
  }'

Node.js

const response = await fetch('https://api.postal.so/templates', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Pedido enviado',
    alias: 'order-shipped',
    from: 'Loja Exemplo <[email protected]>',
    subject: 'Seu pedido #{{{ORDER}}} saiu para entrega',
    html: '<p>Olá, {{{NAME}}}! O pedido <b>#{{{ORDER}}}</b> está a caminho.</p>',
    variables: [
      {
        key: 'NAME',
        fallback_value: 'cliente',
      },
      {
        key: 'ORDER',
        type: 'number',
      },
    ],
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/templates",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    json={
        "name": "Pedido enviado",
        "alias": "order-shipped",
        "from": "Loja Exemplo <[email protected]>",
        "subject": "Seu pedido #{{{ORDER}}} saiu para entrega",
        "html": "<p>Olá, {{{NAME}}}! O pedido <b>#{{{ORDER}}}</b> está a caminho.</p>",
        "variables": [
            {
                "key": "NAME",
                "fallback_value": "cliente",
            },
            {
                "key": "ORDER",
                "type": "number",
            },
        ],
    },
    timeout=10,
)

data = response.json()

PHP

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

$response = $client->post('templates', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'json' => [
        'name' => 'Pedido enviado',
        'alias' => 'order-shipped',
        'from' => 'Loja Exemplo <[email protected]>',
        'subject' => 'Seu pedido #{{{ORDER}}} saiu para entrega',
        'html' => '<p>Olá, {{{NAME}}}! O pedido <b>#{{{ORDER}}}</b> está a caminho.</p>',
        'variables' => [
            [
                'key' => 'NAME',
                'fallback_value' => 'cliente',
            ],
            [
                'key' => 'ORDER',
                'type' => 'number',
            ],
        ],
    ],
]);

$data = json_decode($response->getBody(), true);
201Respuesta
{
  "id": "0199f3a0-1b2c-7d3e-9f4a-5b6c7d8e9f0a",
  "object": "template"
}

#Obtener una plantilla

GET/templates/{id}

Clave de acceso total

Devuelve la plantilla con el contenido del borrador. has_unpublished_versions indica si el borrador difiere de la versión publicada.

Parámetros de ruta

  • idstringobligatorio
    ID o alias.

Errores

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

cURL

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

Node.js

const response = await fetch('https://api.postal.so/templates/order-shipped', {
  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/templates/order-shipped",
    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('templates/order-shipped', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "object": "template",
  "id": "0199f3a0-1b2c-7d3e-9f4a-5b6c7d8e9f0a",
  "paused_by_plan": false,
  "name": "Pedido enviado",
  "alias": "order-shipped",
  "status": "published",
  "published_at": "2026-10-10T15:00:00.000000Z",
  "created_at": "2026-10-08T11:30:00.000000Z",
  "updated_at": "2026-10-10T14:58:12.000000Z",
  "has_unpublished_versions": false,
  "current_version_id": "0199f3a0-8c9d-7e0f-a1b2-c3d4e5f6a7b8",
  "from": "Loja Exemplo <[email protected]>",
  "subject": "Seu pedido #{{{ORDER}}} saiu para entrega",
  "reply_to": ["[email protected]"],
  "html": "<p>Olá, {{{NAME}}}! O pedido <b>#{{{ORDER}}}</b> está a caminho.</p>",
  "text": null,
  "variables": [
    {
      "key": "NAME",
      "type": "string",
      "fallback_value": "cliente"
    },
    {
      "key": "ORDER",
      "type": "number",
      "fallback_value": null
    }
  ],
  "editor": null
}

#Actualizar una plantilla

PATCH/templates/{id}

Clave de acceso total

Modifica el borrador. Todos los campos son opcionales; los que no envíes quedan como están. La versión publicada solo cambia cuando vuelves a publicar.

Parámetros de ruta

  • idstringobligatorio
    ID o alias.

Cuerpo de la solicitud (todos opcionales)

  • namestring
    Nombre interno, hasta 255 caracteres. Obligatorio al crear.
  • htmlstring
    HTML del correo, hasta 2 MB, con variables en el formato {{{NAME}}}. Obligatorio al crear.
  • aliasstring | null
    Apodo único para usar en lugar del ID, como order-shipped: letras, números, ., - y _, hasta 100 caracteres. Debe empezar con una letra o un número y no puede parecerse a un ID.
  • fromstring | null
    Remitente predeterminado.
  • subjectstring | null
    Asunto predeterminado; acepta variables.
  • reply_tostring | string[] | null
    Direcciones de respuesta predeterminadas.
  • textstring | null
    Versión en texto. Si se omite, Postal genera el texto a partir del HTML al enviar.
  • variablesobject[] | null
    Hasta 50 variables declaradas.
    • keystringobligatorio
      Nombre de la variable: letras, números y _, hasta 50 caracteres.
    • typestring
      string (predeterminado) o number.
    • fallback_valuestring | number | null
      Valor usado cuando el envío no indica la variable. Si se omite, la variable es obligatoria.
  • editorobject | null
    Estado del editor visual del panel. Puedes ignorarlo.

Errores

  • 422validation_error
    Algún campo no es válido. El mensaje indica cuál.
  • 403paused_by_plan
    La plantilla está pausada porque supera el límite de plantillas del plan.
  • 404not_found
    El recurso no existe o es de otro equipo.
PATCH /templates/{id}

cURL

curl -X PATCH https://api.postal.so/templates/order-shipped \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "Pedido #{{{ORDER}}} a caminho"
  }'

Node.js

const response = await fetch('https://api.postal.so/templates/order-shipped', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    subject: 'Pedido #{{{ORDER}}} a caminho',
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.patch(
    "https://api.postal.so/templates/order-shipped",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    json={
        "subject": "Pedido #{{{ORDER}}} a caminho",
    },
    timeout=10,
)

data = response.json()

PHP

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

$response = $client->patch('templates/order-shipped', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'json' => [
        'subject' => 'Pedido #{{{ORDER}}} a caminho',
    ],
]);

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "id": "0199f3a0-1b2c-7d3e-9f4a-5b6c7d8e9f0a",
  "object": "template"
}

#Eliminar una plantilla

DELETE/templates/{id}

Clave de acceso total

Los envíos que usan la plantilla pasan a recibir 404.

Parámetros de ruta

  • idstringobligatorio
    ID o alias.

Errores

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

cURL

curl -X DELETE https://api.postal.so/templates/0199f3a0-1b2c-7d3e-9f4a-5b6c7d8e9f0a \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/templates/0199f3a0-1b2c-7d3e-9f4a-5b6c7d8e9f0a', {
  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/templates/0199f3a0-1b2c-7d3e-9f4a-5b6c7d8e9f0a",
    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('templates/0199f3a0-1b2c-7d3e-9f4a-5b6c7d8e9f0a', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "object": "template",
  "id": "0199f3a0-1b2c-7d3e-9f4a-5b6c7d8e9f0a",
  "deleted": true
}

#Publicar una plantilla

POST/templates/{id}/publish

Clave de acceso total

Crea una versión nueva a partir del borrador actual. Desde ese momento, los envíos con esta plantilla usan esa versión.

Parámetros de ruta

  • idstringobligatorio
    ID o alias.

Errores

  • 403paused_by_plan
    La plantilla está pausada porque supera el límite de plantillas del plan.
  • 404not_found
    El recurso no existe o es de otro equipo.
POST /templates/{id}/publish

cURL

curl -X POST https://api.postal.so/templates/order-shipped/publish \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/templates/order-shipped/publish', {
  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/templates/order-shipped/publish",
    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('templates/order-shipped/publish', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "id": "0199f3a0-1b2c-7d3e-9f4a-5b6c7d8e9f0a",
  "object": "template"
}

#Duplicar una plantilla

POST/templates/{id}/duplicate

Clave de acceso total

Crea una copia del borrador con el nombre "… (copy)" (o "… (cópia)" con Accept-Language: pt-BR), sin alias y sin versiones publicadas.

Parámetros de ruta

  • idstringobligatorio
    ID o alias.

Errores

  • 403plan_limit_reached
    El plan no permite más plantillas.
  • 404not_found
    El recurso no existe o es de otro equipo.
POST /templates/{id}/duplicate

cURL

curl -X POST https://api.postal.so/templates/order-shipped/duplicate \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/templates/order-shipped/duplicate', {
  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/templates/order-shipped/duplicate",
    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('templates/order-shipped/duplicate', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
201Respuesta
{
  "id": "0199f3a2-4e5f-7a6b-8c7d-9e0f1a2b3c4d",
  "object": "template"
}

#Enviar una prueba

POST/templates/{id}/test

Clave de acceso total

Envía el borrador (publicado o no) a un máximo de 5 direcciones, con el asunto con el prefijo [Teste] (o [Test] en inglés, según el Accept-Language). La prueba es un envío normal: cuenta en la cuota y aparece en GET /emails con la etiqueta template_test.

Parámetros de ruta

  • idstringobligatorio
    ID o alias.

Cuerpo de la solicitud

  • tostring[]obligatorio
    De 1 a 5 direcciones.
  • fromstring
    Remitente; predeterminado: el from de la plantilla.
  • variablesobject
    Valores de las variables para la prueba.

Errores

  • 422validation_error
    Dirección no válida, remitente ausente o variable obligatoria sin valor.
  • 403validation_error
    El dominio del remitente no está verificado.
  • 403sending_paused
    Los envíos del equipo están en pausa.
  • 403paused_by_plan
    La plantilla (o el dominio del remitente) está pausada por el plan.
  • 429daily_quota_exceeded
    La cuota de envío del plan se agotó (también monthly_quota_exceeded).
  • 404not_found
    El recurso no existe o es de otro equipo.
POST /templates/{id}/test

cURL

curl -X POST https://api.postal.so/templates/order-shipped/test \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": ["[email protected]"],
    "variables": {
      "NAME": "Rafaela",
      "ORDER": 10482
    }
  }'

Node.js

const response = await fetch('https://api.postal.so/templates/order-shipped/test', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    to: ['[email protected]'],
    variables: {
      NAME: 'Rafaela',
      ORDER: 10482,
    },
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/templates/order-shipped/test",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    json={
        "to": ["[email protected]"],
        "variables": {
            "NAME": "Rafaela",
            "ORDER": 10482,
        },
    },
    timeout=10,
)

data = response.json()

PHP

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

$response = $client->post('templates/order-shipped/test', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'json' => [
        'to' => ['[email protected]'],
        'variables' => [
            'NAME' => 'Rafaela',
            'ORDER' => 10482,
        ],
    ],
]);

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
  "object": "email"
}

#Listar versiones

GET/templates/{id}/versions

Clave de acceso total

Las versiones publicadas, de la más reciente a la más antigua (hasta 100). current marca la que está en uso.

Parámetros de ruta

  • idstringobligatorio
    ID o alias.

Errores

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

cURL

curl https://api.postal.so/templates/order-shipped/versions \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/templates/order-shipped/versions', {
  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/templates/order-shipped/versions",
    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('templates/order-shipped/versions', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "template_version",
      "id": "0199f3a0-8c9d-7e0f-a1b2-c3d4e5f6a7b8",
      "version": 2,
      "subject": "Seu pedido #{{{ORDER}}} saiu para entrega",
      "published_at": "2026-10-10T15:00:00.000000Z",
      "published_by": "Rafaela Lima",
      "current": true
    },
    {
      "object": "template_version",
      "id": "0199f3a0-5a6b-7c7d-8e9f-0a1b2c3d4e5f",
      "version": 1,
      "subject": "Seu pedido saiu para entrega",
      "published_at": "2026-10-08T12:10:00.000000Z",
      "published_by": "Rafaela Lima",
      "current": false
    }
  ]
}

#Restaurar una versión

POST/templates/{id}/versions/{version_id}/restore

Clave de acceso total

Copia una versión publicada de vuelta al borrador. Para ponerla en uso, publícala a continuación.

Parámetros de ruta

  • idstringobligatorio
    ID o alias.
  • version_idstringobligatorio
    ID de la versión.

Errores

  • 403paused_by_plan
    La plantilla está pausada porque supera el límite de plantillas del plan.
  • 404not_found
    El recurso no existe o es de otro equipo.
POST /templates/{id}/versions/{version_id}/restore

cURL

curl -X POST https://api.postal.so/templates/order-shipped/versions/0199f3a0-5a6b-7c7d-8e9f-0a1b2c3d4e5f/restore \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/templates/order-shipped/versions/0199f3a0-5a6b-7c7d-8e9f-0a1b2c3d4e5f/restore', {
  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/templates/order-shipped/versions/0199f3a0-5a6b-7c7d-8e9f-0a1b2c3d4e5f/restore",
    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('templates/order-shipped/versions/0199f3a0-5a6b-7c7d-8e9f-0a1b2c3d4e5f/restore', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "id": "0199f3a0-1b2c-7d3e-9f4a-5b6c7d8e9f0a",
  "object": "template"
}

#Enviar una imagen

POST/assets

Clave de acceso total

Aloja una imagen para usarla en tus correos electrónicos. La URL devuelta es pública (los clientes de correo necesitan cargarla), permanente y difícil de adivinar. Envíala como multipart/form-data.

Cuerpo de la solicitud (multipart/form-data)

  • filearchivoobligatorio
    PNG, JPG, GIF o WebP, hasta 5 MB.

Errores

  • 422validation_error
    Archivo ausente, de otro tipo o mayor de 5 MB.
POST /assets

cURL

curl -X POST https://api.postal.so/assets \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -F "[email protected]"

Node.js

import fs from 'node:fs/promises';

const form = new FormData();
form.append('file', new Blob([await fs.readFile('banner.png')]), 'banner.png');

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

const data = await response.json();

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/assets",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    files={"file": open("banner.png", "rb")},
    timeout=10,
)

data = response.json()

PHP

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

$response = $client->post('assets', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'multipart' => [
        ['name' => 'file', 'contents' => fopen('banner.png', 'r')],
    ],
]);

$data = json_decode($response->getBody(), true);
201Respuesta
{
  "object": "asset",
  "id": "019a1a00-2b3c-7d4e-8f5a-6b7c8d9e0f1a",
  "url": "https://media.postal.so/uploads/assets/0199f0e0-1a2b-7c3d-8e4f-5a6b7c8d9e0f/Xk2pQ9vLm4Rt7Wb1Yc5Hf3Lz8Ag6Ej0Nd2Sq4UvB/banner.png",
  "filename": "banner.png"
}

#Layouts

Un layout es un diseño de correo guardado por el equipo (las "Mis plantillas" del panel), para empezar nuevas plantillas y campañas sin diseñarlo todo de nuevo. Guarda el HTML listo y, si viene del editor visual, el documento del editor. Un layout no se usa directamente en el envío: el envío usa plantillas.

Los layouts están bajo /layouts y exigen una clave de acceso total.

#Listar layouts

GET/layouts

Clave de acceso total

Lista los layouts del equipo, del más reciente al más antiguo. Cada elemento incluye el HTML, porque el panel dibuja la tarjeta de cada layout a partir de él.

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 nombre y en la descripción.
  • categorystring
    Solo layouts de esta categoría.

Errores

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

cURL

curl 'https://api.postal.so/layouts?category=transactional' \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/layouts?category=transactional', {
  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/layouts",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    params={
        "category": "transactional",
    },
    timeout=10,
)

data = response.json()

PHP

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

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

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "layout",
      "id": "019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b",
      "paused_by_plan": false,
      "name": "Pedido enviado",
      "description": "Aviso de que o pedido saiu para entrega.",
      "category": "transactional",
      "subject": "Seu pedido #{{{ORDER}}} saiu para entrega",
      "preheader": "Acompanhe a entrega.",
      "editor": null,
      "html": "<p>Olá, {{{NAME}}}! O pedido <b>#{{{ORDER}}}</b> está a caminho.</p>",
      "text": null,
      "created_by": "Rafaela Lima",
      "created_at": "2026-10-08T11:30:00.000000Z",
      "updated_at": "2026-10-08T11:30:00.000000Z"
    }
  ]
}

#Crear un layout

POST/layouts

Clave de acceso total

Cuerpo de la solicitud

  • namestringobligatorio
    Nombre del layout, hasta 255 caracteres. Obligatorio al crear.
  • htmlstringobligatorio
    HTML del correo, hasta 2 MB. Obligatorio al crear.
  • descriptionstring | null
    Descripción, hasta 500 caracteres.
  • categorystring | null
    Una de estas: welcome, newsletter, promotion, product, ecommerce, transactional, announcement, event, onboarding, reengagement, feedback, seasonal, content, services.
  • subjectstring | null
    Asunto sugerido, hasta 998 caracteres.
  • preheaderstring | null
    Texto de vista previa, hasta 500 caracteres.
  • textstring | null
    Versión en texto, hasta 2 MB.
  • editorobject | null
    Documento del editor visual del panel. Debe tener version (1 o 2) y blocks (una lista de hasta 300 bloques, cada uno con type); style es opcional. Puedes ignorarlo si solo usas el HTML.

Errores

  • 422validation_error
    Campo ausente o no válido: categoría desconocida, documento del editor sin versión 1 o 2 o sin lista de bloques, o más de 300 bloques.
  • 403plan_limit_reached
    El plan no permite más layouts.
POST /layouts

cURL

curl -X POST https://api.postal.so/layouts \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Pedido enviado",
    "category": "transactional",
    "subject": "Seu pedido #{{{ORDER}}} saiu para entrega",
    "html": "<p>Olá, {{{NAME}}}! O pedido <b>#{{{ORDER}}}</b> está a caminho.</p>"
  }'

Node.js

const response = await fetch('https://api.postal.so/layouts', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Pedido enviado',
    category: 'transactional',
    subject: 'Seu pedido #{{{ORDER}}} saiu para entrega',
    html: '<p>Olá, {{{NAME}}}! O pedido <b>#{{{ORDER}}}</b> está a caminho.</p>',
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/layouts",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    json={
        "name": "Pedido enviado",
        "category": "transactional",
        "subject": "Seu pedido #{{{ORDER}}} saiu para entrega",
        "html": "<p>Olá, {{{NAME}}}! O pedido <b>#{{{ORDER}}}</b> está a caminho.</p>",
    },
    timeout=10,
)

data = response.json()

PHP

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

$response = $client->post('layouts', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'json' => [
        'name' => 'Pedido enviado',
        'category' => 'transactional',
        'subject' => 'Seu pedido #{{{ORDER}}} saiu para entrega',
        'html' => '<p>Olá, {{{NAME}}}! O pedido <b>#{{{ORDER}}}</b> está a caminho.</p>',
    ],
]);

$data = json_decode($response->getBody(), true);
201Respuesta
{
  "object": "layout",
  "id": "019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b",
  "paused_by_plan": false,
  "name": "Pedido enviado",
  "description": "Aviso de que o pedido saiu para entrega.",
  "category": "transactional",
  "subject": "Seu pedido #{{{ORDER}}} saiu para entrega",
  "preheader": "Acompanhe a entrega.",
  "editor": null,
  "html": "<p>Olá, {{{NAME}}}! O pedido <b>#{{{ORDER}}}</b> está a caminho.</p>",
  "text": null,
  "created_by": "Rafaela Lima",
  "created_at": "2026-10-08T11:30:00.000000Z",
  "updated_at": "2026-10-08T11:30:00.000000Z"
}

#Obtener un layout

GET/layouts/{id}

Clave de acceso total

Parámetros de ruta

  • idstringobligatorio
    ID del layout.

Errores

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

cURL

curl https://api.postal.so/layouts/019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/layouts/019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b', {
  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/layouts/019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b",
    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('layouts/019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "object": "layout",
  "id": "019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b",
  "paused_by_plan": false,
  "name": "Pedido enviado",
  "description": "Aviso de que o pedido saiu para entrega.",
  "category": "transactional",
  "subject": "Seu pedido #{{{ORDER}}} saiu para entrega",
  "preheader": "Acompanhe a entrega.",
  "editor": null,
  "html": "<p>Olá, {{{NAME}}}! O pedido <b>#{{{ORDER}}}</b> está a caminho.</p>",
  "text": null,
  "created_by": "Rafaela Lima",
  "created_at": "2026-10-08T11:30:00.000000Z",
  "updated_at": "2026-10-08T11:30:00.000000Z"
}

#Actualizar un layout

PATCH/layouts/{id}

Clave de acceso total

Todos los campos son opcionales; los que no envíes se quedan como están.

Parámetros de ruta

  • idstringobligatorio
    ID del layout.

Cuerpo de la solicitud (todos opcionales)

  • namestring
    Nombre del layout, hasta 255 caracteres. Obligatorio al crear.
  • htmlstring
    HTML del correo, hasta 2 MB. Obligatorio al crear.
  • descriptionstring | null
    Descripción, hasta 500 caracteres.
  • categorystring | null
    Una de estas: welcome, newsletter, promotion, product, ecommerce, transactional, announcement, event, onboarding, reengagement, feedback, seasonal, content, services.
  • subjectstring | null
    Asunto sugerido, hasta 998 caracteres.
  • preheaderstring | null
    Texto de vista previa, hasta 500 caracteres.
  • textstring | null
    Versión en texto, hasta 2 MB.
  • editorobject | null
    Documento del editor visual del panel. Debe tener version (1 o 2) y blocks (una lista de hasta 300 bloques, cada uno con type); style es opcional. Puedes ignorarlo si solo usas el HTML.

Errores

  • 422validation_error
    Algún campo no es válido. El mensaje indica cuál.
  • 403paused_by_plan
    El layout está pausado porque supera el límite de layouts del plan.
  • 404not_found
    El recurso no existe o es de otro equipo.
PATCH /layouts/{id}

cURL

curl -X PATCH https://api.postal.so/layouts/019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Aviso de pedido enviado, com o botão de rastrear."
  }'

Node.js

const response = await fetch('https://api.postal.so/layouts/019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    description: 'Aviso de pedido enviado, com o botão de rastrear.',
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.patch(
    "https://api.postal.so/layouts/019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    json={
        "description": "Aviso de pedido enviado, com o botão de rastrear.",
    },
    timeout=10,
)

data = response.json()

PHP

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

$response = $client->patch('layouts/019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'json' => [
        'description' => 'Aviso de pedido enviado, com o botão de rastrear.',
    ],
]);

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "object": "layout",
  "id": "019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b",
  "paused_by_plan": false,
  "name": "Pedido enviado",
  "description": "Aviso de pedido enviado, com o botão de rastrear.",
  "category": "transactional",
  "subject": "Seu pedido #{{{ORDER}}} saiu para entrega",
  "preheader": "Acompanhe a entrega.",
  "editor": null,
  "html": "<p>Olá, {{{NAME}}}! O pedido <b>#{{{ORDER}}}</b> está a caminho.</p>",
  "text": null,
  "created_by": "Rafaela Lima",
  "created_at": "2026-10-08T11:30:00.000000Z",
  "updated_at": "2026-10-24T14:10:00.000000Z"
}

#Eliminar un layout

DELETE/layouts/{id}

Clave de acceso total

Las plantillas y las campañas que empezaron a partir del layout no cambian.

Parámetros de ruta

  • idstringobligatorio
    ID del layout.

Errores

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

cURL

curl -X DELETE https://api.postal.so/layouts/019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/layouts/019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b', {
  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/layouts/019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b",
    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('layouts/019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Respuesta
{
  "object": "layout",
  "id": "019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b",
  "deleted": true
}

#Duplicar un layout

POST/layouts/{id}/duplicate

Clave de acceso total

Crea una copia del layout con el nombre "… (copy)" (o "… (cópia)" con Accept-Language: pt-BR).

Parámetros de ruta

  • idstringobligatorio
    ID del layout.

Errores

  • 403plan_limit_reached
    El plan no permite más layouts.
  • 404not_found
    El recurso no existe o es de otro equipo.
POST /layouts/{id}/duplicate

cURL

curl -X POST https://api.postal.so/layouts/019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b/duplicate \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/layouts/019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b/duplicate', {
  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/layouts/019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b/duplicate",
    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('layouts/019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b/duplicate', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
201Respuesta
{
  "object": "layout",
  "id": "019a1a02-4d5e-7f60-8a7b-8c9d0e1f2a3b",
  "paused_by_plan": false,
  "name": "Pedido enviado (cópia)",
  "description": "Aviso de que o pedido saiu para entrega.",
  "category": "transactional",
  "subject": "Seu pedido #{{{ORDER}}} saiu para entrega",
  "preheader": "Acompanhe a entrega.",
  "editor": null,
  "html": "<p>Olá, {{{NAME}}}! O pedido <b>#{{{ORDER}}}</b> está a caminho.</p>",
  "text": null,
  "created_by": "Rafaela Lima",
  "created_at": "2026-10-08T11:30:00.000000Z",
  "updated_at": "2026-10-08T11:30:00.000000Z"
}

#Límites del plan

El plan define cuántas plantillas y layouts puede tener el equipo. Superar el límite al crear o duplicar una plantilla, o al crear o duplicar un layout, responde 403 plan_limit_reached. Los valores de cada plan están en Límites y en GET /plans.

Si el plan baja y el equipo se queda con más plantillas o layouts que el nuevo límite, Postal no borra nada: los más antiguos, hasta el límite, siguen activos y los más nuevos quedan pausados (paused_by_plan: true). Un elemento pausado sigue en la lista y se puede leer, pero modificarlo, publicarlo, restaurar una versión o enviar una prueba responde 403 paused_by_plan, y un envío con template también. Vuelve a la normalidad cuando el plan lo permite de nuevo o cuando eliminas otros elementos. Mira también Límites del plan en Correos electrónicos.