Iniciar sesión Crear cuenta
Audiencia

Contactos

Las personas de tu audiencia: datos, propiedades, segmentos y suscripción a temas.

Los contactos son las personas de tu audiencia de marketing: correo electrónico, nombre, propiedades personalizadas, segmentos y la suscripción a temas. Cada correo electrónico aparece una vez por equipo. El número de contactos depende del plan de marketing (Gratis: 1.000, Profesional: 5.000, Escala: 50.000, Empresarial: sin límite). En los endpoints con {id}, puedes usar el ID o el propio correo electrónico del contacto. Para traer muchos contactos de una vez, usa la importación de CSV. Las direcciones que están en la lista de supresión nunca se convierten en contactos (consulta Crear un contacto).

Cada contacto también guarda el external_id (alias user_id): el ID de la persona en tu sistema, único en el equipo. En cualquier endpoint con {id}, puedes usar external_id:user_42 (o user_id:user_42) en lugar del ID y del correo electrónico. Las etiquetas marcan contactos sin necesidad de un segmento, y el lote crea o actualiza hasta 10.000 contactos en una llamada. Todos los endpoints de esta página exigen una clave de API de acceso total (una clave solo de envío recibe 401 restricted_api_key).

Quien recibe un correo transaccional tuyo también se convierte en contacto, solo, pero sin confirmar para marketing: mira Contactos guardados de los correos transaccionales y los campos marketing_consent, origin y consent_confirmed_at.

#Origen de los cambios

Cada cambio de un contacto guarda de dónde vino en last_change_source: api, dashboard, form, import, automation, partner, system, transactional, postal_lifecycle, crm:{id da conexão} o una etiqueta tuya. Para ponerle una etiqueta al cambio, envía la cabecera X-Postal-Source o el campo source en el cuerpo (si llegan los dos, vale el campo): hasta 64 caracteres entre letras minúsculas, números, ., _, : y -. Los nombres que usa Postal y el prefijo crm: están reservados y generan 422. Los webhooks contact.* traen el campo source, y un endpoint de webhook puede ignorar orígenes (skip_sources), lo que evita el eco entre dos sistemas que se sincronizan en ambos sentidos. En la lista, ?source= filtra por origen.

#Contactos guardados de los correos transaccionales

Toda dirección que recibe un correo transaccional tuyo queda guardada, sola, en la audiencia del equipo, para que puedas enviar marketing después a quien ya interactuó con tu empresa (quien rellenó un formulario de contacto, quien se registró…). Entran los destinatarios de POST /emails, de POST /emails/batch, de SMTP y de los correos de autenticación de las conexiones de Supabase, solo en to y cc (nunca bcc). No entran los destinatarios de campañas, automatizaciones, pruebas de plantilla y de campaña, reenvíos, respuestas de la inbox y envíos a las direcciones de prueba. El contacto se crea en segundo plano, después de que el proveedor aceptó el correo, sin retrasar el envío. El nombre para mostrar (Ana Souza <[email protected]>) pasa a ser first_name y last_name, cuando existe.

