# API Spec — PG-API
> Comportamento esperado da API, documentado a partir do binário em produção em `stc-db:/opt/prod/pg-api/pg-api` e dos clientes em `stc0:/opt/sentric/beta/scripts/`.
---
## Base URL
Produção: `https://data.timelapseobras.com.br`
---
## Autenticação
Header `X-API-Key` com chave no formato `sk_<prefixo>_<hash_hex>`.
| `X-API-Key` | `sk_cam_prod_app_d35c3...` |
| `Authorization: Bearer <key>` | Alternativa |
Endpoints públicos: `/health` apenas. `/docs` e `/openapi.json` requerem autenticação.
---
## Resposta Padrão
### Sucesso
```json
{
"success": true,
"data": { ... },
"error": null,
"metadata": {
"request_id": "uuid",
"execution_time_ms": 0,
"rows_affected": 1,
"instance_id": "default",
"timestamp": "2026-05-19T23:47:56.474925008Z"
}
}
```
### Erro
```json
{
"success": false,
"data": null,
"error": {
"code": "QUERY_ERROR",
"message": "db error: ERROR: relation \"alerts\" does not exist",
"details": null
},
"metadata": { ... }
}
```
Campos do `error`:
| `code` | string | ✅ | Código do erro (`QUERY_ERROR`, `PERMISSION_DENIED`, `CONNECTION_ERROR`, `RATE_LIMIT_EXCEEDED`) |
| `message` | string | ✅ | Mensagem descritiva |
| `details` | null | ❌ | Sempre `null` na versão atual |
---
## Endpoints
### GET `/health` — Health Check
Público. Sem autenticação.
Resposta:
```json
{
"service": "pg-api",
"status": "healthy",
"version": "0.1.0"
}
```
---
### GET `/v1/status` — Status do Serviço
Autenticação: requerida.
Resposta:
```json
{
"active_connections": 4,
"instances": 1,
"status": "operational"
}
```
---
### GET `/v1/account` — Info da Conta
Autenticação: requerida.
Resposta:
```json
{
"success": true,
"data": {
"active_connections": 279,
"databases": ["camera", "camera_dev"],
"id": "acc_camera_prod_app",
"max_connections": 800,
"name": "camera-prod-app",
"rate_limit": 0,
"role": "developer"
},
"error": null,
"metadata": { ... }
}
```
---
### GET `/v1/account/usage` — Estatísticas de Uso
Autenticação: requerida.
Resposta:
```json
{
"success": true,
"data": {
"active_connections": 0,
"average_query_time_ms": 0.0,
"data_transferred_mb": 0.0,
"errors_today": 0,
"peak_connections": 0,
"period": "2026-05-19",
"queries_this_month": 0,
"queries_today": 0
},
"error": null,
"metadata": { ... }
}
```
---
### GET `/v1/databases` — Listar Databases
Autenticação: requerida.
Resposta (array plano):
```json
{
"success": true,
"data": ["camera", "camera_dev"],
"error": null,
"metadata": { ... }
}
```
---
### POST `/v1/query` — Executar Query
Autenticação: requerida.
Request:
```json
{
"database": "camera",
"query": "SELECT id, camera_id, status FROM alerts LIMIT 3",
"params": [],
"options": {
"timeout_ms": 30000,
"read_only": true,
"as_transaction": false
}
}
```
| `database` | string | ✅ | — | Nome do database |
| `query` | string | ✅ | — | SQL |
| `params` | array | ❌ | `[]` | Parâmetros posicionais (`$1`, `$2`) |
| `options.timeout_ms` | number | ❌ | `30000` | Timeout em ms |
| `options.read_only` | bool | ❌ | `true` | Se false, permite writes |
| `options.as_transaction` | bool | ❌ | `false` | Se true, executa em transação |
Resposta sucesso (SELECT):
```json
{
"success": true,
"data": {
"rows": [
{ "test": 1, "db": "camera" }
],
"fields": [
{ "name": "test", "data_type": "int4", "nullable": true },
{ "name": "db", "data_type": "name", "nullable": true }
]
},
"error": null,
"metadata": {
"request_id": "...",
"execution_time_ms": 0,
"rows_affected": 1,
"instance_id": "default",
"timestamp": "..."
}
}
```
Resposta sucesso (INSERT/UPDATE/DELETE — sem `rows_affected` no metadata):
```json
{
"success": true,
"data": {
"rows": [],
"fields": []
},
"error": null,
"metadata": {
"request_id": "...",
"execution_time_ms": 0,
"instance_id": "default",
"timestamp": "..."
}
}
```
> ⚠️ `rows_affected` está ausente no metadata para queries que falham.
---
### POST `/v1/batch` — Batch de Queries
Autenticação: requerida.
Request: array de objetos `QueryRequest` (mesmo formato do `/v1/query`).
```json
[
{ "query": "SELECT 1 as a", "database": "camera", "params": [], "options": { "read_only": true } },
{ "query": "SELECT 2 as b", "database": "camera", "params": [], "options": { "read_only": true } }
]
```
Resposta:
```json
{
"success": true,
"data": [
{ "rows": [{ "a": 1 }], "fields": [{ "name": "a", "data_type": "int4", "nullable": true }] },
{ "rows": [{ "b": 2 }], "fields": [{ "name": "b", "data_type": "int4", "nullable": true }] }
],
"error": null,
"metadata": {
"rows_affected": 2,
...
}
}
```
---
### POST `/v1/transaction` — Transação
Autenticação: requerida.
Request: **mesmo formato de `/v1/batch`** (array de `QueryRequest`).
Resposta: **mesmo formato de `/v1/batch`**.
---
### GET `/v1/databases/{db}/tables` — Listar Tabelas
Autenticação: requerida.
Resposta:
```json
{
"success": true,
"data": [
{ "name": "active_simcards", "owner": "sentric", "schema": "public", "type": "table" },
{ "name": "alert_config", "owner": "postgres", "schema": "public", "type": "table" }
],
"error": null,
"metadata": { ... }
}
```
---
### GET `/v1/databases/{db}/schema` — Schema do Database
Autenticação: requerida.
Resposta: `{ "success": true, "data": {}, ... }` (TODO — retorna objeto vazio).
---
### POST `/v1/databases` — Criar Database
Autenticação: requerida. Requer role `Owner` ou `Admin`.
Resposta: `{ "success": true, "data": { "created": true }, ... }` (TODO — não cria de fato).
---
### DELETE `/v1/databases/{name}` — Dropar Database
Autenticação: requerida. Requer role `Owner`.
Resposta: `{ "success": true, "data": { "dropped": "<name>" }, ... }` (TODO — não dropa de fato).
---
### GET `/docs` — Swagger UI
Autenticação: requerida (diferente do que diz o openapi.json).
### GET `/openapi.json` — Especificação OpenAPI
Autenticação: requerida.
---
## Tipos de Dados Suportados
| `bool` | `true` / `false` |
| `int2`, `int4` | number |
| `int8` | number |
| `float4`, `float8` | number |
| `numeric`, `decimal` | string (ex: `"2.5"`) |
| `text`, `varchar`, `bpchar` | string |
| `json`, `jsonb` | object |
| `timestamp`, `timestamptz` | string (`"YYYY-MM-DD HH:MM:SS"`) |
| `date` | string (`"YYYY-MM-DD"`) |
| `uuid` | string |
| `null` | `null` |
---
## Códigos de Erro
| `UNAUTHORIZED` | 401 | API key inválida ou ausente |
| `PERMISSION_DENIED` | 403 | Sem permissão para o database/operação |
| `CONNECTION_ERROR` | 500 | Erro de conexão com PostgreSQL |
| `QUERY_ERROR` | 400 | Erro na query SQL |
| `RATE_LIMIT_EXCEEDED` | 429 | Rate limit excedido |
| `CONNECTION_LIMIT_EXCEEDED` | 503 | Máximo de conexões excedido |
---
## Headers de Rate Limit
| `X-RateLimit-Limit` | Limite total |
| `X-RateLimit-Remaining` | Restantes |
| `X-RateLimit-Reset` | Segundos até reset |
---
## Exemplos de Uso (dos clientes Python em stc0)
### Cliente básico
```python
import requests
response = requests.post(
"https://data.timelapseobras.com.br/v1/query",
headers={
"X-API-Key": "sk_...",
"Content-Type": "application/json"
},
json={
"query": "SELECT * FROM cameras WHERE id = $1",
"database": "camera",
"params": [123],
"options": {
"timeout_ms": 30000,
"read_only": True,
"as_transaction": False
}
}
)
result = response.json()
rows = result["data"]["rows"]
fields = result["data"]["fields"]
```
### Compatibilidade psycopg2
```python
from pg_api import executesql
# SELECT
rows = executesql("SELECT * FROM cameras WHERE id = $1", [123])
# INSERT/UPDATE/DELETE
executesql("UPDATE cameras SET status = $1 WHERE id = $2", ['active', 123], commit=True)
```
---
## Configuração de Conta (accounts.json)
```json
{
"accounts": [
{
"id": "acc_camera_prod_app",
"name": "camera-prod-app",
"api_key": "sk_cam_prod_app_<hash>",
"instance_id": "default",
"databases": [
{
"database": "camera",
"username": "camera_user",
"password": "...",
"permissions": ["Select", "Insert", "Update", "Delete"]
}
],
"role": "developer",
"rate_limit": 1000,
"max_connections": 50
}
]
}
```
Roles válidas: `"superuser"`, `"admin"`, `"developer"`, `"application"`, `"readonly"`.