Entrar Criar conta
Audiência

Contatos

As pessoas da sua audiência: dados, propriedades, segmentos e inscrição em tópicos.

Contatos são as pessoas da sua audiência de marketing: e-mail, nome, propriedades personalizadas, segmentos e a inscrição em tópicos. Cada e-mail aparece uma vez por time. O número de contatos depende do plano de marketing (Grátis: 1.000, Profissional: 5.000, Escala: 50.000, Empresarial: sem limite). Nos endpoints com {id}, você pode usar o ID ou o próprio e-mail do contato. Para trazer muitos contatos de uma vez, use a importação de CSV. Endereços que estão na lista de supressão nunca viram contatos (veja Criar um contato).

Cada contato também guarda o external_id (apelido user_id): o ID da pessoa no seu sistema, único no time. Em qualquer endpoint com {id}, você pode usar external_id:user_42 (ou user_id:user_42) no lugar do ID e do e-mail. As etiquetas marcam contatos sem precisar de um segmento, e o lote cria ou atualiza até 10.000 contatos numa chamada. Todos os endpoints desta página exigem uma chave de API de acesso total (uma chave só de envio recebe 401 restricted_api_key).

Quem recebe um e-mail transacional seu também vira contato, sozinho, mas não confirmado para marketing: veja Contatos guardados dos e-mails transacionais e os campos marketing_consent, origin e consent_confirmed_at.

#Origem das alterações

Cada alteração de um contato guarda de onde veio em last_change_source: api, dashboard, form, import, automation, partner, system, transactional, postal_lifecycle, crm:{id da conexão} ou um rótulo seu. Para dar um rótulo à alteração, envie o cabeçalho X-Postal-Source ou o campo source no corpo (se os dois vierem, vale o campo): até 64 caracteres entre letras minúsculas, números, ., _, : e -. Os nomes que a Postal usa e o prefixo crm: são reservados e geram 422. Os webhooks contact.* trazem o campo source, e um endpoint de webhook pode ignorar origens (skip_sources), o que evita eco entre dois sistemas que se sincronizam nos dois sentidos. Na lista, ?source= filtra por origem.

#Contatos guardados dos e-mails transacionais

Todo endereço que recebe um e-mail transacional seu fica guardado, sozinho, na audiência do time, para você poder mandar marketing depois a quem já interagiu com a sua empresa (quem preencheu um formulário de contato, quem se cadastrou…). Entram os destinatários de POST /emails, de POST /emails/batch, do SMTP e dos e-mails de autenticação das conexões do Supabase, só em to e cc (nunca bcc). Não entram os destinatários de campanhas, automações, testes de template e de campanha, encaminhamentos, respostas da inbox e envios para os endereços de teste. O contato é criado em segundo plano, depois que o provedor aceitou o e-mail, sem atrasar o envio. O nome de exibição (Ana Souza <[email protected]>) vira first_name e last_name, quando existe.

CampoValoresO que diz
marketing_consentconfirmed, unconfirmedSe o contato pode receber marketing (campanhas e automações). Todo contato que já existia e todo criado de outro jeito (POST /contacts, lote, eventos, importação, formulário, CRM) é confirmed. O contato guardado de um e-mail transacional nasce unconfirmed.
origintransactional, nullDe onde o contato veio: transactional para os guardados dos e-mails transacionais; null para os demais. Não muda quando o contato é confirmado.
consent_confirmed_atdata ou nullQuando o contato foi confirmado pela declaração do dono ou de um admin (ação em massa confirm_consent) ou por uma ação explícita. null nos que nunca foram confirmados dessa forma.
  • Fora de campanhas e automações. Um contato unconfirmed não entra na lista de envio de uma campanha (nem em recipients_count e GET /broadcasts/audience), não inicia automações e o passo de enviar e-mail pula quem deixou de estar confirmado. Os seus envios transacionais para ele não mudam. Ele está na audiência de verdade: conta no limite de contatos, aparece em GET /contacts, nos contadores e nos segmentos.
  • Silencioso. A captura não dispara o webhook contact.created, nenhuma automação (contact.created, segment.added…) e não empurra o contato para um CRM conectado. A origem da alteração é transactional.
  • Nunca mexe no que existe. Um contato que já existe (com o consentimento, os tópicos, o descadastro e os dados que tem) fica como está, e um endereço da lista de supressão nunca vira contato.
  • Nunca passa do limite do plano. A captura usa o limite de contatos do plano de marketing e para ao chegar nele, mesmo com o excedente ligado (o excedente cobra contatos que você adicionou de propósito; a captura automática não cobra ninguém). Ela pausa em silêncio e volta sozinha quando houver espaço. O bloco audience de GET /usage diz se está pausada.
  • Segmento padrão. Na primeira captura, se o plano inclui segmentos dinâmicos e tem espaço para mais um, nasce o segmento dinâmico Envios transacionais (origin é transactional), uma vez só: se você apagar, não volta.
  • O que confirma um contato. A confirmação em massa e as ações explícitas: reimportar o endereço por CSV, POST /contacts/batch e POST /events com o contato, PATCH /contacts/{id} e a inscrição da pessoa num formulário. Confirmar não dispara contact.created, e uma captura nunca rebaixa um contato já confirmado.
  • Desligar. A captura vem ligada. A chave do time, capture_transactional_contacts, muda com PATCH /team e {"capture_transactional_contacts": false}, mas só o dono e os admins, e só pelo painel (Configurações › Time): a rota usa a sessão do painel e não aceita chave de API. Os e-mails que já estavam na fila antes do recurso entrar no ar não são capturados.

Para listar só esses contatos, use os filtros consent=unconfirmed e origin=transactional em GET /contacts; eles valem também nas seleções das ações em massa, nas exportações e (como as condições consent e origin) nos segmentos dinâmicos. Para ver quantos esperam confirmação, use GET /contacts/consent-summary.

#Listar contatos

GET/contacts

Chave de acesso total

Do mais recente para o mais antigo. A lista não traz os segmentos de cada contato.

