> ## Knowledge Base Index
> Fetch the complete knowledge base index at: https://help.goandance.com/sitemap.xml
> Use this file to discover available pages before exploring further.
> Pure-Markdown content can be obtained by appending a '.md' suffix to the content URLs listed in the sitemap (without the trailing slash).

# Integración de webhooks

# Integración de webhooks

Cuando ocurre uno de los eventos a los que está suscrito un webhook activo, go&dance envía una petición HTTP a la URL de destino configurada.

La petición contiene:

- Cabeceras con información sobre el evento y la entrega.
- Un cuerpo (`body`) en formato JSON con los datos del evento.
- Una firma HMAC-SHA256 que permite verificar que la petición ha sido generada por go&dance.

## Cabeceras de la petición

Las cabeceras específicas enviadas por go&dance son:

| Cabecera | Descripción |
| ---- | ---- |
| `X-GoDance-Log-Id` | Identificador único de la acción que ha originado el webhook. Se mantiene entre reintentos y debe utilizarse para evitar procesar una misma acción varias veces. |
| `X-GoDance-Delivery-Id` | Identificador único de un intento de entrega. Cada reintento genera un nuevo identificador. |
| `X-GoDance-Signature` | Firma HMAC-SHA256 utilizada para verificar la autenticidad de la petición. |
| `X-GoDance-Timestamp` | Timestamp utilizado como parte del cálculo de la firma. |
| `X-GoDance-Type` | Tipo de evento que ha generado la notificación. |

Ejemplo:

```http
User-Agent: GoDance/Webhooks
Content-Type: application/json
X-GoDance-Log-Id: 2d0df0a0-63c5-48bc-8484-535a5fb0aad6
X-GoDance-Delivery-Id: 6d498a41-d1ad-4ccc-8abf-1045cc52cbac
X-GoDance-Signature: 8ae2fe919f8412ed9001397356a38fa19c4939235511c7a550041454b628473c
X-GoDance-Timestamp: 1790245818
X-GoDance-Type: PURCHASE.UPDATED
```

### `X-GoDance-Log-Id`

Identifica de forma única la acción que ha generado el webhook.

Una misma acción puede tener varios intentos de entrega. Por ejemplo, si una entrega falla y posteriormente se reintenta, todos esos intentos corresponden al mismo `X-GoDance-Log-Id`.

Tu integración debe utilizar este valor para evitar procesar una misma acción más de una vez.

### `X-GoDance-Delivery-Id`

Identifica un intento concreto de entrega.

Si se vuelve a intentar el envío de un webhook, se generará un nuevo `X-GoDance-Delivery-Id`, aunque el `X-GoDance-Log-Id` seguirá siendo el mismo.

Por tanto:

```text
Acción
X-GoDance-Log-Id: A
    │
    ├── Entrega 1 → X-GoDance-Delivery-Id: B
    │
    ├── Entrega 2 → X-GoDance-Delivery-Id: C
    │
    └── Entrega 3 → X-GoDance-Delivery-Id: D
```

Utiliza `X-GoDance-Log-Id`, y no `X-GoDance-Delivery-Id`, como clave de idempotencia.

### `X-GoDance-Type`

Indica el evento que ha provocado la notificación.

Por ejemplo:

```text
PURCHASE.CREATED
```

o:

```text
PURCHASE.UPDATED
```

El mismo tipo se incluye en la propiedad `type` del cuerpo de la petición.

### `X-GoDance-Timestamp`

Timestamp generado para la entrega.

Este valor forma parte de los datos utilizados para calcular la firma.

### `X-GoDance-Signature`

Firma HMAC-SHA256 generada utilizando el secreto del webhook.

Debes validar esta firma antes de procesar la información recibida.

---

# Validar la firma

Cada webhook dispone de un secreto con un formato similar a:

```text
gd_sec_********************************
```

go&dance utiliza este secreto para generar una firma HMAC-SHA256 para cada entrega.

La firma se calcula sobre la concatenación:

```text
{deliveryId}|{timestamp}|{body}
```

donde:

- `deliveryId` es el valor de `X-GoDance-Delivery-Id`.
- `timestamp` es el valor de `X-GoDance-Timestamp`.
- `body` es **exactamente el cuerpo original recibido en la petición HTTP**.

El cálculo es equivalente a:

```text
HMAC-SHA256(
    deliveryId + "|" + timestamp + "|" + body,
    secret
)
```

El resultado debe coincidir con:

```text
X-GoDance-Signature
```

## Importante: utiliza siempre el body original

Para validar correctamente la firma debes utilizar el **body original de la petición, exactamente como se ha recibido**.

No debes parsear, transformar, modificar ni volver a serializar el JSON antes de calcular la firma.

Por ejemplo, este flujo es incorrecto:

```text
Body recibido
    ↓
JSON.parse(...)
    ↓
JSON.stringify(...)
    ↓
Calcular firma  ❌
```