CampoValoresQué dice
marketing_consentconfirmed, unconfirmedSi el contacto puede recibir marketing (campañas y automatizaciones). Todo contacto que ya existía y todo el que se creó de otra forma (POST /contacts, lote, eventos, importación, formulario, CRM) es confirmed. El contacto guardado de un correo transaccional nace unconfirmed.
origintransactional, nullDe dónde vino el contacto: transactional para los guardados de los correos transaccionales; null para los demás. No cambia cuando el contacto se confirma.
consent_confirmed_atfecha o nullCuándo se confirmó el contacto por la declaración del propietario o de un administrador (acción masiva confirm_consent) o por una acción explícita. null en los que nunca se confirmaron de esa forma.
  • Fuera de campañas y automatizaciones. Un contacto unconfirmed no entra en la lista de envío de una campaña (ni en recipients_count y GET /broadcasts/audience), no inicia automatizaciones y el paso de enviar correo se salta a quien dejó de estar confirmado. Tus envíos transaccionales a ese contacto no cambian. Está en la audiencia de verdad: cuenta en el límite de contactos y aparece en GET /contacts, en los contadores y en los segmentos.
  • Silenciosa. La captura no dispara el webhook contact.created, ninguna automatización (contact.created, segment.added…) y no empuja el contacto a un CRM conectado. El origen del cambio es transactional.
  • Nunca toca lo que ya existe. Un contacto que ya existe (con su consentimiento, sus temas, su baja y sus datos) queda como está, y una dirección de la lista de supresión nunca se convierte en contacto.
  • Nunca supera el límite del plan. La captura usa el límite de contactos del plan de marketing y se detiene al llegar a él, incluso con el excedente activado (el excedente cobra contactos que añadiste a propósito; la captura automática no cobra a nadie). Se pausa en silencio y se reanuda sola cuando haya espacio. El bloque audience de GET /usage dice si está en pausa.
  • Segmento predeterminado. En la primera captura, si el plan incluye segmentos dinámicos y tiene espacio para uno más, nace el segmento dinámico Envíos transaccionales (origin es transactional), una sola vez: si lo eliminas, no vuelve.
  • Qué confirma un contacto. La confirmación masiva y las acciones explícitas: reimportar la dirección por CSV, POST /contacts/batch y POST /events con el contacto, PATCH /contacts/{id} y la suscripción de la persona en un formulario. Confirmar no dispara contact.created, y una captura nunca rebaja un contacto ya confirmado.
  • Desactivar. La captura viene activada. El interruptor del equipo, capture_transactional_contacts, cambia con PATCH /team y {"capture_transactional_contacts": false}, pero solo el propietario y los administradores, y solo desde el panel (Configuración › Equipo): la ruta usa la sesión del panel y no acepta clave de API. Los correos que ya estaban en la cola antes de que la función se activara no se capturan.

Para listar solo esos contactos, usa los filtros consent=unconfirmed y origin=transactional en GET /contacts; también valen en las selecciones de las acciones masivas, en las exportaciones y (como las condiciones consent y origin) en los segmentos dinámicos. Para ver cuántos esperan confirmación, usa GET /contacts/consent-summary.

#Listar contactos

GET/contacts

Clave de acceso total

De los más recientes a los más antiguos. La lista no trae los segmentos de cada contacto.

Parámetros de consulta

  • limitinteger
    De 1 a 100. Predeterminado: 20.
  • afterstring
    Cursor de la página siguiente.
  • beforestring
    Cursor de la página anterior.
  • searchstring
    Busca en el correo electrónico, en el external_id y en el nombre.
  • segment_idstring
    Solo contactos de un segmento: los de la lista, si el segmento es estático, o los que coinciden ahora con los filtros, si es dinámico.
  • topic_idstring
    Solo contactos suscritos al tema: los que eligieron recibirlo y, si el tema es opt_in por defecto, los que no eligieron nada.
  • tagstring
    Solo contactos con la etiqueta (ID o nombre, sin distinguir mayúsculas).
  • unsubscribedboolean
    true para los que cancelaron la suscripción; false para los demás.
  • email_statusstring
    Resultado de la verificación de correos, separado por comas: valid, invalid, risky y unverified (nunca verificado).
  • external_idstring
    Solo el contacto con este external_id (también aceptado como user_id).
  • sourcestring
    Solo contactos cuyo último cambio vino de este origen.
  • consentstring
    confirmed o unconfirmed: solo contactos que pueden (o que todavía no pueden) recibir marketing. Mira Contactos guardados de los correos transaccionales.
  • originstring
    transactional: solo los contactos guardados de los correos transaccionales.

Errores

  • 422validation_error
    Algún campo no es válido. El mensaje indica cuál.
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);
200Respuesta
{
  "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"
        }
      ]
    }
  ]
}

#Crear un contacto

POST/contacts

Clave de acceso total

Crea el contacto y, si quieres, ya lo pone en segmentos estáticos, aplica etiquetas y registra su elección en los temas. Dispara el webhook contact.created.