Parâmetros de consulta

  • limitinteger
    De 1 a 100. Padrão: 20.
  • afterstring
    Cursor da próxima página.
  • beforestring
    Cursor da página anterior.
  • searchstring
    Busca no e-mail, no external_id e no nome.
  • segment_idstring
    Só contatos de um segmento: os da lista, se o segmento for estático, ou os que combinam com os filtros agora, se for dinâmico.
  • topic_idstring
    Só contatos inscritos no tópico: os que escolheram receber e, se o tópico for opt_in por padrão, os que não escolheram nada.
  • tagstring
    Só contatos com a etiqueta (ID ou nome, sem diferenciar maiúsculas).
  • unsubscribedboolean
    true para os descadastrados; false para os demais.
  • email_statusstring
    Resultado da validação de e-mails, separado por vírgula: valid, invalid, risky e unverified (nunca validado).
  • external_idstring
    Só o contato com este external_id (também aceito como user_id).
  • sourcestring
    Só contatos cuja última alteração veio desta origem.
  • consentstring
    confirmed ou unconfirmed: só contatos que podem (ou que ainda não podem) receber marketing. Veja Contatos guardados dos e-mails transacionais.
  • originstring
    transactional: só os contatos guardados dos e-mails transacionais.

Erros

  • 422validation_error
    Algum campo é inválido. A mensagem diz qual.
GET /contacts

cURL

curl 'https://api.postal.so/contacts?segment_id=019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a&unsubscribed=false&tag=Cliente+VIP' \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/contacts?segment_id=019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a&unsubscribed=false&tag=Cliente+VIP', {
  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/contacts",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    params={
        "segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
        "unsubscribed": "false",
        "tag": "Cliente VIP",
    },
    timeout=10,
)

data = response.json()

PHP

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

$response = $client->get('contacts', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'query' => [
        'segment_id' => '019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a',
        'unsubscribed' => 'false',
        'tag' => 'Cliente VIP',
    ],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "list",
  "has_more": true,
  "data": [
    {
      "object": "contact",
      "id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
      "email": "[email protected]",
      "external_id": "user_42",
      "user_id": "user_42",
      "first_name": "Ana",
      "last_name": "Souza",
      "unsubscribed": false,
      "marketing_consent": "confirmed",
      "origin": null,
      "consent_confirmed_at": null,
      "email_status": "valid",
      "email_verified_at": "2026-10-20T09:30:00.000000Z",
      "properties": {
        "plan": "pro",
        "city": "Recife",
        "customer_since": "2024-03-18"
      },
      "last_change_source": "api",
      "created_at": "2026-10-24T14:00:00.000000Z",
      "updated_at": "2026-10-24T14:00:00.000000Z",
      "tags": [
        {
          "id": "019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f",
          "name": "Cliente VIP",
          "color": "purple"
        }
      ]
    }
  ]
}

#Criar um contato

POST/contacts

Chave de acesso total

Cria o contato e, se quiser, já o coloca em segmentos estáticos, aplica etiquetas e registra a escolha dele nos tópicos. Dispara o webhook contact.created.

Um endereço que está na lista de supressão do time nunca vira contato: a resposta é 422 com email_suppressed. Para cadastrá-lo, remova-o antes da lista de supressão. Contatos que já existem não são afetados.

Quer criar ou atualizar sem saber se o contato já existe? Use o lote, que procura por ID, external_id e e-mail.

Corpo da requisição

  • emailstringobrigatório
    E-mail, até 320 caracteres. Guardado em minúsculas.
  • external_idstring | null
    ID da pessoa no seu sistema, até 255 caracteres, único no time. Também aceito como user_id.
  • first_namestring | null
    Nome, até 255 caracteres.
  • last_namestring | null
    Sobrenome, até 255 caracteres.
  • unsubscribedboolean
    Descadastrado de todos os envios de marketing. Padrão: false.
  • propertiesobject
    Valores das propriedades, como {"plan": "pro"}. Cada chave precisa existir antes; os valores são convertidos para o tipo da propriedade.
  • segmentsstring[]
    IDs de até 100 segmentos estáticos (ou objetos {"id": "…"}). Segmentos dinâmicos não aceitam contatos à mão.
  • topicsobject[]
    Até 100 escolhas de tópicos.
    • idstringobrigatório
      ID do tópico.
    • subscriptionstringobrigatório
      opt_in (quer receber) ou opt_out (não quer).
  • tagsstring[]
    Até 50 etiquetas, por ID ou por nome. Os nomes que ainda não existem são criados.
  • sourcestring
    Rótulo da origem da alteração. Também aceito no cabeçalho X-Postal-Source.

Erros

  • 422validation_error
    E-mail inválido ou já cadastrado, external_id já usado por outro contato, propriedade desconhecida, valor com o tipo errado, ou segmento dinâmico em segments.
  • 422email_suppressed
    O e-mail está na lista de supressão do time, então não pode virar contato.
  • 403plan_limit_reached
    O plano não permite mais contatos, contando os pacotes do Adicional de contatos (a mensagem diz como continuar acima do limite ou adicionar um pacote, se o plano permitir).
  • 404not_found
    Algum segmento, tópico ou etiqueta (dada por ID) não existe.
POST /contacts

cURL

curl -X POST https://api.postal.so/contacts \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "external_id": "user_42",
    "first_name": "Ana",
    "last_name": "Souza",
    "properties": {
      "plan": "pro",
      "city": "Recife"
    },
    "segments": ["019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a"],
    "topics": [
      {
        "id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
        "subscription": "opt_in"
      }
    ],
    "tags": ["Cliente VIP"]
  }'

Node.js

const response = await fetch('https://api.postal.so/contacts', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    email: '[email protected]',
    external_id: 'user_42',
    first_name: 'Ana',
    last_name: 'Souza',
    properties: {
      plan: 'pro',
      city: 'Recife',
    },
    segments: ['019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a'],
    topics: [
      {
        id: '019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b',
        subscription: 'opt_in',
      },
    ],
    tags: ['Cliente VIP'],
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/contacts",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    json={
        "email": "[email protected]",
        "external_id": "user_42",
        "first_name": "Ana",
        "last_name": "Souza",
        "properties": {
            "plan": "pro",
            "city": "Recife",
        },
        "segments": ["019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a"],
        "topics": [
            {
                "id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
                "subscription": "opt_in",
            },
        ],
        "tags": ["Cliente VIP"],
    },
    timeout=10,
)

