# Carllog — API pública de leitura (v1)

> API REST somente leitura para os dados de carros BYD conectados ao Carllog (carllog.com): carros, estado atual (bateria, autonomia, carga, travas), localização, viagens, recargas e abastecimentos. Cada token lê só os carros da conta de quem o criou.

Este documento é a referência completa, pensado para ser lido por uma IA que vai escrever uma integração (Home Assistant, Node-RED, script, planilha, painel). Versão para pessoas: https://carllog.com/developers/api

## Resumo para quem vai integrar

- Base URL: `https://carllog.com/api/public/v1`
- Só `GET`. Qualquer outro método responde 405. A API não envia comandos ao carro.
- Autenticação: cabeçalho `Authorization: Bearer <token>` em toda chamada. O token começa com `cl_live_` e é criado pelo dono em https://carllog.com/account/api (aparece uma única vez).
- Respostas em JSON UTF-8. Datas em RFC 3339 (UTC na resposta). Unidades no nome do campo: `_km`, `_kmh`, `_percent`/`_soc`, `kwh_`, `_kw`, `liters_`, `_sec`.
- Campos numéricos e de estado podem ser `null` quando o carro não informou o dado; trate sempre.
- Os dados vêm da coleta periódica do Carllog, não do carro em tempo real: mudam a cada poucos minutos e o carro parado dorme. Respostas trazem `Cache-Control: private, max-age=60`. Consulte no máximo uma vez por minuto; a cada 5 minutos é o recomendado.
- Limites: 60 chamadas/minuto por token (picos de 10) e 120/minuto por conta. Acima disso, 429 com `Retry-After` em segundos.
- Sem CORS: chame de um servidor, nunca do navegador. Guarde o token como segredo (variável de ambiente, `secrets.yaml`), nunca no código ou numa página.
- A API exige assinatura ativa (o período de teste conta). Plano vencido responde 402; o token volta a funcionar na renovação.

## Fluxo típico

1. `GET /vehicles` para descobrir o `vin` de cada carro.
2. `GET /vehicles/{vin}/state` para bateria, autonomia, carga e travas.
3. `GET /vehicles/{vin}/location` para a posição (precisa de `location:read`).
4. `GET /vehicles/{vin}/trips`, `/charges` ou `/refuels` para o histórico, paginando por cursor.

```bash
curl https://carllog.com/api/public/v1/vehicles \
  -H "Authorization: Bearer $CARLLOG_TOKEN"
```

## Rotas

| Rota | Permissão | O que devolve |
|---|---|---|
| `GET /vehicles` | `vehicles:read` | Listar carros |
| `GET /vehicles/{vin}/state` | `vehicles:read` | Estado atual |
| `GET /vehicles/{vin}/location` | `location:read` | Localização |
| `GET /vehicles/{vin}/trips` | `history:read` | Viagens |
| `GET /vehicles/{vin}/charges` | `history:read` | Recargas |
| `GET /vehicles/{vin}/refuels` | `history:read` | Abastecimentos |

## Permissões (escopos)

Cada token tem um ou mais escopos, escolhidos na criação. Rota fora dos escopos do token: 403.

- `vehicles:read` (Carros e estado): A lista dos carros e o estado atual de cada um: bateria, autonomia, odômetro, carga e travas. Nunca inclui coordenadas.
- `location:read` (Localização): A última posição de GPS e, junto com o histórico, os pontos de início e fim e o trajeto de cada viagem. Dê só a quem precisa de saber onde o carro está.
- `history:read` (Histórico): Viagens, recargas e abastecimentos. Sem a permissão de localização, os lugares vêm só pelo nome.

Um token pode ser limitado a alguns carros: um carro da conta fora da lista responde 403; um VIN que não é da conta responde 404. `/vehicles` lista só os carros que o token pode ler.

## Paginação

`/trips`, `/charges` e `/refuels` devolvem `{ "items": [...], "next_cursor": ... }`, do mais recente para o mais antigo. Para a próxima página, repita a chamada com `cursor=<next_cursor>`; `next_cursor: null` indica a última. `limit` vai de 1 a 100 (padrão 50). `from`/`to` filtram pelo início do evento, em RFC 3339 com fuso — codifique o `+` de um fuso positivo como `%2B`. Data mal formatada: 400.

## Referência por rota

### Listar carros — `GET /vehicles`

Permissão: `vehicles:read`.

Os carros da sua conta que o token pode ler. Use o vin de cada um nas outras rotas.

Um token restrito a alguns carros só lista esses. Sem restrição, entram também os carros que você conectar depois.

Campos da resposta:

