<mxfile host="app.diagrams.net" agent="ApiPay Hub docs-portal" type="device" version="24.7.17">
  <diagram id="apipay-entrega-webhooks" name="Entrega de webhooks salientes">
    <mxGraphModel dx="1600" dy="1100" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" math="0" shadow="0">
      <root>
        <mxCell id="0" />
        <mxCell id="1" parent="0" />

        <mxCell id="ll-k" value="Kafka&lt;br&gt;apipay.outbound-webhooks.v1" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=0;collapsible=0;recursiveResize=0;outlineConnect=0;size=55;fillColor=#E8F5E9;strokeColor=#2E7D32;fontColor=#1A1A1A;fontSize=11" vertex="1" parent="1">
          <mxGeometry x="40" y="40" width="200" height="700" as="geometry" />
        </mxCell>
        <mxCell id="ll-c" value="OutboundWebhookConsumer&lt;br&gt;grupo webhook-dispatcher" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=0;collapsible=0;recursiveResize=0;outlineConnect=0;size=55;fillColor=#E3F2FD;strokeColor=#1565C0;fontColor=#1A1A1A;fontSize=11" vertex="1" parent="1">
          <mxGeometry x="280" y="40" width="200" height="700" as="geometry" />
        </mxCell>
        <mxCell id="ll-db" value="PostgreSQL&lt;br&gt;outbound_webhook_deliveries" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=0;collapsible=0;recursiveResize=0;outlineConnect=0;size=55;fillColor=#E8F5E9;strokeColor=#2E7D32;fontColor=#1A1A1A;fontSize=11" vertex="1" parent="1">
          <mxGeometry x="520" y="40" width="200" height="700" as="geometry" />
        </mxCell>
        <mxCell id="ll-rs" value="RetryScheduler&lt;br&gt;+ WebhookSigner" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=0;collapsible=0;recursiveResize=0;outlineConnect=0;size=55;fillColor=#E3F2FD;strokeColor=#1565C0;fontColor=#1A1A1A;fontSize=11" vertex="1" parent="1">
          <mxGeometry x="760" y="40" width="200" height="700" as="geometry" />
        </mxCell>
        <mxCell id="ll-ep" value="webhook_endpoint&lt;br&gt;del comercio" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=0;collapsible=0;recursiveResize=0;outlineConnect=0;size=55;fillColor=#FFF8E1;strokeColor=#F9A825;fontColor=#1A1A1A;fontSize=11" vertex="1" parent="1">
          <mxGeometry x="1000" y="40" width="200" height="700" as="geometry" />
        </mxCell>

        <mxCell id="w01" value="1. envelope: id evt_..., type, created, livemode, data.object" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;verticalAlign=bottom;labelBackgroundColor=#FFFFFF;strokeColor=#455A64;fontColor=#1A1A1A;fontSize=10;endArrow=block;endFill=1;exitX=1;exitY=0.114;exitDx=0;exitDy=0;entryX=0;entryY=0.114;entryDx=0;entryDy=0" edge="1" parent="1" source="ll-k" target="ll-c">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="w02" value="2. hay ya una delivery con event_public_id = evt_ ?" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;verticalAlign=bottom;labelBackgroundColor=#FFFFFF;strokeColor=#455A64;fontColor=#1A1A1A;fontSize=10;endArrow=block;endFill=1;exitX=1;exitY=0.179;exitDx=0;exitDy=0;entryX=0;entryY=0.179;entryDx=0;entryDy=0" edge="1" parent="1" source="ll-c" target="ll-db">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="w03" value="3. no existe: el evento es nuevo" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;verticalAlign=bottom;labelBackgroundColor=#FFFFFF;strokeColor=#455A64;fontColor=#1A1A1A;fontSize=10;dashed=1;endArrow=open;endFill=0;endSize=10;exitX=0;exitY=0.243;exitDx=0;exitDy=0;entryX=1;entryY=0.243;entryDx=0;entryDy=0" edge="1" parent="1" source="ll-db" target="ll-c">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="w04" value="4. INSERT deliveries whd_... PENDING, una por endpoint suscrito" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;verticalAlign=bottom;labelBackgroundColor=#FFFFFF;strokeColor=#455A64;fontColor=#1A1A1A;fontSize=10;endArrow=block;endFill=1;exitX=1;exitY=0.307;exitDx=0;exitDy=0;entryX=0;entryY=0.307;entryDx=0;entryDy=0" edge="1" parent="1" source="ll-c" target="ll-db">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="w05" value="5. ack del offset, en el afterCommit de la transaccion" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;verticalAlign=bottom;labelBackgroundColor=#FFFFFF;strokeColor=#455A64;fontColor=#1A1A1A;fontSize=10;endArrow=block;endFill=1;exitX=0;exitY=0.371;exitDx=0;exitDy=0;entryX=1;entryY=0.371;entryDx=0;entryDy=0" edge="1" parent="1" source="ll-c" target="ll-k">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="w06" value="6. reclama las vencidas: SELECT ... FOR UPDATE SKIP LOCKED, lease 60 s" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;verticalAlign=bottom;labelBackgroundColor=#FFFFFF;strokeColor=#455A64;fontColor=#1A1A1A;fontSize=10;endArrow=block;endFill=1;exitX=0;exitY=0.436;exitDx=0;exitDy=0;entryX=1;entryY=0.436;entryDx=0;entryDy=0" edge="1" parent="1" source="ll-rs" target="ll-db">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="w07" value="7. POST firmado con ApiPay-Signature - timeout 10 s" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;verticalAlign=bottom;labelBackgroundColor=#FFFFFF;strokeColor=#1565C0;fontColor=#0D47A1;fontSize=10;fontStyle=1;endArrow=block;endFill=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0" edge="1" parent="1" source="ll-rs" target="ll-ep">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="w08" value="8. 2xx" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;verticalAlign=bottom;labelBackgroundColor=#FFFFFF;strokeColor=#2E7D32;fontColor=#1B5E20;fontSize=10;dashed=1;endArrow=open;endFill=0;endSize=10;exitX=0;exitY=0.564;exitDx=0;exitDy=0;entryX=1;entryY=0.564;entryDx=0;entryDy=0" edge="1" parent="1" source="ll-ep" target="ll-rs">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="w09" value="9. status DELIVERED - el ciclo termina aqui" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;verticalAlign=bottom;labelBackgroundColor=#FFFFFF;strokeColor=#2E7D32;fontColor=#1B5E20;fontSize=10;endArrow=block;endFill=1;exitX=0;exitY=0.629;exitDx=0;exitDy=0;entryX=1;entryY=0.629;entryDx=0;entryDy=0" edge="1" parent="1" source="ll-rs" target="ll-db">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="w10" value="10. rama alternativa: 5xx, 4xx o timeout" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;verticalAlign=bottom;labelBackgroundColor=#FFFFFF;strokeColor=#C62828;fontColor=#B71C1C;fontSize=10;dashed=1;endArrow=open;endFill=0;endSize=10;exitX=0;exitY=0.693;exitDx=0;exitDy=0;entryX=1;entryY=0.693;entryDx=0;entryDy=0" edge="1" parent="1" source="ll-ep" target="ll-rs">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="w11" value="11. status RETRYING, attempt_count + 1, next_attempt_at = ahora + backoff" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;verticalAlign=bottom;labelBackgroundColor=#FFFFFF;strokeColor=#C62828;fontColor=#B71C1C;fontSize=10;endArrow=block;endFill=1;exitX=0;exitY=0.757;exitDx=0;exitDy=0;entryX=1;entryY=0.757;entryDx=0;entryDy=0" edge="1" parent="1" source="ll-rs" target="ll-db">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="w12" value="12. reintentos a 1m, 5m, 30m, 2h y 12h: el MISMO evt_ y el MISMO cuerpo" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;verticalAlign=bottom;labelBackgroundColor=#FFFFFF;strokeColor=#C62828;fontColor=#B71C1C;fontSize=10;endArrow=block;endFill=1;exitX=1;exitY=0.821;exitDx=0;exitDy=0;entryX=0;entryY=0.821;entryDx=0;entryDy=0" edge="1" parent="1" source="ll-rs" target="ll-ep">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>
        <mxCell id="w13" value="13. cinco intentos agotados: status FAILED con failed_at" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;verticalAlign=bottom;labelBackgroundColor=#FFFFFF;strokeColor=#C62828;fontColor=#B71C1C;fontSize=10;fontStyle=1;endArrow=block;endFill=1;exitX=0;exitY=0.886;exitDx=0;exitDy=0;entryX=1;entryY=0.886;entryDx=0;entryDy=0" edge="1" parent="1" source="ll-rs" target="ll-db">
          <mxGeometry relative="1" as="geometry" />
        </mxCell>

        <mxCell id="nota-firma" value="La firma, paso 7. ApiPay-Signature: t=&amp;lt;unix_seconds&amp;gt;,v1=&amp;lt;hex hmac-sha256&amp;gt;, calculada sobre la concatenacion del timestamp, un punto y el CUERPO CRUDO que viaja en el POST, nunca sobre una re-serializacion del JSON. Durante una rotacion la cabecera lleva DOS elementos v1=, activo primero, y no expone ningun kid: un identificador de clave solo le diria a un atacante que hay rotacion en curso. Tolerancia anti-replay que debe aplicar el receptor: 300 s." style="shape=note;size=16;whiteSpace=wrap;html=1;fillColor=#FFFDE7;strokeColor=#F9A825;fontColor=#1A1A1A;fontSize=10;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
          <mxGeometry x="40" y="790" width="570" height="130" as="geometry" />
        </mxCell>
        <mxCell id="nota-dedup" value="La deduplicacion, pasos 2 y 3. El outbox entrega at-least-once: tras un rebalanceo o un reinicio el mismo evento puede llegar dos veces. El consumidor deduplica contra event_public_id, que es el evt_ del envelope, la misma clave que el comercio debe usar en su extremo. Hay exactamente una fila de outbox por evento de negocio, asi que el id de la fila y el evt_ son equivalentes." style="shape=note;size=16;whiteSpace=wrap;html=1;fillColor=#FFFDE7;strokeColor=#F9A825;fontColor=#1A1A1A;fontSize=10;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
          <mxGeometry x="630" y="790" width="570" height="130" as="geometry" />
        </mxCell>
        <mxCell id="nota-cierre" value="Por que la entrega vive fuera del request path: se asume que los endpoints de los comercios fallan a menudo. Cada delivery es a la vez registro de auditoria y cola de reintentos, y el reclamo con FOR UPDATE SKIP LOCKED permite escalar el dispatcher a N replicas sin entregar dos veces. Una respuesta 2xx corta el ciclo en cualquier intento. Tras los cinco reintentos la delivery pasa a FAILED, queda consultable con su codigo y latencia por intento, y se puede reintentar a mano con POST /admin/v1/webhook-deliveries/{id}/retry, que la devuelve a PENDING. Un endpoint que falla el 100% de las veces durante 7 dias se deshabilita automaticamente." style="shape=note;size=16;whiteSpace=wrap;html=1;fillColor=#E8EAF6;strokeColor=#283593;fontColor=#1A1A1A;fontSize=11;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
          <mxGeometry x="40" y="940" width="1160" height="110" as="geometry" />
        </mxCell>
      </root>
    </mxGraphModel>
  </diagram>
</mxfile>