data = response.json()

PHP

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

$response = $client->post('contacts', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'json' => [
        'email' => '[email protected]',
        'external_id' => 'user_42',
        'first_name' => 'Ana',
        'last_name' => 'Souza',
        'properties' => [
            'plan' => 'pro',
            'city' => 'Recife',
        ],
        'segments' => ['019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a'],
        'topics' => [
            [
                'id' => '019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b',
                'subscription' => 'opt_in',
            ],
        ],
        'tags' => ['Cliente VIP'],
    ],
]);

$data = json_decode($response->getBody(), true);
201Resposta
{
  "object": "contact",
  "id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f"
}

#Obter um contato

GET/contacts/{id}

Chave de acesso total

Devolve o contato com os segmentos de que ele faz parte e as etiquetas. email_status e email_verified_at vêm da última validação de e-mails do endereço e são null se ele nunca foi validado.

Parâmetros de caminho

  • idstringobrigatório
    ID ou e-mail do contato.

Erros

  • 404not_found
    O recurso não existe ou é de outro time.
GET /contacts/{id}

cURL

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

Node.js

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

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "contact",
  "id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
  "email": "[email protected]",
  "external_id": "user_42",
  "user_id": "user_42",
  "first_name": "Ana",
  "last_name": "Souza",
  "unsubscribed": false,
  "marketing_consent": "confirmed",
  "origin": null,
  "consent_confirmed_at": null,
  "email_status": "valid",
  "email_verified_at": "2026-10-20T09:30:00.000000Z",
  "properties": {
    "plan": "pro",
    "city": "Recife",
    "customer_since": "2024-03-18"
  },
  "last_change_source": "api",
  "created_at": "2026-10-24T14:00:00.000000Z",
  "updated_at": "2026-10-24T14:00:00.000000Z",
  "tags": [
    {
      "id": "019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f",
      "name": "Cliente VIP",
      "color": "purple"
    }
  ],
  "segments": [
    {
      "id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
      "name": "Clientes ativos"
    }
  ]
}

#Atualizar um contato

PATCH/contacts/{id}

Chave de acesso total

Só os campos enviados mudam. Em properties, as chaves enviadas são atualizadas e as outras ficam como estão; mande null ou "" para apagar um valor. Segmentos e tópicos têm endpoints próprios (abaixo). Se algo mudou, dispara contact.updated.

Em tags, a lista enviada substitui as etiquetas do contato; para só acrescentar ou tirar uma, use os endpoints de etiquetas do contato. No lote, quem se descadastrou só é reinscrito com a prova do novo consentimento (resubscribe).

Atualizar um contato unconfirmed (guardado de um e-mail transacional) é uma ação explícita: ele passa a confirmed.

Parâmetros de caminho

  • idstringobrigatório
    ID ou e-mail do contato.

Corpo da requisição (todos opcionais)

  • emailstring
    Novo e-mail.
  • external_idstring | null
    Novo external_id (ou user_id).
  • first_namestring | null
    Nome.
  • last_namestring | null
    Sobrenome.
  • unsubscribedboolean
    Descadastra (true) ou reinscreve (false).
  • propertiesobject
    Valores a mudar.
  • tagsstring[]
    Até 50 etiquetas (ID ou nome) que passam a ser as únicas do contato.
  • sourcestring
    Rótulo da origem da alteração.

Erros

  • 422validation_error
    E-mail ou external_id já usado por outro contato, propriedade desconhecida, valor inválido, ou segments/topics no corpo.
  • 404not_found
    O recurso não existe ou é de outro time.
PATCH /contacts/{id}

cURL

curl -X PATCH https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "properties": {
      "plan": "scale",
      "city": null
    }
  }'

Node.js

const response = await fetch('https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    properties: {
      plan: 'scale',
      city: null,
    },
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.patch(
    "https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    json={
        "properties": {
            "plan": "scale",
            "city": None,
        },
    },
    timeout=10,
)

data = response.json()

PHP

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

$response = $client->patch('contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'json' => [
        'properties' => [
            'plan' => 'scale',
            'city' => null,
        ],
    ],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "contact",
  "id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f"
}

#Excluir um contato

DELETE/contacts/{id}

Chave de acesso total

Remove o contato da audiência, com os segmentos e as escolhas de tópicos. Dispara contact.deleted. Para só parar de enviar, prefira unsubscribed: true.

Parâmetros de caminho

  • idstringobrigatório
    ID ou e-mail do contato.

Erros

  • 404not_found
    O recurso não existe ou é de outro time.
DELETE /contacts/{id}

cURL

curl -X DELETE https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f', {
  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/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
    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('contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "contact",
  "id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
  "deleted": true
}

#Segmentos de um contato

GET/contacts/{id}/segments

Chave de acesso total

Os segmentos de que o contato faz parte: os estáticos a que foi adicionado e os dinâmicos cujos filtros ele combina (conforme a última checagem). Aceita limit, after e before.

Parâmetros de caminho

  • idstringobrigatório
    ID ou e-mail do contato.

Erros

  • 404not_found
    O recurso não existe ou é de outro time.
GET /contacts/{id}/segments

cURL

curl https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/segments \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/segments', {
  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/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/segments",
    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('contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/segments', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "segment",
      "id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
      "paused_by_plan": false,
      "name": "Clientes ativos",
      "description": "Compraram nos últimos 90 dias.",
      "type": "static",
      "created_at": "2026-10-20T10:00:00.000000Z",
      "updated_at": "2026-10-20T10:00:00.000000Z"
    }
  ]
}

#Adicionar a um segmento

POST/contacts/{id}/segments/{segment_id}

Chave de acesso total

Coloca o contato no segmento. Se ele já estiver lá, nada muda. Só segmentos estáticos aceitam contatos à mão.

Parâmetros de caminho

  • idstringobrigatório
    ID ou e-mail do contato.
  • segment_idstringobrigatório
    ID do segmento.

Erros

  • 404not_found
    O contato ou o segmento não existe.
  • 422validation_error
    O segmento é dinâmico: ele guarda quem combina com os filtros, e não aceita contatos à mão.
