Con este módulo tu CRM o tu call center gestiona las PQRS (peticiones, quejas, reclamos y sugerencias) de tus afiliados server-to-server, sin entrar al panel de Compense: radicas a nombre del afiliado, listas y filtras (incluidas las vencidas), consultas el detalle con su historial, respondes como la entidad y cambias estado o prioridad. Las reglas son exactamente las mismas de la app y del panel: radicado PQRS-AAAA-NNNNNN, plazo legal, máquina de estados y notificación al afiliado cuando se le responde.

Los ejemplos usan a Caja Horizonte, cuyo call center registra los casos en su CRM con referencias HORIZONTE-CRM-….

Antes de empezar #

Scopes #

ScopePermite
pqrs:readListar, ver el detalle con historial y estadísticas
pqrs:submitRadicar PQRS a nombre de un afiliado
pqrs:manageResponder y cambiar estado o prioridad como la entidad

Pide solo lo que tu integración necesita: un call center que solo radica no necesita pqrs:manage. pqrs:manage es un scope de alto valor (equivale a firmar una respuesta oficial) y está en la lista de scopes que exigirán firma de la petición cuando se active esa protección en tu llave.

La PQRS es obligatoria para las entidades vigiladas, así que hoy está disponible para todas. Si en algún momento tu entidad no tuviera el módulo, el gateway responde 403 con «El módulo PQRS no está contratado para esta entidad.».

Identificadores #

Cada PQRS tiene un id (UUID) y un radicado legible (PQRS-2026-000123). Todas las rutas con {pqrs_id} aceptan cualquiera de los dos. Guarda ambos en tu CRM junto a tu propia referencia.

OperaciónRutaScope
RadicarPOST/v1/gateway/pqrs/pqrs:submit
ListarGET/v1/gateway/pqrs/pqrs:read
EstadísticasGET/v1/gateway/pqrs/stats/pqrs:read
DetalleGET/v1/gateway/pqrs/{pqrs_id}/pqrs:read
ResponderPOST/v1/gateway/pqrs/{pqrs_id}/respond/pqrs:manage
Cambiar estado / prioridadPOST/v1/gateway/pqrs/{pqrs_id}/status/pqrs:manage

Las escrituras devuelven el sobre estándar del gateway; data trae la PQRS completa, con su historial. Envía siempre Idempotency-Key: si reintentas con la misma llave, el gateway devuelve el resultado original (idempotent_replay: true) en vez de radicar o responder dos veces.

Estados y plazos de ley #

stateDiagram-v2
    [*] --> opened: radicar
    opened --> in_progress
    opened --> escalated
    in_progress --> escalated
    escalated --> in_progress
    opened --> resolved: respuesta final
    in_progress --> resolved: respuesta final
    escalated --> resolved: respuesta final
    opened --> closed
    in_progress --> closed
    escalated --> closed
    resolved --> closed
    closed --> [*]
  • opened, in_progress y escalated admiten respuestas. Una respuesta con is_final: true deja la PQRS en resolved.
  • resolved solo puede pasar a closed; closed es terminal.
  • Repetir el estado actual es un no-op: no crea historial ni webhook.
  • Prioridades: low, normal (por defecto), high, urgent.

El plazo se calcula al radicar: 15 días hábiles según la Ley 1755 de 2015, sin contar sábados, domingos ni festivos. Cada PQRS trae deadline (fecha límite) e is_overdue, que pasa a true cuando el plazo se superó sin resolverla. Úsalos para tus alertas internas; el filtro overdue=true te da directamente las vencidas.

Radicar en nombre del afiliado #

Tu agente atiende la llamada, identifica al afiliado por su documento y radica el caso. El afiliado debe existir en la base sincronizada de tu entidad (si no, 422).

import os
from compense_gateway import CompenseClient

client = CompenseClient(api_key=os.environ["COMPENSE_API_KEY"], environment="sandbox")

tx = client.pqrs.submit(
    document="1000000001",
    issue_type="claim",
    service_area="wallet",
    subject="Cobro no reconocido",
    description="El afiliado reporta un cobro de 45.000 que no reconoce en la billetera.",
    electronic_notification_consented=True,       # lo que respondió el afiliado
    origin_channel="call_center",
    external_reference="HORIZONTE-CRM-88231",
    agent="ana.p",
    idempotency_key="horizonte-crm-88231-radicar",
)
pqrs = tx.data
print(pqrs["radicado"], pqrs["id"], pqrs["status"], pqrs["deadline"])
CampoObligatorioValores
documentsíDocumento del afiliado (debe existir en tu base sincronizada)
issue_typesípetition · complaint · claim · suggestion
service_areasísubsidies · credits · health · marketplace · benefits · wallet · payments · general
subjectsíHasta 255 caracteres
descriptionsíTexto libre
electronic_notification_consentedsítrue / false
origin_channelno (por defecto api)call_center · crm · phone · email · in_person · chat · whatsapp · web · api · other
external_referencenoId del caso en tu CRM (hasta 100). Sirve para filtrar y viaja en los webhooks
agentnoReferencia de tu agente, para trazabilidad (no pongas correos)
related_type + related_*_idnoCorrelación con una solicitud o producto del mismo afiliado

