Actualizaciones asíncronas y webhooks

Solicite el re-scrape de una entidad a la fuente original y reciba el resultado completo por webhook cuando el job termina. Disponible hoy en Panamá.

Qué es y cuándo usarlo

El endpoint GET /v4/panama/entidades/{id} devuelve el snapshot actual que Panadata tiene de la entidad. Si necesita garantizar que ese snapshot está al día contra la fuente registral original, dispare una actualización asíncrona con POST .../entidades/{id}/update. El backend re-extrae las fuentes y, cuando termina, hace un POST a su URL de webhook con el resultado completo firmado.

Tiempo típico de completado: de minutos a una hora según la fuente y la disponibilidad del scraper. No es un endpoint síncrono — no espere la respuesta en la conexión del POST.

Flujo completo

  1. POST /v4/panama/entidades/{id}/update?include=<CÓDIGOS> → recibe 202 con update_request_id.
  2. El backend re-extrae las fuentes y re-serializa la entidad.
  3. Cuando el job termina con éxito, Panadata hace POST a la URL configurada en /webhook_settings con el body firmado.
  4. Si el webhook no llega, caiga a GET /v4/update_requests/{update_request_id} para recuperar el estado y el resultado.

Configurar URL y firma

Configure su URL de webhook y genere la signing key desde /webhook_settings. Panadata firma cada entrega con HMAC-SHA256 usando esa key. Guárdela inmediatamente — se muestra una sola vez.

La URL debe ser HTTPS, pública y aceptar POST con Content-Type: application/json. Responda 2xx para acusar recibo; cualquier otro código se cuenta como fallo.

Disparar una actualización

curl -X POST 'https://api.panadata.net/v4/panama/entidades/4294622/update?include=DAT-CORE' \
  --header 'Authorization: Bearer pk_su_llave'

La API responde 202 Accepted con el shape de UpdateRequestAccepted:

{
  "update_request_id": 16,
  "status": "pending",
  "entity_id": 4294622,
  "entity_sid": "PERSONA_JURDICA_Folio_N_1779_M",
  "jurisdiction": "panama",
  "requested_codes": ["DAT-CORE"],
  "estimated_cost": "0.15",
  "webhook_url": "https://su-dominio.example.com/panadata/webhooks"
}

Facturación: idéntica al GET de detalle (base $0.01 + suma de códigos solicitados). El cargo se aplica al completarse el job, no al disparar el POST.

Cada POST crea un Update Request nuevo: la API no deduplica por entidad+include. Si dispara dos solicitudes idénticas en sucesión rápida, recibirá dos update_request_id distintos y se le cobrarán ambas al completarse.

Payload del webhook

El body es JSON UTF-8 (sin \uXXXX escaping). La key data contiene el detalle de la entidad con el mismo shape que la respuesta del GET de detalle para los códigos resueltos.

POST https://su-dominio.example.com/panadata/webhooks
Content-Type: application/json
X-Panadata-Webhook-Signature: sha256=0e5152ba4b87290c94c1ea79153a67456d1e4ee5ced3f0a86f5082d930bb334f
X-Panadata-Webhook-Timestamp: 1779918984

{
  "event": "update_request.completed",
  "update_request_id": 16,
  "jurisdiction": "panama",
  "data": {
    "id": 4294622,
    "panadata_id": 4294622,
    "sid": "PERSONA_JURDICA_Folio_N_1779_M",
    "nombre": "ASOCIACIÓN DE SECRETARIAS DEL BANCO NACIONAL DE PANAMÁ (ASOSEBANAL).",
    "ruc": null,
    "tipo_organizacion": "SOCIEDAD COMÚN",
    "status": "VIGENTE",
    "vigencia": "PERPETUA",
    "fecha_registro": "1982-09-21T05:00:00+00:00",
    "folio": "(PERSONA JURÍDICA) Folio Nº 1779 (M)",
    "ficha": "1779",
    "domicilio": "CIUDAD DE PANAMÁ, DISTRITO PANAMÁ, PROVINCIA PANAMÁ",
    "capital": null,
    "contacto": null,
    "source_updated_at": "2026-05-27 21:56:23.771506+00:00",
    "entity_events": [ … ]
  }
}

