> ## 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.

# Convenções da API

> As regras que toda página de API do SalesOS segue — autenticação, formato de erro, lotes, resolução de identidade e versionamento — para você aprender um contrato só, em vez de um por endpoint.

# Convenções da API

Toda API do SalesOS segue as regras desta página. Leia uma vez e as páginas de cada endpoint ficam curtas: elas contam apenas o que é específico delas.

<Note>
  Esta página é o contrato ao qual nos prendemos. Onde um endpoint se afasta dela, a página dele diz isso **explicitamente** — exceção silenciosa é defeito, e queremos saber.
</Note>

***

## URLs base

<Tabs>
  <Tab title="Produção">
    `https://api.play2sell.com`
  </Tab>

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

Todo endpoint vive em `/functions/v1/<nome>`. Não existe superfície REST em `/v1/...` — se encontrar uma página descrevendo isso, ela está desatualizada; avise a gente.

***

## Autenticação

Dois esquemas, e são **disjuntos** — um endpoint aceita um ou outro, nunca os dois para o mesmo chamador.

### Parceiro (servidor a servidor)

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

A assinatura é um HMAC sobre `{api_key_id}.{timestamp}.{raw_body}`. Veja [Autenticação](/pt/api/authentication) para o helper `signedRequest`.

* Timestamps com mais de **300 segundos** são rejeitados (proteção contra replay)
* Assine os **bytes crus do corpo**, antes de qualquer reserialização
* Cada chave pertence a uma única empresa; um documento de outra empresa responde `found: false`

### Modo self (sessão federada)

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

Usado quando o seu app já tem o usuário final autenticado. A resposta cobre **apenas esse usuário**.

<Warning>
  Enviar uma lista `collaborators` junto com um token de usuário é rejeitado com `SELF_MODE_NO_COLLABORATORS`. Consultas em lote exigem uma API Key de parceiro — do contrário, qualquer usuário autenticado poderia ler dados de outras pessoas por CPF.
</Warning>

***

## Formatos de erro

As requisições falham em duas camadas diferentes, e **elas respondem diferente**. Trate as duas — é o erro de integração mais comum que vemos.

### Camada de autenticação

Antes de a requisição chegar à lógica do endpoint:

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

`error` é uma **string**. Códigos comuns: `header_missing`, `header_malformed`, `invalid_key`, `signature_mismatch`, `timestamp_expired`.

### Camada do endpoint

Depois que a autenticação passou:

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

`error` é um **objeto**. `details` aponta o item problemático pelo `index` quando a requisição trouxe uma lista.

<Warning>
  **`error` é string num caso e objeto no outro.** Código que assume `error.code` vai ler `undefined` em toda falha de autenticação — e código que assume `error` string vai imprimir `[object Object]` nas falhas de validação. Ramifique pelo tipo:

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

  Sabemos que não é o ideal. Está documentado em vez de escondido porque as duas camadas sobem separadas, e um parceiro descobrir isso em produção é pior do que ler aqui.
</Warning>

### Pagamentos