POST /contacts/{id}/segments/{segment_id}

cURL

curl -X POST https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/segments/019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/segments/019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a', {
  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/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/segments/019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
    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('contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/segments/019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
201Resposta
{
  "object": "contact_segment",
  "contact_id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
  "segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a"
}

#Remover de um segmento

DELETE/contacts/{id}/segments/{segment_id}

Chave de acesso total

Parâmetros de caminho

  • idstringobrigatório
    ID ou e-mail do contato.
  • segment_idstringobrigatório
    ID do segmento.

Erros

  • 404not_found
    O contato ou o segmento não existe.
  • 422validation_error
    O segmento é dinâmico: ele guarda quem combina com os filtros, e não aceita contatos à mão.
DELETE /contacts/{id}/segments/{segment_id}

cURL

curl -X DELETE https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/segments/019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/segments/019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a', {
  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/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/segments/019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
    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('contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/segments/019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "contact_segment",
  "contact_id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
  "segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
  "deleted": true
}

#Tópicos de um contato

GET/contacts/{id}/topics

Chave de acesso total

Todos os tópicos do time, em ordem alfabética, com a inscrição do contato: a escolha dele ou, se ele não escolheu, o padrão do tópico.

Parâmetros de caminho

  • idstringobrigatório
    ID ou e-mail do contato.

Erros

  • 404not_found
    O recurso não existe ou é de outro time.
GET /contacts/{id}/topics

cURL

curl https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/topics \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/topics', {
  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/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/topics",
    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('contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/topics', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
      "name": "Novidades",
      "description": "Lançamentos e promoções, uma vez por semana.",
      "visibility": "public",
      "default_subscription": "opt_in",
      "subscription": "opt_in"
    },
    {
      "id": "019a1d95-4c5e-7f70-9b8c-9d0e1f2a3b4c",
      "name": "Pesquisas",
      "description": null,
      "visibility": "private",
      "default_subscription": "opt_out",
      "subscription": "opt_out"
    }
  ]
}

#Mudar a inscrição em tópicos

PATCH/contacts/{id}/topics

Chave de acesso total

Registra a escolha do contato em um ou mais tópicos. Os tópicos não enviados ficam como estão.

Parâmetros de caminho

  • idstringobrigatório
    ID ou e-mail do contato.

Corpo da requisição

  • topicsobject[]obrigatório
    De 1 a 100 escolhas, sem tópicos repetidos.
    • idstringobrigatório
      ID do tópico.
    • subscriptionstringobrigatório
      opt_in ou opt_out.

Erros

  • 422validation_error
    Algum campo é inválido. A mensagem diz qual.
  • 404not_found
    O contato ou algum tópico não existe.
PATCH /contacts/{id}/topics

cURL

curl -X PATCH https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/topics \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "topics": [
      {
        "id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
        "subscription": "opt_out"
      }
    ]
  }'

Node.js

const response = await fetch('https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/topics', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    topics: [
      {
        id: '019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b',
        subscription: 'opt_out',
      },
    ],
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.patch(
    "https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/topics",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    json={
        "topics": [
            {
                "id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
                "subscription": "opt_out",
            },
        ],
    },
    timeout=10,
)

data = response.json()

PHP

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

$response = $client->patch('contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/topics', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'json' => [
        'topics' => [
            [
                'id' => '019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b',
                'subscription' => 'opt_out',
            ],
        ],
    ],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "contact",
  "id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f"
}

#Criar ou atualizar contatos em lote

POST/contacts/batch

Chave de acesso total

Cria ou atualiza vários contatos de uma vez (upsert). Cada item procura o contato pelo id, depois pelo external_id e depois pelo e-mail; se não achar e houver e-mail, cria. Achado pelo external_id, um e-mail novo substitui o antigo; achado pelo e-mail, o external_id é gravado se o contato ainda não tinha um.

Até 1.000 itens são processados na própria requisição e a resposta traz um resultado por item (200): um item com erro não derruba os outros. Acima disso (até 10.000), o lote vai para a fila e a resposta é 202 com o id do lote, para acompanhar em GET /contacts/batches/{id}. Dá para repetir a chamada com segurança: use o cabeçalho Idempotency-Key.

Regras por item: etiquetas e segmentos são só acrescentados (nunca removidos); tópicos são gravados; propriedades são conferidas com as definições do time e mescladas; endereços da lista de supressão não viram contatos (o item falha com code: "email_suppressed"), mas contatos que já existem podem ser atualizados. Quem se descadastrou não volta a receber só porque o item traz unsubscribed: false: o item ganha o aviso kept_unsubscribed. Para reinscrever, envie resubscribe com a data e a prova do novo consentimento.

Cabeçalhos

  • Idempotency-Keystring
    Opcional. Repetir a chamada com a mesma chave devolve a resposta guardada.

Corpo da requisição

  • contactsobject[]obrigatório
    De 1 a 10.000 itens.
    • idstring
      ID do contato (procurado primeiro).
    • external_idstring
      ID da pessoa no seu sistema (também aceito como user_id).
    • emailstring
      E-mail. Obrigatório para criar um contato; sem id nem external_id, é a chave de busca.
    • first_namestring | null
      Nome.
    • last_namestring | null
      Sobrenome.
    • unsubscribedboolean
      true descadastra; false só vale para contatos novos ou que já estavam inscritos.
    • resubscribeobject
      Reinscreve quem se descadastrou, junto com unsubscribed: false: consented_at (data, não pode ser futura) e proof (como a pessoa consentiu, até 500 caracteres).
    • propertiesobject
      Valores das propriedades.
    • tagsstring[]
      Até 50 etiquetas (ID ou nome), só acrescentadas.
    • segmentsstring[]
      Até 100 segmentos estáticos, só acrescentados.
    • topicsobject[]
      Até 100 escolhas {"id", "subscription"}.
  • sourcestring
    Rótulo da origem das alterações. Também aceito no cabeçalho X-Postal-Source.

Em cada item de results, status é created, updated, unchanged ou failed. Itens que falharam trazem error (e code, quando há) e id: null; itens com avisos trazem warnings. index é a posição do item na lista enviada.

Erros

  • 422validation_error
    contacts ausente, vazio ou com mais de 10.000 itens. Erros de um item não geram este erro: eles ficam em results.
  • 403plan_limit_reached
    O plano não permite mais contatos: os itens que criariam contatos falham com esta mensagem em results.
POST /contacts/batch

cURL

curl -X POST https://api.postal.so/contacts/batch \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Idempotency-Key: sync-2026-10-24-01" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      {
        "external_id": "user_42",
        "email": "[email protected]",
        "first_name": "Ana",
        "properties": {
          "plan": "pro"
        },
        "tags": ["Cliente VIP"]
      },
      {
        "external_id": "user_43",
        "email": "[email protected]",
        "topics": [
          {
            "id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
            "subscription": "opt_in"
          }
        ]
      }
    ]
  }'