Consentimiento de notificación electrónica. La notificación electrónica exige aceptación expresa del afiliado (Ley 1437 de 2011, art. 56), así que por API no se asume: el campo es obligatorio y debe reflejar lo que el afiliado respondió en la llamada: si no aceptó, envía false (no lo omitas ni lo asumas en true).

Canal de origen. app está reservado a la app del afiliado. Elige el canal real por el que llegó el caso: es lo que verá el back-office y lo que alimenta las estadísticas por canal.

Correlación. Para atar el caso a un producto, envía related_type (credit_application, subsidy_application, credit o subsidy) y el id correspondiente (related_credit_application_id, related_subsidy_application_id, related_credit_id o related_subsidy_active_id). En los SDK van como campos adicionales: extra en Python, JavaScript, PHP y .NET; Extra en Go. Un id que no sea del mismo afiliado responde 422.

Respuesta (data, resumida):

JSON
{
  "id": "4f0c…",
  "radicado": "PQRS-2026-000123",
  "issue_type": "claim", "service_area": "wallet",
  "subject": "Cobro no reconocido",
  "status": "opened", "priority": "normal",
  "origin_channel": "call_center", "external_reference": "HORIZONTE-CRM-88231",
  "electronic_notification_consented": true,
  "deadline": "2026-11-24T14:03:11-05:00", "is_overdue": false,
  "affiliate": {"document_number": "1000000001", "name": "S*** E***"},
  "description": "…", "responses": [], "attachments": [],
  "history": [
    {"from_status": null, "to_status": "opened", "at": "…", "source": "gateway",
     "actor": "integration:<id de tu llave> agent:ana.p", "note": "Radicación"}
  ]
}

El nombre del afiliado va enmascarado y su correo nunca se devuelve.

Listar y filtrar #

GET/v1/gateway/pqrs/ pagina por número de página (page, page_size hasta 100) y devuelve un Page con count y results, de la más reciente a la más antigua. Los resultados no traen description ni historial: para eso está el detalle.

Filtros, todos opcionales: status, issue_type, service_area, origin_channel, document, external_reference, date_from y date_to (AAAA-MM-DD, inclusivos, sobre la fecha de radicación) y overdue=true para las vencidas (plazo superado y sin resolver). Un document que no existe devuelve la lista vacía.

vencidas = client.pqrs.list(status="opened", overdue=True, page_size=50)
print(vencidas.count)
for p in vencidas.results:
    print(p["radicado"], p["service_area"], p["deadline"], p["external_reference"])

# Buscar el caso por la referencia de tu CRM
caso = client.pqrs.list(external_reference="HORIZONTE-CRM-88231").results
JSON
{"count": 37, "page": 1, "page_size": 50, "results": [ { "id": "…", "radicado": "PQRS-2026-000123", "status": "opened", "…": "…" } ]}

Estadísticas #

GET/v1/gateway/pqrs/stats/ resume por estado, tipo y área, y cuenta las vencidas. Acepta issue_type, service_area, date_from y date_to.

stats = client.pqrs.stats(date_from="2026-10-01", date_to="2026-10-31")
print(stats["total"], stats["overdue"], stats["by_status"], stats["by_service_area"])
JSON
{"total": 37, "overdue": 2,
 "by_status": {"opened": 20, "in_progress": 9, "resolved": 8},
 "by_service_area": {"wallet": 15, "payments": 12, "general": 10},
 "by_type": {"claim": 25, "petition": 12}}

Detalle con historial #

El detalle incluye description, responses (texto, si es final, fecha y responder), attachments e history: cada cambio de estado con from_status, to_status, at, source (app, panel o gateway), actor y note. En el actor nunca van nombres ni correos de funcionarios: staff para el panel, affiliate para la app e integration:<llave> agent:<ref> para la API.

detalle = client.pqrs.get("PQRS-2026-000123")      # o el id (UUID)
for h in detalle["history"]:
    print(h["at"], h["from_status"], "→", h["to_status"], h["source"], h["actor"])

Responder como la entidad #

Solo se responde mientras la PQRS está opened, in_progress o escalated; si no, 409 («No se puede responder una solicitud resuelta.»). Con is_final: true la PQRS pasa a resolved y se registra resolved_at. El afiliado recibe una notificación push con la respuesta.

