Templates
Templates com variáveis, rascunho, versões publicadas, testes e imagens.
Templates guardam o conteúdo de e-mails que você envia sempre, com variáveis. Você edita um rascunho e publica quando estiver pronto: os envios usam sempre a última versão publicada, então editar o rascunho não muda nada em produção. Para enviar com um template, use o campo template em POST /emails. Em todos os endpoints de templates abaixo, {id} aceita o ID ou o alias. Os layouts guardam desenhos de e-mail para reaproveitar, e o plano define quantos templates e layouts o time tem (veja Limites do plano).
#Listar templates
GET/templates
Parâmetros de consulta
limitintegerDe 1 a 100. Padrão: 20.afterstringCursor da próxima página.beforestringCursor da página anterior.searchstringBusca no nome e no alias.statusstringpublishedoudraft(nunca publicados).
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"
}
]
}
#Criar um template
POST/templates
Cria o template como rascunho. Publique com POST /templates/{id}/publish antes de usar nos envios.
Corpo da requisição
namestringobrigatórioNome interno, até 255 caracteres. Obrigatório na criação.htmlstringobrigatórioHTML do e-mail, até 2 MB, com variáveis no formato{{{NAME}}}. Obrigatório na criação.aliasstring | nullApelido único para usar no lugar do ID, comoorder-shipped: letras, números,.,-e_, até 100 caracteres. Precisa começar com letra ou número e não pode parecer um ID.fromstring | nullRemetente padrão.subjectstring | nullAssunto padrão; aceita variáveis.reply_tostring | string[] | nullEndereços de resposta padrão.textstring | nullVersão em texto. Sem ela, a Postal gera o texto a partir do HTML no envio.variablesobject[] | nullAté 50 variáveis declaradas.keystringobrigatórioNome da variável: letras, números e_, até 50 caracteres.typestringstring(padrão) ounumber.fallback_valuestring | number | nullValor usado quando o envio não informa a variável. Sem ele, a variável é obrigatória.
editorobject | nullEstado do editor visual do painel. Pode ignorar.
Erros
- 422
validation_errorCampo inválido, alias já usado ou variável repetida. - 403
plan_limit_reachedO plano não permite mais 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);
{
"id": "0199f3a0-1b2c-7d3e-9f4a-5b6c7d8e9f0a",
"object": "template"
}
#Obter um template
GET/templates/{id}
Devolve o template com o conteúdo do rascunho. has_unpublished_versions indica se o rascunho difere da versão publicada.
Parâmetros de caminho
idstringobrigatórioID ou alias.
Erros
- 404
not_foundO recurso não existe ou é de outro time.
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
}
#Atualizar um template
PATCH/templates/{id}
Altera o rascunho. Todos os campos são opcionais; os que você não enviar ficam como estão. A versão publicada só muda quando você publicar de novo.
Parâmetros de caminho
idstringobrigatórioID ou alias.
Corpo da requisição (todos opcionais)
namestringNome interno, até 255 caracteres. Obrigatório na criação.htmlstringHTML do e-mail, até 2 MB, com variáveis no formato{{{NAME}}}. Obrigatório na criação.aliasstring | nullApelido único para usar no lugar do ID, comoorder-shipped: letras, números,.,-e_, até 100 caracteres. Precisa começar com letra ou número e não pode parecer um ID.fromstring | nullRemetente padrão.subjectstring | nullAssunto padrão; aceita variáveis.reply_tostring | string[] | nullEndereços de resposta padrão.textstring | nullVersão em texto. Sem ela, a Postal gera o texto a partir do HTML no envio.variablesobject[] | nullAté 50 variáveis declaradas.keystringobrigatórioNome da variável: letras, números e_, até 50 caracteres.typestringstring(padrão) ounumber.fallback_valuestring | number | nullValor usado quando o envio não informa a variável. Sem ele, a variável é obrigatória.
editorobject | nullEstado do editor visual do painel. Pode ignorar.
Erros
- 422
validation_errorAlgum campo é inválido. A mensagem diz qual. - 403
paused_by_planO template está pausado porque passa do limite de templates do plano. - 404
not_foundO recurso não existe ou é de outro time.
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"
}
#Excluir um template
DELETE/templates/{id}
Envios que usam o template passam a receber 404.
Parâmetros de caminho
idstringobrigatórioID ou alias.
Erros
- 404
not_foundO recurso não existe ou é de outro time.
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 um template
POST/templates/{id}/publish
Cria uma nova versão a partir do rascunho atual. A partir daí, os envios com este template usam essa versão.
Parâmetros de caminho
idstringobrigatórioID ou alias.
Erros
- 403
paused_by_planO template está pausado porque passa do limite de templates do plano. - 404
not_foundO recurso não existe ou é de outro time.
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 um template
POST/templates/{id}/duplicate
Cria uma cópia do rascunho com o nome "… (copy)" (ou "… (cópia)" com Accept-Language: pt-BR), sem alias e sem versões publicadas.
Parâmetros de caminho
idstringobrigatórioID ou alias.
Erros
- 403
plan_limit_reachedO plano não permite mais templates. - 404
not_foundO recurso não existe ou é de outro time.
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 um teste
POST/templates/{id}/test
Envia o rascunho (publicado ou não) para até 5 endereços, com o assunto prefixado por [Teste] (ou [Test] em inglês, conforme o Accept-Language). O teste é um envio normal: conta na cota e aparece em GET /emails com a tag template_test.
Parâmetros de caminho
idstringobrigatórioID ou alias.
Corpo da requisição
tostring[]obrigatórioDe 1 a 5 endereços.fromstringRemetente; padrão: ofromdo template.variablesobjectValores das variáveis para o teste.
Erros
- 422
validation_errorEndereço inválido, remetente ausente ou variável obrigatória sem valor. - 403
validation_errorO domínio do remetente não está verificado. - 403
sending_pausedOs envios do time estão pausados. - 403
paused_by_planO template (ou o domínio do remetente) está pausado pelo plano. - 429
daily_quota_exceededA cota de envio do plano acabou (tambémmonthly_quota_exceeded). - 404
not_foundO recurso não existe ou é de outro time.
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 versões
GET/templates/{id}/versions
As versões publicadas, da mais recente para a mais antiga (até 100). current marca a que está em uso.
Parâmetros de caminho
idstringobrigatórioID ou alias.
Erros
- 404
not_foundO recurso não existe ou é de outro time.
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 uma versão
POST/templates/{id}/versions/{version_id}/restore
Copia uma versão publicada de volta para o rascunho. Para colocá-la em uso, publique em seguida.
Parâmetros de caminho
idstringobrigatórioID ou alias.version_idstringobrigatórioID da versão.
Erros
- 403
paused_by_planO template está pausado porque passa do limite de templates do plano. - 404
not_foundO recurso não existe ou é de outro time.
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 uma imagem
POST/assets
Hospeda uma imagem para usar nos seus e-mails. A URL devolvida é pública (os clientes de e-mail precisam carregá-la), permanente e difícil de adivinhar. Envie como multipart/form-data.
Corpo da requisição (multipart/form-data)
filearquivoobrigatórioPNG, JPG, GIF ou WebP, até 5 MB.
Erros
- 422
validation_errorArquivo ausente, de outro tipo ou maior que 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
Um layout é um desenho de e-mail salvo pelo time (os "Meus modelos" do painel), para começar novos templates e campanhas sem desenhar tudo de novo. Ele guarda o HTML pronto e, se vier do editor visual, o documento do editor. Um layout não é usado diretamente no envio: o envio usa templates.
Os layouts ficam a partir de /layouts e exigem uma chave de acesso total.
#Listar layouts
GET/layouts
Lista os layouts do time, do mais recente para o mais antigo. Cada item traz o HTML, porque o painel desenha o cartão de cada layout a partir dele.
Parâmetros de consulta
limitintegerDe 1 a 100. Padrão: 20.afterstringCursor da próxima página.beforestringCursor da página anterior.searchstringBusca no nome e na descrição.categorystringSó layouts desta categoria.
Erros
- 422
validation_errorAlgum campo é inválido. A mensagem diz qual.
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"
}
]
}
#Criar um layout
POST/layouts
Corpo da requisição
namestringobrigatórioNome do layout, até 255 caracteres. Obrigatório na criação.htmlstringobrigatórioHTML do e-mail, até 2 MB. Obrigatório na criação.descriptionstring | nullDescrição, até 500 caracteres.categorystring | nullUma destas:welcome,newsletter,promotion,product,ecommerce,transactional,announcement,event,onboarding,reengagement,feedback,seasonal,content,services.subjectstring | nullAssunto sugerido, até 998 caracteres.preheaderstring | nullTexto de pré-visualização, até 500 caracteres.textstring | nullVersão em texto, até 2 MB.editorobject | nullDocumento do editor visual do painel. Precisa terversion(1ou2) eblocks(uma lista de até 300 blocos, cada um comtype);styleé opcional. Pode ignorar se você só usa o HTML.
Erros
- 422
validation_errorCampo ausente ou inválido: categoria desconhecida, documento do editor sem versão 1 ou 2 ou sem lista de blocos, ou mais de 300 blocos. - 403
plan_limit_reachedO plano não permite mais 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"
}
#Obter um layout
GET/layouts/{id}
Parâmetros de caminho
idstringobrigatórioID do layout.
Erros
- 404
not_foundO recurso não existe ou é de outro time.
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"
}
#Atualizar um layout
PATCH/layouts/{id}
Todos os campos são opcionais; os que você não enviar ficam como estão.
Parâmetros de caminho
idstringobrigatórioID do layout.
Corpo da requisição (todos opcionais)
namestringNome do layout, até 255 caracteres. Obrigatório na criação.htmlstringHTML do e-mail, até 2 MB. Obrigatório na criação.descriptionstring | nullDescrição, até 500 caracteres.categorystring | nullUma destas:welcome,newsletter,promotion,product,ecommerce,transactional,announcement,event,onboarding,reengagement,feedback,seasonal,content,services.subjectstring | nullAssunto sugerido, até 998 caracteres.preheaderstring | nullTexto de pré-visualização, até 500 caracteres.textstring | nullVersão em texto, até 2 MB.editorobject | nullDocumento do editor visual do painel. Precisa terversion(1ou2) eblocks(uma lista de até 300 blocos, cada um comtype);styleé opcional. Pode ignorar se você só usa o HTML.
Erros
- 422
validation_errorAlgum campo é inválido. A mensagem diz qual. - 403
paused_by_planO layout está pausado porque passa do limite de layouts do plano. - 404
not_foundO recurso não existe ou é de outro time.
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"
}
#Excluir um layout
DELETE/layouts/{id}
Os templates e as campanhas que começaram a partir do layout não mudam.
Parâmetros de caminho
idstringobrigatórioID do layout.
Erros
- 404
not_foundO recurso não existe ou é de outro time.
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 um layout
POST/layouts/{id}/duplicate
Cria uma cópia do layout com o nome "… (copy)" (ou "… (cópia)" com Accept-Language: pt-BR).
Parâmetros de caminho
idstringobrigatórioID do layout.
Erros
- 403
plan_limit_reachedO plano não permite mais layouts. - 404
not_foundO recurso não existe ou é de outro time.
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"
}
#Limites do plano
O plano define quantos templates e layouts o time pode ter. Passar do limite em criar ou duplicar um template, ou em criar ou duplicar um layout, responde 403 plan_limit_reached. Os valores de cada plano estão em Limites e em GET /plans.
Se o plano diminui e o time fica com mais templates ou layouts do que o novo limite, a Postal não apaga nada: os mais antigos, até o limite, continuam ativos e os mais novos ficam pausados (paused_by_plan: true). Um item pausado continua na lista e pode ser lido, mas alterá-lo, publicá-lo, restaurar uma versão ou enviar um teste responde 403 paused_by_plan, e um envio com template também. Ele volta ao normal quando o plano permite de novo ou quando você apaga outros itens. Veja também Limites do plano em E-mails.