Node.js

import { Postal } from 'postal-so'

const postal = new Postal() // lê POSTAL_API_KEY

const batch = await postal.contacts.batch([
  { external_id: 'user_42', email: '[email protected]', first_name: 'Ana', properties: { plan: 'pro' }, tags: ['Cliente VIP'] },
  { external_id: 'user_43', email: '[email protected]', topics: [{ id: '019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b', subscription: 'opt_in' }] },
])

console.log(batch.counts) // { created: 1, updated: 1, unchanged: 0, failed: 0 }

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/contacts/batch",
    headers={
        "Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}",
        "Idempotency-Key": "sync-2026-10-24-01",
    },
    json={
        "contacts": [
            {
                "external_id": "user_42",
                "email": "[email protected]",
                "first_name": "Ana",
                "properties": {
                    "plan": "pro",
                },
                "tags": ["Cliente VIP"],
            },
            {
                "external_id": "user_43",
                "email": "[email protected]",
                "topics": [
                    {
                        "id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
                        "subscription": "opt_in",
                    },
                ],
            },
        ],
    },
    timeout=10,
)

data = response.json()

PHP

use Postal\Client;

$postal = new Client(); // lê POSTAL_API_KEY

$batch = $postal->contacts->batch([
    ['external_id' => 'user_42', 'email' => '[email protected]', 'first_name' => 'Ana', 'properties' => ['plan' => 'pro'], 'tags' => ['Cliente VIP']],
    ['external_id' => 'user_43', 'email' => '[email protected]', 'topics' => [['id' => '019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b', 'subscription' => 'opt_in']]],
]);

print_r($batch['counts']); // created, updated, unchanged, failed
200Resposta
{
  "object": "contact_batch",
  "status": "completed",
  "total": 2,
  "counts": {
    "created": 1,
    "updated": 1,
    "unchanged": 0,
    "failed": 0
  },
  "results": [
    {
      "index": 0,
      "status": "updated",
      "id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
      "external_id": "user_42",
      "user_id": "user_42",
      "email": "[email protected]"
    },
    {
      "index": 1,
      "status": "created",
      "id": "019a1e00-2b3c-7d4e-8f50-6b7c8d9e0f1a",
      "external_id": "user_43",
      "user_id": "user_43",
      "email": "[email protected]"
    }
  ]
}

#Obter um lote de contatos

GET/contacts/batches/{id}

Chave de acesso total

Mostra o andamento de um lote grande (acima de 1.000 itens). status: queued, processing, completed ou failed (com error). processed conta os itens já tratados, e results traz o resultado de cada um quando o lote termina.

Parâmetros de caminho

  • idstringobrigatório
    ID do lote.

Erros

  • 404not_found
    O recurso não existe ou é de outro time.
GET /contacts/batches/{id}

cURL

curl https://api.postal.so/contacts/batches/019a1e30-9aa1-7db3-8e2f-4a5b6c7d8e9f \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/contacts/batches/019a1e30-9aa1-7db3-8e2f-4a5b6c7d8e9f', {
  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/contacts/batches/019a1e30-9aa1-7db3-8e2f-4a5b6c7d8e9f",
    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('contacts/batches/019a1e30-9aa1-7db3-8e2f-4a5b6c7d8e9f', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "contact_batch",
  "id": "019a1e30-9aa1-7db3-8e2f-4a5b6c7d8e9f",
  "status": "completed",
  "source": "api",
  "total": 2,
  "processed": 2,
  "counts": {
    "created": 1,
    "updated": 1,
    "unchanged": 0,
    "failed": 0
  },
  "error": null,
  "results": [
    {
      "index": 0,
      "status": "updated",
      "id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
      "external_id": "user_42",
      "user_id": "user_42",
      "email": "[email protected]"
    },
    {
      "index": 1,
      "status": "created",
      "id": "019a1e00-2b3c-7d4e-8f50-6b7c8d9e0f1a",
      "external_id": "user_43",
      "user_id": "user_43",
      "email": "[email protected]"
    }
  ],
  "created_at": "2026-10-24T14:20:00.000000Z",
  "started_at": "2026-10-24T14:20:01.000000Z",
  "completed_at": "2026-10-24T14:20:03.000000Z"
}

#Resumo do consentimento

GET/contacts/consent-summary

Chave de acesso total

Quantos contatos esperam a confirmação para receber marketing (unconfirmed, os guardados dos e-mails transacionais), quantos já estão confirmados e a última confirmação em massa. É o que alimenta o cartão contatos aguardando a sua confirmação do painel.

unconfirmed e confirmed somam o total de contatos. Em times muito grandes o número pode ser uma estimativa, e então a resposta traz estimated: true. last_confirmation é null enquanto ninguém confirmou; user é null se a conta de quem confirmou não existe mais. O resultado fica em cache por 30 segundos por time e é renovado a cada bloco confirmado.

GET /contacts/consent-summary

cURL

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

Node.js

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

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "contact_consent_summary",
  "unconfirmed": 4213,
  "confirmed": 9871,
  "last_confirmation": {
    "at": "2026-10-27T13:05:00Z",
    "user": {
      "id": 7,
      "name": "Carla Mendes"
    },
    "count": 1150
  }
}

#Ação em massa nos contatos

POST/contacts/bulk

Chave de acesso total

