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

# Convenciones de la API

> Las reglas que sigue cada página de API de SalesOS — autenticación, formato de error, lotes, resolución de identidad y versionado — para que aprenda un solo contrato en lugar de uno por endpoint.

# Convenciones de la API

Cada API de SalesOS sigue las reglas de esta página. Léala una vez y las páginas de cada endpoint quedan cortas: solo cuentan lo que es específico de ellas.

<Note>
  Esta página es el contrato al que nos sujetamos. Donde un endpoint se aparta de ella, su página lo dice **explícitamente** — una excepción silenciosa es un defecto, y queremos saberlo.
</Note>

***

## URLs base

<Tabs>
  <Tab title="Producción">
    `https://api.play2sell.com`
  </Tab>

  <Tab title="Staging">
    `https://api-staging.play2sell.com`
  </Tab>
</Tabs>

Cada endpoint vive en `/functions/v1/<nombre>`. No existe una superficie REST en `/v1/...` — si encuentra una página que la describa, está desactualizada; avísenos.

***

## Autenticación

Dos esquemas, y son **disjuntos** — un endpoint acepta uno u otro, nunca ambos para el mismo llamador.

### Socio (servidor a servidor)

```
Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE
```

La firma es un HMAC sobre `{api_key_id}.{timestamp}.{raw_body}`. Consulte [Autenticación](/es/api/authentication) para el helper `signedRequest`.

* Los timestamps con más de **300 segundos** se rechazan (protección contra replay)
* Firme los **bytes crudos del cuerpo**, antes de cualquier reserialización
* Cada clave pertenece a una sola empresa; un documento de otra empresa responde `found: false`

### Modo self (sesión federada)

```
Authorization: Bearer <jwt>
```

Se usa cuando su app ya tiene al usuario final autenticado. La respuesta cubre **solo a ese usuario**.

<Warning>
  Enviar una lista `collaborators` junto con un token de usuario se rechaza con `SELF_MODE_NO_COLLABORATORS`. Las consultas en lote requieren una API Key de socio — de lo contrario, cualquier usuario autenticado podría leer datos de otras personas por CPF.
</Warning>

***

## Formatos de error

Las solicitudes fallan en dos capas distintas, y **responden diferente**. Maneje ambas — es el error de integración más común que vemos.

### Capa de autenticación

Antes de que la solicitud llegue a la lógica del endpoint:

```json theme={null}
{ "error": "Missing Authorization header", "code": "header_missing" }
```

`error` es una **cadena**. Códigos comunes: `header_missing`, `header_malformed`, `invalid_key`, `signature_mismatch`, `timestamp_expired`.

### Capa del endpoint

Una vez que la autenticación pasó:

```json theme={null}
{ "error": { "code": "VALIDATION_ERROR", "message": "Human-readable description", "details": [] } }
```

`error` es un **objeto**. `details` señala el ítem problemático por su `index` cuando la solicitud traía una lista.

<Warning>
  **`error` es una cadena en un caso y un objeto en el otro.** El código que asume `error.code` leerá `undefined` en toda falla de autenticación — y el que asume `error` como cadena imprimirá `[object Object]` en las fallas de validación. Ramifique por el tipo:

  ```js theme={null}
  const code = typeof body.error === "string" ? body.code : body.error?.code;
  ```

  Sabemos que no es lo ideal. Está documentado en lugar de oculto porque las dos capas se despliegan por separado, y que un socio lo descubra en producción es peor que leerlo aquí.
</Warning>

### Pagos

