# 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](/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](/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](/docs/conceptos/jurisdicciones) para la matriz actualizada.

Anterior: [← Sandbox vs API](/docs/guias/sandbox-vs-api)

Siguiente: [Errores →](/docs/guias/errores)