Roda uma ação em vários contatos de uma vez: os que você escolheu (ids) ou todos os que combinam com filtros. A seleção é fixada ao aceitar o pedido: contatos criados depois não entram. Até 200 contatos terminam na própria requisição (200, com a operação pronta); acima disso a resposta é 202 com a operação em queued, para acompanhar em GET /contacts/bulk/{id}. O time faz uma operação em massa por vez.

Ações: delete, unsubscribe, resubscribe, set_topic (topic_id e subscription), add_tags e remove_tags (tags), add_to_segment e remove_from_segment (segment_id, só estático), verify, export e confirm_consent. Toda ação é idempotente por contato: quem já está no estado final conta em skipped e o motivo vai em result.skipped_reasons. Para ver antes quantos contatos a ação mudaria, use POST /contacts/bulk/estimate com o mesmo corpo (ele não pede a declaração); POST /contacts/bulk/{id}/cancel cancela uma operação em andamento e mantém o que ela já fez.

confirm_consent: confirmar que aceitam receber. Marca os contatos unconfirmed da seleção como confirmed (e grava consent_confirmed_at), para que passem a receber campanhas e automações. É a declaração de que essas pessoas aceitaram receber e-mails de marketing da sua empresa e de que você é o responsável por esse consentimento: por isso params.accept_responsibility precisa ser true. Só o dono e os admins do time podem, e só pelo painel: uma chave de API recebe 403 forbidden, assim como um membro comum. Sem a declaração, a resposta é 422.

Para confirmar todos os contatos não confirmados, selecione por filtro: {"mode": "filter", "filters": {"consent": "unconfirmed"}}. Muda só quem ainda é unconfirmed; quem já estava confirmado conta em skipped (already_confirmed) e não perde a data. Não manda o webhook contact.updated nem inicia automações. O teto da seleção é de 2.000.000 de contatos (nas outras ações, 200.000). Cada pedido grava um registro de auditoria: quem confirmou, quando, quantos contatos, a seleção, a versão do texto da declaração (statement_version) e o IP. Repetir o pedido é seguro e só gera outro registro, com 0 contatos.

Cabeçalhos

  • Idempotency-Keystring
    Opcional. Repetir a chamada com a mesma chave devolve a resposta guardada.

Corpo da requisição

  • actionstringobrigatório
    A ação: delete, unsubscribe, resubscribe, set_topic, add_tags, remove_tags, add_to_segment, remove_from_segment, verify, export ou confirm_consent.
  • selectionobjectobrigatório
    Quais contatos.
    • modestringobrigatório
      ids ou filter.
    • idsstring[]
      Com mode: "ids": de 1 a 1.000 IDs de contato. IDs de outro time são ignorados.
    • filtersobject
      Com mode: "filter": os mesmos filtros de GET /contacts (search, segment_id, tag, consent, origin…). Um filtro desconhecido dá 422.
    • excluded_idsstring[]
      Com mode: "filter": até 5.000 contatos a deixar de fora.
    • expected_countinteger
      O número de contatos que a pessoa viu. Se o real for muito diferente, a resposta é 409 selection_changed.
  • paramsobject
    Os parâmetros da ação.
    • accept_responsibilitybooleanobrigatório
      Só em confirm_consent, e obrigatório: true é a declaração de que os contatos aceitaram receber marketing e de que você é o responsável por esse consentimento.

Erros

  • 403forbidden
    confirm_consent por uma chave de API ou por um membro que não é dono nem admin.
  • 422validation_error
    Ação, seleção ou filtro inválido, seleção vazia ou acima do limite, ou accept_responsibility ausente em confirm_consent (a mensagem diz para enviar accept_responsibility: true).
  • 409selection_changed
    O número de contatos mudou em relação a expected_count; a resposta traz o count atual.
  • 409bulk_operation_running
    O time já tem uma operação em massa na fila ou em andamento (vem em operation).
  • 401restricted_api_key
    A chave é só de envio.
POST /contacts/bulk

cURL

curl -X POST https://api.postal.so/contacts/bulk \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "add_to_segment",
    "selection": {
      "mode": "ids",
      "ids": ["019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f"]
    },
    "params": {
      "segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a"
    }
  }'

Node.js

const response = await fetch('https://api.postal.so/contacts/bulk', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    action: 'add_to_segment',
    selection: {
      mode: 'ids',
      ids: ['019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f'],
    },
    params: {
      segment_id: '019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a',
    },
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/contacts/bulk",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    json={
        "action": "add_to_segment",
        "selection": {
            "mode": "ids",
            "ids": ["019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f"],
        },
        "params": {
            "segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
        },
    },
    timeout=10,
)

data = response.json()

PHP

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

$response = $client->post('contacts/bulk', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'json' => [
        'action' => 'add_to_segment',
        'selection' => [
            'mode' => 'ids',
            'ids' => ['019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f'],
        ],
        'params' => [
            'segment_id' => '019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a',
        ],
    ],
]);

$data = json_decode($response->getBody(), true);
200Resposta (uma seleção pequena termina na hora)
{
  "object": "contact_bulk_operation",
  "id": "019a1e50-bcc3-7fd5-8a40-6c7d8e9fa0b1",
  "action": "add_to_segment",
  "status": "completed",
  "total": 1,
  "processed": 1,
  "changed": 1,
  "skipped": 0,
  "failed": 0,
  "errors": [],
  "params": {
    "segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a"
  },
  "selection_mode": "ids",
  "cancel_requested": false,
  "error": null,
  "created_at": "2026-10-29T15:00:00.000000Z",
  "started_at": "2026-10-29T15:00:01.000000Z",
  "completed_at": "2026-10-29T15:00:09.000000Z",
  "created_by": null,
  "result": {
    "not_found": 0,
    "skipped_reasons": {}
  }
}
Corpo de confirm_consent (só com a sessão do painel)
{
  "action": "confirm_consent",
  "selection": { "mode": "filter", "filters": { "consent": "unconfirmed" } },
  "params": { "accept_responsibility": true }
}
202Resposta de uma seleção grande: na fila
{
  "object": "contact_bulk_operation",
  "id": "019a1e50-bcc3-7fd5-8a40-6c7d8e9fa0b1",
  "action": "confirm_consent",
  "status": "queued",
  "total": 4213,
  "processed": 0,
  "changed": 0,
  "skipped": 0,
  "failed": 0,
  "errors": [],
  "params": {
    "accept_responsibility": true,
    "statement_version": "2026-10-v1"
  },
  "selection_mode": "filter",
  "cancel_requested": false,
  "error": null,
  "created_at": "2026-10-29T15:00:00.000000Z",
  "started_at": null,
  "completed_at": null,
  "created_by": {
    "id": 7,
    "name": "Carla Mendes"
  },
  "result": {
    "not_found": 0
  }
}