| Campo | Tipo | Descrição |
|---|---|---|
| `items` | lista | Um item por carro, com os campos abaixo. |
| `vin` | string | O chassi (VIN), que identifica o carro nas outras rotas. |
| `name` | string | O apelido que você deu no app da BYD, ou o modelo quando não há apelido. |
| `model` | string \| null | Modelo, como a BYD informa (ex.: DOLPHIN MINI). |
| `brand` | string \| null | Marca. |
| `plate` | string \| null | Placa, em maiúsculas. |
| `last_seen_at` | data-hora | Última vez que a coleta encontrou o carro na sua conta da BYD. |

Exemplo de resposta:

```json
{
  "items": [
    {
      "vin": "LC0EXAMPLE0000001",
      "name": "Dolphin Mini",
      "model": "DOLPHIN MINI",
      "brand": "BYD",
      "plate": "ABC1D23",
      "last_seen_at": "2026-10-26T14:05:12Z"
    }
  ]
}
```

### Estado atual — `GET /vehicles/{vin}/state`

Permissão: `vehicles:read`.

A leitura mais recente do carro: bateria, autonomia, odômetro, combustível nos híbridos, carga e travas. Nunca traz coordenadas — para isso, use /location.

Qualquer campo pode vir null quando o carro não informou aquele dado na última leitura (comum com o carro dormindo).

Os valores de texto podem ganhar novas opções no futuro; trate um valor desconhecido como "desconhecido". Um código que ainda não decodificamos vem como code=N.

Campos da resposta:

| Campo | Tipo | Descrição |
|---|---|---|
| `vin` | string | O chassi pedido. |
| `captured_at` | data-hora \| null | Quando a leitura foi feita. null se o carro ainda não tem nenhuma. |
| `soc_percent` | inteiro \| null | Carga da bateria, em %. |
| `range_km` | inteiro \| null | Autonomia elétrica que o carro estima, em km. |
| `odometer_km` | inteiro \| null | Odômetro, em km. |
| `fuel_percent` | inteiro \| null | Tanque, em % (só híbridos). |
| `fuel_range_km` | inteiro \| null | Autonomia a combustão, em km (só híbridos). |
| `charging` | boolean \| null | true quando charge_state é charging. |
| `charge_state` | string \| null | not_connected (sem cabo), charging (carregando), scheduled (carga agendada), idle (cabo ligado, sem carregar) ou idle_asleep (idem, com o carro dormindo). |
| `connector` | string \| null | plugged_in (cabo conectado), not_connected ou unknown_asleep (o carro dormia e não respondeu). |
| `power_state` | string \| null | on (ligado), off (desligado) ou unknown. |
| `locked` | boolean \| null | true só quando todas as portas com leitura estão trancadas; null quando nenhuma porta informou. |
| `doors_locked` | objeto | Uma trava por porta: front_left, front_right, rear_left e rear_right. Cada uma true (trancada), false (destrancada) ou null (sem leitura). |
| `trunk_locked` | boolean \| null | Porta-malas trancado. |

Exemplo de resposta:

```json
{
  "vin": "LC0EXAMPLE0000001",
  "captured_at": "2026-10-26T14:05:12Z",
  "soc_percent": 78,
  "range_km": 221,
  "odometer_km": 18342,
  "fuel_percent": null,
  "fuel_range_km": null,
  "charging": false,
  "charge_state": "not_connected",
  "connector": "not_connected",
  "power_state": "off",
  "locked": true,
  "doors_locked": {
    "front_left": true,
    "front_right": true,
    "rear_left": true,
    "rear_right": true
  },
  "trunk_locked": true
}
```

### Localização — `GET /vehicles/{vin}/location`

Permissão: `location:read`.

A última posição de GPS que o carro informou. Parado, o carro costuma não responder ao GPS; aí vem a última posição conhecida, com a hora em que ela foi lida.

Campos da resposta:

| Campo | Tipo | Descrição |
|---|---|---|
| `vin` | string | O chassi pedido. |
| `captured_at` | data-hora \| null | Quando essa posição foi lida. |
| `latitude` | número \| null | Latitude, em graus decimais (WGS 84). |
| `longitude` | número \| null | Longitude, em graus decimais (WGS 84). |

Exemplo de resposta:

```json
{
  "vin": "LC0EXAMPLE0000001",
  "captured_at": "2026-10-26T13:58:40Z",
  "latitude": -23.5614,
  "longitude": -46.6559
}
```

### Viagens — `GET /vehicles/{vin}/trips`

Permissão: `history:read`.

As viagens do carro, da mais recente para a mais antiga, as mesmas que você vê no app: viagens removidas não aparecem e viagens juntadas vêm como uma só.

Parâmetros de query (todos opcionais):

- `limit`: Quantos itens por página, de 1 a 100. Padrão: 50.
- `cursor`: O next_cursor da página anterior, para pedir a seguinte. Vazio na primeira chamada.
- `from`: Só itens que começaram nesse instante ou depois. Data e hora RFC 3339, como 2026-10-01T00:00:00-03:00.
- `to`: Só itens que começaram até esse instante, inclusive. Mesmo formato de from.