Aunque el JSON resultante represente exactamente los mismos datos, su representación puede haber cambiado.

Cambios aparentemente irrelevantes como espacios, saltos de línea, escape de caracteres u orden de propiedades pueden producir un contenido diferente y, por tanto, una firma HMAC distinta.

El flujo correcto es:

```text
Body original recibido
       │
       ├──────────────► Calcular y validar firma
       │
       ▼
   JSON.parse(...)
       │
       ▼
 Procesar evento
```

**Primero valida la firma utilizando el body original. Después puedes parsear y procesar el JSON.**

---

## Ejemplo en TypeScript

El siguiente ejemplo utiliza un servidor HTTP de Node.js y conserva el cuerpo original antes de convertirlo a JSON:

```typescript
import crypto from 'node:crypto';

const secret = 'gd_sec_...';

// Obtener las cabeceras
const deliveryId = request.headers['x-godance-delivery-id'];
const timestamp = request.headers['x-godance-timestamp'];
const signature = request.headers['x-godance-signature'];
const logId = request.headers['x-godance-log-id'];

// IMPORTANTE: obtener el body original, sin parsear ni serializar
const rawBody = request.rawBody;

// Calcular la firma esperada
const expectedSignature = crypto
  .createHmac('sha256', secret)
  .update(`${deliveryId}|${timestamp}|${rawBody}`)
  .digest('hex');

// Validar la firma
if (signature !== expectedSignature) {
  return response.status(401).send('Invalid signature');
}

// Evitar procesar dos veces la misma acción
if (await alreadyProcessed(logId)) {
  return response.status(200).send('OK');
}

// Una vez validada la firma, ya podemos parsear el body
const payload = JSON.parse(rawBody);

await processWebhook(payload);
await markAsProcessed(logId);

return response.status(200).send('OK');
```

> Si utilizas Express, NestJS u otro framework que transforme automáticamente el cuerpo de la petición, asegúrate de conservar el **raw body** antes de que el framework lo procese. La firma debe calcularse siempre sobre los bytes originales recibidos.

---

# Validación recomendada

Antes de procesar un webhook:

1. Comprueba que existen las cabeceras necesarias.
2. Calcula y valida `X-GoDance-Signature` utilizando el body original.
3. Comprueba que `X-GoDance-Log-Id` no haya sido procesado anteriormente.
4. Procesa el contenido del webhook.
5. Devuelve un código HTTP `2xx`.

El flujo recomendado es:

```text
Petición recibida
      │
      ▼
Conservar body original
      │
      ▼
Leer cabeceras
      │
      ▼
Calcular HMAC-SHA256
      │
      ▼
¿La firma coincide?
   │           │
   No          Sí
   │           │
   ▼           ▼
Rechazar    Comprobar Log-Id
               │
               ▼
         ¿Ya procesado?
           │       │
          Sí       No
           │       │
           │       ▼
           │   Procesar evento
           │       │
           └───┬───┘
               ▼
            HTTP 2xx
```

---

# Evitar procesamientos duplicados

Los webhooks deben procesarse de forma **idempotente**.

Una misma acción puede generar varios intentos de entrega si, por ejemplo, un intento anterior ha fallado.

Cada intento tendrá un `X-GoDance-Delivery-Id` diferente, pero todos compartirán el mismo:

```text
X-GoDance-Log-Id
```

Por tanto, debes utilizar `X-GoDance-Log-Id` para determinar si una acción ya ha sido procesada.

Una estrategia habitual es:

1. Recibir y validar la firma.
2. Consultar si el `X-GoDance-Log-Id` ya existe en tu sistema.
3. Si ya existe, no volver a ejecutar la operación.
4. Si no existe, procesar el evento y registrar el `X-GoDance-Log-Id`.

> No utilices `X-GoDance-Delivery-Id` como clave de idempotencia. Un reintento de la misma acción tendrá un identificador de entrega diferente.

---

# Respuesta del endpoint

go&dance considera correcta cualquier respuesta HTTP con un código de estado de la familia **`2xx`**.

Por ejemplo:

```text
200 OK
201 Created
202 Accepted
204 No Content
```

Cualquier código `2xx` se considera una entrega satisfactoria.

Las respuestas fuera del rango `2xx` se consideran entregas fallidas y aparecerán como tales en **Registros de webhooks**.

---

# Payload

El cuerpo de la petición se envía en formato JSON:

```http
Content-Type: application/json
```

Su estructura general es:

```json
{
  "type": "PURCHASE.UPDATED",
  "data": {
    ...
  }
}
```

Actualmente están disponibles los siguientes eventos:

| Evento | Descripción |
| ---- | ---- |
| `PURCHASE.CREATED` | Se produce cuando se crea una compra, tanto online como offline. |
| `PURCHASE.UPDATED` | Se produce cuando se actualiza una compra. |

En ambos casos, `data` contiene una compra con la siguiente estructura.

---

# Estructura de una compra

