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
Parámetros de consulta
limitintegerDe 1 a 100. Predeterminado: 20.afterstringCursor de la página siguiente.beforestringCursor de la página anterior.searchstringBusca en el nombre y en el alias.statusstringpublishedodraft(nunca publicadas).
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);
{
"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
Crea la plantilla como borrador. Publícala con POST /templates/{id}/publish antes de usarla en los envíos.
Cuerpo de la solicitud
namestringobligatorioNombre interno, hasta 255 caracteres. Obligatorio al crear.htmlstringobligatorioHTML del correo, hasta 2 MB, con variables en el formato{{{NAME}}}. Obligatorio al crear.aliasstring | nullApodo único para usar en lugar del ID, comoorder-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 | nullRemitente predeterminado.subjectstring | nullAsunto predeterminado; acepta variables.reply_tostring | string[] | nullDirecciones de respuesta predeterminadas.textstring | nullVersión en texto. Si se omite, Postal genera el texto a partir del HTML al enviar.variablesobject[] | nullHasta 50 variables declaradas.keystringobligatorioNombre de la variable: letras, números y_, hasta 50 caracteres.typestringstring(predeterminado) onumber.fallback_valuestring | number | nullValor usado cuando el envío no indica la variable. Si se omite, la variable es obligatoria.
editorobject | nullEstado del editor visual del panel. Puedes ignorarlo.
Errores
- 422
validation_errorCampo no válido, alias ya usado o variable repetida. - 403
plan_limit_reachedEl plan no permite más plantillas.
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);
{
"id": "0199f3a0-1b2c-7d3e-9f4a-5b6c7d8e9f0a",
"object": "template"
}
#Obtener una plantilla
GET/templates/{id}
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
idstringobligatorioID o alias.
Errores
- 404
not_foundEl recurso no existe o es de otro equipo.
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);
{
"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}
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
idstringobligatorioID o alias.
Cuerpo de la solicitud (todos opcionales)
namestringNombre interno, hasta 255 caracteres. Obligatorio al crear.htmlstringHTML del correo, hasta 2 MB, con variables en el formato{{{NAME}}}. Obligatorio al crear.aliasstring | nullApodo único para usar en lugar del ID, comoorder-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 | nullRemitente predeterminado.subjectstring | nullAsunto predeterminado; acepta variables.reply_tostring | string[] | nullDirecciones de respuesta predeterminadas.textstring | nullVersión en texto. Si se omite, Postal genera el texto a partir del HTML al enviar.variablesobject[] | nullHasta 50 variables declaradas.keystringobligatorioNombre de la variable: letras, números y_, hasta 50 caracteres.typestringstring(predeterminado) onumber.fallback_valuestring | number | nullValor usado cuando el envío no indica la variable. Si se omite, la variable es obligatoria.
editorobject | nullEstado del editor visual del panel. Puedes ignorarlo.
Errores
- 422
validation_errorAlgún campo no es válido. El mensaje indica cuál. - 403
paused_by_planLa plantilla está pausada porque supera el límite de plantillas del plan. - 404
not_foundEl recurso no existe o es de otro equipo.
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);
{
"id": "0199f3a0-1b2c-7d3e-9f4a-5b6c7d8e9f0a",
"object": "template"
}
#Eliminar una plantilla
DELETE/templates/{id}
Los envíos que usan la plantilla pasan a recibir 404.
Parámetros de ruta
idstringobligatorioID o alias.
Errores
- 404
not_foundEl recurso no existe o es de otro equipo.
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);
{
"object": "template",
"id": "0199f3a0-1b2c-7d3e-9f4a-5b6c7d8e9f0a",
"deleted": true
}
#Publicar una plantilla
POST/templates/{id}/publish
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
idstringobligatorioID o alias.
Errores
- 403
paused_by_planLa plantilla está pausada porque supera el límite de plantillas del plan. - 404
not_foundEl recurso no existe o es de otro equipo.
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);
{
"id": "0199f3a0-1b2c-7d3e-9f4a-5b6c7d8e9f0a",
"object": "template"
}
#Duplicar una plantilla
POST/templates/{id}/duplicate
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
idstringobligatorioID o alias.
Errores
- 403
plan_limit_reachedEl plan no permite más plantillas. - 404
not_foundEl recurso no existe o es de otro equipo.
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);
{
"id": "0199f3a2-4e5f-7a6b-8c7d-9e0f1a2b3c4d",
"object": "template"
}
#Enviar una prueba
POST/templates/{id}/test
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
idstringobligatorioID o alias.
Cuerpo de la solicitud
tostring[]obligatorioDe 1 a 5 direcciones.fromstringRemitente; predeterminado: elfromde la plantilla.variablesobjectValores de las variables para la prueba.
Errores
- 422
validation_errorDirección no válida, remitente ausente o variable obligatoria sin valor. - 403
validation_errorEl dominio del remitente no está verificado. - 403
sending_pausedLos envíos del equipo están en pausa. - 403
paused_by_planLa plantilla (o el dominio del remitente) está pausada por el plan. - 429
daily_quota_exceededLa cuota de envío del plan se agotó (tambiénmonthly_quota_exceeded). - 404
not_foundEl recurso no existe o es de otro equipo.
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);
{
"id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
"object": "email"
}
#Listar versiones
GET/templates/{id}/versions
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
idstringobligatorioID o alias.
Errores
- 404
not_foundEl recurso no existe o es de otro equipo.
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);
{
"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
Copia una versión publicada de vuelta al borrador. Para ponerla en uso, publícala a continuación.
Parámetros de ruta
idstringobligatorioID o alias.version_idstringobligatorioID de la versión.
Errores
- 403
paused_by_planLa plantilla está pausada porque supera el límite de plantillas del plan. - 404
not_foundEl recurso no existe o es de otro equipo.
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);
{
"id": "0199f3a0-1b2c-7d3e-9f4a-5b6c7d8e9f0a",
"object": "template"
}
#Enviar una imagen
POST/assets
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)
filearchivoobligatorioPNG, JPG, GIF o WebP, hasta 5 MB.
Errores
- 422
validation_errorArchivo ausente, de otro tipo o mayor de 5 MB.
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);
{
"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
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
limitintegerDe 1 a 100. Predeterminado: 20.afterstringCursor de la página siguiente.beforestringCursor de la página anterior.searchstringBusca en el nombre y en la descripción.categorystringSolo layouts de esta categoría.
Errores
- 422
validation_errorAlgún campo no es válido. El mensaje indica cuál.
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);
{
"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
Cuerpo de la solicitud
namestringobligatorioNombre del layout, hasta 255 caracteres. Obligatorio al crear.htmlstringobligatorioHTML del correo, hasta 2 MB. Obligatorio al crear.descriptionstring | nullDescripción, hasta 500 caracteres.categorystring | nullUna de estas:welcome,newsletter,promotion,product,ecommerce,transactional,announcement,event,onboarding,reengagement,feedback,seasonal,content,services.subjectstring | nullAsunto sugerido, hasta 998 caracteres.preheaderstring | nullTexto de vista previa, hasta 500 caracteres.textstring | nullVersión en texto, hasta 2 MB.editorobject | nullDocumento del editor visual del panel. Debe tenerversion(1o2) yblocks(una lista de hasta 300 bloques, cada uno contype);stylees opcional. Puedes ignorarlo si solo usas el HTML.
Errores
- 422
validation_errorCampo 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. - 403
plan_limit_reachedEl plan no permite más 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);
{
"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}
Parámetros de ruta
idstringobligatorioID del layout.
Errores
- 404
not_foundEl recurso no existe o es de otro equipo.
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);
{
"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}
Todos los campos son opcionales; los que no envíes se quedan como están.
Parámetros de ruta
idstringobligatorioID del layout.
Cuerpo de la solicitud (todos opcionales)
namestringNombre del layout, hasta 255 caracteres. Obligatorio al crear.htmlstringHTML del correo, hasta 2 MB. Obligatorio al crear.descriptionstring | nullDescripción, hasta 500 caracteres.categorystring | nullUna de estas:welcome,newsletter,promotion,product,ecommerce,transactional,announcement,event,onboarding,reengagement,feedback,seasonal,content,services.subjectstring | nullAsunto sugerido, hasta 998 caracteres.preheaderstring | nullTexto de vista previa, hasta 500 caracteres.textstring | nullVersión en texto, hasta 2 MB.editorobject | nullDocumento del editor visual del panel. Debe tenerversion(1o2) yblocks(una lista de hasta 300 bloques, cada uno contype);stylees opcional. Puedes ignorarlo si solo usas el HTML.
Errores
- 422
validation_errorAlgún campo no es válido. El mensaje indica cuál. - 403
paused_by_planEl layout está pausado porque supera el límite de layouts del plan. - 404
not_foundEl recurso no existe o es de otro equipo.
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);
{
"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}
Las plantillas y las campañas que empezaron a partir del layout no cambian.
Parámetros de ruta
idstringobligatorioID del layout.
Errores
- 404
not_foundEl recurso no existe o es de otro equipo.
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);
{
"object": "layout",
"id": "019a1a01-3c4d-7e5f-9a6b-7c8d9e0f1a2b",
"deleted": true
}
#Duplicar un layout
POST/layouts/{id}/duplicate
Crea una copia del layout con el nombre "… (copy)" (o "… (cópia)" con Accept-Language: pt-BR).
Parámetros de ruta
idstringobligatorioID del layout.
Errores
- 403
plan_limit_reachedEl plan no permite más layouts. - 404
not_foundEl recurso no existe o es de otro equipo.
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);
{
"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.