Campos da resposta:

| Campo | Tipo | Descrição |
|---|---|---|
| `id` | string | Identificador do evento: o início em segundos desde 1970 (Unix), em texto. |
| `start_at` | data-hora | Quando começou. |
| `end_at` | data-hora | Quando terminou, ou a última leitura se ainda está em andamento. |
| `duration_sec` | inteiro | Duração, em segundos. |
| `is_open` | boolean | true enquanto o evento ainda está acontecendo; os números podem mudar até ele fechar. |
| `distance_km` | inteiro \| null | Distância pelo odômetro, em km. |
| `start_soc` | inteiro \| null | Bateria na saída, em %. |
| `end_soc` | inteiro \| null | Bateria na chegada, em %. |
| `kwh_used` | número \| null | Energia gasta na viagem, em kWh, estimada pela bateria consumida e a capacidade do carro. |
| `start_fuel` | inteiro \| null | Tanque na saída, em % (híbridos). |
| `end_fuel` | inteiro \| null | Tanque na chegada, em % (híbridos). |
| `liters_used` | número \| null | Combustível gasto, em litros (híbridos). |
| `max_speed_kmh` | número \| null | Velocidade máxima registrada, em km/h. |
| `avg_speed_kmh` | número \| null | Velocidade média, em km/h. |
| `start_place` | string \| null | Nome do lugar de saída: um lugar seu (Casa, Trabalho) ou o endereço. |
| `end_place` | string \| null | Nome do lugar de chegada. |
| `start` | { latitude, longitude } | Ponto de saída. Omitido quando não houve posição. Só vem quando o token tem `location:read`; sem ela o campo é omitido. |
| `end` | { latitude, longitude } | Ponto de chegada. Omitido quando não houve posição. Só vem quando o token tem `location:read`; sem ela o campo é omitido. |
| `path` | lista de [longitude, latitude] | O trajeto pelos pontos de GPS registrados, na ordem do GeoJSON (longitude primeiro). Não é a rua exata. Omitido quando a viagem não tem pontos. Só vem quando o token tem `location:read`; sem ela o campo é omitido. |
| `next_cursor` | data-hora \| null | Passe em cursor para a próxima página. null na última. |

Exemplo de resposta:

```json
{
  "items": [
    {
      "id": "1761483600",
      "start_at": "2026-10-26T13:00:00Z",
      "end_at": "2026-10-26T13:32:10Z",
      "duration_sec": 1930,
      "is_open": false,
      "distance_km": 18,
      "start_soc": 86,
      "end_soc": 78,
      "kwh_used": 2.4,
      "start_fuel": null,
      "end_fuel": null,
      "liters_used": null,
      "max_speed_kmh": 74,
      "avg_speed_kmh": 33.6,
      "start_place": "Casa",
      "end_place": "Trabalho"
    }
  ],
  "next_cursor": "2026-10-26T13:00:00Z"
}
```

### Recargas — `GET /vehicles/{vin}/charges`

Permissão: `history:read`.

As recargas do carro, da mais recente para a mais antiga.

Parâmetros de query (todos opcionais):

- `limit`: Quantos itens por página, de 1 a 100. Padrão: 50.
- `cursor`: O next_cursor da página anterior, para pedir a seguinte. Vazio na primeira chamada.
- `from`: Só itens que começaram nesse instante ou depois. Data e hora RFC 3339, como 2026-10-01T00:00:00-03:00.
- `to`: Só itens que começaram até esse instante, inclusive. Mesmo formato de from.

Campos da resposta:

| Campo | Tipo | Descrição |
|---|---|---|
| `id` | string | Identificador do evento: o início em segundos desde 1970 (Unix), em texto. |
| `start_at` | data-hora | Quando começou. |
| `end_at` | data-hora | Quando terminou, ou a última leitura se ainda está em andamento. |
| `duration_sec` | inteiro | Duração, em segundos. |
| `is_open` | boolean | true enquanto o evento ainda está acontecendo; os números podem mudar até ele fechar. |
| `start_soc` | inteiro \| null | Bateria no início, em %. |
| `end_soc` | inteiro \| null | Bateria no fim, em %. |
| `kwh_added` | número \| null | Energia que entrou na bateria, em kWh, estimada pela capacidade do carro. |
| `avg_power_kw` | número \| null | Potência média, em kW. |
| `place` | string \| null | O nome do carregador que você cadastrou ou, sem ele, o endereço. |
| `location` | { latitude, longitude } | Onde o carro carregou. Omitido quando não houve posição. Só vem quando o token tem `location:read`; sem ela o campo é omitido. |
| `next_cursor` | data-hora \| null | Passe em cursor para a próxima página. null na última. |