| Campo | Tipo | Obligatorio | Descripción |
| ---- | ---- | ---- | ---- |
| `uuid` | `string` | Sí | Identificador único de la compra. |
| `recordLocator` | `string` | Sí | Localizador de la compra. |
| `event` | `Event` | Sí | Evento asociado a la compra. |
| `buyer` | `Buyer | null` | No | Información del comprador. |
| `type` | `"manual" | "online"` | Sí | Origen de la compra. |
| `amount` | `number` | Sí | Importe de la compra. Incluye todos los cargos extra como gastos de gestión, seguro de cancelación, etc. |
| `promoterCode` | `string | null` | No | Código de promotor asociado, cuando exista. |
| `discountCode` | `string | null` | No | Código de descuento asociado, cuando exista. |
| `products` | `Product[]` | Sí | Productos incluidos en la compra. |
| `createdAt` | `string` | Sí | Fecha de creación de la compra. |

## Event

La propiedad `event` tiene la siguiente estructura:

| Campo | Tipo | Obligatorio | Descripción |
| ---- | ---- | ---- | ---- |
| `uuid` | `string` | Sí | Identificador único del evento. |
| `customId` | `string | null` | No | Identificador personalizado del evento, cuando exista. |
| `name` | `string` | Sí | Nombre del evento. |

## Buyer

Cuando existe, `buyer` contiene:

| Campo | Tipo | Obligatorio |
| ---- | ---- | ---- |
| `name` | `string` | Sí |
| `email` | `string` | Sí |

La propiedad `buyer` puede no estar presente o tener el valor `null`.

## Product

`products` contiene los productos asociados a la compra.

Cada producto tiene la siguiente estructura:

| Campo | Tipo | Obligatorio | Descripción |
| ---- | ---- | ---- | ---- |
| `uuid` | `string` | Sí | Identificador único del producto. |
| `recordLocator` | `string | null` | No | Localizador del producto, cuando exista. |
| `type` | `string` | Sí | Tipo de producto. |
| `ticket` | `Ticket | null` | No | Información de la entrada, cuando corresponda. |
| `price` | `number` | Sí | Precio del producto. Es el precio de venta que como organizador has puesto al producto |
| `platformFee` | `number` | Sí | Comisión de plataforma. Es la comisión que se cobrará al organizador por la venta de esta entrada en el momento de realizar la liquidación. |
| `attendees` | `Attendee[]` | Sí | Asistentes asociados al producto. |
| `onBehalfOfOrganization` | `boolean` | Sí | Indica si el producto corresponde a un producto del organizador o a un producto de go&dance como pueden ser los gastos distribución. |

### Tipos de producto

Entre los tipos de producto actualmente utilizados se encuentran:

```text
ticket
distribution-fee
gateway-fee
cancellation-insurance
```

La integración debe estar preparada para recibir otros valores de `type` en el futuro.

> No asumas que todos los productos son entradas. Dependiendo de `type`, propiedades como `ticket` pueden no existir o ser `null`.

## Ticket

Cuando el producto corresponde a una entrada, `ticket` contiene:

| Campo | Tipo | Obligatorio |
| ---- | ---- | ---- |
| `uuid` | `string` | Sí |
| `customId` | `string | null` | No |
| `name` | `string` | Sí |

## Attendee

Cada elemento de `attendees` tiene la siguiente estructura:

| Campo | Tipo | Obligatorio | Descripción |
| ---- | ---- | ---- | ---- |
| `uuid` | `string` | Sí | Identificador único del asistente. |
| `position` | `number` | Sí | Posición del asistente dentro del producto. |
| `completed` | `boolean` | Sí | Indica si la información del asistente está completa. |
| `values` | `object` | Sí | Valores asociados a los campos de información del asistente. |

El contenido de `values` es dinámico y depende de la información solicitada para la entrada.

Por ejemplo:

```json
{
  "fullName": "Example Attendee",
  "email": "attendee@example.com",
  "gender": "male",
  "role": "leader",
  "residenceCountry": "IT",
  "phone": "+00 000 000 000"
}
```

La integración no debe asumir que estas propiedades estarán siempre presentes ni que serán las únicas disponibles.

---

# Seguridad y buenas prácticas

Para una integración segura y fiable:

- Utiliza HTTPS en la URL de destino.
- Guarda el secreto del webhook de forma segura.
- No expongas el secreto en código cliente o repositorios públicos.
- Valida siempre `X-GoDance-Signature` antes de procesar el payload.
- Calcula la firma exclusivamente sobre el **body original recibido**, sin parsearlo ni volverlo a serializar.
- Utiliza una comparación resistente a ataques de temporización para comprobar la firma.
- Utiliza `X-GoDance-Log-Id` como clave de idempotencia.
- No utilices `X-GoDance-Delivery-Id` para detectar acciones duplicadas.
- No asumas que los tipos de producto se limitan a los valores conocidos actualmente.
- Regenera el secreto si crees que puede haberse visto comprometido.