A API de Pagamentos responde em [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"
}
```

É um serviço separado, com convenções próprias. `instance` é o id da requisição — cite-o ao abrir um chamado.

***

## Códigos de status

| Código | Significado                                                | O que fazer                                              |
| ------ | ---------------------------------------------------------- | -------------------------------------------------------- |
| `200`  | Sucesso. **Inclui "não encontrou nada"**                   | Leia o payload — lista vazia é resposta válida, não erro |
| `400`  | Requisição malformada ou acima de um limite                | Corrija; repetir igual vai falhar de novo                |
| `401`  | Chave ausente, inválida, expirada ou assinatura divergente | Confira a chave e o relógio                              |
| `403`  | Chave válida, sem o escopo                                 | Peça o escopo na chave                                   |
| `405`  | Método errado                                              | Todo endpoint de parceiro é `POST`                       |
| `429`  | Limite da hora atingido                                    | Aguarde `retry_after` segundos                           |
| `5xx`  | Erro nosso                                                 | Tente de novo com espera progressiva (2s, 4s, 8s)        |

<Tip>
  **Ausência não é erro.** Nenhuma campanha vigente, ninguém em plantão, nenhuma missão hoje — tudo responde `200` com estrutura vazia. Um endpoint que devolvesse `404` para "nada hoje" faria a sua tela dizer que algo quebrou quando o dia apenas começou.
</Tip>

***

## Lotes e identidade

Endpoints de parceiro recebem um array `collaborators` e respondem **na mesma ordem**, um item por item enviado.

### Resolução de identidade

A precedência é `user_id` > `cpf` > `email`. Quando mais de um vem, o primeiro presente vence — os outros são ignorados, não usados como alternativa.

* **CPF** é aceito em qualquer formato; os dígitos são normalizados. Precisa ter exatamente 11 dígitos.
* Pessoa desconhecida responde `"found": false`. Isso **não** é erro, e mantém o array alinhado com a sua requisição.
* `"ambiguous": true` aparece quando mais de uma pessoa casou — trate como não resolvido.

### Limites

Os limites de lote são por ação e ficam declarados em cada página, porque acompanham o payload que cada ação produz. Como regra: ação de status permite **500**; ação que expande uma lista por pessoa permite **100 ou menos**.

<Note>
  Dado compartilhado — metadados da campanha, turnos da empresa — sai **uma vez no topo da resposta**, não repetido por pessoa. Num lote de 500 pessoas isso é a diferença entre uma resposta enxuta e uma de megabytes.
</Note>

***

## Configuração é da empresa, nunca do seu código

Esta é a regra que mais quebra integração meses depois de ela subir.

Nomes, rótulos, quantidades, limiares e moedas são **configurados por empresa** e devolvidos na resposta. Não são constantes.

| O que você pode fixar no código   | Por que quebra                                            |
| --------------------------------- | --------------------------------------------------------- |
| O nome da moeda                   | Cada empresa dá o seu; a API devolve o rótulo configurado |
| Três turnos, em horários fixos    | Quantidade, rótulos e horários vêm da agenda da empresa   |
| Um número fixo de missões diárias | Varia por dia e por papel                                 |
| Nomes e cores de nível            | A escada é configuração, não um enum                      |

<Warning>
  Renderize o que a resposta trouxer. Uma tela que fixa um rótulo funciona até o dia em que o cliente renomeia — e aí ela mente, em silêncio, sem erro nenhum.
</Warning>

***

## Fusos e datas

Todo "hoje" é calculado no **fuso da empresa**, nunca em UTC. A resposta declara qual usou:

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

Timestamps são ISO 8601 em UTC. Converta para exibição usando o `timezone` da mesma resposta — não o do aparelho.

***

## Limites de uso

Configurável por chave, com padrão de **1000 requisições/hora**. No `429`, a resposta traz `retry_after` em segundos. Espere; não fique tentando.

***

## Versionamento

Contratos são estendidos, não quebrados. Podemos **acrescentar** campos a qualquer momento, então faça um parse permissivo e ignore o que não conhecer.

Comportamento novo que muda um payload existente sobe **opt-in**, atrás de uma flag na requisição (por exemplo `include_day`). Omitir a flag mantém a resposta que você já trata.

***

## Segurança

* As APIs de parceiro são **somente leitura**, salvo quando a página disser o contrário — nunca resgatam, nunca alteram saldo, nunca aceitam termos
* As requisições são assinadas por HMAC e registradas para auditoria; documentos e e-mails **nunca** vão para os logs
* Nunca chame estas APIs pelo lado do cliente: a chave ficaria exposta

***

## Próximos passos

<CardGroup cols={2}>
  <Card title="Autenticação" icon="key" href="/pt/api/authentication">
    Criar chaves e assinar requisições
  </Card>

  <Card title="API de Check-in" icon="location-check" href="/pt/api/integrations/checkin">
    Presença no plantão e a trilha de turnos do dia
  </Card>

  <Card title="API de Missões" icon="bullseye-arrow" href="/pt/api/integrations/missions">
    Progresso, pontos conquistados e pontos disponíveis
  </Card>

  <Card title="API de Campanhas" icon="gift" href="/pt/api/integrations/campaigns">
    Vitrine de prêmios e saldo disponível
  </Card>
</CardGroup>
