Planos
A lista pública de planos, preços e limites e, para o dono e os admins, o Adicional de envios do plano de envio e o Adicional de contatos do plano de marketing.
A lista pública de planos, a mesma usada na página de preços. Não precisa de chave de API. Mais abaixo, o Adicional de envios do plano de envio e o Adicional de contatos do plano de marketing, que o dono e os admins gerenciam com a sessão do painel.
#Listar planos
GET/plans
Planos ativos e públicos, na ordem de exibição, dos dois produtos: kind é transactional (envio) ou marketing (contatos e campanhas). Preços em prices estão em centavos, por moeda (brl, usd e eur) e intervalo (month, year); overage_prices é o preço por mil além do limite, quando o plano permite (allows_overage): e-mails além da cota mensal nos planos de envio e contatos além do limite nos planos de marketing (overage_unit diz qual). Nos limits, null é ilimitado, 0 é não incluído e rate_limit é o número de requisições por segundo. A resposta também traz o feature_catalog, e o idioma dos textos segue o cabeçalho Accept-Language (pt-BR ou en); sem ele, os textos vêm em português.
Limites e recursos. limits traz todas as chaves do produto do plano, já resolvidas: números (null ilimitado, 0 não incluído), recursos liga/desliga (1 incluído, 0 não; postal_branding é invertido: 1 é ter o selo) e recursos informativos. O feature_catalog descreve cada chave: sections, cada uma com key, label e features, e cada recurso com key, label, description, type (number ou boolean), unit e unit_one (plural e singular, só nos números), inverted (só quando verdadeiro), kind (transactional, marketing ou both) e enforced (a API já recusa o que o plano não inclui). Veja o que cada chave bloqueia em Limites e recursos do plano.
Faixas definidas pela Postal. A escala de volume do painel é feita de faixas que a Postal cadastra por produto (volume_bands: {"transactional": [{"id", "volume"}], "marketing": [...]}, na ordem cadastrada; vazio enquanto o produto não tem faixas). Cada plano diz quais atende: bands lista os volumes das faixas que ele atende, em ordem crescente, e cada preço em prices traz o volume_band_id da faixa a que serve (ou null). Um plano que não atende uma faixa simplesmente não vende aquele volume; acima da última faixa só o plano sob medida atende. O volume de um preço ligado a uma faixa é o da faixa. Sem faixas cadastradas, vale o que está descrito a seguir.
Faixas de volume. Um plano pode ter vários preços por moeda e intervalo, um para cada faixa: volume é o volume mensal da faixa (e-mails no plano de envio, contatos no de marketing; volume_unit diz qual) e vale no lugar de emails_per_month ou contacts de limits para quem assina aquela faixa. emails_per_month, dentro do preço, repete volume nos planos de envio. overage_tiers lista o preço do excedente de cada faixa, e overage_prices resume o preço por moeda. O time liga o excedente de cada produto no painel; cada pacote de 1.000 começado entra na próxima fatura. purchasable é false em um plano ou preço que não pode ser comprado (não está ligado ao Stripe), e is_default marca o plano em que todo time começa. highlights é o que o cartão de preços do site destaca: null (padrão compacto) ou {"inherit", "items"}, com inherit abrindo o cartão com “Tudo do plano anterior, mais:” e items em ordem, cada um {"type": "feature", "key"} ou {"type": "text", "text"}.
Adicional de contatos. contact_packs lista os pacotes do Adicional de contatos que estão à venda para os planos de marketing pagos: [{"object": "contact_pack", "id", "name", "contacts", "max_quantity", "interval", "prices": [{"currency", "amount"}]}], com os preços em centavos. A lista vem vazia enquanto não há nenhum à venda. Veja Adicional de contatos.
cURL
curl https://api.postal.so/plans
Node.js
const response = await fetch('https://api.postal.so/plans');
const data = await response.json();
Python
import requests
response = requests.get(
"https://api.postal.so/plans",
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('plans');
$data = json_decode($response->getBody(), true);
{
"object": "list",
"data": [
{
"object": "plan",
"id": "0199e000-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"kind": "transactional",
"slug": "pro",
"name": "Pro",
"description": "Para produtos em produção com volume constante.",
"features": [
"50.000 e-mails por mês",
"Sem limite diário",
"10 domínios"
],
"highlights": {
"inherit": true,
"items": [
{
"type": "feature",
"key": "emails_per_month"
},
{
"type": "feature",
"key": "domains"
},
{
"type": "text",
"text": "Suporte por e-mail"
}
]
},
"limits": {
"emails_per_month": 50000,
"emails_per_day": null,
"domains": 10,
"inboxes": 10,
"custom_inbound_domains": 1,
"postal_branding": 0,
"team_members": 10,
"automation_runs": 10000,
"ai_credits": 100,
"layouts": null,
"rate_limit": 10,
"automation_experiments": 1,
"batch_sending": 1,
"scheduled_sending": 1,
"attachments_mb": null,
"engagement_tracking": 1,
"custom_domain_records": 1,
"test_addresses": 1,
"smtp_access": 1,
"shared_email_links": 1,
"inbox_agent": 1,
"templates": null,
"api_keys": null,
"webhooks": null,
"mcp_access": 1,
"exports": 1,
"retention_days": 30,
"automations": null,
"restricted_api_keys": 1,
"priority_support": 0,
"uptime_sla": 0,
"dedicated_manager": 0
},
"allows_overage": true,
"trial_days": 0,
"is_default": false,
"purchasable": true,
"volume_unit": "emails",
"overage_unit": "emails",
"bands": [50000, 100000],
"prices": [
{
"id": "0199e000-2b3c-7d4e-8f5a-6b7c8d9e0f1a",
"currency": "brl",
"interval": "month",
"amount": 9900,
"volume": 50000,
"volume_band_id": "0199e000-5e70-7b82-9d9e-0f1a2b3c4d5e",
"emails_per_month": 50000,
"purchasable": true
},
{
"id": "0199e000-2b3d-7e5f-9a6b-7c8d9e0f1a2b",
"currency": "usd",
"interval": "month",
"amount": 2000,
"volume": 50000,
"volume_band_id": "0199e000-5e70-7b82-9d9e-0f1a2b3c4d5e",
"emails_per_month": 50000,
"purchasable": true
},
{
"id": "0199e000-2b3e-7f60-8a7b-8c9d0e1f2a3b",
"currency": "brl",
"interval": "month",
"amount": 17900,
"volume": 100000,
"volume_band_id": "0199e000-5e71-7c93-8eaf-1a2b3c4d5e6f",
"emails_per_month": 100000,
"purchasable": true
},
{
"id": "0199e000-2b3f-7071-9b8c-9d0e1f2a3b4c",
"currency": "usd",
"interval": "month",
"amount": 3600,
"volume": 100000,
"volume_band_id": "0199e000-5e71-7c93-8eaf-1a2b3c4d5e6f",
"emails_per_month": 100000,
"purchasable": true
},
{
"id": "0199e000-2b40-7182-8c9d-0e1f2a3b4c5d",
"currency": "eur",
"interval": "month",
"amount": 1900,
"volume": 50000,
"volume_band_id": "0199e000-5e70-7b82-9d9e-0f1a2b3c4d5e",
"emails_per_month": 50000,
"purchasable": true
}
],
"overage_prices": {
"brl": 450,
"usd": 90,
"eur": 80
},
"overage_tiers": [
{
"volume": 50000,
"currency": "brl",
"amount": 450
},
{
"volume": 50000,
"currency": "usd",
"amount": 90
},
{
"volume": 50000,
"currency": "eur",
"amount": 80
},
{
"volume": 100000,
"currency": "brl",
"amount": 400
},
{
"volume": 100000,
"currency": "usd",
"amount": 80
}
]
},
{
"object": "plan",
"id": "0199e000-3c4d-7e5f-8a6b-7c8d9e0f1a2b",
"kind": "marketing",
"slug": "pro",
"name": "Pro",
"description": "Para quem envia campanhas e automações a uma audiência em crescimento.",
"features": [],
"highlights": null,
"limits": {
"contacts": 5000,
"segments": null,
"broadcasts_per_month": null,
"email_verifications_per_month": 5000,
"crm_connections": 1,
"app_integrations": 3,
"dynamic_segments": 1,
"topics": null,
"forms": null,
"form_analytics": 1,
"priority_support": 0,
"uptime_sla": 0,
"dedicated_manager": 0
},
"allows_overage": true,
"trial_days": 0,
"is_default": false,
"purchasable": true,
"volume_unit": "contacts",
"overage_unit": "contacts",
"bands": [],
"prices": [
{
"id": "0199e000-4d5e-7f60-9b7c-8d9e0f1a2b3c",
"currency": "brl",
"interval": "month",
"amount": 14900,
"volume": 5000,
"emails_per_month": null,
"purchasable": true
}
],
"overage_prices": {
"brl": 700
},
"overage_tiers": [
{
"volume": 5000,
"currency": "brl",
"amount": 700
}
]
}
],
"volume_bands": {
"transactional": [
{
"id": "0199e000-5e6f-7a71-8c8d-9e0f1a2b3c4d",
"volume": 3000
},
{
"id": "0199e000-5e70-7b82-9d9e-0f1a2b3c4d5e",
"volume": 50000
},
{
"id": "0199e000-5e71-7c93-8eaf-1a2b3c4d5e6f",
"volume": 100000
}
],
"marketing": []
},
"feature_catalog": {
"object": "feature_catalog",
"sections": [
{
"key": "sending",
"label": "Envio e recebimento",
"features": [
{
"key": "emails_per_month",
"label": "E-mails por mês",
"description": "Cota mensal de envio (mês UTC). Ao atingi-la, a API recusa novos envios, a menos que o plano permita excedente.",
"type": "number",
"unit": "e-mails",
"unit_one": "e-mail",
"kind": "transactional",
"enforced": true
},
{
"key": "smtp_access",
"label": "Envio por SMTP",
"description": "Enviar pelo servidor SMTP (smtp.postal.so) com uma chave de API como senha.",
"type": "boolean",
"kind": "transactional",
"enforced": true
},
{
"key": "postal_branding",
"label": "Selo “Enviado com Postal”",
"description": "O selo da Postal nos e-mails enviados e nos formulários públicos. Nos planos pagos costuma ficar desligado.",
"type": "boolean",
"inverted": true,
"kind": "transactional",
"enforced": true
}
]
},
{
"key": "inboxes",
"…": "…"
}
]
},
"contact_packs": [
{
"object": "contact_pack",
"id": "019a2e10-4b5c-7d6e-8f70-1a2b3c4d5e6f",
"name": "Pacote A",
"contacts": 10000,
"max_quantity": 10,
"interval": "month",
"prices": [
{
"currency": "brl",
"amount": 4900
},
{
"currency": "usd",
"amount": 900
}
]
}
]
}
#Adicional de envios
Quando o plano de envio do time tem um limite mensal e oferece o Adicional de envios, o dono ou um admin compra pacotes de envios extras, num pagamento único (não é uma assinatura). Os créditos formam uma reserva, à parte da cota do plano, e só são usados depois que a cota mensal acaba. O guia do painel mostra o fluxo na tela: Adicional de envios.
Só com a sessão do painel
As duas rotas abaixo usam a sessão do painel de um dono ou admin do time (GET /billing/email-credits também responde a outros membros, mas sem as compras e sem poder comprar). Uma chave de API, de acesso total ou só de envio, recebe 403 na compra. Por isso os exemplos mostram a requisição como o painel a envia, sem cabeçalho de chave. Para ler a reserva com uma chave, use emails.credits de GET /usage.
- Ordem de uso. A cota mensal do plano, depois a reserva (os créditos que vencem antes primeiro, os sem validade por último), depois o excedente, se o time o ligou (só os e-mails que a reserva não cobriu são cobrados), e então a recusa:
429 monthly_quota_exceeded. - Um crédito por e-mail, contado como a cota conta. Os e-mails para os endereços de teste não gastam crédito. Um lote que a reserva não cobre (sem excedente ligado) é recusado inteiro, sem gastar crédito.
- Campanhas não usam a reserva, mas contam na mesma cota mensal.
- O limite diário do plano continua valendo: a reserva não o levanta, e o envio recebe
429 daily_quota_exceeded. - Validade. Cada pacote tem a sua, em dias (
validity_days, contados da confirmação do pagamento), ou nenhuma: comvalidity_days: 0,expires_aténulle os créditos não vencem. - Estorno. Reembolsar o pagamento retira só os créditos que ainda não foram usados; o que já saiu da reserva continua usado.
- Quem pode comprar. Só o dono e os admins. Um plano sem limite mensal não tem o que estender:
403plan_limit_reached. Time cobrado por um parceiro:403billed_by_partner. - Pagamento. Cartão e, quando os preços do pacote estão em reais e o Pix está ligado, Pix (
payment_methods). No Pix, os créditos entram quando o pagamento é confirmado.
Onde a reserva aparece nas respostas
| Rota | Campo | O que traz |
|---|---|---|
GET /usage | emails.credits | A reserva ao lado da cota: emails.monthly continua com o used e o limit do plano. Chaves só de envio veem o saldo, mas não a lista grants. |
GET /billing | emails.monthly, email_credits | O resumo do time, só com a sessão do painel (dono ou admin): veja a tabela abaixo. |
GET /billing/email-credits | toda a resposta | A visão da seção Adicional de envios: a reserva, os pacotes à venda e as compras. |
Em GET /billing, a cota do plano e a reserva vêm separadas:
| Campo | O que diz |
|---|---|
emails.monthly | A cota mensal do plano: used, limit (null: sem limite) e resets_at. Os créditos nunca são somados a ela. |
email_credits | A reserva, no mesmo formato de balance em GET /billing/email-credits: reserve, balance, used_this_month, in_use, next_expires_at e grants. |
#Ver o Adicional de envios do time
GET/billing/email-credits
O que a seção Adicional de envios mostra: a reserva, os pacotes à venda e, para o dono e os admins, o histórico de compras.
available é false quando a cobrança não está no ar. included diz se o plano tem um limite mensal de envios que a reserva pode estender (false num plano ilimitado; então packs vem vazio). can_purchase é true para o dono e os admins de um time cobrado pela Postal, num plano com limite mensal. payment_methods lista card e, quando o Pix está ligado, pix.
balance é a reserva: balance (os créditos que dá para usar agora), used_this_month, in_use (true quando a cota mensal acabou e há saldo), next_expires_at (o primeiro vencimento, ou null) e grants, os pacotes e bônus com saldo na ordem em que são usados; expires_at: null é um pacote sem validade. packs são os pacotes à venda: emails, amount (centavos, na moeda principal currency), prices (um preço por moeda), validity_days (0: sem validade) e popular. purchases traz as 50 compras e bônus mais recentes (vazio para quem não é dono nem admin): status é pending (Pix ainda não pago), active, depleted, expired, failed, refunded ou revoked; source é purchase ou staff (bônus); emails é o que veio no pacote e remaining o que resta; receipt_url é o recibo, quando há.
Erros
- 403
invalid_accessNenhum time está selecionado na sessão.
GET /billing/email-credits HTTP/1.1
Host: api.postal.so
{
"object": "email_credits",
"available": true,
"included": true,
"can_purchase": true,
"payment_methods": ["card", "pix"],
"balance": {
"reserve": true,
"balance": 20000,
"used_this_month": 0,
"in_use": false,
"next_expires_at": "2027-01-15T12:00:00Z",
"grants": [
{
"id": "019a3f20-6c7d-7e8f-9a01-2b3c4d5e6f70",
"source": "purchase",
"name": "Pacote A",
"emails": 20000,
"remaining": 20000,
"expires_at": "2027-01-15T12:00:00Z"
}
]
},
"packs": [
{
"object": "email_credit_pack",
"id": "019a3f20-5b6c-7d7e-8f90-1a2b3c4d5e6f",
"name": "Pacote A",
"emails": 20000,
"amount": 4900,
"currency": "brl",
"prices": [
{
"currency": "brl",
"amount": 4900
},
{
"currency": "usd",
"amount": 900
}
],
"validity_days": 90,
"popular": true
},
{
"object": "email_credit_pack",
"id": "019a3f20-5b6d-7e8f-9a01-2b3c4d5e6f71",
"name": "Pacote B",
"emails": 100000,
"amount": 19900,
"currency": "brl",
"prices": [
{
"currency": "brl",
"amount": 19900
},
{
"currency": "usd",
"amount": 3900
}
],
"validity_days": 0,
"popular": false
}
],
"purchases": [
{
"object": "email_credit_grant",
"id": "019a3f20-6c7d-7e8f-9a01-2b3c4d5e6f70",
"source": "purchase",
"status": "active",
"name": "Pacote A",
"emails": 20000,
"remaining": 20000,
"amount": 4900,
"currency": "brl",
"validity_days": 90,
"expires_at": "2027-01-15T12:00:00Z",
"paid_at": "2026-10-17T12:00:00Z",
"refunded_at": null,
"receipt_url": "https://receipts.example.com/r/abc123",
"created_at": "2026-10-17T11:58:40Z"
}
]
}
#Comprar um pacote do Adicional de envios
POST/billing/email-credits/checkout
Cria a compra de um pacote e devolve o endereço da página de pagamento (url): leve a pessoa até ele. A compra ainda não existe como crédito: os envios entram na reserva quando o pagamento é confirmado (na hora no cartão; no Pix, quando ele é pago). O pagamento é único e usa a moeda da cobrança do time quando o pacote tem preço nela; senão, a moeda principal do pacote.
Ao terminar ou desistir, o navegador volta para Configurações › Cobrança do painel com ?extra_sends=success (pagamento recebido) ou ?extra_sends=canceled (compra cancelada, nada cobrado). Para saber se os créditos já entraram, leia GET /billing/email-credits de novo: a compra aparece em purchases como pending e passa a active. A rota tem limite de requisições mais baixo que o resto da API, como as outras que mexem na cobrança.
Corpo da requisição
pack_idstringobrigatórioO ID do pacote, como empacks[].iddeGET /billing/email-credits.
Erros
- 403
forbiddenQuem chamou não é dono nem admin do time, ou usou uma chave de API (estas rotas só aceitam a sessão do painel). - 403
billed_by_partnerO time é cobrado por um parceiro, que contrata o adicional. - 403
plan_limit_reachedO plano não tem limite mensal de envios, então não há o que estender. - 422
validation_errorpack_idausente, que não é um UUID ou inexistente, ou um pacote que não está mais à venda (a mensagem diz qual). - 503
billing_unavailableA cobrança não está disponível agora.
POST /billing/email-credits/checkout HTTP/1.1
Host: api.postal.so
Content-Type: application/json
{
"pack_id": "019a3f20-5b6c-7d7e-8f90-1a2b3c4d5e6f"
}
{
"url": "https://checkout.example.com/c/pay/cs_abc123"
}
#Adicional de contatos
Quando o plano de marketing do time oferece o Adicional de contatos, o dono ou um admin soma contatos ao limite sem trocar de plano. Cada pacote tem um preço recorrente, somado ao da assinatura de marketing, e o limite de contatos passa a ser contatos do plano + contatos de cada pacote × quantidade. Os pacotes só existem para um plano de marketing pago e ativo, e só os que a Postal colocou à venda: GET /plans traz os que estão à venda em contact_packs (vazio quando não há nenhum). O guia do painel mostra o fluxo na tela: Adicional de contatos.
Só com a sessão do painel
As três rotas abaixo usam a sessão do painel de um dono ou admin do time. Uma chave de API, de acesso total ou só de envio, recebe 403, e os outros membros do time também. Por isso os exemplos mostram a requisição como o painel a envia, sem cabeçalho de chave. Para ler o limite com os pacotes usando uma chave, use GET /usage.
- Aumentar. É cobrado na hora, de forma proporcional ao resto do período, e depois a cada renovação, na mesma fatura da assinatura. O limite só cresce depois que o pagamento é confirmado: confirmado na hora, a resposta é
200comaction: "updated"; se o pagamento precisa de confirmação (a autenticação do cartão, por exemplo), é202comaction: "payment_pending"e oinvoice_urlda fatura a pagar, e o limite não muda até ela ser paga. Pagamento recusado:422payment_failed, e nada muda. - Reduzir ou remover. Vale na próxima renovação, sem reembolso, e o limite continua o mesmo até lá (
action: "scheduled"). Pedir de novo a quantidade atual desiste da redução agendada (action: "reduction_canceled"). A redução é recusada com422downgrade_blocked(e a listablockers) se o time tem mais contatos do que o limite que sobraria, e também se a assinatura termina no fim do período. - Vários do mesmo pacote. Dá para ter mais de um, até o
max_quantitydo pacote por time. - Quem pode. Só com plano de marketing pago e assinatura ativa. Sem assinatura paga (plano Grátis), ou com a assinatura em teste ou com pagamento pendente:
403contact_packs_unavailable. Time cobrado por um parceiro:403billed_by_partner. A moeda e a cobrança (mensal ou anual) do pacote precisam ser as da assinatura. - Plano e assinatura. Os pacotes ficam quando o plano muda (upgrade ou mudança agendada) e somam ao limite do plano novo. Terminam junto com a assinatura, e o time volta ao limite do plano em que fica. Se o limite de contatos do time foi ajustado individualmente, esse número vale como o final e os pacotes não somam a ele.
Onde os pacotes aparecem nas respostas
| Rota | Campo | O que traz |
|---|---|---|
GET /plans | contact_packs | Os pacotes à venda, sem as quantidades do time: object, id, name, contacts (quantos contatos cada um soma), max_quantity, interval e prices ([{"currency", "amount"}], em centavos). |
GET /usage | contacts | limit é o limite que vale (plano + pacotes); plan_limit, só o do plano; packs traz contacts (o que os pacotes somam), effective, scheduled e, para chaves de acesso total, items. Chaves só de envio veem os totais, mas não quais pacotes o time tem. |
GET /billing | contact_packs | O resumo do time, só com a sessão do painel (dono ou admin): veja a tabela abaixo. |
GET /billing/contact-packs | toda a resposta | A visão da tela Adicional de contatos: o limite, a assinatura e os pacotes à venda. |
O resumo do time (contact_packs de GET /billing) tem estes campos:
| Campo | O que diz |
|---|---|
plan_limit | Os contatos do plano (null: sem limite). |
pack_contacts | Os contatos que os pacotes somam agora. |
effective_limit | O limite que vale agora: plan_limit + pack_contacts (null: sem limite). |
items | Os pacotes que o time tem: pack_id, name, contacts, quantity, scheduled_quantity (a quantidade depois da renovação, ou null sem redução agendada), contacts_total (contacts × quantity), max_quantity, amount (centavos, por unidade), currency e interval. |
scheduled | null, ou a redução que espera a renovação: effective_at, pack_contacts e effective_limit (o limite depois dela). |
#Ver os pacotes de contatos do time
GET/billing/contact-packs
O que a tela Adicional de contatos mostra: o limite, a assinatura de marketing e os pacotes à venda na moeda e na cobrança da assinatura, mais os que o time já tem (mesmo os que saíram de venda; quem já tem continua com o preço que assinou).
available é false sem cobrança no ar ou num time cobrado por parceiro. can_purchase diz se o time pode adicionar pacotes agora, e reason, quando não pode: no_paid_subscription, subscription_not_active, billed_by_partner ou billing_unavailable (null quando pode). currency e interval são os da assinatura. limit traz plan, packs, effective e scheduled (null, ou {"effective_at", "pack_contacts", "effective_limit"} quando há redução agendada). subscription é null sem assinatura de marketing paga. Em packs, amount é o preço de um pacote, em centavos, na moeda da assinatura (null se o pacote não é vendido nela), prices lista as moedas, purchasable diz se dá para aumentar a quantidade e quantity e scheduled_quantity são as do time.
Erros
- 403
forbiddenQuem chamou não é dono nem admin do time, ou usou uma chave de API (estas rotas só aceitam a sessão do painel).
GET /billing/contact-packs HTTP/1.1
Host: api.postal.so
{
"object": "contact_packs",
"available": true,
"can_purchase": true,
"reason": null,
"currency": "brl",
"interval": "month",
"contacts_used": 3120,
"limit": {
"plan": 5000,
"packs": 20000,
"effective": 25000,
"scheduled": null
},
"subscription": {
"id": "019a2e11-6c7d-7e8f-9a01-3b4c5d6e7f80",
"status": "active",
"currency": "brl",
"interval": "month",
"amount": 5900,
"current_period_end": "2026-11-20T12:00:00+00:00",
"cancel_at_period_end": false
},
"packs": [
{
"object": "contact_pack",
"id": "019a2e10-4b5c-7d6e-8f70-1a2b3c4d5e6f",
"name": "Pacote A",
"contacts": 10000,
"max_quantity": 10,
"amount": 4900,
"currency": "brl",
"interval": "month",
"prices": [
{
"currency": "brl",
"amount": 4900
},
{
"currency": "usd",
"amount": 900
}
],
"purchasable": true,
"quantity": 2,
"scheduled_quantity": null
},
{
"object": "contact_pack",
"id": "019a2e10-4b5d-7e7f-9a81-2b3c4d5e6f70",
"name": "Pacote B",
"contacts": 25000,
"max_quantity": 10,
"amount": 9900,
"currency": "brl",
"interval": "month",
"prices": [
{
"currency": "brl",
"amount": 9900
},
{
"currency": "usd",
"amount": 1900
}
],
"purchasable": true,
"quantity": 0,
"scheduled_quantity": null
}
]
}
#Cotar uma quantidade de pacotes
POST/billing/contact-packs/quote
Mostra o que aconteceria com esta quantidade do pacote, sem mudar nada: é o que o painel pergunta antes do botão Confirmar. Uma mudança que não pode ser feita não é um erro: a resposta é 200 com change.allowed: false e o motivo em change.reason.
pack é o pacote, como na listagem, mas com quantity e scheduled_quantity zerados: as quantidades do time estão em current (quantity, scheduled_quantity e o limit de hoje). change.type é increase, decrease, keep (desiste de uma redução agendada) ou same, e change.effective é now ou next_period (com effective_at na redução). change.proration_amount é o valor cobrado agora num aumento, em centavos, proporcional ao resto do período (null fora de um aumento ou quando o cálculo não está disponível). change.reason (null quando permitido) é um destes: no_paid_subscription, subscription_not_active, billed_by_partner, billing_unavailable, pack_unavailable, currency_mismatch, interval_mismatch, max_quantity, same_quantity, subscription_ending ou contacts_above_limit (com os blockers, no formato do downgrade_blocked).
after traz limit_after_payment (o limite depois que o aumento for pago), limit_after_renewal (o limite depois da próxima renovação), pack_amount (este pacote vezes a quantidade, em centavos) e recurring_total (o que a assinatura passa a custar por período: plano mais pacotes). Os valores em dinheiro são null quando não dá para calcular.
Corpo da requisição
pack_idstringobrigatórioO ID do pacote, como empacks[].iddeGET /billing/contact-packs.quantityintegerobrigatórioA quantidade total que o time quer ter deste pacote, de 0 a 1000 (não é quanto somar). Pode ser maior (aumento), menor ou0(redução).
Erros
- 403
forbiddenQuem chamou não é dono nem admin do time, ou usou uma chave de API (estas rotas só aceitam a sessão do painel). - 422
validation_errorpack_idinexistente ouquantityque não é um inteiro de 0 a 1000.
POST /billing/contact-packs/quote HTTP/1.1
Host: api.postal.so
Content-Type: application/json
{
"pack_id": "019a2e10-4b5c-7d6e-8f70-1a2b3c4d5e6f",
"quantity": 3
}
{
"object": "contact_pack_quote",
"pack": {
"object": "contact_pack",
"id": "019a2e10-4b5c-7d6e-8f70-1a2b3c4d5e6f",
"name": "Pacote A",
"contacts": 10000,
"max_quantity": 10,
"amount": 4900,
"currency": "brl",
"interval": "month",
"prices": [
{
"currency": "brl",
"amount": 4900
},
{
"currency": "usd",
"amount": 900
}
],
"purchasable": true,
"quantity": 0,
"scheduled_quantity": null
},
"quantity": 3,
"currency": "brl",
"interval": "month",
"current": {
"quantity": 1,
"scheduled_quantity": null,
"limit": 15000
},
"change": {
"type": "increase",
"effective": "now",
"effective_at": null,
"proration_amount": 4000,
"allowed": true,
"reason": null,
"blockers": []
},
"after": {
"limit_after_payment": 35000,
"limit_after_renewal": 35000,
"pack_amount": 14700,
"recurring_total": 20600
}
}
{
"object": "contact_pack_quote",
"pack": {
"object": "contact_pack",
"id": "019a2e10-4b5c-7d6e-8f70-1a2b3c4d5e6f",
"name": "Pacote A",
"contacts": 10000,
"max_quantity": 10,
"amount": 4900,
"currency": "brl",
"interval": "month",
"prices": [
{
"currency": "brl",
"amount": 4900
},
{
"currency": "usd",
"amount": 900
}
],
"purchasable": true,
"quantity": 0,
"scheduled_quantity": null
},
"quantity": 0,
"currency": "brl",
"interval": "month",
"current": {
"quantity": 3,
"scheduled_quantity": null,
"limit": 35000
},
"change": {
"type": "decrease",
"effective": "next_period",
"effective_at": "2026-11-20T12:00:00+00:00",
"proration_amount": null,
"allowed": false,
"reason": "contacts_above_limit",
"blockers": [
{
"key": "contacts",
"label": "Contatos",
"type": "quantity",
"used": 16000,
"limit": 15000,
"excess": 1000,
"action": "manage",
"manage_path": "/audience"
}
]
},
"after": {
"limit_after_payment": 35000,
"limit_after_renewal": 5000,
"pack_amount": 0,
"recurring_total": 5900
}
}
#Definir a quantidade de um pacote
PUT/billing/contact-packs
Define quantos deste pacote o time tem (0 remove). POST no mesmo caminho faz o mesmo. A quantidade é a total, e a resposta diz o que foi feito em action:
| action | Status | O que significa |
|---|---|---|
updated | 200 | O aumento foi cobrado e pago: o limite já vale. A resposta traz o contact_packs atualizado. |
payment_pending | 202 | O pagamento não foi confirmado (o cartão pede confirmação, por exemplo). invoice_url é o link da fatura para pagar. O limite não muda até o pagamento ser confirmado: leia GET /billing/contact-packs de novo depois. |
scheduled | 200 | Uma redução ou remoção ficou para a próxima renovação (effective_at), sem reembolso. O limite continua o mesmo até lá. |
reduction_canceled | 200 | Você pediu a quantidade atual enquanto havia uma redução agendada, e ela foi desfeita. |
A rota tem limite de requisições mais baixo que o resto da API, como as outras que mexem na cobrança. Antes de confirmar, use a cotação para mostrar o valor.
Corpo da requisição
pack_idstringobrigatórioO ID do pacote, como empacks[].iddeGET /billing/contact-packs.quantityintegerobrigatórioA quantidade total que o time quer ter deste pacote, de 0 a 1000 (não é quanto somar).0remove o pacote na próxima renovação.
Erros
- 403
forbiddenQuem chamou não é dono nem admin do time, ou usou uma chave de API (estas rotas só aceitam a sessão do painel). - 403
contact_packs_unavailableSem plano de marketing pago, ou com a assinatura em teste ou com pagamento pendente. - 403
billed_by_partnerO time é cobrado por um parceiro, que muda os pacotes. - 422
validation_errorPacote fora de venda, não vendido na moeda ou na cobrança da assinatura, quantidade acima domax_quantity, quantidade igual à atual ou assinatura que termina no fim do período. A mensagem diz qual. - 422
payment_failedO cartão foi recusado. Nada mudou. - 422
downgrade_blockedO time tem mais contatos do que o limite que sobraria.blockersdiz quantos contatos passam. - 502
stripe_unavailableO serviço de pagamento não respondeu como esperado. Tente de novo em instantes; nada mudou. - 503
billing_unavailableA cobrança não está disponível agora.
PUT /billing/contact-packs HTTP/1.1
Host: api.postal.so
Content-Type: application/json
{
"pack_id": "019a2e10-4b5c-7d6e-8f70-1a2b3c4d5e6f",
"quantity": 2
}
{
"action": "updated",
"contact_packs": {
"object": "contact_packs",
"available": true,
"can_purchase": true,
"reason": null,
"currency": "brl",
"interval": "month",
"contacts_used": 3120,
"limit": {
"plan": 5000,
"packs": 20000,
"effective": 25000,
"scheduled": null
},
"subscription": {
"id": "019a2e11-6c7d-7e8f-9a01-3b4c5d6e7f80",
"status": "active",
"currency": "brl",
"interval": "month",
"amount": 5900,
"current_period_end": "2026-11-20T12:00:00+00:00",
"cancel_at_period_end": false
},
"packs": [
{
"object": "contact_pack",
"id": "019a2e10-4b5c-7d6e-8f70-1a2b3c4d5e6f",
"name": "Pacote A",
"contacts": 10000,
"max_quantity": 10,
"amount": 4900,
"currency": "brl",
"interval": "month",
"prices": [
{
"currency": "brl",
"amount": 4900
},
{
"currency": "usd",
"amount": 900
}
],
"purchasable": true,
"quantity": 2,
"scheduled_quantity": null
},
{
"object": "contact_pack",
"id": "019a2e10-4b5d-7e7f-9a81-2b3c4d5e6f70",
"name": "Pacote B",
"contacts": 25000,
"max_quantity": 10,
"amount": 9900,
"currency": "brl",
"interval": "month",
"prices": [
{
"currency": "brl",
"amount": 9900
},
{
"currency": "usd",
"amount": 1900
}
],
"purchasable": true,
"quantity": 0,
"scheduled_quantity": null
}
]
}
}
{
"action": "payment_pending",
"invoice_url": "https://invoice.example.com/i/inv_abc123",
"contact_packs": {
"object": "contact_packs",
"…": "…"
}
}
{
"action": "scheduled",
"effective_at": "2026-11-20T12:00:00+00:00",
"contact_packs": {
"object": "contact_packs",
"…": "…"
}
}
{
"statusCode": 422,
"name": "downgrade_blocked",
"message": "Você tem mais contatos do que o limite que restaria ao reduzir este pacote. Remova contatos primeiro e depois reduza o pacote.",
"blockers": [
{
"key": "contacts",
"label": "Contatos",
"type": "quantity",
"used": 16000,
"limit": 15000,
"excess": 1000,
"action": "manage",
"manage_path": "/audience"
}
]
}