#Obter uma ação em massa

GET/contacts/bulk/{id}

Chave de acesso total

Mostra o andamento de uma ação em massa. status: queued, running, completed, failed ou canceled. total é o tamanho da seleção fixada; processed, changed, skipped e failed contam o que já foi feito; errors traz os 20 primeiros contatos que falharam. Uma operação que termina completed pode ter failed maior que zero. Em confirm_consent, changed é quantos contatos foram confirmados. GET /contacts/bulk lista as operações dos últimos 30 dias, da mais nova para a mais antiga (filtro status).

Parâmetros de caminho

  • idstringobrigatório
    ID da operação.

Erros

  • 404not_found
    O recurso não existe ou é de outro time.
GET /contacts/bulk/{id}

cURL

curl https://api.postal.so/contacts/bulk/019a1e50-bcc3-7fd5-8a40-6c7d8e9fa0b1 \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/contacts/bulk/019a1e50-bcc3-7fd5-8a40-6c7d8e9fa0b1', {
  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/contacts/bulk/019a1e50-bcc3-7fd5-8a40-6c7d8e9fa0b1",
    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('contacts/bulk/019a1e50-bcc3-7fd5-8a40-6c7d8e9fa0b1', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "contact_bulk_operation",
  "id": "019a1e50-bcc3-7fd5-8a40-6c7d8e9fa0b1",
  "action": "confirm_consent",
  "status": "completed",
  "total": 4213,
  "processed": 4213,
  "changed": 4200,
  "skipped": 13,
  "failed": 0,
  "errors": [],
  "params": {
    "accept_responsibility": true,
    "statement_version": "2026-10-v1"
  },
  "selection_mode": "filter",
  "cancel_requested": false,
  "error": null,
  "created_at": "2026-10-29T15:00:00.000000Z",
  "started_at": "2026-10-29T15:00:01.000000Z",
  "completed_at": "2026-10-29T15:00:09.000000Z",
  "created_by": {
    "id": 7,
    "name": "Carla Mendes"
  },
  "result": {
    "not_found": 0,
    "skipped_reasons": {
      "already_confirmed": 13
    }
  }
}

#Etiquetas

Etiquetas (tags) marcam contatos de forma livre, sem a necessidade de um segmento: "Cliente VIP", "Veio do evento". O nome tem até 64 caracteres e é único no time sem diferenciar maiúsculas. Cada etiqueta tem uma cor: gray, green, yellow, red, blue ou purple. Nos endpoints de contatos, uma etiqueta pode ser dada pelo ID ou pelo nome, e os nomes que ainda não existem são criados. Todo contato que ganha uma etiqueta nova inicia as automações com o gatilho de etiqueta adicionada (tag.added), e as etiquetas também servem de filtro em segmentos dinâmicos e em GET /contacts?tag=.

#Adicionar etiquetas a um contato

POST/contacts/{id}/tags

Chave de acesso total

Acrescenta as etiquetas ao contato (as que ele já tinha ficam como estão) e devolve todas as etiquetas dele. Os nomes que não existem são criados, com a próxima cor da paleta.

Parâmetros de caminho

  • idstringobrigatório
    ID ou e-mail do contato.

Corpo da requisição

  • tagsstring[]obrigatório
    De 1 a 50 etiquetas, por ID ou por nome.

Erros

  • 422validation_error
    Algum campo é inválido. A mensagem diz qual.
  • 404not_found
    O contato ou alguma etiqueta (dada por ID) não existe.
POST /contacts/{id}/tags

cURL

curl -X POST https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/tags \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tags": ["Cliente VIP"]
  }'

Node.js

const response = await fetch('https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/tags', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    tags: ['Cliente VIP'],
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/tags",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    json={
        "tags": ["Cliente VIP"],
    },
    timeout=10,
)

data = response.json()

PHP

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

$response = $client->post('contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/tags', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'json' => [
        'tags' => ['Cliente VIP'],
    ],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f",
      "name": "Cliente VIP",
      "color": "purple"
    }
  ]
}

#Remover uma etiqueta de um contato

DELETE/contacts/{id}/tags/{tag}

Chave de acesso total

Tira a etiqueta do contato e devolve as etiquetas que sobraram. A etiqueta continua existindo.

Parâmetros de caminho

  • idstringobrigatório
    ID ou e-mail do contato.
  • tagstringobrigatório
    ID ou nome da etiqueta.

Erros

  • 404not_found
    O contato ou a etiqueta não existe.
DELETE /contacts/{id}/tags/{tag}

cURL

curl -X DELETE https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f', {
  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/contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f",
    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('contacts/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f/tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "list",
  "has_more": false,
  "data": []
}

#Listar etiquetas

GET/tags

Chave de acesso total

As etiquetas do time, cada uma com contacts_count (quantos contatos a têm).

Parâmetros de consulta

  • limitinteger
    De 1 a 100. Padrão: 20.
  • afterstring
    Cursor da próxima página.
  • beforestring
    Cursor da página anterior.
  • searchstring
    Busca no nome.
GET /tags

cURL

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

Node.js

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

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "tag",
      "id": "019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f",
      "name": "Cliente VIP",
      "color": "purple",
      "contacts_count": 86,
      "created_at": "2026-10-20T09:00:00.000000Z",
      "updated_at": "2026-10-20T09:00:00.000000Z"
    }
  ]
}

#Criar uma etiqueta

POST/tags

Chave de acesso total

Corpo da requisição

  • namestringobrigatório
    Nome, até 64 caracteres, único no time sem diferenciar maiúsculas.
  • colorstring
    gray, green, yellow, red, blue ou purple. Padrão: a próxima cor da paleta.