Exemplo de resposta:

```json
{
  "items": [
    {
      "id": "1761436800",
      "start_at": "2026-10-26T00:00:00Z",
      "end_at": "2026-10-26T04:12:00Z",
      "duration_sec": 15120,
      "is_open": false,
      "start_soc": 31,
      "end_soc": 100,
      "kwh_added": 26.5,
      "avg_power_kw": 6.3,
      "place": "Casa"
    }
  ],
  "next_cursor": null
}
```

### Abastecimentos — `GET /vehicles/{vin}/refuels`

Permissão: `history:read`.

Os abastecimentos dos híbridos, do mais recente para o mais antigo. Num carro só elétrico a lista vem vazia.

Parâmetros de query (todos opcionais):

- `limit`: Quantos itens por página, de 1 a 100. Padrão: 50.
- `cursor`: O next_cursor da página anterior, para pedir a seguinte. Vazio na primeira chamada.
- `from`: Só itens que começaram nesse instante ou depois. Data e hora RFC 3339, como 2026-10-01T00:00:00-03:00.
- `to`: Só itens que começaram até esse instante, inclusive. Mesmo formato de from.

Campos da resposta:

| Campo | Tipo | Descrição |
|---|---|---|
| `id` | string | Identificador do evento: o início em segundos desde 1970 (Unix), em texto. |
| `start_at` | data-hora | Quando começou. |
| `end_at` | data-hora | Quando terminou, ou a última leitura se ainda está em andamento. |
| `is_open` | boolean | true enquanto o evento ainda está acontecendo; os números podem mudar até ele fechar. |
| `start_fuel` | inteiro \| null | Tanque antes, em %. |
| `end_fuel` | inteiro \| null | Tanque depois, em %. |
| `liters_added` | número \| null | Litros colocados, estimados pela capacidade do tanque. |
| `place` | string \| null | O nome do lugar (um lugar que você nomeou ou o endereço). |
| `location` | { latitude, longitude } | Onde o carro abasteceu. Omitido quando não houve posição. Só vem quando o token tem `location:read`; sem ela o campo é omitido. |
| `next_cursor` | data-hora \| null | Passe em cursor para a próxima página. null na última. |

Exemplo de resposta:

```json
{
  "items": [
    {
      "id": "1761310800",
      "start_at": "2026-10-24T13:00:00Z",
      "end_at": "2026-10-24T13:06:00Z",
      "is_open": false,
      "start_fuel": 22,
      "end_fuel": 100,
      "liters_added": 37.4,
      "place": "Posto Shell Av. Paulista"
    }
  ],
  "next_cursor": null
}
```

## Erros

Toda resposta de erro é `{ "error": "<mensagem em inglês>" }` com o status HTTP abaixo.

| Status | Significado |
|---|---|
| 400 | Um parâmetro inválido, como uma data fora do formato RFC 3339. A mensagem diz qual. |
| 401 | Token ausente, incorreto, revogado ou vencido. A resposta é sempre a mesma, de propósito; confira o token na tela Conta › API. |
| 402 | A assinatura da conta não está ativa. O token continua valendo e volta a funcionar quando o plano for renovado. |
| 403 | O token não tem a permissão que a rota pede, ou foi criado só para outros carros. |
| 404 | Esse VIN não é de um carro da sua conta (ou a rota não existe). |
| 405 | Método diferente de GET. A API só lê. |
| 429 | Passou do limite de chamadas. Espere os segundos do cabeçalho Retry-After. |
| 503 | A API está temporariamente desligada. |

## Tokens

- Até 5 tokens ativos por conta, validade de 1 a 365 dias.
- Revogado em https://carllog.com/account/api, o token é recusado já na próxima chamada.
- Um token sem uso por 180 dias é revogado automaticamente.
- 401 não diz o motivo (token errado, revogado ou vencido): confira na tela Conta › API.

## Exemplo: Home Assistant

Integração `rest` com sensores a partir de `/state` (basta `vehicles:read`, rótulo "Carros e estado"):

```yaml
# configuration.yaml
rest:
  - resource: https://carllog.com/api/public/v1/vehicles/SEU_VIN/state
    headers:
      Authorization: !secret carllog_authorization
    scan_interval: 300
    sensor:
      - name: "Bateria do carro"
        value_template: "{{ value_json.soc_percent }}"
        unit_of_measurement: "%"
        device_class: battery
      - name: "Autonomia do carro"
        value_template: "{{ value_json.range_km }}"
        unit_of_measurement: "km"
      - name: "Carro carregando"
        value_template: "{{ value_json.charging }}"

# secrets.yaml — a palavra Bearer, um espaço e o token
carllog_authorization: "Bearer cole o token aqui"
```