La API de Pagos responde en [RFC 7807](https://datatracker.ietf.org/doc/html/rfc7807) `problem+json`:

```json theme={null}
{
  "type": "https://docs.play2sell.com/errors/unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Missing Authorization header",
  "instance": "099d23e0-5d6e-4882-9208-9e8038419c3c",
  "code": "unauthorized"
}
```

Es un servicio separado, con convenciones propias. `instance` es el id de la solicitud — cítelo al abrir un ticket.

***

## Códigos de estado

| Código | Significado                                               | Qué hacer                                                             |
| ------ | --------------------------------------------------------- | --------------------------------------------------------------------- |
| `200`  | Éxito. **Incluye "no encontró nada"**                     | Lea el payload — una lista vacía es una respuesta válida, no un error |
| `400`  | Solicitud malformada o por encima de un límite            | Corríjala; repetirla igual fallará otra vez                           |
| `401`  | Clave ausente, inválida, expirada o firma que no coincide | Revise la clave y el reloj                                            |
| `403`  | Clave válida, sin el alcance                              | Solicite el alcance en la clave                                       |
| `405`  | Método incorrecto                                         | Todo endpoint de socio es `POST`                                      |
| `429`  | Límite de la hora alcanzado                               | Espere `retry_after` segundos                                         |
| `5xx`  | Error nuestro                                             | Reintente con espera progresiva (2s, 4s, 8s)                          |

<Tip>
  **La ausencia no es un error.** Ninguna campaña vigente, nadie en turno, ninguna misión hoy — todo responde `200` con una estructura vacía. Un endpoint que devolviera `404` para "nada hoy" haría que su pantalla dijera que algo se rompió cuando el día apenas empezó.
</Tip>

***

## Lotes e identidad

Los endpoints de socio reciben un arreglo `collaborators` y responden **en el mismo orden**, un ítem por cada ítem enviado.

### Resolución de identidad

La precedencia es `user_id` > `cpf` > `email`. Cuando llega más de uno, gana el primero presente — los demás se ignoran, no se usan como alternativa.

* **CPF** se acepta en cualquier formato; los dígitos se normalizan. Debe tener exactamente 11 dígitos.
* Una persona desconocida responde `"found": false`. Eso **no** es un error, y mantiene el arreglo alineado con su solicitud.
* `"ambiguous": true` aparece cuando coincidió más de una persona — trátelo como no resuelto.

### Límites

Los límites de lote son por acción y se declaran en cada página, porque siguen el payload que cada acción produce. Como regla: una acción de estado permite **500**; una acción que expande una lista por persona permite **100 o menos**.

<Note>
  Los datos compartidos — metadatos de la campaña, turnos de la empresa — salen **una sola vez arriba de la respuesta**, no repetidos por persona. En un lote de 500 personas esa es la diferencia entre una respuesta liviana y una de megabytes.
</Note>

***

## La configuración es de la empresa, nunca de su código

Esta es la regla que más rompe integraciones meses después de haberse desplegado.

Nombres, etiquetas, cantidades, umbrales y monedas se **configuran por empresa** y se devuelven en la respuesta. No son constantes.

| Lo que podría fijar en el código   | Por qué se rompe                                                   |
| ---------------------------------- | ------------------------------------------------------------------ |
| El nombre de la moneda             | Cada empresa pone el suyo; la API devuelve la etiqueta configurada |
| Tres turnos, en horarios fijos     | Cantidad, etiquetas y horarios vienen del horario de la empresa    |
| Un número fijo de misiones diarias | Varía por día y por rol                                            |
| Nombres y colores de nivel         | La escalera es configuración, no un enum                           |

<Warning>
  Renderice lo que traiga la respuesta. Una pantalla que fija una etiqueta funciona hasta el día en que el cliente la renombra — y entonces miente, en silencio, sin ningún error.
</Warning>

***

## Zonas horarias y fechas

Cada "hoy" se calcula en la **zona horaria de la empresa**, nunca en UTC. La respuesta declara cuál usó:

```json theme={null}
{ "timezone": "America/Sao_Paulo", "reference_date": "2026-08-13" }
```

Los timestamps son ISO 8601 en UTC. Conviértalos para mostrar usando el `timezone` de la misma respuesta — no el del dispositivo.

***

## Límites de uso

Configurable por clave, con un valor predeterminado de **1000 solicitudes/hora**. En el `429`, la respuesta trae `retry_after` en segundos. Espere; no insista en bucle.

***

## Versionado

Los contratos se extienden, no se rompen. Podemos **agregar** campos en cualquier momento, así que haga un parseo permisivo e ignore lo que no conozca.

El comportamiento nuevo que cambia un payload existente se despliega **opt-in**, detrás de una bandera en la solicitud (por ejemplo `include_day`). Omitirla mantiene la respuesta que usted ya maneja.

***

## Seguridad

* Las APIs de socio son **solo lectura**, salvo que la página diga lo contrario — nunca canjean, nunca alteran saldos, nunca aceptan términos
* Las solicitudes se firman con HMAC y se registran para auditoría; los documentos y correos **nunca** se escriben en los logs
* Nunca llame a estas APIs desde el lado del cliente: la clave quedaría expuesta

***

## Próximos pasos

<CardGroup cols={2}>
  <Card title="Autenticación" icon="key" href="/es/api/authentication">
    Crear claves y firmar solicitudes
  </Card>

  <Card title="API de Check-in" icon="location-check" href="/es/api/integrations/checkin">
    Presencia en turno y el recorrido de turnos del día
  </Card>

  <Card title="API de Misiones" icon="bullseye-arrow" href="/es/api/integrations/missions">
    Progreso, puntos logrados y puntos disponibles
  </Card>

  <Card title="API de Campañas" icon="gift" href="/es/api/integrations/campaigns">
    Catálogo de premios y saldo disponible
  </Card>
</CardGroup>
