API REST do Star Tracker: guia completo#
Este documento é a referência única da API REST do Star Tracker. A API usa
Django REST Framework, é versionada sob /api/v1/ e autentica clientes com JWT
no formato Bearer.
Rotas iniciadas por /api/ aceitam qualquer hostname HTTP válido, inclusive
10.0.2.2 no emulador Android. ALLOWED_HOSTS restringe somente portal, admin e
WebSockets. A API continua protegida por autenticação, permissões e rate limits.
O CORS é aplicado apenas a /api/ e aceita as origens listadas em
CORS_ALLOWED_ORIGINS. Host e origem são conceitos diferentes: aplicativos
nativos não aplicam CORS; clientes executados em navegador devem cadastrar cada
origem permitida ou habilitar CORS_ALLOW_ALL_ORIGINS conscientemente.
Fluxo geral#
- Obtenha os tokens informando usuário, senha e conta.
- Envie o
accessem todas as requisições protegidas. - Acesse somente recursos autorizados para o perfil naquela conta.
- Renove o
accesscom orefreshquando necessário. - Faça um novo login para trabalhar em outra conta.
flowchart LR
C[Cliente] -->|Login| J[JWT com account_id]
J --> A[API v1]
A --> T[Conta autorizada]
T --> M[Módulo habilitado]
M --> P[Permissão do perfil]
P --> Q[Dados da conta]
1. Autenticação#
Login comum#
identifier aceita username ou e-mail. account_id pode ser omitido somente
quando o usuário pertence a exatamente uma conta.
POST /api/v1/auth/token/
Content-Type: application/json
{
"identifier": "[email protected]",
"password": "senha-segura",
"account_id": 1
}
Resposta 200 OK:
{
"access": "eyJ...",
"refresh": "eyJ...",
"token_type": "Bearer",
"user": {"id": 10, "username": "usuario", "name": "Usuário Exemplo"},
"account": {"id": 1, "name": "Conta Exemplo"}
}
Se o usuário possuir várias contas e não enviar account_id, a resposta
informa que a seleção é obrigatória e apresenta as contas autorizadas.
Login exclusivo do entregador#
Aplicativos de entrega devem usar:
POST /api/v1/auth/entregadores/token/
Content-Type: application/json
{
"identifier": "[email protected]",
"password": "senha-segura",
"account_id": 1
}
O token somente é emitido se o usuário estiver ativo, acessar a conta indicada
e possuir nela o perfil delivery_driver (Entregador). Um perfil de entregador
em outra conta não autoriza o login. Além dos campos do login comum, a resposta
inclui a política vigente de comprovantes:
{
"access": "eyJ...",
"refresh": "eyJ...",
"token_type": "Bearer",
"user": {"id": 10, "username": "entregador", "name": "Entregador"},
"account": {"id": 1, "name": "Conta Exemplo"},
"delivery_config": {
"version": 6,
"require_photo": true,
"minimum_photos": 1,
"maximum_photos": 5,
"require_signature": false,
"require_recipient_name": true,
"require_document": false,
"require_note_on_failure": true,
"capture_timestamp": true,
"pickup_enabled": false,
"pickup_barcode_source": "order_number",
"label_granularity": "volume",
"label_code_format": "barcode",
"label_width_mm": 80,
"label_height_mm": 120,
"availability": {
"can_edit": true,
"driver_id": 7,
"value": "available",
"label": "Disponível",
"options": [
{"value": "available", "label": "Disponível"},
{"value": "busy", "label": "Ocupado"},
{"value": "offline", "label": "Desconectado"}
],
"update_url": "/api/v1/delivery/entregadores/7/disponibilidade/",
"method": "POST"
},
"confirmation_location": {
"enabled": true,
"required": false,
"request_fresh_location": true,
"complete_url_template": "/api/v1/delivery/paradas/{stop_id}/complete/",
"method": "POST",
"latitude_field": "delivery_actual_lat",
"longitude_field": "delivery_actual_lng",
"compare_with_planned_destination": true,
"planned_destination_radius_m": 200,
"compare_with_tracker_when_available": true,
"tracker_radius_m": 200,
"tracker_time_tolerance_minutes": 10
}
}
}
O aplicativo pode atualizar essa política sem refazer o login:
GET /api/v1/auth/entregadores/configuracao/
Authorization: Bearer ACCESS_TOKEN
Resposta 200 OK:
{
"delivery_config": {
"version": 6,
"require_photo": true,
"minimum_photos": 1,
"maximum_photos": 5,
"require_signature": false,
"require_recipient_name": true,
"require_document": false,
"require_note_on_failure": true,
"capture_timestamp": true,
"pickup_enabled": false,
"pickup_barcode_source": "order_number",
"label_granularity": "volume",
"label_code_format": "barcode",
"label_width_mm": 80,
"label_height_mm": 120,
"availability": {
"can_edit": true,
"driver_id": 7,
"value": "available",
"label": "Disponível",
"options": [
{"value": "available", "label": "Disponível"},
{"value": "busy", "label": "Ocupado"},
{"value": "offline", "label": "Desconectado"}
],
"update_url": "/api/v1/delivery/entregadores/7/disponibilidade/",
"method": "POST"
},
"confirmation_location": {
"enabled": true,
"required": false,
"request_fresh_location": true,
"complete_url_template": "/api/v1/delivery/paradas/{stop_id}/complete/",
"method": "POST",
"latitude_field": "delivery_actual_lat",
"longitude_field": "delivery_actual_lng",
"compare_with_planned_destination": true,
"planned_destination_radius_m": 200,
"compare_with_tracker_when_available": true,
"tracker_radius_m": 200,
"tracker_time_tolerance_minutes": 10
}
}
}
Campos da política:
| Campo | Tipo | Uso no aplicativo |
|---|---|---|
version |
inteiro | Versão do contrato da política; atualmente 6 |
require_photo |
booleano | Exige foto para concluir a entrega |
minimum_photos |
inteiro | Quantidade mínima de fotos exigida |
maximum_photos |
inteiro | Quantidade máxima de fotos permitida |
require_signature |
booleano | Exige assinatura capturada no canvas |
require_recipient_name |
booleano | Exige o nome do recebedor |
require_document |
booleano | Exige CPF ou documento do recebedor |
require_note_on_failure |
booleano | Exige observação ao registrar ausência ou falha |
capture_timestamp |
booleano | Data e hora são registradas pelo servidor |
pickup_enabled |
booleano | A carga precisa ser conferida antes de ser roteirizada |
pickup_barcode_source |
texto | O que a etiqueta traz: order_number ou external_id |
label_granularity |
texto | Conferência por volume, item ou order |
label_code_format |
texto | Como o valor aparece na etiqueta: barcode (CODE128) ou qrcode |
label_width_mm |
inteiro | Largura da etiqueta impressa, em milímetros |
label_height_mm |
inteiro | Altura da etiqueta impressa, em milímetros |
availability |
objeto | Estado atual, opções e endpoint para o entregador editar a própria disponibilidade |
confirmation_location |
objeto | Orienta a captura do GPS, a conclusão e as comparações com destino e rastreador |
Esse endpoint usa exclusivamente a conta do JWT, não aceita troca de tenant por
account_id e responde 403 Forbidden se o usuário não possuir o perfil
Entregador nessa conta. capture_timestamp informa que a data e a hora são
registradas pelo servidor.
version sobe sempre que o contrato ganha campo. A 2 acrescentou
pickup_enabled, pickup_barcode_source e as medidas da etiqueta. A 3
acrescentou availability, permitindo ao app montar o seletor e salvar no
update_url sem fixar o ID do entregador ou as opções no código do aplicativo.
A 4 acrescentou label_code_format, que informa se a etiqueta usa CODE128
ou QR Code. A 5 acrescentou label_granularity. A 6 acrescentou
confirmation_location, tornando descobríveis pelo app os campos de localização
e os parâmetros da auditoria com o rastreador.
O fluxo recomendado para o aplicativo está em Configuração do app mobile do entregador.
Respostas de erro:
| Status | Situação |
|---|---|
401 Unauthorized |
Token ausente, inválido, expirado ou sem conta válida |
403 Forbidden |
Usuário autenticado sem perfil Entregador na conta do JWT |
O token do Mapbox não faz parte desse payload. Ele continua sendo consultado
separadamente em GET /api/v1/mapbox/, permitindo sua troca ou revogação sem
exigir um novo login.
Usar o access token#
GET /api/v1/auth/me/
Authorization: Bearer eyJ...
O endpoint retorna o usuário e a conta vinculada ao JWT. Nas demais chamadas,
use o mesmo header Authorization.
O claim account_id existe no access e no refresh. A API revalida se o usuário
continua ativo e vinculado à conta; remover o vínculo invalida o acesso mesmo
antes da expiração. Enviar outro X-Account-ID não troca o tenant.
Obter a configuração Mapbox da conta#
Use o access token para obter o token público efetivo do mapa. A API usa a
conta vinculada ao JWT e ignora qualquer account_id enviado pelo cliente.
GET /api/v1/mapbox/
Authorization: Bearer eyJ...
{
"access_token": "pk.eyJ...",
"token_source": "tenant",
"available": true,
"account": {"id": 1, "name": "Conta Exemplo"}
}
token_source pode ser tenant, environment ou unavailable. No aplicativo,
atribua access_token ao SDK do Mapbox antes de inicializar o mapa.
Renovar ou verificar tokens#
POST /api/v1/auth/token/refresh/
Content-Type: application/json
{"refresh": "eyJ..."}
POST /api/v1/auth/token/verify/
Content-Type: application/json
{"token": "eyJ..."}
Por padrão, o access dura 15 minutos e o refresh, 7 dias. Os valores usam
JWT_ACCESS_MINUTES e JWT_REFRESH_DAYS. A assinatura deriva de SECRET_KEY.
Paginação#
As listagens devolvem 50 registros por página. O cliente pode pedir menos com
?page_size=, até o teto de 100:
GET /api/v1/delivery/pedidos/?page=2&page_size=20
Pedir mais que o teto devolve o teto, não erro. Prefira filtrar (abaixo) a percorrer páginas: é menos trabalho para o servidor e para o cliente.
Filtro por querystring#
As listagens aceitam filtro exato por campo do próprio recurso:
GET /api/v1/delivery/pedidos/?external_id=33260939501895000183650010006317991008773530
GET /api/v1/veiculos/?plate=ABC1D23
GET /api/v1/delivery/pedidos/?status=pending&status=assigned
Regras:
- vale para campos concretos do model, incluindo o alias
<relacao>_id; - o mesmo parâmetro repetido filtra por qualquer um dos valores;
- parâmetro desconhecido é ignorado (
_map_refresh, por exemplo), assim comopage,page_size,format,orderingesearch; - os filtros
cpfecnpjdo cadastro de clientes aceitam valor formatado ou somente dígitos; - valor inválido para um campo numérico devolve lista vazia, não erro;
- o filtro nunca amplia o escopo: a conta do token continua delimitando tudo.
Consultar pelo identificador do ERP com ?external_id= é a forma correta de
saber se uma nota já foi importada. Sem o filtro, a resposta traria a primeira
página inteira de pedidos da conta a cada consulta.
WebSocket mobile autenticado#
O aplicativo mobile usa o mesmo access JWT da API para abrir os canais em
tempo real:
| Canal | Endpoint |
|---|---|
| Frota, pedidos e rotas | /ws/mobile/fleet/ |
| Notificações do usuário | /ws/mobile/notifications/ |
Em produção use wss://; localmente pode ser usado ws://. O token deve ser
enviado no header do handshake e nunca na query string:
GET /ws/mobile/fleet/ HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Authorization: Bearer eyJ...
O servidor aceita somente access token assinado, não expirado, pertencente a um
usuário ativo e com acesso vigente à conta do claim account_id. A conta do
socket sempre vem do JWT; ?account_id= não consegue trocá-la.
Ao conectar em fleet, a primeira mensagem confirma o tenant:
{"type": "connected", "account_id": 1}
O canal pode enviar vehicle_update, vehicle_batch, delivery_order_update,
delivery_order_remove, delivery_route_update, delivery_route_remove e
alert. O canal de notificações envia notification e aceita:
{"action": "mark_read", "id": 15}
{"action": "mark_all_read"}
Token ausente, malformado, expirado ou de tipo refresh fecha a conexão com
4401. Conta removida ou não autorizada fecha com 4403. Quando o access
expirar, renove-o via HTTP e abra uma nova conexão WebSocket.
Notificações pessoais#
As notificações são limitadas simultaneamente pelo account_id do JWT e pelo
usuário autenticado. Outro usuário da mesma conta não aparece na listagem nem
pode ter sua notificação marcada como lida por esse token.
| Método | Endpoint | Operação |
|---|---|---|
GET |
/api/v1/notificacoes/ |
Lista as notificações do usuário |
GET |
/api/v1/notificacoes/{id}/ |
Consulta uma notificação própria |
POST |
/api/v1/notificacoes/{id}/ler/ |
Marca uma notificação própria como lida |
POST |
/api/v1/notificacoes/ler-todas/ |
Marca como lidas somente as notificações do usuário |
{
"id": 15,
"kind": "wave_assigned",
"title": "Uma wave foi atribuída a você",
"message": "A wave #42 está disponível para atendimento.",
"icon": "local_shipping",
"level": "info",
"url": "/app/minhas-entregas/",
"data": {"wave_id": 42},
"is_read": false,
"read_at": null,
"created_at": "2026-08-19T22:10:00-03:00"
}
Tipo (kind) |
Perfis destinatários |
|---|---|
general |
Todos os perfis |
vehicle_alert e geofence |
Administrador, gestor e operador |
maintenance |
Administrador, gestor e operador |
wave_assigned |
Somente o entregador atribuído à wave |
routing_failed |
Administrador, gestor e operador |
delivery_out_of_radius |
Administrador, gestor e operador |
O canal /ws/mobile/notifications/ usa o mesmo isolamento por usuário e conta.
Uma mensagem de outro usuário nunca entra no grupo WebSocket do token atual.
2. Conta, módulo e permissões#
Toda operação combina três controles:
conta do JWT + módulo ativo + permissão do perfil
Por exemplo, alterar um veículo exige tracking.change_vehicle no perfil e um
módulo habilitado que contenha essa permissão. Querysets, detalhes e relações
são limitados à conta do JWT. O cliente não escolhe o proprietário enviando o
campo account; a API o define pela autenticação.
Objetos de outra conta normalmente retornam 404, evitando revelar sua
existência. Para mudar de conta, obtenha outro par de tokens com o account_id
desejado.
3. Padrão dos CRUDs#
Para qualquer recurso registrado, como veiculos:
| Método | Endpoint | Operação |
|---|---|---|
GET |
/api/v1/veiculos/ |
Listar |
POST |
/api/v1/veiculos/ |
Criar |
GET |
/api/v1/veiculos/{id}/ |
Consultar |
PUT |
/api/v1/veiculos/{id}/ |
Substituir |
PATCH |
/api/v1/veiculos/{id}/ |
Alterar parcialmente |
DELETE |
/api/v1/veiculos/{id}/ |
Excluir |
As listas têm 50 itens por página por padrão e aceitam page_size até 100:
{
"count": 250,
"next": "http://servidor/api/v1/veiculos/?page=2",
"previous": null,
"results": []
}
A raiz autenticada GET /api/v1/ apresenta as rotas disponíveis no ambiente.
Os campos de cada payload podem ser consultados com uma requisição OPTIONS
ao endpoint.
notificacoes é a exceção ao CRUD genérico: permite somente leitura e as ações
ler/ler-todas; clientes não podem criar, alterar destinatário ou excluir
notificações.
4. Catálogo de recursos#
Todos os nomes abaixo são relativos a /api/v1/.
| Domínio | Recursos |
|---|---|
| Ativos | ativos, veiculos, celulares, rastreadores |
| Operação | posicoes, eventos, comandos, notificacoes |
| Cercas | cercas, atribuicoes-cerca |
| Manutenção | manutencoes, logs-manutencao, materiais-manutencao, servicos-manutencao |
| Telemetria | logs-telemetria, configuracoes-telemetria, leituras-telemetria |
| Rastreadores | fabricantes-rastreador, fornecedores-rastreador, modelos-rastreador, fornecedores-modelo, servidores-rastreador, templates-comando |
| Acesso | contas, usuarios, perfis-acesso, acessos-conta, modulos, modulos-conta, configuracao-mapa |
Catálogos globais permitem leitura autorizada, mas somente superusuários podem alterá-los. Excluir um usuário remove primeiro o vínculo com a conta atual; o registro global só é apagado se não pertencer a outra conta e não for superusuário. Segredos de mapa são somente escrita.
Dados sensíveis#
tokens-servidor, requisicoes-rejeitadas, logs-seguranca e
nonces-webhook exigem superusuário. ServerToken.token nunca volta na
resposta; somente seu fingerprint é apresentado.
5. Entregas de ponta a ponta#
O módulo de entregas usa /api/v1/delivery/. Seu catálogo inclui
configuracao, configuracoes-veiculo, clientes, entregadores,
vinculos-entregador-veiculo, turnos, areas, armazens,
regras-distribuicao, pedidos, pedidos-wave, waves, otimizacoes,
nao-atribuidos, rotas, paradas, comprovantes, fotos-comprovante,
eventos e tentativas.
Entrada automática de pedidos em cargas#
Cada conta possui um único registro em delivery/configuracao. O campo
waves_enabled controla a entrada automática dos pedidos em cargas:
| Valor | Comportamento |
|---|---|
true |
O pedido elegível entra em waiting_wave e pode ser distribuído automaticamente para uma carga. |
false |
O pedido fica em ready_for_routing, sem vínculo automático com uma carga, e continua disponível para planejamento manual. |
Para desativar a automação, consulte o registro da conta e altere-o pelo ID:
GET /api/v1/delivery/configuracao/
Authorization: Bearer ACCESS_TOKEN
PATCH /api/v1/delivery/configuracao/7/
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{"waves_enabled": false}
A alteração afeta os próximos pedidos que tiverem endereço e coordenadas resolvidos. Ela não remove pedidos de cargas já formadas.
area_proximity_max_distance_km (padrão 5) define até quantos quilômetros um pedido fora
de todos os polígonos pode herdar a área mais próxima. A distância é medida até
a borda real do Polygon/MultiPolygon; 0 desativa essa aproximação. Acima do
limite, o pedido permanece sem área e segue a distribuição geral.
Armazéns#
O CRUD de delivery/armazens usa o padrão descrito na seção 3. Seus campos são:
| Campo | Tipo | Descrição |
|---|---|---|
id |
inteiro (leitura) | Identificador do armazém. |
account |
inteiro (leitura) | Conta definida pelo JWT. |
name |
texto | Nome do armazém. |
cnpj |
texto, opcional | CNPJ do estabelecimento, com ou sem formatação. |
active |
booleano | Indica se o armazém pode ser usado como origem. |
address |
texto | Logradouro. |
address_number |
texto, opcional | Número do endereço. |
address_complement |
texto, opcional | Complemento. |
neighborhood |
texto, opcional | Bairro. |
city |
texto | Cidade. |
state |
texto | Estado. |
postal_code |
texto, opcional | CEP. |
lat |
decimal, opcional | Latitude entre -90 e 90. Se omitida (junto com lon), é calculada pela geocodificação do endereço — que então precisa de logradouro, número, bairro, cidade, UF e CEP; se a geocodificação falhar, a API responde 400 pedindo as coordenadas. |
lon |
decimal, opcional | Longitude entre -180 e 180. Mesma regra de lat. |
created_at, updated_at |
data e hora (leitura) | Auditoria do cadastro. |
Exemplo de criação:
POST /api/v1/delivery/armazens/
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"name": "CD Principal",
"cnpj": "12.345.678/0001-90",
"active": true,
"address": "Avenida Brasil",
"address_number": "1000",
"city": "Rio de Janeiro",
"state": "RJ",
"postal_code": "20000-000",
"lat": -22.9068,
"lon": -43.1729
}
O cnpj é retornado nas operações de listagem e consulta e pode ser alterado
por PUT ou PATCH. O campo account nunca deve ser enviado; listagens,
consultas e alterações permanecem limitadas à conta do JWT.
Clientes#
delivery/clientes é o cadastro de quem recebe: nome, documento, contato e um
endereço principal. Ele segue o CRUD padrão da seção 3
(/api/v1/delivery/clientes/) e existe para dois usos opcionais — o pedido
herdar o endereço do cadastro em vez do endereço que a integração enviou, e o
cadastro nascer sozinho a partir dos pedidos. Os dois são ligados por conta e
estão descritos em Cliente do pedido. Sem eles, o cadastro
é apenas uma agenda consultável.
| Campo | Tipo | Descrição |
|---|---|---|
id |
inteiro (leitura) | Identificador do cliente. |
account |
inteiro (leitura) | Conta definida pelo JWT. |
kind |
texto | person (padrão) ou company. |
name |
texto | Nome ou razão social; obrigatório. |
external_id |
texto, opcional | Código do cliente no ERP/OMS. Único por conta quando preenchido. |
phone |
texto, opcional | Telefone de contato. |
cpf |
texto, opcional | 11 dígitos. Aceita formatado (123.456.789-01) e é gravado só com os dígitos. Único por conta. |
cnpj |
texto, opcional | 14 dígitos, mesma regra de formatação e unicidade. |
address, address_number, address_complement, neighborhood, city, state, postal_code |
texto, opcionais | Endereço principal aplicado aos pedidos. |
city_ibge_code |
texto de 7 dígitos, opcional | Código IBGE do município. Se omitido ou vazio para um município do RJ listado na planilha do projeto, o servidor o preenche automaticamente. |
lat, lon |
decimal, opcionais | Coordenadas do endereço principal. |
geocode_provider |
texto (leitura) | Provedor que resolveu a coordenada. |
address_precision |
texto (leitura) | ROOFTOP, RANGE_INTERPOLATED, GEOMETRIC_CENTER, APPROXIMATE ou NOMINATIM. |
geocode_is_estimated |
booleano (leitura) | Coordenada estimada, e não exata. |
active |
booleano | Cliente inativo não é vinculado a pedido novo. |
created_at, updated_at |
data e hora (leitura) | Auditoria do cadastro. |
Regras de validação, todas respondendo 400:
nameé obrigatório;cpfprecisa ter 11 dígitos ecnpj, 14;- CPF e CNPJ não convivem no mesmo cadastro — informe um dos dois;
personnão aceitacnpjecompanynão aceitacpf;external_id,cpfecnpjsão únicos por conta quando preenchidos.
Exemplo de criação:
POST /api/v1/delivery/clientes/
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"kind": "person",
"name": "Maria da Silva",
"external_id": "ERP-900",
"phone": "(21) 99999-0000",
"cpf": "123.456.789-01",
"address": "Rua do Cadastro",
"address_number": "45",
"neighborhood": "Centro",
"city": "Niterói",
"state": "RJ",
"city_ibge_code": "3303302",
"postal_code": "24000-000"
}
No portal, os campos Estado e Cidade consultam a API pública de Localidades do
IBGE; ao selecionar o município, seu código oficial também é enviado e salvo.
Integrações podem enviar city_ibge_code diretamente. O servidor completa
automaticamente códigos ausentes para municípios do RJ conhecidos localmente.
Resposta 201 Created, abreviada:
{
"id": 4,
"account": 1,
"kind": "person",
"name": "Maria da Silva",
"external_id": "ERP-900",
"cpf": "12345678901",
"cnpj": "",
"lat": null,
"lon": null,
"address_precision": "",
"geocode_is_estimated": false,
"active": true
}
lat, lon e address_precision podem ser enviados na criação, mas também se
preenchem sozinhos: quando um pedido com o mesmo endereço do cadastro é
geocodificado, o resultado é gravado de volta no cliente. Endereço divergente
não sobrescreve o cadastro — só o pedido daquela visita fica com a coordenada
nova.
O campo account nunca deve ser enviado; listagens, consultas e alterações
permanecem limitadas à conta do JWT. Um cliente com pedidos vinculados não pode
ser excluído (o vínculo é PROTECT): use PATCH {"active": false} para tirá-lo
de circulação sem perder o histórico.
Se o perfil do usuário estiver marcado em Config. entrega como "Perfis que
precisam de aprovação para editar endereço", um PATCH/PUT que altere o
endereço precisa enviar address_change_reason. O endereço do cadastro não
muda na hora: a alteração vira uma correção pendente que o administrador aprova
no portal. address_corrected_at (somente leitura) informa quando o endereço
foi corrigido ou aprovado pela última vez.
PATCH /api/v1/delivery/clientes/4/
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{"address": "Rua Nova", "address_change_reason": "Cliente mudou de endereço."}
No portal, o cadastro fica em Clientes (clientes-entrega), com busca por
nome, código externo, documento, telefone, endereço, cidade e CEP.
Consultar cliente por CPF ou CNPJ#
CPF e CNPJ podem ser enviados com ou sem pontuação na querystring. A API normaliza o valor antes de filtrar e devolve o cadastro completo, incluindo endereço e coordenadas. O escopo continua limitado à conta do JWT:
GET /api/v1/delivery/clientes/?cpf=123.456.789-01
Authorization: Bearer ACCESS_TOKEN
Para pessoa jurídica, use o filtro cnpj:
GET /api/v1/delivery/clientes/?cnpj=12.345.678/0001-90
Authorization: Bearer ACCESS_TOKEN
Resposta 200 OK, abreviada:
{
"count": 1,
"results": [
{
"id": 4,
"name": "Maria da Silva",
"cpf": "12345678901",
"phone": "(21) 99999-0000",
"address": "Rua do Cadastro",
"address_number": "45",
"address_complement": "Casa",
"neighborhood": "Centro",
"city": "Niterói",
"state": "RJ",
"postal_code": "24000-000",
"lat": -22.9,
"lon": -43.2,
"active": true
}
]
}
Documento inexistente retorna count: 0; não é erro. Para localizar pelo
documento e criar um pedido na mesma operação, envie customer_cpf ou
customer_cnpj no corpo do pedido, como no exemplo da próxima seção.
Listar todos os documentos sem paginação#
Para sincronizações que precisam somente dos documentos cadastrados, use a
ação documentos. Ela retorna uma única resposta JSON, sem count, next,
previous, results ou limite de página:
GET /api/v1/delivery/clientes/documentos/
Authorization: Bearer ACCESS_TOKEN
{
"CNPJ": ["12345678000190", "98765432000110"],
"CPF": ["12345678901", "98765432100"]
}
Os documentos são devolvidos somente com dígitos, como estão armazenados. Clientes sem CPF ou CNPJ não entram nas listas. A consulta exige permissão de visualização de clientes e inclui exclusivamente registros da conta vinculada ao JWT, sem aceitar troca de conta por parâmetro.
A ação aceita os mesmos filtros exatos por querystring da listagem de clientes, para qualquer campo concreto do cadastro. Filtros diferentes são combinados; um parâmetro repetido aceita qualquer um dos valores. Por exemplo, para listar os documentos de clientes inativos de uma cidade e data de criação específicas:
GET /api/v1/delivery/clientes/documentos/?active=false&city=Campinas&created_at=2026-09-28T10%3A05%3A00-03%3A00
cpf e cnpj também aceitam valores formatados ou somente dígitos. Os filtros
são por igualdade exata; campos desconhecidos são ignorados. O filtro nunca
altera o escopo da conta do JWT.
Cliente do pedido#
Por padrão o pedido é autossuficiente: o endereço que a integração enviou é o
endereço da entrega e nenhum cadastro é consultado. Duas opções da conta mudam
isso — ambas começam desligadas e vivem em AccountMapConfig:
| Opção no portal | Campo da API | Efeito |
|---|---|---|
| Usar o endereço do cadastro do cliente | use_customer_address_for_orders |
O pedido localiza o cadastro e passa a usar o endereço dele, descartando o endereço recebido |
| Criar cliente quando não existir | create_customers_from_orders |
Pedido cujo cliente não está cadastrado cria o cadastro com os dados recebidos e já o vincula |
As duas ficam na página Configurações do portal, no bloco de clientes, e também podem ser alteradas pela API do mesmo registro usado para o token do mapa e a roteirização (veja Configuração do tenant):
GET /api/v1/configuracao-mapa/
Authorization: Bearer ACCESS_TOKEN
PATCH /api/v1/configuracao-mapa/5/
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"use_customer_address_for_orders": true,
"create_customers_from_orders": true
}
Uma opção não depende da outra: só a primeira faz o pedido puxar o endereço de quem já está cadastrado; só a segunda alimenta o cadastro sem mexer no endereço do pedido; as duas juntas fazem o primeiro pedido de cada cliente criar o cadastro e os seguintes herdarem o endereço dele.
Como o pedido aponta para o cliente#
O pedido carrega quatro campos de referência, todos opcionais:
| Campo | Uso |
|---|---|
customer |
ID do cadastro. Vínculo explícito, dispensa busca |
customer_external_id |
Código do cliente no ERP; busca pelo external_id do cadastro |
customer_cpf |
Busca pelo CPF; comparado só pelos dígitos, então 123.456.789-01 acha 12345678901 |
customer_cnpj |
Busca pelo CNPJ, mesma regra |
customer_name e customer_phone continuam sendo texto livre do pedido: eles
não localizam ninguém, mas alimentam o cadastro quando ele é criado.
A resolução acontece no clean()/save() do pedido, então vale igual para
POST /api/v1/delivery/pedidos/, POST /api/v1/delivery/pedidos/import/,
portal e simuladores. A ordem é:
- com
customerenviado, esse é o cadastro — precisa ser da conta do JWT e estaractive, senão a resposta é400; - sem
customer, procura um cadastro cujocpf,cnpjouexternal_idbata com o que veio no pedido (qualquer um dos três já serve); - se os campos enviados apontarem para clientes diferentes, a resposta é
400("CPF/CNPJ e código externo apontam para clientes diferentes."); - se o cadastro encontrado estiver inativo, a resposta é
400; - se nada for encontrado e Criar cliente quando não existir estiver ligada, um cadastro novo é criado e vinculado;
- se nada for encontrado e a criação estiver desligada, o pedido segue sem vínculo, com o endereço que veio nele.
Enviar customer funciona mesmo com as duas opções desligadas — é o vínculo
manual. O que ele não faz nesse caso é trocar o endereço: sem
use_customer_address_for_orders, o endereço do pedido continua sendo o
enviado.
O que muda no pedido depois do vínculo#
customerpassa a apontar para o cadastro;customer_data(somente leitura) traz o cadastro vinculado completo, inclusive seu endereço; permanecenullquando o pedido não está vinculado;customer_external_id,customer_cpfecustomer_cnpjsão normalizados com os valores do cadastro (documento só com dígitos);customer_nameecustomer_phonevazios recebem os dados do cadastro; valores enviados no pedido são preservados — é comum a nota trazer o nome de quem vai receber naquele dia;- com
use_customer_address_for_ordersligada, os sete campos de endereço passam a ser os do cadastro, inclusive quando um deles está vazio lá: o endereço cadastrado substitui o recebido por inteiro, não campo a campo; lat,lone a precisão só são copiados quando o cadastro tem as duas coordenadas. Sem elas, o pedido segue para a geocodificação normal — e o resultado volta para o cadastro.
Com o endereço vindo do cadastro, o pedido pode ser criado só com a referência do cliente:
POST /api/v1/delivery/pedidos/
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"order_number": "PED-2026-050",
"customer_cpf": "123.456.789-01"
}
Resposta 201 Created, abreviada:
{
"id": 91,
"order_number": "PED-2026-050",
"customer": 4,
"customer_data": {
"id": 4,
"name": "Maria da Silva",
"cpf": "12345678901",
"phone": "(21) 99999-0000",
"address": "Rua do Cadastro",
"address_number": "45",
"neighborhood": "Centro",
"city": "Niterói",
"state": "RJ",
"postal_code": "24000-000",
"lat": -22.9,
"lon": -43.2,
"active": true
},
"customer_cpf": "12345678901",
"customer_name": "Maria da Silva",
"address": "Rua do Cadastro",
"address_number": "45",
"neighborhood": "Centro",
"city": "Niterói",
"state": "RJ",
"postal_code": "24000-000"
}
O endereço do cadastro entra antes da validação de endereço obrigatório,
então address, city e state podem ser omitidos quando o cadastro os tem.
Se o cadastro não for encontrado, essa mesma requisição volta a cair na regra
geral: endereço obrigatório, salvo a política Pedido sem endereço de entrega
da conta.
Cadastro criado a partir do pedido#
Com Criar cliente quando não existir ligada, o cadastro nasce com o que o
pedido trouxe: kind é company quando há CNPJ e person caso contrário,
name recebe customer_name (ou Cliente sem nome, se vier vazio), phone
recebe customer_phone, e o endereço, as coordenadas e a precisão são copiados
do pedido.
O pedido precisa trazer pelo menos um identificador — customer_external_id,
customer_cpf ou customer_cnpj. Um pedido com apenas customer_name não gera
cadastro: sem documento nem código externo não haveria como reencontrá-lo no
próximo pedido. Nesse caso o pedido é criado normalmente, só que sem customer
— não há erro. É o comportamento esperado para a NF-e de consumidor não
identificado.
Como a criação usa os mesmos campos únicos, dois pedidos do mesmo CPF criam um único cadastro: o segundo encontra o cliente do primeiro e apenas se vincula.
Turno (WorkSchedule)#
turnos é o horário reutilizável — um "Comercial 08:00–17:00" atende vários
entregadores ao mesmo tempo (com almoço opcional) e só decide quem pode
receber carga agora. Não existe mais um recurso separado para "a operação
em curso": veículo, posição inicial e capacidade do entregador vêm direto de
DriverProfile.default_vehicle e de configuracoes-veiculo — não há um
registro manual de jornada para criar ou manter.
Campos de turnos:
| Campo | Tipo | Descrição |
|---|---|---|
name |
texto | Nome do turno; único por conta |
active |
booleano | Turno desativado vale como turno nenhum |
start_time |
hora HH:MM |
Início do expediente |
lunch_start |
hora ou null |
Início do almoço; opcional, mas exige lunch_end junto |
lunch_end |
hora ou null |
Fim do almoço |
end_time |
hora HH:MM |
Fim do expediente |
end_time menor que start_time descreve um turno noturno que vira a
meia-noite (ex.: 22:00–06:00). O almoço precisa cair dentro do expediente.
Um entregador só enxerga o turno ao qual está vinculado; perfis
administrativos listam todos os turnos da conta. No portal, a tela do turno
tem um multi-select de entregadores: marcar/desmarcar ali atribui ou libera
o work_schedule de vários entregadores de uma vez, em vez de abrir o
cadastro de cada um. A API continua atribuindo turno pelo campo
work_schedule do próprio entregadores (veja abaixo) — o multi-select é
só um atalho do portal para editar o mesmo campo em lote.
Turno e disponibilidade do entregador#
O recurso entregadores possui dois campos graváveis e quatro de leitura:
| Campo | Tipo | Descrição |
|---|---|---|
work_schedule |
inteiro ou null |
Turno vinculado; null significa elegível a qualquer hora |
availability |
texto | available, busy ou offline |
schedule |
objeto ou null (leitura) |
O turno vinculado já expandido, para o app não precisar de uma segunda chamada |
availability_label |
texto (leitura) | Rótulo pronto para exibir: Disponível, Ocupado ou Desconectado |
is_on_duty |
booleano (leitura) | Se está em horário de trabalho agora |
can_edit_availability |
booleano (leitura) | Se o usuário autenticado pode alterar a disponibilidade desse entregador |
is_on_duty não é o mesmo que estar livre: quem está numa rota continua em
horário, só que busy. Sem turno vinculado ele é sempre true.
{
"id": 7,
"user": 10,
"availability": "available",
"availability_label": "Disponível",
"can_edit_availability": true,
"is_on_duty": true,
"work_schedule": 3,
"schedule": {
"id": 3,
"name": "Comercial",
"active": true,
"start_time": "08:00:00",
"lunch_start": "12:00:00",
"lunch_end": "13:00:00",
"end_time": "17:00:00"
}
}
availability muda sozinho nas pontas da operação: iniciar uma rota marca o
entregador como busy, e concluí-la devolve para available. Quem foi posto
em offline manualmente não é reativado pelo fim de uma rota.
available/busy são só o rótulo que a operação vê no mapa — turno e rota
ativa continuam sendo quem decide elegibilidade para a alocação automática de
carga. offline é a exceção: um entregador marcado assim nunca recebe
pedido pela alocação automática (allocate_orders), mesmo dentro do turno.
Atribuição manual de wave continua permitida.
Alterar só a disponibilidade#
Para não reenviar o cadastro inteiro só para dizer "estou de volta":
POST /api/v1/delivery/entregadores/7/disponibilidade/
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{"availability": "offline"}
A resposta 200 OK traz o entregador já atualizado. Um valor fora de
available/busy/offline responde 400.
No app do entregador, não construa essa URL manualmente: use
delivery_config.availability.update_url, envie uma das opções publicadas em
delivery_config.availability.options e atualize o seletor com o objeto
retornado. A configuração é consultável novamente após o login e sempre traz o
estado atual do próprio entregador.
Quem pode chamar:
| Quem | Pode alterar |
|---|---|
| Entregador | Apenas a própria disponibilidade — o escopo do perfil já esconde os colegas, então o id de outro entregador responde 404 |
Perfis com delivery.change_driverprofile |
A de qualquer entregador da conta |
Para interfaces administrativas, use can_edit_availability em cada objeto da
listagem. Um perfil que possua apenas delivery.view_driverprofile enxerga o
entregador, mas recebe can_edit_availability: false e uma tentativa de alteração
responde 403 Forbidden.
Regra de horário na distribuição de carga#
Antes de incluir um entregador numa carga, o servidor avalia o turno dele:
| Situação | Recebe carga? |
|---|---|
| Sem turno vinculado (ou turno inativo) | Sim — regra 24/7 |
| Dentro do expediente | Sim |
| Dentro do intervalo de almoço | Não |
| Fora do expediente | Não |
availability = offline |
Não, independente do turno |
A verificação acontece no backend, em available_drivers(), que é o ponto por
onde passa toda distribuição automática — não é validação só de tela. Uma wave
com entregador fixado manualmente pelo operador continua honrando essa escolha.
Itens e volumes não possuem endpoints CRUD independentes. Eles são
sub-recursos aninhados no payload de pedidos e sempre respeitam a conta do
pedido.
Prioridade de entregador por área (DeliveryAreaDriver)#
Cada DeliveryArea define driver_mode: all aceita qualquer entregador
elegível, selected usa somente a lista ordenada e none não cria carga para
os pedidos da área. DeliveryAreaDriver guarda area, driver e priority
(inteiro positivo, 1 é o mais prioritário). Quando a alocação automática
resolve um pedido daquela área, tenta o entregador de prioridade 1; se ele
estiver ocupado, sem vaga ou offline, cai pro 2, depois o 3, e assim por
diante. Uma lista selecionada vazia equivale a ninguém, nunca a todos. Se não
houver entregador elegível, o pedido permanece waiting_wave naquela mesma
área e nenhuma carga sem entregador é criada.
Os campos opcionais minimum_orders e maximum_orders da área sobrescrevem os
limites gerais de DeliveryConfig. minimum_wait_minutes libera um lote abaixo
do mínimo depois da espera do pedido mais antigo; vazio deixa essa liberação
desativada.
Quando áreas se sobrepõem, o pedido pertence à área ativa com maior priority.
Fora de todos os polígonos, a área mais próxima só é usada dentro do limite da
configuração geral. Uma área distante é ignorada. Dentro da mesma prioridade de
pedido, os destinos mais próximos do veículo prioritário são carregados antes.
Uma DeliveryWave trava na área do primeiro pedido que recebeu
(DeliveryWave.delivery_area): enquanto ela estiver aberta (collecting/
ready), só aceita pedido novo da mesma área, mesmo que o entregador dela não
tenha restrição. O entregador não recebe uma segunda carga aberta: pedidos de
outra área tentam o próximo entregador elegível ou permanecem aguardando.
O CRUD de DeliveryAreaDriver segue o padrão genérico
(/api/v1/delivery/area-entregadores/ — ver Padrão dos CRUDs).
No portal, a atribuição acontece na tela de editar área
(/app/entregas/areas/{id}/editar/), com um seletor que já monta a lista
ordenada.
Pedidos#
O recurso delivery/pedidos possui o CRUD completo e sempre usa a conta do
JWT. Estes são os endpoints principais:
| Método | Endpoint | Operação |
|---|---|---|
GET |
/api/v1/delivery/pedidos/ |
Listar e filtrar pedidos da conta |
GET |
/api/v1/delivery/pedidos/{id}/ |
Consultar um pedido completo |
POST |
/api/v1/delivery/pedidos/ |
Criar um pedido |
PATCH |
/api/v1/delivery/pedidos/{id}/ |
Alterar somente os campos enviados |
PUT |
/api/v1/delivery/pedidos/{id}/ |
Substituir os dados graváveis do pedido |
DELETE |
/api/v1/delivery/pedidos/{id}/ |
Excluir o pedido quando seus relacionamentos permitirem |
O campo account não deve ser enviado: criação, consulta e atualização ficam
limitadas à conta vinculada ao token. Um ID de outra conta retorna 404.
Consultar pedidos (GET)#
A listagem é paginada e aceita os filtros por querystring.
Para localizar uma nota pelo identificador do ERP sem percorrer todas as
páginas, filtre por external_id:
GET /api/v1/delivery/pedidos/?external_id=987654
Authorization: Bearer ACCESS_TOKEN
Para consultar todos os campos, itens, volumes e o cliente expandido de um pedido conhecido, use seu ID:
GET /api/v1/delivery/pedidos/81/
Authorization: Bearer ACCESS_TOKEN
A resposta 200 OK contém o mesmo formato usado na criação e atualização,
incluindo items, os totais calculados e customer_data:
{
"id": 81,
"account": 1,
"external_source": "erp",
"external_id": "987654",
"order_number": "PED-2026-001",
"customer": 4,
"customer_data": {
"id": 4,
"name": "Maria da Silva",
"cpf": "12345678901",
"cnpj": "",
"address": "Rua do Cadastro",
"city": "Niterói",
"state": "RJ"
},
"warehouse": 1,
"address": "Rua do Cadastro",
"address_number": "45",
"city": "Niterói",
"state": "RJ",
"postal_code": "24000-000",
"lat": -22.9,
"lon": -43.2,
"status": "waiting_wave",
"volume": 24000,
"weight": 2500,
"units": 1,
"pending_volume": 24000,
"pending_weight": 2500,
"pending_units": 1,
"items": [],
"created_at": "2026-09-28T10:00:00-03:00",
"updated_at": "2026-09-28T10:05:00-03:00"
}
O exemplo está abreviado; a resposta real também contém os demais campos do
modelo. customer_data é null quando não há cadastro de cliente vinculado.
Pedido inexistente ou de outra conta retorna 404.
Modelo de pedido, item e volume#
| Modelo | Responsabilidade | Relação |
|---|---|---|
DeliveryOrder |
Pedido, endereço, prioridade, janela, coordenadas e totais da carga | Pertence à conta e pode possuir vários itens |
DeliveryOrderItem |
Produto ou conjunto lógico, como “Guarda-roupa” | Pertence a um pedido; exclusão do pedido remove seus itens |
DeliveryOrderItemVolume |
Pacote físico com dimensões e peso próprios | Pertence a um item; cada volume representa uma unidade física |
Cada item carrega seu próprio resultado de entrega, resolvido na conclusão da parada (veja Entrega parcial por item):
| Campo | Tipo | Descrição |
|---|---|---|
status |
texto (somente leitura) | pending, delivered ou failed |
failure_reason |
texto (somente leitura) | Motivo quando status é failed |
failure_notes |
texto (somente leitura) | Observação registrada junto do motivo |
Esses três campos são calculados pelo servidor a partir de POST
paradas/{id}/complete/ ou POST paradas/{id}/fail/ e não podem ser enviados
na criação ou atualização do pedido.
Os principais campos de DeliveryOrder são:
| Grupo | Campos |
|---|---|
| Identificação | id, order_number, external_source, external_id |
| Cliente (opcional) | customer, customer_external_id, customer_cpf, customer_cnpj, customer_name, customer_phone, notes (observação interna do pedido; não é impressa na etiqueta). customer_data é somente leitura e expande o cadastro vinculado com seus dados e endereço. Os campos graváveis podem ficar vazios — só o endereço de entrega é obrigatório. Os quatro primeiros ligam o pedido a um cadastro de cliente |
| Origem | warehouse |
| Endereço | address, address_number, address_complement, neighborhood, city, state, postal_code. address, city e state são obrigatórios, salvo quando os três vêm vazios e a conta configurou Pedido sem endereço de entrega (usar o endereço do armazém informado em warehouse, ou criar sem endereço) |
| Coordenadas | lat, lon |
| Carga agregada | volume em cm³, weight em gramas e units |
| Carga pendente (leitura) | pending_volume, pending_weight, pending_units |
| Planejamento | priority, effective_priority, skills, service_duration_seconds, delivery_window_start, delivery_window_end, ready_at, promised_at |
| Pagamento (opcional) | payment_method, payment_value |
| Operação | status, delivery_area, unassigned_reason, proof_overrides |
| Desmembro (leitura) | split_from — id do pedido original, quando esta nota nasceu de um desmembro (ver Reentrega do que ficou pendente) |
| Auditoria | created_at, updated_at |
account é definido pelo JWT e é somente leitura. Relacionamentos como
warehouse e delivery_area precisam pertencer à mesma conta. Quando
preenchido, external_id é único dentro da conta, independentemente de
external_source; uma tentativa de criar outro pedido com o mesmo código
responde 400. Contas diferentes podem reutilizar o mesmo código e o valor
vazio continua permitido.
priority vai de -2 a 1 (inteiro; 1 é o mais prioritário, -2 o
menos). Além de influenciar a alocação em waves, ele também vira regra rígida
na sequência das paradas dentro de uma rota já otimizada: pedidos com
prioridade maior sempre vêm antes dos de prioridade menor, mesmo que o
otimizador (VROOM) preferisse outra ordem por distância — dentro de cada
grupo de mesma prioridade, a ordem calculada pelo otimizador é preservada.
effective_priority (0-100, somente leitura de fato — recalculado pelo
servidor) é só o sinal usado internamente para desempatar rotas dentro de um
mesmo grupo de prioridade, não precisa ser enviado.
⚠️ Breaking change: antes,
priorityia de0a100. Integrações OMS que enviem um valor fora de-2..1recebem400na criação/importação do pedido. Pedidos já existentes com valor fora da faixa foram reamassados (> 0virou1) numa migration; não há mais valor negativo herdado do período anterior a esta mudança.
payment_method aceita cash, card, pix, to_arrange ou vazio (não
informado). payment_value é um decimal opcional (até 2 casas). Nenhum dos
dois participa da roteirização — é só informação exibida no pedido.
Quando o pedido possui volumes, os agregados são calculados pelo servidor:
volume = soma(length_cm × width_cm × height_cm)
weight = soma(weight_g)
units = quantidade de volumes
Um item pode conter vários volumes. Pedido sem itemização mantém os valores
agregados enviados diretamente em volume, weight e units, preservando a
compatibilidade com integrações OMS antigas.
pending_volume, pending_weight e pending_units são somente leitura e
calculados na hora a partir dos itens que ainda não foram entregues. Num
pedido inteiro os dois conjuntos são iguais; depois de uma entrega parcial, os
pending_* encolhem enquanto volume/weight/units continuam descrevendo o
pedido contratado. É o par pending_* que a capacidade do veículo e o
roteirizador consomem. Pedido sem itemização repete o total declarado.
Criar um pedido (POST)#
O pedido pode informar o cliente pelo ID (customer) ou, conforme as opções da
conta descritas em Cliente do pedido, por
customer_external_id, customer_cpf ou customer_cnpj. Exemplo mínimo com
um cliente já cadastrado:
POST /api/v1/delivery/pedidos/
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"order_number": "PED-2026-050",
"customer": 4,
"address": "Rua da Entrega",
"city": "Niterói",
"state": "RJ"
}
Para localizar uma empresa pelo documento, substitua customer por
"customer_cnpj": "12.345.678/0001-90". Para pessoa física, use
customer_cpf. Se a conta estiver configurada para usar o endereço do cadastro
e o cliente possuir endereço completo, os campos de endereço podem ser
omitidos, como explicado em O que muda no pedido depois do vínculo.
O exemplo abaixo mostra a criação completa com itemização:
POST /api/v1/delivery/pedidos/
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"order_number": "PED-2026-001",
"external_source": "erp",
"external_id": "987654",
"customer_name": "Cliente Exemplo",
"customer_phone": "(21) 99999-0000",
"warehouse": 1,
"address": "Estrada dos Menezes",
"address_number": "300",
"address_complement": "Casa 2",
"neighborhood": "Colubandê",
"city": "São Gonçalo",
"state": "RJ",
"postal_code": "24451-230",
"priority": 1,
"service_duration_seconds": 300,
"notes": "Deixar com o porteiro se não houver ninguém",
"payment_method": "pix",
"payment_value": "249.90",
"items": [
{
"name": "Guarda-roupa",
"volumes": [
{
"length_cm": 100,
"width_cm": 50,
"height_cm": 20,
"weight_g": 8000
},
{
"length_cm": 60,
"width_cm": 40,
"height_cm": 10,
"weight_g": 3000
}
]
}
]
}
A resposta 201 Created inclui os IDs gerados e os totais recalculados:
{
"id": 81,
"order_number": "PED-2026-001",
"volume": 124000,
"weight": 11000,
"units": 2,
"items": [
{
"id": 15,
"name": "Guarda-roupa",
"volumes": [
{
"id": 31,
"length_cm": 100.0,
"width_cm": 50.0,
"height_cm": 20.0,
"weight_g": 8000
},
{
"id": 32,
"length_cm": 60.0,
"width_cm": 40.0,
"height_cm": 10.0,
"weight_g": 3000
}
]
}
]
}
Em PUT ou PATCH, enviar items substitui toda a coleção anterior de itens
e volumes. Omitir items preserva a coleção atual. Os IDs aninhados são
gerados pelo servidor; a alteração não é incremental por ID.
Dimensões usam centímetros e aceitam zero ou valores positivos. weight_g é
um inteiro não negativo. Na importação OMS, todo item informado deve possuir
nome e ao menos um volume.
Payload inválido, relacionamento de outra conta ou external_id já usado na
conta retorna 400. Falta de permissão para criar pedidos retorna 403.
Atualizar um pedido (PATCH e PUT)#
Prefira PATCH para alterar apenas parte do pedido. Os campos omitidos são
preservados:
PATCH /api/v1/delivery/pedidos/81/
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"customer": 4,
"customer_name": "Maria da Silva",
"customer_phone": "(21) 99999-0000",
"priority": 1,
"notes": "Entregar no setor de recebimento",
"payment_method": "pix",
"payment_value": "249.90"
}
A resposta 200 OK devolve o pedido completo já atualizado. Quando a resolução
por cadastro estiver habilitada na conta, o cliente também pode ser localizado
por customer_cpf, customer_cnpj ou customer_external_id, seguindo as
regras de Cliente do pedido. CPF e CNPJ podem ser enviados
com ou sem formatação. Para trocar um pedido que já possui cliente vinculado,
prefira enviar o novo ID em customer.
Para atualizar o endereço, envie somente os campos que mudaram:
PATCH /api/v1/delivery/pedidos/81/
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"address": "Rua Nova",
"address_number": "150",
"neighborhood": "Centro",
"city": "Niterói",
"state": "RJ",
"postal_code": "24000-000"
}
Se a conta estiver configurada para usar o endereço do cadastro do cliente,
esse endereço cadastrado volta a prevalecer durante a validação do pedido.
Nesse caso, altere o cadastro em /api/v1/delivery/clientes/{id}/ ou desative
a opção antes de tentar manter um endereço diferente somente no pedido.
PUT usa o mesmo endpoint, mas deve enviar a representação completa dos campos
graváveis que serão mantidos:
PUT /api/v1/delivery/pedidos/81/
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"order_number": "PED-2026-001",
"external_source": "erp",
"external_id": "987654",
"customer": 4,
"customer_name": "Maria da Silva",
"customer_phone": "(21) 99999-0000",
"warehouse": 1,
"address": "Rua do Cadastro",
"address_number": "45",
"neighborhood": "Centro",
"city": "Niterói",
"state": "RJ",
"postal_code": "24000-000",
"priority": 1,
"service_duration_seconds": 300,
"notes": "Entregar no setor de recebimento",
"payment_method": "pix",
"payment_value": "249.90"
}
Regras importantes para PUT e PATCH:
account,customer_data,pending_volume,pending_weight,pending_units,created_ateupdated_atsão somente leitura;external_id, quando preenchido, deve continuar único dentro da conta;- relacionamentos como
customer,warehouseedelivery_areaprecisam pertencer à conta do JWT; - omitir
itemspreserva os itens e volumes existentes; - enviar
itemssubstitui toda a coleção; enviar"items": []remove a itemização e não faz uma alteração incremental pelos IDs anteriores; - os campos de resultado do item (
status,failure_reasonefailure_notes) são somente leitura e não podem ser atualizados por esse endpoint.
Erros de campo, vínculo entre contas, documento, endereço ou código externo
duplicado retornam 400. Falta de permissão retorna 403; pedido inexistente
ou fora da conta retorna 404.
Importação OMS idempotente#
POST /api/v1/delivery/pedidos/import/
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"source": "erp",
"orders": [
{
"external_id": "987654",
"order_number": "PED-2026-001",
"customer_name": "Cliente Exemplo",
"warehouse_id": 1,
"address": "Estrada dos Menezes",
"address_number": "300",
"neighborhood": "Colubandê",
"city": "São Gonçalo",
"state": "RJ",
"postal_code": "24451-230",
"items": [
{
"name": "Caixa",
"volumes": [
{
"length_cm": 40,
"width_cm": 30,
"height_cm": 20,
"weight_g": 2500
}
]
}
]
}
]
}
orders precisa ser uma lista não vazia e cada entrada exige external_id.
A combinação da conta e external_id cria ou atualiza o mesmo pedido. O
source identifica a origem da sincronização, mas trocá-lo não cria uma cópia
do pedido: o registro existente é atualizado e passa a refletir a nova origem.
warehouse_id e warehouse são aceitos como referência do armazém, e
customer_id/customer como referência do cadastro de cliente — um cliente de
outra conta ou inativo devolve 400 em
orders[<índice>].customer_id. Sem esse campo, customer_external_id,
customer_cpf e customer_cnpj continuam valendo como busca (veja
Cliente do pedido). Quando
items é omitido, a itemização existente é preservada; quando enviado, o
conjunto é substituído. A resposta é 200 OK com uma lista de pedidos, sem o
envelope de paginação.
Geocodificação do pedido#
POST /api/v1/delivery/pedidos/81/geocode/
Authorization: Bearer ACCESS_TOKEN
A consulta exige logradouro, número, bairro, cidade, UF e CEP. O endereço é enviado como:
Estrada dos Menezes, 300 - Colubandê, São Gonçalo - RJ, 24451-230, Brasil
Com uma chave Google disponível, ROOFTOP é aceito como posição exata.
RANGE_INTERPOLATED é aceito somente quando não é uma correspondência parcial
e confirma logradouro, número, cidade, UF e país; nesse caso a coordenada é
marcada como estimada. O CEP continua sendo enviado ao Google, mas uma
divergência apenas gera um aviso e não descarta latitude e longitude quando os
demais componentes conferem. APPROXIMATE, GEOMETRIC_CENTER e divergências
no endereço validado são rejeitados. Em erro, a API responde 400, limpa as
coordenadas e os metadados anteriores e mantém o pedido em awaiting_geocode
com unassigned_reason igual a geocode_failed.
A representação do pedido informa a origem e a qualidade da coordenada:
{
"lat": -22.8419,
"lon": -43.0842,
"geocode_provider": "google",
"geocode_location_type": "RANGE_INTERPOLATED",
"geocode_is_estimated": true
}
Os campos geocode_* são somente leitura. Para Google, o tipo será ROOFTOP
ou RANGE_INTERPOLATED; no fallback ele será NOMINATIM e a coordenada também
será marcada como estimada.
Na simulação local, simulate_delivery_operation não utiliza latitude ou
longitude predefinidas nos pedidos demonstrativos. O comando limpa esses campos,
ignora o cache e aguarda uma nova resposta síncrona para cada endereço. O log do
terminal informa quando a consulta usa Google Maps ou Nominatim e mostra as
coordenadas retornadas. O fallback para Nominatim só ocorre em falha técnica do
Google. RANGE_INTERPOLATED validado é aceito como estimado; APPROXIMATE,
GEOMETRIC_CENTER e resultados divergentes continuam sendo rejeitados.
O comando não delega essa etapa ao Celery, pois precisa bloquear o início da
operação até todas as coordenadas estarem disponíveis. Ele considera pedidos da
seed e pedidos do counter-sales-simulator vinculados a waves ou rotas
operacionais. Se não houver candidatos, o terminal registra que nenhuma
consulta foi necessária. Depois dessa preparação, aguarda por padrão cinco
segundos entre cada ação operacional; --interval-seconds permite controlar
esse ritmo. O simulate_counter_sales também cria pedidos sem
lat e lon predefinidos, deixando esses campos a cargo da geocodificação. O
catálogo do comando usa somente endereços previamente conferidos sem
partial_match; divergência apenas de CEP segue a política de aviso e não
descarta as coordenadas. Os pedidos sintéticos podem conter de um a quatro itens
de autopeças, com volumes, dimensões e pesos próprios, sempre sem lat e lon
predefinidos.
Roteirizar a própria wave como entregador#
O perfil Entregador possui view_deliverywave e a permissão dedicada
prepare_deliverywave, mas não recebe change_deliverywave. Assim, ele pode
consultar waves atribuídas ao seu DriverProfile em qualquer estado sem obter
acesso às alterações administrativas do recurso:
GET /api/v1/delivery/waves/?page=1
Authorization: Bearer ACCESS_TOKEN
O queryset usa simultaneamente a conta do JWT e o entregador autenticado. Waves
de outro entregador não aparecem na lista e retornam 404 no detalhe.
Para uma wave própria em ready, use a ação atômica:
POST /api/v1/delivery/waves/10/prepare-route/
Authorization: Bearer ACCESS_TOKEN
A ação não recebe payload e executa, em sequência:
- confirma que a wave pertence ao entregador autenticado;
- confirma o estado
readye um turno válido do mesmo entregador; - executa
DeliveryRoutingService.optimize(); - exige que a otimização gere uma única rota para esse entregador;
- altera a rota de
plannedparareleased; - altera a wave para
releasede preenchereleased_at.
Resposta 200 OK:
{
"wave_id": 10,
"status": "released",
"route_id": 25,
"route_number": "ROTA-25"
}
Wave própria que não esteja em ready, ausência de turno elegível ou falha do
roteirizador retorna 400. Uma wave de outro entregador permanece invisível e
retorna 404. A permissão dedicada não autoriza PUT, PATCH, DELETE nem as
ações administrativas optimize, release, close e cancel.
O trajeto da rota#
GET delivery/rotas/{route_id}/ traz o CRUD padrão do recurso, incluindo o
trajeto calculado pelo roteirizador:
{
"id": 25,
"status": "started",
"planned_distance": 25057,
"planned_duration": 1860,
"path_geometry": {
"type": "LineString",
"coordinates": [[-43.0994, -22.8569], [-43.0993, -22.8567], "..."]
},
"planned_waypoints": [
{"kind": "pickup", "warehouse_id": 1, "name": "Centro de Distribuição Principal", "location": [-43.0027, -22.8298]},
{"kind": "delivery", "order_id": 370, "name": "PED-2026-001", "location": [-43.0004, -22.8218]}
],
"path_updated_at": "2026-08-22T19:21:54-03:00"
}
| Campo | Descrição |
|---|---|
path_geometry |
LineString GeoJSON em [lon, lat], na ordem em que o trajeto é percorrido |
planned_waypoints |
Origem(ns) e paradas na ordem devolvida pelo roteirizador, com o order_id/warehouse_id de cada uma |
path_updated_at |
Quando o trajeto foi recalculado pela última vez |
path_geometry cobre só as paradas ainda pendentes — não é o trajeto
completo da rota desde o início. Uma parada já completed não aparece nele;
o primeiro ponto da geometria fica próximo da localização atual do veículo (ou
da primeira parada pendente), não da primeira parada da rota. É por isso que
recalcular o trajeto ao concluir uma parada ou ao adiar uma
(skip/) só mexe no trecho restante: o
GET seguinte já reflete a rota encolhida.
Para saber a que parada cada ponto do trajeto corresponde, cruze com
GET delivery/paradas/?route={route_id}, ordenado por sequence — cada
parada traz o order_id, e a distância entre as coordenadas do pedido
(lat/lon) e o ponto mais próximo em path_geometry.coordinates é o
desvio real de percurso. O mapa e o app usam esse cruzamento para desenhar a
rota junto com as paradas.
Retirada da carga (conferência por código de barras)#
Com pickup_enabled ligado na configuração da conta, a wave só pode ser
roteirizada depois que todos os pedidos dela forem retirados — ou removidos
da carga. A conferência é feita lendo a etiqueta de cada volume do
pedido: um pedido com três volumes só conta como retirado depois das três
leituras.
O que a etiqueta traz vem de pickup_barcode_source:
| Valor | Código impresso e lido |
|---|---|
order_number (padrão) |
DeliveryOrder.order_number |
external_id |
DeliveryOrder.external_id; pedido sem código externo cai no número do pedido, para a etiqueta nunca sair em branco |
label_code_format escolhe somente a apresentação desse valor: barcode
imprime CODE128 e qrcode imprime QR Code. O conteúdo e os sufixos por volume
permanecem iguais, portanto os dois formatos alimentam a mesma conferência.
Esse valor é a base do código. Pedido sem itemização, ou com um único volume,
usa a base pura (PED-2026-001). Pedido com mais de um volume ganha um
sufixo por posição — PED-2026-001-1, PED-2026-001-2 — na mesma ordem e
com o mesmo total que aparece impresso em cada etiqueta, uma por volume (veja
Etiquetas: uma por volume).
Consultar o progresso#
GET /api/v1/delivery/waves/10/retirada/
Authorization: Bearer ACCESS_TOKEN
{
"wave_id": 10,
"pickup_enabled": true,
"barcode_source": "order_number",
"total": 3,
"picked_up": 1,
"pending": 2,
"is_complete": false,
"orders": [
{
"order_id": 81,
"order_number": "PED-2026-001",
"customer": "Cliente Exemplo",
"codes": [
{"code": "PED-2026-001-1", "scanned_at": "2026-08-22T09:14:00-03:00"},
{"code": "PED-2026-001-2", "scanned_at": null}
],
"picked_up_at": null
}
]
}
total/picked_up/pending/is_complete continuam por pedido, não por
volume — é o que trava a roteirização. codes é a lista de etiquetas daquele
pedido; picked_up_at do pedido só deixa de ser null quando todo codes
estiver com scanned_at preenchido.
Registrar a retirada#
O app manda o que a câmera leu; o portal manda o que foi digitado — é o mesmo endpoint, um código de volume por chamada. A resposta é o mesmo payload de progresso, já atualizado.
POST /api/v1/delivery/waves/10/retirada/
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{"code": "PED-2026-001-1"}
O código é comparado ignorando maiúsculas e espaços, porque leitor de mão e
digitação manual chegam com formatações diferentes. Cada volume só é aceito
uma vez; ler o último volume pendente de um pedido é o que marca
picked_up_at dele.
| Status | Situação |
|---|---|
400 |
Código vazio, não corresponde a nenhum volume da carga, o pedido já está totalmente retirado, ou aquele volume específico já foi lido |
403 |
Usuário não é o entregador da carga nem tem delivery.change_deliverywave |
404 |
Wave fora da conta do JWT ou de outro entregador |
Quem pode registrar: o entregador dono da carga (conferindo o que vai levar) e
quem tem delivery.change_deliverywave (o conferente do armazém).
Efeito na roteirização#
Enquanto faltar pedido, POST waves/{id}/optimize/ e
waves/{id}/prepare-route/ respondem 400 com a contagem do que falta:
["A carga ainda não foi retirada por completo: 2 de 3 pedido(s) aguardando conferência."]
A trava vive no serviço de roteirização, não só no botão do portal — vale igual para API, tarefa periódica e simulador. Remover o pedido da carga também libera: o que saiu da wave deixa de ser cobrado na conferência.
Efeito no status do pedido#
Com a conferência ligada, o pedido entra na carga em awaiting_pickup — está
esperando ser retirado do armazém — e passa para ready_for_routing assim que
todos os volumes forem lidos. Com a conferência desligada, ele vai direto
para ready_for_routing: a retirada continua existindo, só não exige leitura.
A retirada é gravada no vínculo WaveOrder, não no pedido — e cada leitura de
volume, num registro à parte ligado a esse mesmo vínculo. Tirar o pedido da
carga apaga a conferência inteira junto, volumes lidos inclusive: na carga
seguinte ele precisa ser bipado de novo, do zero.
Assinatura, fotos e conclusão da entrega#
Assinatura e fotos são enviadas como multipart/form-data, não como JSON ou
Base64. O fluxo usa três etapas:
- criar o comprovante da parada e enviar a assinatura;
- enviar cada foto vinculada ao ID do comprovante;
- concluir a parada, que também marca o pedido como entregue.
As listagens e consultas do perfil Entregador são limitadas aos comprovantes de
suas próprias rotas. Todos os IDs relacionados precisam pertencer à conta do
JWT. O campo account nunca deve ser enviado.
1. Criar o comprovante e enviar a assinatura#
POST /api/v1/delivery/comprovantes/
Authorization: Bearer ACCESS_TOKEN
Content-Type: multipart/form-data
Campos aceitos:
| Campo | Tipo | Descrição |
|---|---|---|
route_stop |
inteiro | ID da parada que será concluída; obrigatório na criação |
recipient_name |
texto | Nome de quem recebeu a entrega |
recipient_document |
texto | Documento do recebedor, quando exigido |
signature_file |
arquivo | Imagem da assinatura |
lat |
decimal | Latitude onde o comprovante foi coletado |
lon |
decimal | Longitude onde o comprovante foi coletado |
notes |
texto | Observações da entrega |
Exemplo com curl:
curl -X POST "https://servidor/api/v1/delivery/comprovantes/" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-F "route_stop=42" \
-F "recipient_name=Maria da Silva" \
-F "recipient_document=12345678900" \
-F "lat=-22.9064" \
-F "lon=-43.1752" \
-F "notes=Recebido na portaria" \
-F "[email protected];type=image/png"
Resposta 201 Created, abreviada:
{
"id": 12,
"account": 1,
"route_stop": 42,
"recipient_name": "Maria da Silva",
"recipient_document": "12345678900",
"signature_file": "https://servidor/media/delivery/signatures/2026/08/assinatura.png",
"lat": -22.9064,
"lon": -43.1752,
"completed_at": null,
"notes": "Recebido na portaria",
"items": []
}
items lista os IDs de DeliveryOrderItem cobertos por este comprovante —
isto é, os itens que efetivamente saíram nesta visita. O servidor a preenche
ao concluir a parada (veja Entrega parcial por item);
não a envie manualmente. items e completed_at são campos somente leitura;
se um cliente antigo ainda os enviar na criação ou atualização do comprovante,
eles serão ignorados e nunca anteciparão a conclusão.
Existe somente um DeliveryProof por parada. Se o comprovante já existir,
atualize seus dados ou substitua a assinatura usando o ID retornado:
curl -X PATCH "https://servidor/api/v1/delivery/comprovantes/12/" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-F "recipient_name=Maria da Silva" \
-F "[email protected];type=image/png"
Em aplicações que capturam a assinatura em um <canvas>, converta o conteúdo
para Blob/File, dê um nome com extensão permitida — por exemplo,
assinatura.png — e envie esse arquivo no campo signature_file. A API não
recebe o Data URL do canvas diretamente.
Não envie completed_at: esse campo é preenchido pelo servidor somente depois
que a parada for concluída com sucesso.
2. Enviar as fotos#
Cada foto é enviada em uma requisição própria para
fotos-comprovante. Use em delivery_proof o ID recebido na etapa anterior e
uma sequência única dentro desse comprovante.
POST /api/v1/delivery/fotos-comprovante/
Authorization: Bearer ACCESS_TOKEN
Content-Type: multipart/form-data
curl -X POST "https://servidor/api/v1/delivery/fotos-comprovante/" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-F "delivery_proof=12" \
-F "sequence=1" \
-F "[email protected];type=image/jpeg"
Para uma segunda foto:
curl -X POST "https://servidor/api/v1/delivery/fotos-comprovante/" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-F "delivery_proof=12" \
-F "sequence=2" \
-F "[email protected];type=image/jpeg"
Resposta 201 Created:
{
"id": 30,
"account": 1,
"delivery_proof": 12,
"file": "https://servidor/media/delivery/photos/2026/08/foto-entrega-1.jpg",
"sequence": 1,
"created_at": "2026-08-21T10:30:00-03:00"
}
Assinatura e fotos aceitam arquivos .jpg, .jpeg, .png e .webp, com no
máximo 10 MB por arquivo. A quantidade máxima de fotos vem de
DeliveryConfig.maximum_photos ou de order.proof_overrides.maximum_photos.
A combinação delivery_proof + sequence não pode se repetir.
3. Finalizar a parada e o pedido#
Depois de persistir todos os dados exigidos, conclua a mesma parada usada em
route_stop:
curl -X POST "https://servidor/api/v1/delivery/paradas/42/complete/" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"delivery_actual_lat":-22.8301,"delivery_actual_lng":-43.0402}'
Essa chamada não recebe assinatura ou fotos. Ela aceita, opcionalmente, o par
delivery_actual_lat/delivery_actual_lng e o resultado por item (veja
Entrega parcial por item logo abaixo). As duas
coordenadas devem ser enviadas juntas e válidas; um par incompleto responde
400. Antes de concluir, o servidor consulta DeliveryConfig e os
valores opcionais de order.proof_overrides e valida:
- existência do comprovante;
- quantidade mínima de fotos, quando
require_photoestiver ativo; - assinatura, quando
require_signatureestiver ativo; - nome do recebedor, quando
require_recipient_nameestiver ativo; - documento, quando
require_documentestiver ativo; - observação de cada item marcado como
failed, quandorequire_note_on_failureestiver ativo.
As chaves aceitas em proof_overrides são require_photo, minimum_photos,
maximum_photos, require_signature, require_recipient_name,
require_document e require_note_on_failure. Para cada chave, um valor não
nulo no pedido tem prioridade sobre delivery_config; uma chave ausente ou com
valor null herda a política da conta. Portanto, false e 0 são overrides
válidos. capture_timestamp é uma regra global e não pode ser sobrescrita pelo
pedido.
Exemplo recebido em um pedido:
{
"id": 42,
"order_number": "PED-2026-0042",
"proof_overrides": {
"require_photo": true,
"minimum_photos": 2,
"maximum_photos": 4,
"require_signature": true,
"require_recipient_name": true,
"require_document": true,
"require_note_on_failure": true
}
}
O aplicativo deve calcular cada chave desta forma:
configuração efetiva = proof_overrides[chave] ?? delivery_config[chave]
Não substitua o objeto inteiro: combine chave por chave. Assim, um pedido pode exigir assinatura sem perder os limites de fotos definidos pela conta. Chaves desconhecidas são ignoradas pelo backend.
Perfis administrativos com a permissão correspondente podem consultar a
configuração administrativa completa em GET /api/v1/delivery/configuracao/.
O Entregador consulta somente a política segura destinada ao app em
GET /api/v1/auth/entregadores/configuracao/. Por padrão, contas novas exigem um
comprovante, nome do recebedor e ao menos uma foto; assinatura e documento
começam como opcionais. O fluxo operacional esperado antes do comprovante é
registrar POST paradas/{id}/arrive/ e POST paradas/{id}/begin/.
Quando a validação passa, a API:
- altera a parada para
completed; - altera o pedido para
delivered,partially_deliveredoudelivery_failed, conforme o resultado de seus itens (veja abaixo); - vincula ao comprovante os itens efetivamente entregues;
- preenche
DeliveryProof.completed_at; - preserva a localização recebida, compara-a com o destino planejado e busca a posição do rastreador mais próxima dentro da janela configurada;
- recalcula o trajeto restante ou encerra a rota e a wave quando não houver outras paradas pendentes.
O proof retornado inclui delivery_distance_from_planned_m,
delivery_within_planned_radius, tracker_distance_m,
tracker_validation_status e tracker_validation_label. Os códigos possíveis
são MATCH, DIVERGENT, NO_TRACKER, NO_DATA e NO_LOCATION. O app não
consulta o rastreador nem calcula distâncias; essa auditoria pertence ao backend.
Se faltar algum requisito, a resposta será 400 Bad Request, por exemplo:
[
"São necessárias pelo menos 2 fotos",
"Assinatura obrigatória"
]
O comprovante permanece salvo e pode ser corrigido com PATCH ou receber novas
fotos antes de repetir POST paradas/{id}/complete/.
Entrega parcial por item#
Um pedido com vários itens pode ter uma entrega parcial — por exemplo, o
entregador leva dois produtos e só um chega ao destinatário. POST
paradas/{id}/complete/ aceita um corpo JSON opcional descrevendo o resultado
de cada item:
POST /api/v1/delivery/paradas/42/complete/
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"items": [
{"id": 15, "status": "delivered"},
{
"id": 16,
"status": "failed",
"reason": "recipient_absent",
"notes": "Cliente não estava para receber o segundo volume"
}
]
}
| Campo (por item) | Tipo | Descrição |
|---|---|---|
id |
inteiro | ID do DeliveryOrderItem; itens de outro pedido são ignorados |
status |
texto | delivered ou failed; qualquer outro valor é tratado como delivered |
reason |
texto | Motivo da falha; aceita os mesmos códigos de paradas/{id}/fail/ |
notes |
texto | Observação da falha; obrigatória quando require_note_on_failure estiver ativo |
Regras:
- o checklist cobre apenas os itens ainda não entregues. Numa reentrega, o
que o cliente já recebeu não volta a ser perguntado e não pode ser
desmarcado — nem por um
complete/nem por umfail/posterior; - omitir
items, enviar lista vazia ou omitir um item da lista marca esse item (ou todos os pendentes, seitemsnão vier) comodelivered— pedidos sem itens cadastrados continuam funcionando exatamente como antes deste recurso, sem enviar nada; - todo item
deliveredentra emDeliveryProof.items; nenhum itemfailedentra lá — o motivo e a observação da falha ficam no próprio item (DeliveryOrderItem.failure_reason/failure_notes), não no comprovante; - o status do pedido sai de todos os seus itens, não só dos conferidos
agora:
deliveredquando todos foram entregues,delivery_failedquando nenhum foi. Um resultado misto (parte entregue, parte não) nunca virapartially_delivered: o(s) item(ns) que faltou(aram) é(são) desmembrado(s) numa nota nova,manual_assignment(ver Reentrega do que ficou pendente), e este pedido fecha comodelivered— o que ficou nele foi mesmo entregue. Pedido sem itemização segue tudo-ou-nada — o resultado da visita é o do pedido.
Uma falha total da parada — o entregador não conseguiu entregar nada, sem
comprovante — continua usando POST paradas/{id}/fail/ (sem relação com
items); nesse caso os itens ainda pendentes são marcados failed com o
mesmo motivo e observação da parada. Um pedido que já tinha item entregue
passa pelo mesmo desmembro acima (fecha delivered, o resto vira nota
manual_assignment) em vez de delivery_failed.
Reentrega do que ficou pendente#
Item que não sai numa visita — porque não saiu do armazém (conferência,
POST waves/{id}/nao-vai/) ou porque a entrega deixou pra trás (resultado
misto em paradas/{id}/complete/ ou fail/) — não fica preso ao pedido
original. Ele é desmembrado numa nota nova:
- a nota filha nasce com
status = manual_assignment, os mesmos dados de cliente/endereço/agendamento/pagamento do pedido original (split_fromaponta pra ele) e só o(s) item(ns) que faltou(aram), com status resetado prapending; volume/weight/unitsda nota filha descrevem só o que ela carrega, não o pedido original inteiro;- o pedido original fecha como
delivered(ou é cancelado, se todos os itens foram desmembrados — nada dele de fato saiu) e não guarda mais nenhuma pendência; - a nota filha conta como exceção no despacho, pra não sumir da operação, mas
não é pega pela alocação automática de wave — precisa ser roteirizada à
mão (
/app/entregas/roteirizacoes/nova/), igual a qualquer pedidomanual_assignment.
Na conferência de retirada (wave_pickup/POST waves/{id}/nao-vai/), marcar
um pedido como "não vai" tem o mesmo efeito: gera a nota manual_assignment
e tira o pedido da carga atual — sem essa marcação, ele fica preso esperando
uma bipagem que não vai vir.
Antes desta versão, um resultado misto deixava o pedido inteiro em
partially_deliverede a reentrega reaproveitava a mesma nota (via "Incluir pedidos com entrega falha ou parcial" na tela de criar wave). Isso foi substituído pelo desmembro acima;partially_deliverednão é mais oferecido para reenfileiramento automático, sódelivery_failed.
Transferir um pedido para outra carga#
Para a urgência: o veículo quebra no meio do roteiro e os pedidos que sobraram precisam sair hoje, quase sempre com um entregador que já está na rua. Por isso a transferência funciona mesmo com a carga de origem em rota.
GET /api/v1/delivery/pedidos/81/transferir/
Authorization: Bearer ACCESS_TOKEN
{
"order_id": 81,
"order_number": "PED-2026-001",
"status": "out_for_delivery",
"wave_id": 10,
"available_waves": [
{
"id": 12,
"status": "released",
"driver": "Carlos Souza",
"vehicle": "STR2A26",
"on_the_road": true
}
]
}
available_waves traz só as cargas que podem receber o pedido: da mesma conta,
em collecting/ready/planned/released, e nunca a carga em que ele já
está. on_the_road avisa que aquele entregador já saiu — o pedido entraria no
fim do roteiro dele.
Para mover, mande a carga escolhida. A resposta é o mesmo payload, já com o
wave_id novo:
POST /api/v1/delivery/pedidos/81/transferir/
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{"wave": 12}
O que acontece, em ordem:
- as paradas ainda pendentes do pedido nas rotas em andamento são canceladas — as concluídas ficam intactas, são histórico e o comprovante coletado continua valendo;
- o vínculo com a carga antiga é apagado e um novo é criado;
- se a carga destino ainda não foi roteirizada, o pedido entra na fila dela;
se o entregador já está na rua, o pedido vira parada no fim do roteiro
dele, com
is_manual; - só o trecho restante das duas rotas é recalculado.
O otimizador não roda de novo: nenhuma OptimizationRun é criada e o que
o entregador já cumpriu não é remexido.
Com pickup_enabled ligado, a conferência zera: como ela mora no vínculo
WaveOrder, o pedido volta a awaiting_pickup e quem for levar precisa bipar
a etiqueta. É o comportamento desejado — quem assume a carga confere o que
está pegando.
| Status | Situação |
|---|---|
400 |
Carga não informada, de outra conta, encerrada, ou é a mesma em que o pedido já está; pedido já entregue, cancelado ou devolvido |
403 |
Perfil sem delivery.change_deliverywave |
404 |
Pedido fora da conta do JWT |
No portal a ação fica na tela da carga, por pedido, e continua disponível com a carga travada — é justamente em rota que a urgência acontece.
Próxima entrega: adiar uma parada#
Quando o entregador prefere deixar uma entrega para depois — cliente não atende agora, rua interditada, prefere fechar o quarteirão primeiro — sem cancelar nem registrar falha:
POST /api/v1/delivery/paradas/42/skip/
Authorization: Bearer ACCESS_TOKEN
A chamada não recebe payload. A parada vai para o fim das paradas pendentes
e a resposta 200 OK traz a parada já com a nova sequence.
Numa rota A B C D E com A concluída, adiar B resulta em:
antes: 1 A✓ 2 B 3 C 4 D 5 E
depois: 1 A✓ 2 C 3 D 4 E 5 B
As pendentes apenas permutam entre as posições que já ocupavam, então uma parada concluída mantém a sequência que teve na execução e o histórico da rota continua legível.
O que não muda:
- o pedido continua pendente — não vira
delivery_failednem geraDeliveryAttempt; adiar não é falha; - paradas concluídas, comprovantes e a ordem já executada ficam intactos;
- o otimizador não roda de novo: nenhuma
OptimizationRuné criada e a wave inteira não é reprocessada. Só o trecho restante do trajeto é recalculado, o mesmo recálculo que já acontece ao concluir uma parada.
Se a parada estava em arrived ou delivering, ela volta para planned — é
de novo uma entrega por fazer. O actual_arrival continua gravado: a visita
aconteceu de verdade e o histórico não se apaga.
O mapa e o app recebem a nova ordem pelo order_ids do trajeto da rota, que
acompanha a sequência das paradas.
Respostas de erro:
| Status | Situação |
|---|---|
400 |
A parada já foi encerrada, ou é a última pendente e não há outra para assumir a vez |
404 |
Parada de outro entregador ou fora da conta do JWT |
skip/não é o estadoskippedda parada. Aquele encerra a parada e tira o pedido da rota; este só troca a ordem, mantendo a entrega pendente.
Endpoints e ações do delivery#
| Recurso | Ações adicionais ao CRUD |
|---|---|
clientes |
GET clientes/documentos/ para listar CPF e CNPJ sem paginação |
pedidos |
POST pedidos/import/, POST pedidos/{id}/geocode/, GET/POST pedidos/{id}/transferir/ |
entregadores |
POST entregadores/{id}/disponibilidade/ |
waves |
GET/POST waves/{id}/retirada/; POST waves/{id}/nao-vai/ (marca um pedido da carga como não vai sair, gera nota manual_assignment); POST waves/{id}/prepare-route/ para o dono; optimize/, release/, close/, cancel/ para perfis administrativos |
rotas |
POST rotas/{id}/accept/, start/, release/, lock/, unlock/, cancel/ |
paradas |
POST paradas/{id}/arrive/, begin/, complete/, fail/, skip/ |
comprovantes |
CRUD com validação de conta e parada |
fotos-comprovante |
CRUD e upload associado ao comprovante |
Estados operacionais#
| Modelo | Valores de status |
|---|---|
| Pedido | created, awaiting_geocode, outside_delivery_area, waiting_wave, ready_for_routing, routing, awaiting_pickup, routed, released, out_for_delivery, delivered, partially_delivered (legado, não é mais gerado — ver nota abaixo), delivery_failed, routing_failed, returned, cancelled, manual_assignment |
| Item do pedido | pending, delivered, failed |
Entregador (availability) |
available, busy, offline |
| Wave | collecting, ready, optimizing, planned, released, closed, cancelled, failed, completed, partially_completed |
| Rota | planned, released, accepted, started, completed, partially_completed, cancelled, superseded |
| Parada | planned, approaching, arrived, delivering, completed, failed, skipped, cancelled |
awaiting_pickupé o pedido esperando ser retirado do armazém. O quepickup_enabledmuda é como essa retirada acontece: com a conferência desligada, é só pegar e ir; com ela ligada, é preciso bipar a etiqueta. Nesse caso o pedido entra na carga já emawaiting_pickupe só volta paraready_for_routingquando o código é lido — e a wave não roteiriza enquanto sobrar pedido para conferir.
manual_assignmenté uma nota nascida de um desmembro (split_fromaponta pro pedido original) — item que não saiu na conferência ou que voltou de uma entrega parcial. Nunca é pego pela alocação automática de wave (allocate_orderssó buscawaiting_wave); precisa ser roteirizado à mão em/app/entregas/roteirizacoes/nova/ou viacreate_manual_waves.partially_deliveredé mantido no schema só por compatibilidade com dados antigos — o fluxo atual nunca deixa um pedido nesse status (ver Reentrega do que ficou pendente).
Fluxo administrativo#
- Cadastre ou importe pedidos com
POST delivery/pedidos/ouPOST delivery/pedidos/import/. - Se necessário, geocodifique um pedido com
POST delivery/pedidos/{id}/geocode/. - Cadastre entregadores, veículos, vínculos, turnos, áreas e regras.
- Crie uma wave e associe seus pedidos.
- Execute
POST delivery/waves/{id}/optimize/para gerar as rotas. - Confira
delivery/otimizacoes/edelivery/nao-atribuidos/. - Libere a wave com
POST delivery/waves/{id}/release/. - Ao final, use
POST delivery/waves/{id}/close/quando aplicável.
Waves também aceitam POST .../{id}/cancel/. A ação registra o usuário em
cancelled_by, remove todos os pedidos da carga e devolve os não finalizados
para waiting_wave, sem redistribuição automática imediata. Rotas
administrativas aceitam release, lock, unlock e cancel no mesmo padrão
de URL.
Fora desse fluxo automático, o portal permite montar uma carga à mão em
/app/entregas/waves/nova-manual/, escolhendo o entregador e marcando os
pedidos numa tela só. E qualquer pedido pode mudar de carga a qualquer momento
com pedidos/{id}/transferir/,
inclusive com a carga de origem já em rota.
Com waves_enabled = false, o despacho manual continua disponível e aplica as
mesmas regras de área, prioridade de entregadores, proximidade, máximo e
capacidade aos pedidos em ready_for_routing; somente o gatilho automático e o
mínimo deixam de atuar.
Fluxo do entregador#
- Faça login em
/api/v1/auth/entregadores/token/. - Liste as waves próprias em
GET delivery/waves/. - Para uma wave
ready, usePOST delivery/waves/{id}/prepare-route/e abra a rota indicada porroute_idna resposta. - Liste as rotas em
GET delivery/rotas/quando precisar sincronizar o estado. - A rota já estará
released; aceite-a opcionalmente comPOST delivery/rotas/{id}/accept/ou inicie comPOST .../{id}/start/. - Ao chegar, envie
POST delivery/paradas/{id}/arrive/. Para deixar essa entrega para o fim do roteiro, usePOST delivery/paradas/{id}/skip/. - Inicie o atendimento com
POST delivery/paradas/{id}/begin/. - Crie o comprovante, envie a assinatura e depois cada foto conforme o fluxo de assinatura, fotos e conclusão da entrega.
- Conclua com
POST delivery/paradas/{id}/complete/ou informe falha:
POST /api/v1/delivery/paradas/42/fail/
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"reason": "destinatario_ausente",
"notes": "Nova tentativa solicitada",
"lat": -23.5505,
"lon": -46.6333
}
As transições de estado e exigências de prova são validadas pelo servidor. Consulte Entregas e roteirização para regras operacionais, configuração do ORS/VROOM e estados do domínio.
6. Assets estáticos e modelos 3D#
| Operação | Endpoint |
|---|---|
| Listar | GET /api/v1/assets/ |
| Filtrar 3D | GET /api/v1/assets/?kind=3d |
| Consultar | GET /api/v1/assets/3d/carro.glb/ |
| Baixar | GET /api/v1/assets/3d/carro.glb/?download=1 |
| Enviar | POST /api/v1/assets/ com multipart path e file |
| Substituir | PUT /api/v1/assets/3d/carro.glb/ com multipart file |
| Excluir | DELETE /api/v1/assets/3d/carro.glb/ |
Leitura exige autenticação; escrita e exclusão exigem superusuário. A API
bloqueia caminhos absolutos, .., extensões desconhecidas e uploads acima de
20 MB. Os formatos 3D aceitos são GLB, glTF e BIN.
7. Respostas e erros#
| HTTP | Significado comum |
|---|---|
200 |
Consulta ou alteração concluída |
201 |
Registro ou asset criado |
204 |
Exclusão concluída |
400 |
Payload, estado ou relacionamento inválido |
401 |
JWT ausente, expirado ou inválido |
403 |
Conta, módulo, perfil ou tipo de usuário sem autorização |
404 |
Recurso inexistente ou fora da conta ativa |
Erros de validação normalmente retornam um objeto JSON cujas chaves indicam os campos inválidos. O cliente deve tratar o status HTTP e não depender apenas do texto da mensagem.
8. Exemplo executável#
curl -X POST http://localhost:8000/api/v1/auth/token/ \
-H "Content-Type: application/json" \
-d '{"identifier":"admin","password":"senha","account_id":1}'
Copie o valor de access e consulte um recurso:
curl http://localhost:8000/api/v1/veiculos/ \
-H "Authorization: Bearer ACCESS_TOKEN"
Percorra next até que seu valor seja null. Quando o access expirar, renove-o
com o refresh; quando precisar mudar de conta, repita o login.
9. Changelog#
A API continua em /api/v1/ — as mudanças abaixo são aditivas, exceto onde
marcado como breaking change.
2026-09-29
- Novo
GET /api/v1/delivery/clientes/documentos/, que devolve todos os CPF e CNPJ da conta em duas listas, sem paginação. - A referência ganhou uma seção própria de Pedidos, com tabela de endpoints,
exemplos de listagem, detalhe, criação,
PATCHePUT, além das regras para cliente, itemização, multi-tenancy e erros. - A consulta de clientes passou a mostrar exemplos separados para CPF e CNPJ.
2026-09-28
- A referência da API passou a ser publicada como página navegável em
/docs/, gerada a partir deste arquivo e com índice, busca e cópia dos exemplos. - Pedidos passaram a rejeitar
external_idduplicado dentro da mesma conta, mesmo quandoexternal_sourceé diferente. A importação OMS continua idempotente e atualiza o registro já existente. - Consultas de clientes por
cpfecnpjaceitam documentos formatados. Pedidos vinculados agora incluemcustomer_data(somente leitura) com o cadastro completo e seu endereço.
2026-09-26
DeliveryConfigganhouaddress_correction_enabled(padrãofalse) eaddress_correction_min_distance_m(padrão1000), legíveis e graváveis emdelivery/configuracaocomo os demais campos. Ligada, a baixa registrada longe do destino abre uma pendência de revisão do endereço do cadastro do cliente, resolvida no portal em/app/entregas/correcoes/— o modeloDeliveryAddressCorrectionnão tem endpoint em/api/v1/. Ver Correção de endereço do cliente.- Novo perfil de acesso Balconista (
counter_clerk). Ele lê pedidos e correções de endereço, cria e edita clientes, e não possuitracking.view_vehicle—/api/veiculos/passou a exigir essa permissão explicitamente, em vez de confiar apenas no escopo por conta.
2026-08-29
DeliveryConfig.label_code_formatpermite imprimir o código de conferência comobarcode(CODE128, padrão) ouqrcode. A mudança é somente visual: o conteúdo definido porpickup_barcode_sourcee os sufixos dos volumes não mudam. A política segura do app passa à versão4.
2026-08-28
DeliveryAreaganhoudriver_mode(all,selected,none), limites opcionaisminimum_orders/maximum_orderseminimum_wait_minutes. Área sem atendimento mantém os pedidos emwaiting_wavee nunca cria carga sem entregador. O mapa ao vivo pode exibir os polígonos e nomes das áreas.DeliveryConfig.area_proximity_max_distance_kmlimita a associação por proximidade até a borda da área. Sobreposições usamDeliveryArea.priority, espera vazia fica desativada e cada entregador mantém no máximo uma carga aberta, restrita à sua área. A mesma alocação vale para automação e despacho manual.
2026-08-26 (v2.2.1)
- Com os dois provedores de geocodificação ativos (
geocoding_provider = auto), o Google Maps agora cai para o Nominatim também quando responde mas nenhum resultado bate a precisão mínima configurada (inclusiveZERO_RESULTS) — antes, só uma falha técnica do Google (chave inválida, cota, erro de rede) caía para o Nominatim; uma rejeição por precisão deixava o pedido sem coordenada mesmo com o Nominatim disponível. Conta fixada em "somente Google" continua sem substituir o provedor.
2026-08-26
- ⚠️ Breaking:
DeliveryOrder.priorityestreitou de0..100para-2..1. Valor fora da faixa responde400na criação/importação. Dados existentes foram reamassados numa migration (> 0virou1). Passa a valer também como regra rígida de ordenação das paradas dentro de uma rota (prioridade maior sempre primeiro, dentro do que o otimizador já calculou). - Novos campos opcionais em
DeliveryOrder:payment_method(cash,card,pix,to_arrange) epayment_value(decimal). - Novo status de pedido
manual_assignmente camposplit_from(somente leitura): resultado misto numa parada, ou item marcado "não vai" na conferência, desmembra o que faltou numa nota nova nesse status, fora da alocação automática — o pedido original fechadelivered. Substitui o fluxo antigo departially_delivered(mantido só por compatibilidade com dados anteriores a esta versão). Ver Reentrega do que ficou pendente. - Novo recurso
DeliveryAreaDriver(/api/v1/delivery/area-entregadores/): prioridade de entregador por área, usada no fallback da alocação automática. Ver Prioridade de entregador por área. - Nova ação
POST /api/v1/delivery/waves/{id}/nao-vai/({"order_id": <id>}): marca um pedido da carga como não vai sair, equivalente à ação "não vai" da conferência no portal. availability = offlinedo entregador passou a bloquear a alocação automática de carga (turno e carga ativa também bloqueiam — ver Turno e disponibilidade do entregador).DeliveryArea.geometrypassou a aceitarMultiPolygon, além dePolygon(o editor de área no portal ganhou união/recorte de formas desenhadas).- Documentação corrigida:
notes(observação do pedido, já suportada pela API antes desta versão) estava faltando na tabela de campos e no exemplo de criação — não era uma lacuna da API, só da doc.