Screening de sanciones en Python: OFAC, ONU, UE y UK

Tu formulario de alta tiene un campo de nombre y, en algún punto entre ese campo y el primer pago, la ley te obliga a comprobar si la persona que lo rellenó aparece en una lista de sanciones. Comparar cadenas con == contra una hoja de cálculo descargada no cuenta: los nombres listados llegan transliterados, abreviados, en otro orden y bajo alias, y las listas cambian varias veces por semana.
Este tutorial construye el screening de sanciones en Python desde cero. Al terminar tendrás un pequeño módulo SanctionsClient que verifica un nombre o un CSV completo contra las listas de OFAC, ONU, UE y Reino Unido, una función de decisión que distingue una coincidencia real de un falso positivo, un registro de auditoría con la versión de cada lista en el momento de la consulta, y un trabajo nocturno que vuelve a verificar tu base de clientes cuando cualquier lista cambia.
El matching lo hace la API de Screening de Sanciones, así que la parte en Python es corta. Todo lo que sigue funciona tal cual con pip install requests y una clave de API.
La versión de diez líneas
Una petición GET verifica un nombre contra todas las listas a la vez. La grafía de abajo está mal a propósito (la forma listada es Yanukovych); el matching difuso es justamente el punto:
import requests
API = "https://api-sanctions.dataandapis.com/v1/screen"
HEADERS = {"X-Api-Key": "TU_CLAVE_DE_API"}
r = requests.get(API, params={"name": "Viktor Yanukovich", "min_score": 0.85},
headers=HEADERS, timeout=15)
r.raise_for_status()
result = r.json()
print(result["hit"], result["count"])
for m in result["matches"]:
print(m["score"], m["match_type"], m["matched_name"], m["entity"]["source"])
True 3
1 exact Viktor Fedorovich Yanukovich UK-SL
0.9765 strong Viktor Yanukovych WIKIDATA
0.95 strong Viktor Fedorovych YANUKOVYCH OFAC-SDN
Tres listas, tres grafías distintas, una sola respuesta. La lista británica contiene exactamente el alias que escribiste, OFAC recoge la transliteración ucraniana, y la tercera fila es un registro de persona políticamente expuesta (PEP) procedente de Wikidata, no una sanción. El resto del artículo trata de convertir ese JSON en una decisión de cumplimiento defendible.
Contra qué estás verificando
Las listas de sanciones las publican los gobiernos como descargas gratuitas, cada una en su propio formato XML o CSV y con sus propias convenciones de fechas. La API consolida las que importan en la mayoría de programas de cumplimiento de fintechs y marketplaces:
- OFAC SDN (
ofac-sdn): la lista de Nacionales Especialmente Designados del Tesoro de EE. UU. Personas, empresas, buques y aeronaves con activos bloqueados. Unas 19.500 entradas. - OFAC Consolidada (
ofac-cons): las listas no SDN (sanciones sectoriales, evasores de sanciones y otras). Más pequeña, pero una coincidencia aquí también requiere revisión humana. - Lista consolidada del Consejo de Seguridad de la ONU (
un): la lista que todo Estado miembro está obligado a aplicar. - Lista consolidada de sanciones financieras de la UE (
eu-fsf): todas las personas y entidades sujetas a congelación de activos en la UE, publicada por la Comisión Europea. - UK Sanctions List (
uk-sl): publicada por el Foreign, Commonwealth & Development Office. Desde enero de 2026 es la única lista británica; la antigua lista consolidada de la OFSI se retiró, y la API sigue aceptandosource=UK-OFSIcomo alias para que las integraciones existentes no se rompan. - Personas políticamente expuestas (
wikidata-pep): jefes de Estado y de Gobierno y ministros, a partir de Wikidata. Un PEP no está sancionado; el registro indica que aplica la diligencia debida reforzada.
GET /v1/lists devuelve el editor, la licencia, la URL del fichero, el número de entradas y la fecha de publicación de cada lista. En el paso 5 usarás ese endpoint para detectar cambios.
Por qué falla la comparación exacta
Una persona listada tiene un nombre principal y, normalmente, varios alias, a menudo en más de un alfabeto. El mismo nombre árabe o cirílico se romaniza de forma distinta en EE. UU., Reino Unido y la UE: Yanukovych, Yanukovich, Ianukovytch. Los clientes escriben su nombre con tildes, sin segundo nombre, con el apellido primero o con una transliteración que ningún editor eligió. Cualquiera de estas variantes derrota una comparación de cadenas, y cada fallo es un incumplimiento tuyo, no del cliente.
La API puntúa cada alias de cada entidad candidata y devuelve el mejor. Por debajo, tanto la consulta como los nombres listados se normalizan (descomposición Unicode, diacríticos eliminados, puntuación fuera), los tokens se reducen a un esqueleto consonántico insensible a la transliteración para que Yanukovych e Ianukovich caigan en la misma clave, y la puntuación final combina la similitud Jaro-Winkler con un ratio de conjunto de tokens. La comparación de tokens es asimétrica a propósito: las palabras extra en el nombre listado no cuentan en contra, así que una consulta de dos palabras puede coincidir por completo con una entrada formal de cinco.
Tres campos de cada coincidencia te dicen cuán buena es:
- score: de 0 a 1. Solo se devuelven coincidencias iguales o superiores a
min_score(por defecto 0,75, mínimo 0,5). - match_type:
exact(puntuación 1),strong(0,9 o más) ofuzzy. - matched_name_kind y matched_name_quality: si coincidió el nombre principal o un alias, y si el editor califica ese alias como fuerte o débil. Un alias débil a 0,86 merece menos alarma que un nombre principal a 0,86.
Paso 1: un cliente reutilizable
Concentra los detalles HTTP en un solo sitio: una sesión que lleva la clave, un timeout y un reintento con espera exponencial para límites de tasa y errores transitorios. Guárdalo como sanctions.py.
import time
import requests
BASE = "https://api-sanctions.dataandapis.com/v1"
class SanctionsClient:
def __init__(self, api_key, min_score=0.85):
self.s = requests.Session()
self.s.headers["X-Api-Key"] = api_key
self.min_score = min_score
def _call(self, method, path, **kw):
for attempt in range(4):
r = self.s.request(method, BASE + path, timeout=20, **kw)
if r.status_code in (429, 502, 503, 504) and attempt < 3:
time.sleep(2 ** attempt)
continue
r.raise_for_status()
return r.json()
def screen(self, name, **filters):
params = {"name": name, "min_score": self.min_score, "limit": 10, **filters}
return self._call("GET", "/screen", params=params)
def screen_many(self, names, **filters):
body = {"names": list(names), "min_score": self.min_score, **filters}
return self._call("POST", "/screen", json=body)
def lists(self):
return self._call("GET", "/lists")["lists"]
Los argumentos filters se corresponden uno a uno con los parámetros de la API: list (sanction o pep), source (el slug de una lista), type (individual, entity, vessel, aircraft) y country. Funcionan igual en el endpoint individual y en el de lotes.
Paso 2: verificar un nombre y decidir
hit por sí solo no es una decisión. Una coincidencia fuerte en una lista de sanciones y una coincidencia difusa en un registro PEP lo ponen en true por igual, y exigen respuestas muy distintas. Codifica la política una sola vez:
from sanctions import SanctionsClient
client = SanctionsClient("TU_CLAVE_DE_API", min_score=0.85)
def decide(result):
"""clear | hold | review | edd (diligencia debida reforzada)"""
if not result["hit"]:
return "clear"
sanctions = [m for m in result["matches"] if m["entity"]["list_type"] == "sanction"]
if sanctions:
top = sanctions[0] # las coincidencias vienen ordenadas por score
return "hold" if top["match_type"] in ("exact", "strong") else "review"
return "edd" # solo PEP: no prohibido, pero mira más de cerca
result = client.screen("Viktor Yanukovich")
print(decide(result)) # hold
hold detiene la operación hasta que un responsable de cumplimiento confirme o descarte la coincidencia; review la encola sin bloquear; edd activa tu procedimiento para PEP. Que una coincidencia fuerte bloquee automáticamente o solo retenga es una decisión de política para tu equipo de cumplimiento, no para el código, y por eso vive en una función pequeña.
Cada coincidencia incluye los campos programs, countries, birth_dates y listed_on de la entidad. Con nombres comunes, eso es lo que el revisor usará para descartar un falso positivo, así que muéstralos en la cola de revisión y no solo la puntuación.
Paso 3: verificar un CSV por lotes
El alta verifica un nombre cada vez, pero la primera pasada sobre una base de clientes existente, y cada re-verificación tras una actualización de listas, es un trabajo por lotes. POST /v1/screen acepta hasta 25 nombres por llamada y devuelve un resultado por nombre, en el mismo orden en que los enviaste.
import csv
def chunks(seq, n=25):
for i in range(0, len(seq), n):
yield seq[i:i + n]
with open("clientes.csv", newline="") as f:
rows = list(csv.DictReader(f)) # columnas: id, full_name
flagged = []
for batch in chunks(rows):
res = client.screen_many((r["full_name"] for r in batch), list="sanction")
for row, item in zip(batch, res["results"]):
if "error" in item: # nombre vacío o demasiado largo
continue
if item["hit"]:
best = item["matches"][0]
flagged.append((row["id"], row["full_name"], best["score"],
best["matched_name"], best["entity"]["source"]))
print(f"{len(flagged)} de {len(rows)} clientes requieren revisión")
Pasar list="sanction" deja fuera los registros PEP del barrido automático para que la cola de revisión siga siendo manejable; lanza la pasada de PEP por separado con list="pep" en los segmentos de clientes donde aplique. Una petición por lotes se factura como una sola llamada a la API, igual que un nombre suelto, así que agrupar ahorra tokens además de viajes de ida y vuelta.
Paso 4: guardar un registro de auditoría
Cuando un regulador pregunte por qué un cliente fue aprobado en marzo, «lo verificamos» no es una respuesta. Necesitas la consulta tal como se escribió, la forma normalizada, el umbral, las coincidencias y la versión de cada lista en ese momento. Toda respuesta de screening incluye screened_against exactamente para esto:
"screened_against": [
{"slug": "eu-fsf", "publish_date": "2026-10-23", "retrieved_at": "2026-10-30", "entity_count": 6269},
{"slug": "ofac-sdn", "publish_date": "10/19/2026", "retrieved_at": "2026-10-30", "entity_count": 19484},
{"slug": "uk-sl", "publish_date": "2026-10-29", "retrieved_at": "2026-10-30", "entity_count": 6420},
...
]
Escribe una línea por verificación en un registro de solo adición. JSON Lines basta para empezar; el mismo registro encaja limpiamente en una tabla de base de datos más adelante.
import json
from datetime import datetime, timezone
def audit_record(customer_id, result, decision):
return {
"customer_id": customer_id,
"screened_at": datetime.now(timezone.utc).isoformat(),
"query": result["query"],
"normalized": result["normalized"],
"min_score": result["min_score"],
"decision": decision,
"matches": [(m["entity"]["source"], m["entity"]["source_ref"], m["score"])
for m in result["matches"]],
"lists": {s["slug"]: s["publish_date"] for s in result["screened_against"]},
}
result = client.screen("Viktor Yanukovich")
with open("screening-log.jsonl", "a") as log:
log.write(json.dumps(audit_record("cust_123", result, decide(result))) + "\n")
source_ref es el identificador que el propio editor asigna a la entrada (un UID de OFAC, una referencia de la ONU, un ID de grupo británico), lo que permite a un auditor consultar la designación en el sitio oficial sin fiarse de tu base de datos.
Paso 5: volver a verificar cuando una lista cambia
Verificar en el alta es lo mínimo. El cliente que estaba limpio en marzo puede ser designado en octubre, y la obligación de enterarte es tuya. Consulta /v1/lists una vez al día, compara las fechas de publicación con las que viste la última vez y relanza el lote del paso 3 cuando algo haya cambiado.
import json, os
STATE = "list-versions.json"
def list_versions(client):
return {l["slug"]: l["publish_date"] for l in client.lists()}
current = list_versions(client)
previous = json.load(open(STATE)) if os.path.exists(STATE) else {}
changed = [slug for slug, date in current.items() if previous.get(slug) != date]
if changed:
print("listas actualizadas:", changed, "- re-verificando todos los clientes activos")
# ejecuta aquí el bucle por lotes del paso 3 sobre todos los clientes activos
with open(STATE, "w") as f:
json.dump(current, f)
Re-verificar toda la base tras cada cambio es la opción simple y defendible. Si tu base es grande, re-verifica solo a los clientes cuyo nombre comparte algún token con las entradas nuevas: GET /v1/entities?source=uk-sl lista todas las entradas de una lista con su fecha listed_on, suficiente para aislar las incorporaciones recientes.
Detalles que importan
- Los nombres comunes dan coincidencias. «María García» devuelve coincidencias exactas porque hay personas con ese nombre realmente listadas. La coincidencia es correcta; el cliente probablemente no es esa persona. Usa las fechas de nacimiento, países y programas de la entidad para descartarlo, y plantéate
country=cuando conozcas la nacionalidad del cliente. - Las consultas cortas sobrecoinciden con nombres PEP largos. Como las palabras extra del lado listado no penalizan, «John Smith» a 0,85 recoge registros PEP difusos como «Admiral Alan William John West, Baron West of Spithead». Mantén la puerta automática en
list=sanctiony trata una coincidencia PEPfuzzycomo elemento de revisión, nunca como alerta automática. - Elige el umbral según el riesgo, no según la demo. 0,85 es una puerta razonable para el alta; una mesa de pagos que verifica contrapartes en cada transacción puede trabajar a 0,8 y asumir más revisiones. Bajar de 0,75 añade sobre todo ruido. Guarda el umbral en el registro de auditoría para que un cambio posterior no reescriba la historia.
- Límites. Nombres de hasta 200 caracteres, 25 nombres por lote, como máximo 50 coincidencias por nombre (
limit). Un 400 en el endpoint de lotes significa que se rechazó toda la petición; unerrorpor nombre dentro deresultssignifica que esa entrada estaba vacía o era demasiado larga. - Verifica también empresas y buques. Los programas de sanciones listan entidades y barcos, no solo personas. Verifica la razón social de cada cliente empresa y, en productos de logística, el nombre del buque, con
type=entityotype=vesselpara acotar los candidatos. - No cachees un resultado limpio. Una decisión de «clear» solo vale para las versiones de lista de
screened_against. Cachea el registro de auditoría, no el veredicto.
Qué puedes construir con esto
- Puerta de alta: llama a
screen()en el handler de registro, guarda el registro de auditoría y envía los resultados hold a una cola de cumplimiento antes de que la cuenta pueda operar. - Comprobación de pagos y beneficiarios: verifica el nombre del beneficiario en cada nuevo destino de pago, incluido el titular de la cuenta bancaria cuando no coincide con el cliente.
- Vetting de vendedores y proveedores en marketplaces: verifica por lotes las razones sociales y sus administradores en el momento de la solicitud y tras cada actualización de listas.
- Cumplimiento en transporte y comercio: verifica buques, navieras y consignatarios contra las listas de sanciones con
type=vesselytype=entity. - Monitorización nocturna: el trabajo del paso 5 como tarea cron, publicando el número de revisiones en Slack para que el equipo de cumplimiento empiece el día con el delta.
Conclusión
El screening de sanciones en Python se reduce a cuatro piezas: un cliente que llama a un endpoint, una función de decisión que separa sanciones de PEP y coincidencias fuertes de difusas, un registro de auditoría que captura las versiones de las listas, y un trabajo que re-verifica cuando las listas se mueven. La parte difícil, el matching difuso entre transliteraciones y alias de seis listas oficiales, la resuelve la API. Para la pregunta más amplia de cuándo verificar y qué hacer tras una coincidencia, consulta nuestra guía para verificar clientes contra listas de sanciones (en inglés).
Consigue una clave de API y prueba la versión de diez líneas con tus propios nombres de prueba en la página de la API de Screening de Sanciones; las 500 llamadas del plan gratuito bastan para montar y probar un flujo de alta.