Una dirección que está en la lista de supresión del equipo nunca se convierte en contacto: la respuesta es 422 con email_suppressed. Para registrarla, quítala antes de la lista de supresión. Los contactos que ya existen no se ven afectados.

¿Quieres crear o actualizar sin saber si el contacto ya existe? Usa el lote, que busca por ID, external_id y correo electrónico.

Cuerpo de la solicitud

  • emailstringobligatorio
    Correo electrónico, hasta 320 caracteres. Se guarda en minúsculas.
  • external_idstring | null
    ID de la persona en tu sistema, hasta 255 caracteres, único en el equipo. También aceptado como user_id.
  • first_namestring | null
    Nombre, hasta 255 caracteres.
  • last_namestring | null
    Apellido, hasta 255 caracteres.
  • unsubscribedboolean
    Dado de baja de todos los envíos de marketing. Por defecto: false.
  • propertiesobject
    Valores de las propiedades, como {"plan": "pro"}. Cada clave debe existir antes; los valores se convierten al tipo de la propiedad.
  • segmentsstring[]
    IDs de hasta 100 segmentos estáticos (u objetos {"id": "…"}). Los segmentos dinámicos no aceptan contactos a mano.
  • topicsobject[]
    Hasta 100 elecciones de temas.
    • idstringobligatorio
      ID del tema.
    • subscriptionstringobligatorio
      opt_in (quiere recibir) u opt_out (no quiere).
  • tagsstring[]
    Hasta 50 etiquetas, por ID o por nombre. Los nombres que aún no existen se crean.
  • sourcestring
    Etiqueta del origen del cambio. También aceptada en la cabecera X-Postal-Source.

Errores

  • 422validation_error
    Correo electrónico no válido o ya registrado, external_id ya usado por otro contacto, propiedad desconocida, valor con el tipo equivocado, o segmento dinámico en segments.
  • 422email_suppressed
    El correo electrónico está en la lista de supresión del equipo, así que no puede convertirse en contacto.
  • 403plan_limit_reached
    El plan no permite más contactos, contando los paquetes del Adicional de contactos (el mensaje indica cómo seguir por encima del límite o añadir un paquete, si el plan lo permite).
  • 404not_found
    Algún segmento, tema o etiqueta (dada por ID) no 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);
201Respuesta
{
  "object": "contact",
  "id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f"
}

#Obtener un contacto

GET/contacts/{id}

Clave de acceso total

Devuelve el contacto con los segmentos de los que forma parte y las etiquetas. email_status y email_verified_at vienen de la última verificación de correos de la dirección y son null si nunca se verificó.

Parámetros de ruta

  • idstringobligatorio
    ID o correo electrónico del contacto.

Errores

  • 404not_found
    El recurso no existe o es de otro equipo.
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);
200Respuesta
{
  "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"
    }
  ]
}

#Actualizar un contacto

PATCH/contacts/{id}

Clave de acceso total

Solo cambian los campos enviados. En properties, las claves enviadas se actualizan y las demás quedan como están; envía null o "" para borrar un valor. Los segmentos y los temas tienen endpoints propios (más abajo). Si algo cambió, dispara contact.updated.

En tags, la lista enviada reemplaza las etiquetas del contacto; para solo añadir o quitar una, usa los endpoints de etiquetas del contacto. En el lote, quien canceló la suscripción solo se vuelve a suscribir con la prueba del nuevo consentimiento (resubscribe).

Actualizar un contacto unconfirmed (guardado de un correo transaccional) es una acción explícita: pasa a confirmed.

Parámetros de ruta

  • idstringobligatorio
    ID o correo electrónico del contacto.

Cuerpo de la solicitud (todos opcionales)

  • emailstring
    Nuevo correo electrónico.
  • external_idstring | null
    Nuevo external_id (o user_id).
  • first_namestring | null
    Nome.
  • last_namestring | null
    Sobrenome.
  • unsubscribedboolean
    Da de baja (true) o vuelve a suscribir (false).
  • propertiesobject
    Valores a cambiar.
  • tagsstring[]
    Hasta 50 etiquetas (ID o nombre) que pasan a ser las únicas del contacto.
  • sourcestring
    Etiqueta del origen del cambio.

