> ## Documentation Index
> Fetch the complete documentation index at: https://developers.mypilotindex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Los eventos grant.revoked y ping, firmados con el esquema Standard Webhooks.

MyPilotIndex informa a un socio sobre sus grants por webhook. Existen dos eventos: `grant.revoked`, cuando un grant termina, y `ping`, cuando pides una prueba.

MyPilotIndex configura tu URL de webhook (solo https) y su secreto de firma cuando te aprueba. El secreto empieza con `whsec_` y se muestra una sola vez.

## Entrega

Cada evento es un `POST` del evento en JSON (`Content-Type: application/json`). Responde con cualquier `2xx` en menos de 10 segundos.

Encabezados, según el esquema Standard Webhooks:

| Encabezado | Contenido |
| - | - |
| `webhook-id` | El id del evento, igual en cada intento. Úsalo para descartar duplicados. |
| `webhook-timestamp` | Segundos Unix enteros del momento en que se firmó este intento. |
| `webhook-signature` | `v1,` seguido del HMAC-SHA256 en base64 de `<webhook-id>.<webhook-timestamp>.<body>`. |

## Verificar la firma

La clave es el contenido en base64 de tu secreto después del prefijo `whsec_`. Calcula el HMAC sobre los bytes exactos que recibiste, no sobre un cuerpo serializado de nuevo, compara en tiempo constante y rechaza un timestamp que difiera más de cinco minutos de tu reloj.

```js theme={null}
const crypto = require('node:crypto')

function verify(secret, id, timestamp, rawBody, signatureHeader) {
  const age = Math.abs(Date.now() / 1000 - Number(timestamp))
  if (!Number.isFinite(age) || age > 300) return false // more than five minutes from our clock
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64')
  const expected = crypto.createHmac('sha256', key)
    .update(`${id}.${timestamp}.${rawBody}`).digest('base64')
  const a = Buffer.from(`v1,${expected}`)
  return signatureHeader.split(' ').some(candidate => {
    const b = Buffer.from(candidate)
    return a.length === b.length && crypto.timingSafeEqual(a, b)
  })
}
```

## grant.revoked

```json theme={null}
{
  "type": "grant.revoked",
  "id": "evt_3b1f6c0e9a2d4f7b8c5e1a2b3c4d5e6f",
  "created_at": "2026-11-05T09:30:00Z",
  "data": {
    "grant_id": "6f1c2a9e-3b7d-4c51-9e2a-0d4b8f6a1c33",
    "pilot_id": "2d9b7c1e-8f4a-4e3b-a6d5-91c0f2e7b844",
    "reason": "pilot"
  }
}
```

`reason` es `pilot` (el piloto te desconectó en Ajustes), `operator` (MyPilotIndex lo revocó) o `account_deleted` (el piloto borró su cuenta, así que el grant ya no aparece en ninguna lista).

Cuando lo recibas, borra lo que guardas de MyPilotIndex sobre ese piloto, como exige el acuerdo de datos. Desde ese momento, cada llamada que indique el grant responde `404`.

## Reintentos

Un `5xx`, un `408`, un `429`, una redirección (nunca se sigue), un timeout o una falla de conexión se reintenta con intervalos crecientes durante cerca de un día, doce intentos en total, con el mismo `webhook-id` y un timestamp y una firma nuevos. Un `4xx` distinto de `408` y `429` se toma como un rechazo y no se reintenta.

La excepción es `grant.revoked` con el motivo `account_deleted`: su grant sale de todas las listas junto con la cuenta, así que no se puede conciliar después. Se reintenta dentro de la misma ventana ante cualquier respuesta que no sea `2xx`, incluido un `4xx`. Confírmalo con un `2xx` en cuanto lo hayas procesado, aunque no guardes nada de ese piloto.

Si te perdiste otros eventos, concilia con `GET /v1/partner/grants?status=revoked&changed_since=<time>`.

## Pruebas

`POST /v1/partner/webhooks/test` pone en cola un `ping` firmado hacia tu URL de webhook y responde `202` antes de la entrega. El evento `ping` tiene un objeto `data` vacío. Si falta la URL de webhook (https) o el secreto, la respuesta es `409` con el motivo `webhook_not_configured`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.