Erros

  • 422validation_error
    Nome vazio, grande demais ou já usado, ou cor fora da lista.
POST /tags

cURL

curl -X POST https://api.postal.so/tags \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Cliente VIP",
    "color": "purple"
  }'

Node.js

const response = await fetch('https://api.postal.so/tags', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Cliente VIP',
    color: 'purple',
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/tags",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    json={
        "name": "Cliente VIP",
        "color": "purple",
    },
    timeout=10,
)

data = response.json()

PHP

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

$response = $client->post('tags', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'json' => [
        'name' => 'Cliente VIP',
        'color' => 'purple',
    ],
]);

$data = json_decode($response->getBody(), true);
201Resposta
{
  "object": "tag",
  "id": "019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f",
  "name": "Cliente VIP",
  "color": "purple",
  "contacts_count": 0,
  "created_at": "2026-10-20T09:00:00.000000Z",
  "updated_at": "2026-10-20T09:00:00.000000Z"
}

#Obter uma etiqueta

GET/tags/{id}

Chave de acesso total

Parâmetros de caminho

  • idstringobrigatório
    ID da etiqueta.

Erros

  • 404not_found
    O recurso não existe ou é de outro time.
GET /tags/{id}

cURL

curl https://api.postal.so/tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f', {
  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/tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f",
    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('tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "tag",
  "id": "019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f",
  "name": "Cliente VIP",
  "color": "purple",
  "contacts_count": 86,
  "created_at": "2026-10-20T09:00:00.000000Z",
  "updated_at": "2026-10-20T09:00:00.000000Z"
}

#Atualizar uma etiqueta

PATCH/tags/{id}

Chave de acesso total

Parâmetros de caminho

  • idstringobrigatório
    ID da etiqueta.

Corpo da requisição

  • namestring
    Novo nome.
  • colorstring
    Nova cor.

Erros

  • 422validation_error
    Algum campo é inválido. A mensagem diz qual.
  • 404not_found
    O recurso não existe ou é de outro time.
PATCH /tags/{id}

cURL

curl -X PATCH https://api.postal.so/tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "color": "blue"
  }'

Node.js

const response = await fetch('https://api.postal.so/tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    color: 'blue',
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.patch(
    "https://api.postal.so/tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    json={
        "color": "blue",
    },
    timeout=10,
)

data = response.json()

PHP

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

$response = $client->patch('tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'json' => [
        'color' => 'blue',
    ],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "tag",
  "id": "019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f",
  "name": "Cliente VIP",
  "color": "blue",
  "contacts_count": 86,
  "created_at": "2026-10-20T09:00:00.000000Z",
  "updated_at": "2026-10-20T09:00:00.000000Z"
}

#Excluir uma etiqueta

DELETE/tags/{id}

Chave de acesso total

Remove a etiqueta de todos os contatos; os contatos continuam na audiência. Uma etiqueta usada por uma automação não pode ser excluída: tire-a da automação antes.

Parâmetros de caminho

  • idstringobrigatório
    ID da etiqueta.

Erros

  • 409tag_in_use
    Alguma automação usa esta etiqueta. A mensagem lista quais.
  • 404not_found
    O recurso não existe ou é de outro time.
DELETE /tags/{id}

cURL

curl -X DELETE https://api.postal.so/tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f \
  -H "Authorization: Bearer $POSTAL_API_KEY"

Node.js

const response = await fetch('https://api.postal.so/tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f', {
  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/tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f",
    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('tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "tag",
  "id": "019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f",
  "deleted": true
}

#Etiquetar vários contatos

POST/tags/{id}/contacts

Chave de acesso total

Aplica a etiqueta a vários contatos de uma vez. Quem já a tinha fica como está; added conta só os que ganharam a etiqueta agora.

Parâmetros de caminho

  • idstringobrigatório
    ID da etiqueta.

Corpo da requisição

  • contactsstring[]obrigatório
    Até 1.000 contatos por chamada, cada um por ID ou por e-mail. Quem não existir é ignorado.

Erros

  • 422validation_error
    Algum campo é inválido. A mensagem diz qual.
  • 404not_found
    O recurso não existe ou é de outro time.
POST /tags/{id}/contacts

cURL

curl -X POST https://api.postal.so/tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f/contacts \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
      "[email protected]"
    ]
  }'

Node.js

const response = await fetch('https://api.postal.so/tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f/contacts', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    contacts: [
      '019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f',
      '[email protected]',
    ],
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.post(
    "https://api.postal.so/tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f/contacts",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    json={
        "contacts": [
            "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
            "[email protected]",
        ],
    },
    timeout=10,
)

data = response.json()

PHP

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

$response = $client->post('tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f/contacts', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'json' => [
        'contacts' => [
            '019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f',
            '[email protected]',
        ],
    ],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "tag_contacts",
  "tag_id": "019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f",
  "contacts": 2,
  "added": 1
}

#Tirar a etiqueta de vários contatos

DELETE/tags/{id}/contacts

Chave de acesso total

Parâmetros de caminho

  • idstringobrigatório
    ID da etiqueta.

Corpo da requisição

  • contactsstring[]obrigatório
    Até 1.000 contatos por chamada, cada um por ID ou por e-mail. Quem não existir é ignorado.

Erros

  • 422validation_error
    Algum campo é inválido. A mensagem diz qual.
  • 404not_found
    O recurso não existe ou é de outro time.
DELETE /tags/{id}/contacts

cURL

curl -X DELETE https://api.postal.so/tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f/contacts \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": ["019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f"]
  }'

Node.js

const response = await fetch('https://api.postal.so/tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f/contacts', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    contacts: ['019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f'],
  }),
});

const data = await response.json();

Python

import os
import requests

response = requests.delete(
    "https://api.postal.so/tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f/contacts",
    headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
    json={
        "contacts": ["019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f"],
    },
    timeout=10,
)

data = response.json()

PHP

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

$response = $client->delete('tags/019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f/contacts', [
    'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
    'json' => [
        'contacts' => ['019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f'],
    ],
]);

$data = json_decode($response->getBody(), true);
200Resposta
{
  "object": "tag_contacts",
  "tag_id": "019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f",
  "contacts": 1,
  "deleted": true
}