Errores

  • 422validation_error
    Correo electrónico o external_id ya usado por otro contacto, propiedad desconocida, valor no válido, o segments/topics en el cuerpo.
  • 404not_found
    El recurso no existe o es de otro equipo.
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);
200Respuesta
{
  "object": "contact",
  "id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f"
}

#Eliminar un contacto

DELETE/contacts/{id}

Clave de acceso total

Quita el contacto de la audiencia, con los segmentos y las elecciones de temas. Dispara contact.deleted. Para solo dejar de enviarle, es preferible unsubscribed: true.

Parámetros de ruta

  • idstringobligatorio
    ID o correo electrónico del contacto.

Errores

  • 404not_found
    El recurso no existe o es de otro equipo.
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);
200Respuesta
{
  "object": "contact",
  "id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
  "deleted": true
}

#Segmentos de un contacto

GET/contacts/{id}/segments

Clave de acceso total

Los segmentos de los que forma parte el contacto: los estáticos a los que se añadió y los dinámicos cuyos filtros cumple (según la última comprobación). Acepta limit, after y before.

Parámetros de ruta

  • idstringobligatorio
    ID o correo electrónico del contacto.

Errores

  • 404not_found
    El recurso no existe o es de otro equipo.
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);
200Respuesta
{
  "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"
    }
  ]
}

#Añadir a un segmento

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

Clave de acceso total

Pone el contacto en el segmento. Si ya estaba, no cambia nada. Solo los segmentos estáticos aceptan contactos a mano.

Parámetros de ruta

  • idstringobligatorio
    ID o correo electrónico del contacto.
  • segment_idstringobligatorio
    ID del segmento.

Errores

  • 404not_found
    El contacto o el segmento no existe.
  • 422validation_error
    El segmento es dinámico: guarda a quien cumple los filtros y no acepta contactos a mano.
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);
201Respuesta
{
  "object": "contact_segment",
  "contact_id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
  "segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a"
}

#Quitar de un segmento

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

Clave de acceso total

Parámetros de ruta

  • idstringobligatorio
    ID o correo electrónico del contacto.
  • segment_idstringobligatorio
    ID del segmento.

Errores

  • 404not_found
    El contacto o el segmento no existe.
  • 422validation_error
    El segmento es dinámico: guarda a quien cumple los filtros y no acepta contactos a mano.
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);
200Respuesta
{
  "object": "contact_segment",
  "contact_id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
  "segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
  "deleted": true
}

#Temas de un contacto

GET/contacts/{id}/topics

Clave de acceso total

Todos los temas del equipo, en orden alfabético, con la suscripción del contacto: su elección o, si no eligió, el valor por defecto del tema.

Parámetros de ruta

  • idstringobligatorio
    ID o correo electrónico del contacto.

Errores

  • 404not_found
    El recurso no existe o es de otro equipo.
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);
200Respuesta
{
  "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"
    }
  ]
}

#Cambiar la suscripción a temas

PATCH/contacts/{id}/topics

Clave de acceso total

Registra la elección del contacto en uno o más temas. Los temas no enviados quedan como están.

Parámetros de ruta

  • idstringobligatorio
    ID o correo electrónico del contacto.

Cuerpo de la solicitud

  • topicsobject[]obligatorio
    De 1 a 100 elecciones, sin temas repetidos.
    • idstringobligatorio
      ID del tema.
    • subscriptionstringobligatorio
      opt_in u opt_out.

Errores

  • 422validation_error
    Algún campo no es válido. El mensaje indica cuál.
  • 404not_found
    El contacto o algún tema no 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);
200Respuesta
{
  "object": "contact",
  "id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f"
}

#Crear o actualizar contactos en lote

POST/contacts/batch