Ejemplo byte-idéntico al delivery real de la UR #16 (2026-05-28), excepto por el array entity_events, que aquí se muestra truncado por brevedad (en el delivery real trae sus 17 elementos).

Headers de delivery

Header Valor Notas
Content-Type application/json UTF-8, sin escapado Unicode.
X-Panadata-Webhook-Signature sha256={hex} HMAC-SHA256 sobre {timestamp}.{body_bytes}.
X-Panadata-Webhook-Timestamp {unix_seconds} Mismo valor en los 3 intentos de delivery.

Verificar la firma

Lea los bytes crudos del body (no el JSON parseado), concatene con el timestamp como {timestamp}.{body_bytes}, calcule HMAC-SHA256 con su signing key, y compare en tiempo constante con el hex que viene en X-Panadata-Webhook-Signature. Rechace si la diferencia entre el timestamp recibido y el actual supera 5 minutos.

# Python
import hmac, hashlib, time

def verify(body_bytes: bytes, timestamp: str, signature: str, signing_key: str) -> bool:
    if abs(time.time() - int(timestamp)) > 300:
        return False
    payload = f"{timestamp}.".encode() + body_bytes
    expected = "sha256=" + hmac.new(signing_key.encode(), payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)
// Node.js
const crypto = require('crypto');

function verify(bodyBuffer, timestamp, signature, signingKey) {
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const payload = Buffer.concat([Buffer.from(`${timestamp}.`), bodyBuffer]);
  const expected = 'sha256=' + crypto.createHmac('sha256', signingKey).update(payload).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
# Ruby
require "openssl"

def verify(body_bytes, timestamp, signature, signing_key)
  return false if (Time.now.to_i - timestamp.to_i).abs > 300
  payload  = "#{timestamp}.".b + body_bytes
  expected = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", signing_key, payload)
  ActiveSupport::SecurityUtils.secure_compare(expected, signature)
end

Reintentos de entrega

Panadata intenta entregar hasta 3 veces con 5 s de timeout por intento, sin backoff. Los 3 intentos se ejecutan dentro de la misma invocación Lambda — ventana total ≤ ~15 s. Se reintenta ante cualquier excepción de red o respuesta no 2xx.

Después de 3 fallos, se registra webhook_status=failed definitivamente. El Update Request en sí queda status=completed — el scrape ya tuvo éxito — y el resultado se recupera con GET /v4/update_requests/{id}.

Idempotencia: el único identificador estable es update_request_id en el body. El mismo timestamp y la misma firma se envían en los 3 intentos — no los use como identificador de intento.

Estados ortogonales

status webhook_status Significado
pending null En cola; no hay evento terminal aún.
completed null Listo; no hubo intento de webhook (sin URL configurada al crear).
completed delivered Listo; el receiver devolvió 2xx.
completed failed Listo; el receiver falló los 3 intentos. Recupere con GET /v4/update_requests/{id}.
failed null Update Request falló; no se enviará webhook.

Recomendación: combine el webhook con polling de bajo costo a GET /v4/update_requests/{id} para los casos donde el webhook nunca llegue (URL caída durante la ventana de 15 s, firewall, etc.).

Cuándo NO se envía un webhook

Solo el evento update_request.completed produce delivery. Un Update Request en status=failed (entidad no encontrada, créditos insuficientes, excepción del scraper) no dispara webhook.

Para observar fallos, consulte GET /v4/update_requests/{id}: el campo error describe la causa cuando status=failed.

Cobertura por jurisdicción

El flujo de escritura + webhook está cableado solo para Panamá hoy. Ecuador y Colombia ya tienen lectura (GET /v4/{jurisdicción}/entidades) pero no aceptan POST .../update todavía.

Vea Jurisdicciones para la matriz actualizada.

Anterior: ← Sandbox vs API

Siguiente: Errores →