tx = client.pqrs.respond(
    "PQRS-2026-000123",
    response_text="Se reversó el cobro el 28 de octubre. Verás el reintegro en tu billetera.",
    is_final=True,
    agent="ana.p",
    idempotency_key="horizonte-crm-88231-resp-1",
)
print(tx.data["status"])   # resolved

Una respuesta parcial (is_final: false u omitido) queda en responses sin cambiar el estado: úsala para informar avances sin cerrar el caso.

Cambiar estado y prioridad #

Cuando el caso pasa a otra área o hay que escalarlo, cambia el estado y, si hace falta, la prioridad. La note queda en el historial.

tx = client.pqrs.change_status(
    "PQRS-2026-000123",
    status="escalated",
    priority="high",
    note="Pasa al área de pagos",
    agent="luis.r",
    idempotency_key="horizonte-crm-88231-estado-1",
)

Una transición inválida responde 409 («No se puede pasar de “cerrada” a “abierta”.»).

Errores frecuentes #

HTTPCódigo del motorCuándodetail (ejemplo)
401—Falta X-API-Key—
403—Llave sin el scope, o módulo no contratadoEl módulo PQRS no está contratado para esta entidad.
404PQRS_NOT_FOUNDPQRS inexistente o de otra entidadPQRS no encontrada.
409INVALID_STATEResponder una PQRS resuelta o cerradaNo se puede responder una solicitud cerrada.
409INVALID_TRANSITIONTransición de estado no permitidaNo se puede pasar de “cerrada” a “abierta”.
409—La misma Idempotency-Key sigue en cursoA request with this Idempotency-Key is already being processed.
422VALIDATION_ERRORCampos faltantes o con valores fuera de catálogoDatos inválidos — subject: Este campo es requerido.
422AFFILIATE_NOT_FOUNDEl afiliado no existe en tu entidadNo existe un afiliado con ese documento en la entidad. Sincronícelo primero.
422—related_* de otro afiliado—
429—Límite por minuto o cuota mensualver Retry-After
502 / 504—El motor falló o no respondióReintenta con la misma llave y cita el trace_id

detail trae el mensaje del motor en español. Para distinguir un 409 de negocio de un conflicto de idempotencia, mira el type del problema (…/upstream-error frente a …/idempotency-conflict); el patrón completo está en Errores.

Eventos #

Los eventos pqrs.* se emiten en la misma transacción del cambio y desde cualquier canal (app, panel o API): así tu CRM se entera de las PQRS que el afiliado radica en la app y de las respuestas que da tu back-office sin sondear.

PQRS #

EventoDesdeCuándo se emite
pqrs.created2026-10-27Se radicó una PQRS.
pqrs.status_changed2026-10-27Cambió el estado de una PQRS (incluye pasar a resolved por una respuesta final).
pqrs.responded2026-10-27La entidad registró una respuesta oficial.
JSON
{
  "event": "pqrs.responded",
  "version": "1",
  "tenant_id": "horizonte",
  "occurred_at": "2026-10-29T16:20:00Z",
  "resource": {
    "pqrs_id": "4f0c…", "radicado": "PQRS-2026-000123",
    "response_id": "9a1e…", "is_final": true, "status": "resolved",
    "external_reference": "HORIZONTE-CRM-88231",
    "source": "gateway", "actor": "integration:<llave> agent:ana.p"
  }
}
  • pqrs.created trae además issue_type, service_area, status, priority, origin_channel, affiliate_id y deadline; pqrs.status_changed trae from_status, to_status y priority.
  • Por privacidad, el webhook no lleva el texto de la respuesta ni el documento: consulta el detalle con pqrs.get.
  • Si enviaste Idempotency-Key, el X-Webhook-Id del evento es estable entre reintentos: úsalo para deduplicar (Reintentos de webhooks).
  • Usa external_reference para encontrar el caso en tu CRM sin guardar el UUID.

Sandbox #

Con una llave de sandbox las respuestas son simuladas y deterministas:

CasoResultado
Radicar con document 1000000001 a 1000000004201; el mismo cuerpo produce el mismo id y radicado
Radicar con document 9999999999422 AFFILIATE_NOT_FOUND
Responder o cambiar estado de PQRS-2026-999999PQRS cerrada: responder → 409; pasar a otro estado → 409; closed → 200 (no-op)
Id que no es pqr-… ni un radicado404
Listar / estadísticasBandeja fija de 5 PQRS de los documentos de prueba (aplica filtros y paginación)

El sandbox no guarda estado: una escritura devuelve el estado nuevo, pero un GET posterior del mismo id vuelve al estado inicial (opened). Los webhooks pqrs.* no se emiten en sandbox; para probar tu receptor usa un evento de prueba.

Lo que sigue #