Clave de acceso total

Crea o actualiza varios contactos a la vez (upsert). Cada elemento busca el contacto por id, luego por external_id y luego por e-mail; si no lo encuentra y hay correo electrónico, lo crea. Si se encuentra por external_id, un correo electrónico nuevo reemplaza al antiguo; si se encuentra por correo electrónico, el external_id se guarda si el contacto aún no tenía uno.

Hasta 1.000 elementos se procesan en la propia solicitud y la respuesta trae un resultado por elemento (200): un elemento con error no tumba a los demás. Por encima de eso (hasta 10.000), el lote pasa a la cola y la respuesta es 202 con el id del lote, para seguirlo en GET /contacts/batches/{id}. Puedes repetir la llamada con seguridad: usa la cabecera Idempotency-Key.

Reglas por elemento: las etiquetas y los segmentos solo se añaden (nunca se quitan); los temas se guardan; las propiedades se comprueban con las definiciones del equipo y se combinan; las direcciones de la lista de supresión no se convierten en contactos (el elemento falla con code: "email_suppressed"), pero los contactos que ya existen pueden actualizarse. Quien canceló la suscripción no vuelve a recibir solo porque el elemento traiga unsubscribed: false: el elemento recibe el aviso kept_unsubscribed. Para volver a suscribir, envía resubscribe con la fecha y la prueba del nuevo consentimiento.

Cabeceras

  • Idempotency-Keystring
    Opcional. Repetir la llamada con la misma clave devuelve la respuesta guardada.

Cuerpo de la solicitud

  • contactsobject[]obligatorio
    De 1 a 10.000 elementos.
    • idstring
      ID del contacto (se busca primero).
    • external_idstring
      ID de la persona en tu sistema (también aceptado como user_id).
    • emailstring
      Correo electrónico. Obligatorio para crear un contacto; sin id ni external_id, es la clave de búsqueda.
    • first_namestring | null
      Nome.
    • last_namestring | null
      Sobrenome.
    • unsubscribedboolean
      true da de baja; false solo vale para contactos nuevos o que ya estaban suscritos.
    • resubscribeobject
      Vuelve a suscribir a quien canceló la suscripción, junto con unsubscribed: false: consented_at (fecha, no puede ser futura) y proof (cómo dio su consentimiento la persona, hasta 500 caracteres).
    • propertiesobject
      Valores de las propiedades.
    • tagsstring[]
      Hasta 50 etiquetas (ID o nombre), solo añadidas.
    • segmentsstring[]
      Hasta 100 segmentos estáticos, solo añadidos.
    • topicsobject[]
      Hasta 100 elecciones {"id", "subscription"}.
  • sourcestring
    Etiqueta del origen de los cambios. También aceptada en la cabecera X-Postal-Source.

En cada elemento de results, status es created, updated, unchanged o failed. Los elementos que fallaron traen error (y code, cuando lo hay) e id: null; los elementos con avisos traen warnings. index es la posición del elemento en la lista enviada.

Errores

  • 422validation_error
    contacts ausente, vacío o con más de 10.000 elementos. Los errores de un elemento no generan este error: quedan en results.
  • 403plan_limit_reached
    El plan no permite más contactos: los elementos que crearían contactos fallan con este mensaje en 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
200Respuesta
{
  "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]"
    }
  ]
}

#Obtener un lote de contactos

GET/contacts/batches/{id}

Clave de acceso total

Muestra el avance de un lote grande (más de 1.000 elementos). status: queued, processing, completed o failed (con error). processed cuenta los elementos ya tratados, y results trae el resultado de cada uno cuando el lote termina.

Parámetros de ruta

  • idstringobligatorio
    ID del lote.

Errores

  • 404not_found
    El recurso no existe o es de otro equipo.
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);
200Respuesta
{
  "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"
}

#Resumen del consentimiento

GET/contacts/consent-summary

Clave de acceso total

Cuántos contactos esperan la confirmación para recibir marketing (unconfirmed, los guardados de los correos transaccionales), cuántos ya están confirmados y la última confirmación masiva. Es lo que alimenta la tarjeta contactos pendientes de tu confirmación del panel.

unconfirmed y confirmed suman el total de contactos. En equipos muy grandes el número puede ser una estimación, y entonces la respuesta trae estimated: true. last_confirmation es null mientras nadie haya confirmado; user es null si la cuenta de quien confirmó ya no existe. El resultado se guarda en caché durante 30 segundos por equipo y se renueva con cada bloque 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);
200Respuesta
{
  "object": "contact_consent_summary",
  "unconfirmed": 4213,
  "confirmed": 9871,
  "last_confirmation": {
    "at": "2026-10-27T13:05:00Z",
    "user": {
      "id": 7,
      "name": "Carla Mendes"
    },
    "count": 1150
  }
}

#Acción masiva sobre contactos

POST/contacts/bulk

Clave de acceso total

Ejecuta una acción sobre varios contactos a la vez: los que elegiste (ids) o todos los que coinciden con unos filtros. La selección se fija al aceptar la solicitud: los contactos creados después no entran. Hasta 200 contactos terminan en la propia solicitud (200, con la operación terminada); por encima de eso la respuesta es 202 con la operación en queued, para seguirla en GET /contacts/bulk/{id}. El equipo hace una operación masiva a la vez.

Acciones: delete, unsubscribe, resubscribe, set_topic (topic_id y subscription), add_tags y remove_tags (tags), add_to_segment y remove_from_segment (segment_id, solo estático), verify, export y confirm_consent. Toda acción es idempotente por contacto: quien ya está en el estado final cuenta en skipped y el motivo va en result.skipped_reasons. Para ver antes cuántos contactos cambiaría la acción, usa POST /contacts/bulk/estimate con el mismo cuerpo (no pide la declaración); POST /contacts/bulk/{id}/cancel cancela una operación en curso y conserva lo que ya hizo.

confirm_consent: confirmar que aceptan recibir. Marca los contactos unconfirmed de la selección como confirmed (y guarda consent_confirmed_at), para que pasen a recibir campañas y automatizaciones. Es la declaración de que esas personas aceptaron recibir correos de marketing de tu empresa y de que tú eres el responsable de ese consentimiento: por eso params.accept_responsibility tiene que ser true. Solo el propietario y los administradores del equipo pueden, y solo desde el panel: una clave de API recibe 403 forbidden, igual que un miembro común. Sin la declaración, la respuesta es 422.

Para confirmar todos los contactos sin confirmar, selecciona por filtro: {"mode": "filter", "filters": {"consent": "unconfirmed"}}. Cambia solo a quien todavía es unconfirmed; quien ya estaba confirmado cuenta en skipped (already_confirmed) y no pierde la fecha. No envía el webhook contact.updated ni inicia automatizaciones. El tope de la selección es de 2.000.000 de contactos (en las demás acciones, 200.000). Cada solicitud guarda un registro de auditoría: quién confirmó, cuándo, cuántos contactos, la selección, la versión del texto de la declaración (statement_version) y la IP. Repetir la solicitud es seguro y solo genera otro registro, con 0 contactos.

Cabeceras

  • Idempotency-Keystring
    Opcional. Repetir la llamada con la misma clave devuelve la respuesta guardada.

Cuerpo de la solicitud

  • actionstringobligatorio
    La acción: delete, unsubscribe, resubscribe, set_topic, add_tags, remove_tags, add_to_segment, remove_from_segment, verify, export o confirm_consent.
  • selectionobjectobligatorio
    Qué contactos.
    • modestringobligatorio
      ids o filter.
    • idsstring[]
      Con mode: "ids": de 1 a 1.000 IDs de contacto. Los IDs de otro equipo se ignoran.
    • filtersobject
      Con mode: "filter": los mismos filtros de GET /contacts (search, segment_id, tag, consent, origin…). Un filtro desconocido da 422.
    • excluded_idsstring[]
      Con mode: "filter": hasta 5.000 contactos que dejar fuera.
    • expected_countinteger
      El número de contactos que vio la persona. Si el real es muy distinto, la respuesta es 409 selection_changed.
  • paramsobject
    Los parámetros de la acción.
    • accept_responsibilitybooleanobligatorio
      Solo en confirm_consent, y obligatorio: true es la declaración de que los contactos aceptaron recibir marketing y de que tú eres el responsable de ese consentimiento.

Errores

  • 403forbidden
    confirm_consent desde una clave de API o desde un miembro que no es propietario ni administrador.
  • 422validation_error
    Acción, selección o filtro no válidos, selección vacía o por encima del límite, o accept_responsibility ausente en confirm_consent (el mensaje dice que envíes accept_responsibility: true).
  • 409selection_changed
    El número de contactos cambió respecto a expected_count; la respuesta trae el count actual.
  • 409bulk_operation_running
    El equipo ya tiene una operación masiva en cola o en curso (viene en operation).
  • 401restricted_api_key
    La clave es solo de envío.
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);
200Respuesta (una selección pequeña termina al instante)
{
  "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": {}
  }
}
Cuerpo de confirm_consent (solo con la sesión del panel)
{
  "action": "confirm_consent",
  "selection": { "mode": "filter", "filters": { "consent": "unconfirmed" } },
  "params": { "accept_responsibility": true }
}
202Respuesta de una selección grande: en cola
{
  "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
  }
}

#Obtener una acción masiva

GET/contacts/bulk/{id}

Clave de acceso total

Muestra el progreso de una acción masiva. status: queued, running, completed, failed o canceled. total es el tamaño de la selección fijada; processed, changed, skipped y failed cuentan lo que ya se hizo; errors trae los 20 primeros contactos que fallaron. Una operación que termina completed puede tener failed mayor que cero. En confirm_consent, changed es cuántos contactos se confirmaron. GET /contacts/bulk lista las operaciones de los últimos 30 días, de la más nueva a la más antigua (filtro status).

Parámetros de ruta

  • idstringobligatorio
    ID de la operación.

Errores

  • 404not_found
    El recurso no existe o es de otro equipo.
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);
200Respuesta
{
  "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

Las etiquetas (tags) marcan contactos de forma libre, sin necesidad de un segmento: "Cliente VIP", "Vino del evento". El nombre tiene hasta 64 caracteres y es único en el equipo sin distinguir mayúsculas. Cada etiqueta tiene un color: gray, green, yellow, red, blue o purple. En los endpoints de contactos, una etiqueta puede darse por ID o por nombre, y los nombres que aún no existen se crean. Todo contacto que recibe una etiqueta nueva inicia las automatizaciones con el disparador de etiqueta añadida (tag.added), y las etiquetas también sirven de filtro en segmentos dinámicos y en GET /contacts?tag=.

#Añadir etiquetas a un contacto

POST/contacts/{id}/tags

Clave de acceso total

Añade las etiquetas al contacto (las que ya tenía quedan como están) y devuelve todas sus etiquetas. Los nombres que no existen se crean, con el siguiente color de la paleta.

Parámetros de ruta

  • idstringobligatorio
    ID o correo electrónico del contacto.

Cuerpo de la solicitud

  • tagsstring[]obligatorio
    De 1 a 50 etiquetas, por ID o por nombre.

Errores

  • 422validation_error
    Algún campo no es válido. El mensaje indica cuál.
  • 404not_found
    El contacto o alguna etiqueta (dada por ID) no 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);
200Respuesta
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f",
      "name": "Cliente VIP",
      "color": "purple"
    }
  ]
}

#Quitar una etiqueta de un contacto

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

Clave de acceso total

Quita la etiqueta del contacto y devuelve las etiquetas que quedaron. La etiqueta sigue existiendo.

Parámetros de ruta

  • idstringobligatorio
    ID o correo electrónico del contacto.
  • tagstringobligatorio
    ID o nombre de la etiqueta.

Errores

  • 404not_found
    El contacto o la etiqueta no 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);
200Respuesta
{
  "object": "list",
  "has_more": false,
  "data": []
}

#Listar etiquetas

GET/tags

Clave de acceso total

Las etiquetas del equipo, cada una con contacts_count (cuántos contactos la tienen).

Parámetros de consulta

  • limitinteger
    De 1 a 100. Predeterminado: 20.
  • afterstring
    Cursor de la página siguiente.
  • beforestring
    Cursor de la página anterior.
  • searchstring
    Búsqueda por nombre.
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);
200Respuesta
{
  "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"
    }
  ]
}

#Crear una etiqueta

POST/tags

Clave de acceso total

Cuerpo de la solicitud

  • namestringobligatorio
    Nombre, hasta 64 caracteres, único en el equipo sin distinguir mayúsculas.
  • colorstring
    gray, green, yellow, red, blue o purple. Por defecto: el siguiente color de la paleta.

Errores

  • 422validation_error
    Nombre vacío, demasiado largo o ya usado, o color fuera de la 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);
201Respuesta
{
  "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"
}

#Obtener una etiqueta

GET/tags/{id}

Clave de acceso total

Parámetros de ruta

  • idstringobligatorio
    ID de la etiqueta.

Errores

  • 404not_found
    El recurso no existe o es de otro equipo.
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);
200Respuesta
{
  "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"
}

#Actualizar una etiqueta

PATCH/tags/{id}

Clave de acceso total

Parámetros de ruta

  • idstringobligatorio
    ID de la etiqueta.

Cuerpo de la solicitud

  • namestring
    Nombre nuevo.
  • colorstring
    Nuevo color.

Errores

  • 422validation_error
    Algún campo no es válido. El mensaje indica cuál.
  • 404not_found
    El recurso no existe o es de otro equipo.
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);
200Respuesta
{
  "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"
}

#Eliminar una etiqueta

DELETE/tags/{id}

Clave de acceso total

Quita la etiqueta de todos los contactos; los contactos siguen en la audiencia. Una etiqueta usada por una automatización no se puede eliminar: quítala de la automatización antes.

Parámetros de ruta

  • idstringobligatorio
    ID de la etiqueta.

Errores

  • 409tag_in_use
    Alguna automatización usa esta etiqueta. El mensaje indica cuáles.
  • 404not_found
    El recurso no existe o es de otro equipo.
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);
200Respuesta
{
  "object": "tag",
  "id": "019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f",
  "deleted": true
}

#Etiquetar varios contactos

POST/tags/{id}/contacts

Clave de acceso total

Aplica la etiqueta a varios contactos a la vez. Quien ya la tenía queda como está; added cuenta solo a los que recibieron la etiqueta ahora.

Parámetros de ruta

  • idstringobligatorio
    ID de la etiqueta.

Cuerpo de la solicitud

  • contactsstring[]obligatorio
    Hasta 1.000 contactos por llamada, cada uno por ID o por correo electrónico. Quien no exista se ignora.

Errores

  • 422validation_error
    Algún campo no es válido. El mensaje indica cuál.
  • 404not_found
    El recurso no existe o es de otro equipo.
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);
200Respuesta
{
  "object": "tag_contacts",
  "tag_id": "019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f",
  "contacts": 2,
  "added": 1
}

#Quitar la etiqueta a varios contactos

DELETE/tags/{id}/contacts

Clave de acceso total

Parámetros de ruta

  • idstringobligatorio
    ID de la etiqueta.

Cuerpo de la solicitud

  • contactsstring[]obligatorio
    Hasta 1.000 contactos por llamada, cada uno por ID o por correo electrónico. Quien no exista se ignora.

Errores

  • 422validation_error
    Algún campo no es válido. El mensaje indica cuál.
  • 404not_found
    El recurso no existe o es de otro equipo.
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);
200Respuesta
{
  "object": "tag_contacts",
  "tag_id": "019a1d70-6a7b-7c8d-9e0f-1a2b3c4d5e6f",
  "contacts": 1,
  "deleted": true
}