API Pública PuzlTask — Guia de Integração
Guia de leitura fácil para desenvolvedores, gerentes de projeto e equipes de suporte que conectam um ERP (ou qualquer sistema externo) ao PuzlTask.
Endereço da API: https://puzltask-api.puzl.place/api/v1/public/
Documentação interativa (Swagger): https://puzltask-api-dev.puzl.place/api/documentation/public#/
Resumo rápido: public-api-v1.md
Leia isto primeiro (2 minutos)
O que o PuzlTask faz
O PuzlTask envia trabalhos para o celular dos colaboradores de campo. Seu sistema cria o trabalho; o colaborador o conclui no aplicativo móvel.
O erro mais comum
| Você cria… | Aparece na agenda? |
|---|---|
| Uma task (modelo de checklist) | Não — aparece na lista de tasks do app, mas não na agenda |
| Uma activity (trabalho agendado + responsável + data) | Sim — isso aparece na agenda |
Lembre-se: Para colocar um trabalho na agenda de alguém, sempre chame
POST /activities, e não apenasPOST /tasks.
As cinco regras mais importantes
- Autenticação: toda requisição (exceto health) precisa do header
X-Integration-Key: puzl_usr_...(token por usuário) — não Bearer/JWT. - Seus códigos: use
integration_idpara seus códigos de pedido/visita (ex.:ACT-SO-8842). Nunca envie oidinterno do PuzlTask nos corpos de criação/atualização. - Responsável: na escrita de activities, não envie
responsible_user_id— a API define com base no usuário do token autenticado. - Horário agendado:
schedule_dateé obrigatório (ISO-8601, normalizado para UTC). A API Pública não rejeita horários no passado — sua integração decide o que enviar. - Início: não envie
start_at— o app móvel define quando o colaborador toca em Iniciar.
Fluxo típico (visão geral)
Seu ERP API PuzlTask App móvel
| | |
|-- GET /users -------------------->| Quem posso atribuir? |
|<-- user_id, user_name -------------| |
| | |
|-- POST /activities -------------->| Salva trabalho agendado |
| |----------------------------->| Aparece na agenda
| | | Colaborador inicia e conclui
|-- GET /activities/ACT-123 ------->| Status: FINISHED |
Quem deve ler qual parte?
| Perfil | Leia |
|---|---|
| Novo no PuzlTask | Entenda o produto → Tutorial prático |
| Montando a integração | Autenticação → Enviar um job do seu ERP → Linhas da checklist da activity |
| Suporte / depuração | Quando algo dá errado |
| Precisa de todos os campos | Todos os endpoints → Referência de campos |
Entenda o produto
Termos que usamos
| Termo | Significado simples |
|---|---|
| Domain | Espaço de trabalho da sua empresa no PuzlTask. O token de usuário resolve para um domain. |
| Task | Modelo de etapa de checklist (“Tirar foto”, “Coletar assinatura”). Fica no catálogo. |
| Activity | Um trabalho agendado para um colaborador em uma data/hora. É isso que aparece na agenda. |
| Activity task | Uma linha na checklist do trabalho (etapa 1, etapa 2, …). |
| Task group | Template reutilizável em branco: quais tasks do catálogo rodam e em qual ordem. Não é onde você envia agenda por visita nem dados preenchidos por linha. |
| Chave de integração | Segredo por usuário para a API (puzl_usr_...). Enviado em X-Integration-Key. |
| integration_id | Seu código para um registro (número do pedido no ERP, id da visita). |
| id | Código interno do PuzlTask — somente leitura, não envie na criação. |
Como as coisas se organizam
Domain (espaço de trabalho da sua empresa)
├── Users → pessoas com o app
├── Tasks → modelos (catálogo)
├── Task groups → modelos agrupados
├── Custom fields → perguntas extras (opcional)
└── Activities → trabalhos agendados ⭐ principal alvo da integração
└── Activity tasks → linhas da checklist dentro do trabalho
API Pública vs API do app móvel
| API Pública (este guia) | API móvel / admin | |
|---|---|---|
| Usada por | ERP, parceiros | App PuzlTask |
| Login | X-Integration-Key |
Token JWT Bearer |
| URL | /api/v1/public/... |
/api/activities, etc. |
Integradores devem usar somente a API Pública, salvo orientação contrária do PuzlTask.
Antes de começar (checklist)
Peça ao administrador PuzlTask:
- [ ] URL base da API:
https://puzltask-api.puzl.place - [ ] Token de integração por usuário (
puzl_usr_+ caracteres aleatórios) do colaborador responsável - [ ] Pelo menos um usuário ativo no domain (alguém que possa testar no celular)
Como obter um token: um admin gera no PuzlTask (Cadastros → Usuários → Editar → Token → Criar, ou API interna PUT /api/domains/{userId}/users/token). O token em texto claro é exibido uma vez — guarde em um gerenciador de segredos. Regenerar invalida o token anterior.
Do seu lado, você precisará de:
- [ ] Ferramenta para enviar HTTPS + JSON (curl, Postman ou cliente HTTP do ERP)
- [ ] Relógio do servidor preciso (validação de agenda usa horário UTC)
Autenticação
Toda chamada protegida precisa de:
X-Integration-Key: puzl_usr_seu_token_de_usuario
Content-Type: application/json
(em POST e PUT)
Teste o token:
curl -s "https://puzltask-api.puzl.place/api/v1/public/ping" \
-H "X-Integration-Key: puzl_usr_seu_token_de_usuario"
Resposta correta:
{ "message": "", "data": { "status": "pong" } }
Chave errada ou ausente → 401 Unauthorized.
Não use
Authorization: Bearer ...nas rotas públicas.
Como são as respostas
Sucesso:
{
"message": "",
"data": { ... }
}
Problema de validação → 422, detalhes geralmente aqui:
{
"message": "The given data was invalid.",
"data": {
"name": ["The name field is required."]
}
}
Exclusão bem-sucedida → 204 com corpo vazio.
Tutorial prático
Substitua puzl_usr_seu_token nos exemplos abaixo.
Passo 1 — A API está no ar?
curl -s "https://puzltask-api.puzl.place/api/v1/public/health"
Esperado: "status": "ok"
Passo 2 — O token é válido?
curl -s "https://puzltask-api.puzl.place/api/v1/public/ping" \
-H "X-Integration-Key: puzl_usr_seu_token"
Esperado: "status": "pong"
Passo 3 — Quem posso atribuir?
curl -s "https://puzltask-api.puzl.place/api/v1/public/users" \
-H "X-Integration-Key: puzl_usr_seu_token"
Exemplo de resposta:
{
"message": "",
"data": [
{ "user_id": "550e8400-e29b-41d4-a716-446655440000", "user_name": "Maria Silva" }
]
}
Copie user_id para mapeamento ERP ↔ Puzl. Na escrita de activities, não envie responsible_user_id — autentique com o token puzl_usr_... desse usuário em X-Integration-Key.
Lista vazia? Não há usuários ativos no domain — adicione usuários no PuzlTask primeiro.
Passo 4 — Criar um modelo de task (opcional, bom para aprender)
curl -s -X POST "https://puzltask-api.puzl.place/api/v1/public/tasks" \
-H "X-Integration-Key: puzl_usr_seu_token" \
-H "Content-Type: application/json" \
-d '{
"integration_id": "TUTORIAL-TASK-1",
"name": "Confirmar chegada",
"description": "Registrar chegada no local",
"status_photo": true,
"status_obs": true,
"is_free_task": true
}'
Esperado 201 Created. Esta task ainda não está na agenda de ninguém.
Passo 5 — Criar o trabalho agendado (activity)
Escolha um schedule_date (UTC). Use o token de integração do colaborador que deve ver o job em X-Integration-Key (token puzl_usr_... da Maria — não o user_id no corpo).
curl -s -X POST "https://puzltask-api.puzl.place/api/v1/public/activities" \
-H "X-Integration-Key: puzl_usr_TOKEN_DA_MARIA" \
-H "Content-Type: application/json" \
-d '{
"integration_id": "TUTORIAL-ACT-1",
"name": "Meu primeiro job via API",
"description": "Teste do guia de integração",
"schedule_date": "2026-12-01T14:00:00Z",
"activity_tasks": [
{
"integration_id": "TUTORIAL-TASK-1",
"task": {
"integration_id": "TUTORIAL-LINE-1",
"name": "Etapa do tutorial",
"status_photo": false,
"status_obs": false
}
}
]
}'
Esperado 201 Created, "status": 0 (pendente).
Passo 6 — Conferir no celular
- Abra o PuzlTask como o usuário atribuído.
- Vá em Agenda no dia agendado.
- Você deve ver “Meu primeiro job via API”.
- Inicie e conclua no app.
- Chame
GET /activities/TUTORIAL-ACT-1de novo — status passa a finished;start_até preenchido pelo app, não por você.
Enviar um job do seu ERP
Exemplo real: pedido ERP SO-8842 → visita de campo para a técnica Maria.
Ordem das chamadas à API
1. GET /users → mapear colaborador ERP → user_id (escolher de qual token usar)
2. POST /tasks → opcional: criar tasks no catálogo
3. POST /task-groups → recomendado: criar templates em branco uma vez
4. POST /activities → despachar o trabalho ⭐ (token do responsável em X-Integration-Key)
5. GET /activities/ACT-... → depois: verificar se concluiu
Task groups: prefira
POST /task-groupsuma vez, depois{ "task_group": { "integration_id": "..." } }em cada activity. Criação inline de grupo dentro da activity só na primeira vez que aqueleintegration_idaparece.
Corpo completo de exemplo (copie e adapte)
{
"integration_id": "ACT-DEMO-2",
"name": "Visita completa ERP",
"description": "Demo",
"schedule_date": "2026-06-11T14:00:00Z",
"activity_tasks": [
{
"integration_id": "ORD-VISIT-1",
"task": {
"integration_id": "ACT-LINE-1",
"name": "Visit step",
"status_photo": false,
"status_obs": false
}
},
{
"task_group": { "integration_id": "GRP-MORNING" }
},
{
"integration_id": "ORD-ACTIVITY-NEW",
"task": {
"integration_id": "ACT-LINE-NEW",
"name": "Foto fachada",
"status_photo": true,
"status_obs": false,
"expected_start_date": "2026-06-11T11:00:00-03:00",
"expected_finish_date": "2026-06-11T12:00:00-03:00",
"expected_duration_seconds": 3600
}
}
]
}
Antes de enviar, confira:
- [ ]
X-Integration-Keyé o tokenpuzl_usr_...do colaborador que deve executar o job (nãoresponsible_user_idno corpo) - [ ]
schedule_dateé um datetime ISO-8601 válido (armazenado em UTC) - [ ] Outer
integration_idem cada linha avulsa aponta para a task do catálogo (find-or-create no domain) - [ ] Inner
task.integration_idé único por linha da activity - [ ]
GRP-MORNINGexiste se você referencia só por integration_id (ou envie definição completa de criação quando o grupo ainda não existir) - [ ] Você não incluiu
start_at,statusoufinished_atnas linhas da activity (sequenceexterno é opcional)
Linhas da checklist da activity (activity_tasks[])
Cada item em activity_tasks[] é uma linha avulsa (Forma A) ou uma referência a task group (Forma B). Você pode enviar um sequence externo opcional (inteiro >= 1) numa linha Forma A para definir a ordem explicitamente; quando omitido, a API atribui sequence pela ordem do array.
Forma A vs Forma B — escolha o formato certo
| Necessidade | Use | Por quê |
|---|---|---|
Planejamento completo por etapa na criação — cada passo com seu código de catálogo, expected_finish_date ou ids de linha não ligados a um template de grupo compartilhado |
Forma A (linha avulsa) | Formato mais simples quando cada etapa é independente no payload. |
| Mesma estrutura de checklist sempre — reutilizar template de grupo; opcionalmente definir ou atualizar início/duração planejados por etapa na activity | Forma B (task group) | Expande o template compartilhado em linhas activity_tasks. Envie expected_start_date / expected_duration_seconds em task_group_items[].task ao criar ou atualizar linhas (somente preenchimento na atualização). |
Task groups são estrutura compartilhada, não armazenamento compartilhado de agenda.
Um task group (POST /task-groupsstandalone ou criação inline na activity) define quais tasks do catálogo aparecem e em qual ordem. O template do grupo não armazenaexpected_*por activity — esses valores ficam em cada linhaactivity_tasksquando você os envia no payload da activity.Ao referenciar um grupo existente (
{ "task_group": { "integration_id": "..." } }), a API expande o template em linhas da activity. Você pode anexar novos itens ao template compartilhado viatask_group_items. Campos existentes do template (name,sequencedo item, link de catálogo, nome da task) são imutáveis via endpoint de activity — divergências do ERP são ignoradas (a activity ainda sucede; dados do app/template não são sobrescritos).Forma A continua sendo a melhor opção quando cada etapa precisa de seu próprio outer id de catálogo e agenda completa (incluindo
expected_finish_date) na criação, sem grupo compartilhado. Forma B serve quando o mesmo template de checklist é reutilizado e você só precisa definir ou atualizarexpected_start_date+expected_duration_seconds(eduration_*) nas linhas da activity — envie emtask_group_items[].taskcom o innertask.integration_idcorrespondente (ex.:GRP-ITEM-3).
Dois integration ids em toda linha avulsa (Forma A)
| Campo que você envia | Gravado em | Significado |
|---|---|---|
Outer activity_tasks[].integration_id |
tasks.integration_id (catálogo) |
Chave da task no catálogo do domain; find-or-create antes de salvar a linha. |
Inner task.integration_id |
activity_tasks.integration_id |
Id único desta ocorrência dentro da activity (seu id de linha, ex.: 6566-1). |
Erro comum: enviar o id da linha por fora e o código do catálogo por dentro. Inverta conforme a tabela acima.
Forma A — Linha avulsa (quando enviar dados por activity)
Use a Forma A quando o ERP precisa enviar dados desta activity específica: agenda planejada, ids de linha únicos por visita, ou qualquer valor que não se repete igual em todo job que compartilha a mesma task de catálogo.
{
"integration_id": "ORD-VISIT-1",
"task": {
"integration_id": "ACT-LINE-1",
"name": "Visit step",
"status_photo": false,
"status_obs": false,
"expected_start_date": "2026-06-11T11:00:00-03:00",
"expected_finish_date": "2026-06-11T12:00:00-03:00",
"expected_duration_seconds": 3600
}
}
Obrigatório no task interno: integration_id, name, status_photo, status_obs.
Campos opcionais de agenda no task interno (gravados na linha activity_tasks, não na task de catálogo):
| Campo | Notas |
|---|---|
expected_start_date |
Datetime ISO-8601; armazenado em UTC. |
expected_finish_date |
Datetime ISO-8601; armazenado em UTC. Não pode ser anterior a expected_start_date quando ambos forem enviados. |
expected_duration_seconds |
Inteiro ≥ 0. Omitido ou null → gravado como 0. |
Ressalva de visibilidade no mobile: no fluxo atual do app, o colaborador vê principalmente início e duração planejados.
expected_finish_dateainda é aceito, validado e armazenado pela API, mas não deve ser tratado como o campo principal de planejamento na UI.Ressalva de nomenclatura:
expected_start_dateé início planejado vindo do ERP.start_até início real de execução definido pelo app móvel quando o colaborador toca em Iniciar.
activity_tasks[].status(flag de ocorrência). Este boolean é a flag de ocorrência da task, não flag de conclusão:true= sem ocorrência (normal),false= ocorrência aberta (bandeira vermelha no app). Ocorrências só podem ser abertas pelo colaborador no app. A API pública ignora qualquerstatusenviado e sempre cria a task comstatus: true(sem ocorrência).
Forma B — Referência a task group (template de checklist compartilhado)
Use a Forma B quando a estrutura da checklist é compartilhada entre jobs via task group. O app móvel ainda registra início/fim reais quando o colaborador executa o job.
O que a Forma B faz: expande o grupo salvo em linhas activity_tasks (task_id do catálogo, ordem do grupo, is_free_task: false). Overlays por activity: quando você inclui task_group_items[], pode enviar expected_start_date, expected_duration_seconds e duration_* no task interno de cada item — gravados na linha activity_tasks correspondente (casamento por inner task.integration_id, ex.: GRP-ITEM-1). Na atualização, só esses campos de preenchimento podem mudar em linhas existentes; expected_finish_date não pode ser alterado numa linha existente (422). Uma referência simples { "task_group": { "integration_id": "..." } } sozinha não carrega campos de preenchimento — inclua task_group_items para definir planejamento ou durações.
Fluxo recomendado (melhor prática)
1. POST /api/v1/public/task-groups → criar o template em branco uma vez (201)
2. POST /api/v1/public/activities → referenciar com task_group simples em cada job
Passo 1 — criar o grupo uma vez (POST /task-groups):
{
"integration_id": "GRP-MORNING",
"name": "Rotina da manhã",
"task_group_items": [ "... pelo menos 2 itens ..." ]
}
Passo 2 — referenciar em cada activity (só integration_id; sem name, sem task_group_items):
{
"activity_tasks": [
{ "task_group": { "integration_id": "GRP-MORNING" } }
]
}
Alternativa: criação inline só na primeira activity
Você pode embutir a definição completa do grupo em activity_tasks[] quando o grupo ainda não existir (mesmo formato de POST /task-groups). A API cria o grupo e expande em uma chamada. Use para protótipos rápidos ou setups pontuais.
Em toda activity seguinte, use a referência simples, ou envie task_group_items para anexar novas etapas ao template compartilhado. Se o payload repetir um item existente (ou um name diferente do grupo), esses campos permanecem como gravados no app — divergências são ignoradas; só integration_ids de itens novos são anexados.
Para alterar itens existentes do template depois que o grupo existe, use POST /task-groups (upsert) ou PUT /task-groups/{integration_id} — não um payload de activity.
Referência simples quando o grupo já existe:
{
"task_group": { "integration_id": "GRP-MORNING" }
}
Se o grupo não existir e você enviar só a referência simples (sem definição inline), recebe 404:
{
"message": "Task group with integration_id \"GRP-MORNING\" was not found in this domain."
}
Definição inline somente para criar (primeira vez; 422 se o grupo já existir; precisa de ≥2 itens):
{
"task_group": {
"integration_id": "GRP-MORNING",
"name": "Rotina da manhã",
"task_group_items": [
{
"integration_id": "TASK-CHECK",
"sequence": 1,
"task": {
"integration_id": "GRP-ITEM-1",
"name": "Checar equipamento",
"status_photo": false,
"status_obs": false
}
},
{
"integration_id": "TASK-CLEAN",
"sequence": 2,
"task": {
"integration_id": "GRP-ITEM-2",
"name": "Limpeza",
"status_photo": true,
"status_obs": false
}
}
]
}
}
Proibido nas linhas da activity: task_id, task_group_id, is_free_task, is_finished.
Preencher planejamento em linhas Forma B existentes (atualização)
Reenvie POST ou PUT na activity com o mesmo grupo inline e envie campos de preenchimento na etapa que deseja atualizar. Casamento por inner task.integration_id (id da linha da activity), não pelo id de catálogo externo:
{
"integration_id": "ACT-GROUP-FLOW-4",
"name": "Job da tarde",
"description": "Atualizar horário planejado na última etapa",
"schedule_date": "2026-07-08T19:45:00Z",
"activity_tasks": [
{
"task_group": {
"integration_id": "GRP-MORNING-TEST",
"name": "Rotina da manhã",
"task_group_items": [
{
"integration_id": "CAT-GRP-STEP-3",
"sequence": 3,
"task": {
"integration_id": "GRP-ITEM-3",
"name": "Assinatura",
"status_photo": true,
"status_obs": false,
"expected_start_date": "2026-07-08T19:45:00Z",
"expected_duration_seconds": 900
}
}
]
}
}
]
}
Você pode repetir todos os itens do grupo no payload (divergências em nome/flags são ignoradas) ou só os itens onde envia campos de preenchimento — linhas existentes omitidas não são apagadas.
Exemplo real — várias linhas avulsa (entrega de concreto)
Mesmo mapeamento em toda linha: outer = código do catálogo (PMIX-CHARGING-*), inner = seu id de linha (6566-1, 6566-2, …):
{
"integration_id": "6566",
"name": "Entrega de concreto",
"description": "Cliente | Obra",
"schedule_date": "2026-07-02T22:04:54-03:00",
"activity_tasks": [
{
"integration_id": "PMIX-CHARGING-DOSING",
"task": {
"integration_id": "6566-1",
"name": "Dosagem",
"status_photo": false,
"status_obs": false,
"expected_start_date": "2026-07-02T22:04:00-03:00",
"expected_finish_date": "2026-07-02T22:22:00-03:00",
"expected_duration_seconds": 1080
}
},
{
"integration_id": "PMIX-CHARGING-OUTBOUND",
"task": {
"integration_id": "6566-2",
"name": "A caminho",
"status_photo": false,
"status_obs": false,
"expected_start_date": "2026-07-02T22:22:00-03:00",
"expected_finish_date": "2026-07-02T22:34:00-03:00",
"expected_duration_seconds": 720
}
}
]
}
Agenda parcial é permitida: você pode enviar só expected_start_date, só expected_finish_date, ou omitir ambos numa linha. expected_duration_seconds omitido é gravado como 0.
O que a API preenche por você
| Campo | O que acontece |
|---|---|
uuid |
Gerado se ausente (necessário no mobile) |
sequence |
Contador sequencial pela ordem do array |
is_next_task |
Primeira linha padrão true |
is_free_task na linha |
Não envie — true na Forma A; false nas linhas expandidas de grupo |
Regras para activities (obrigatório)
| Tópico | Regra |
|---|---|
| Status | Sempre criada como Pending (0). Campo status é ignorado se enviado. |
| Agendada | is_scheduled é forçado true. Não é possível criar jobs não agendados via API pública. |
| Responsável | responsible_user_id proibido na escrita — definido pelo token autenticado. |
| Horário | schedule_date obrigatório; armazenado em UTC. Qualquer datetime válido é aceito (incluindo passado). |
| Início / fim | start_at proibido na escrita. App define quando o colaborador inicia. |
| Edição | Só enquanto Pending. Após início do colaborador → 422 na atualização. |
| Tasks da activity (atualização) | Linhas existentes casadas por activity_tasks.integration_id. Novas linhas são criadas; linhas existentes aceitam só campos de preenchimento (duration_seconds, duration_paused_seconds, duration_finished_seconds, expected_start_date, expected_duration_seconds). Alteração estrutural ou exclusão → 422. Linhas omitidas não são apagadas. is_finished é proibido (edições só enquanto Pending; conclusão é definida pelo app móvel). |
| Transações | Todas as linhas resolvidas de uma vez — não há salvamento parcial. |
Horário agendado e fusos
Envie ISO-8601 com fuso, por exemplo:
2026-06-11T14:00:00Z(UTC)2026-06-11T11:00:00-03:00(Brasil, convertido para UTC pela API)
A API não aplica regra de “deve ser no futuro” nem “5 minutos no passado” na escrita da API Pública. Se você enviar um horário no passado, ele é gravado como enviado (UTC) — posicionamento na agenda no app é responsabilidade da sua integração.
Status da activity (para polling)
| Status | Número | Significado |
|---|---|---|
| Pending | 0 | Na agenda, não iniciada |
| In progress | 1 | Colaborador iniciou |
| Finished | 2 | Concluída |
| Expired | 3 | Expirada |
| Opened | 4 | Precisa de atenção |
Consulte com: GET /activities/SEU-INTEGRATION-ID
Regras para task groups (POST /task-groups e criação inline em activities)
Task groups são templates reutilizáveis em branco. Guardam o blueprint da checklist (chaves de task do catálogo, ordem das etapas, flags de foto/nota no catálogo). Não são o lugar para enviar agenda por activity nem dados de execução preenchidos — use linhas Forma A em POST /activities para isso.
Task groups usam o mesmo mapeamento outer/inner das linhas avulsas da activity:
| Campo que você envia | Gravado em | Significado |
|---|---|---|
Outer task_group_items[].integration_id |
tasks.integration_id (catálogo) |
Chave da task no catálogo; find-or-create no domain. |
Inner task.integration_id |
task_group_items.integration_id |
Id único desta linha dentro do grupo. |
| Regra | Explicação |
|---|---|
| Ids de catálogo + linha | Outer = chave do catálogo; inner task.integration_id = id da linha (espelho da Forma A na activity). |
| Campos inline da task | task interno precisa de name, status_photo e status_obs. |
| Pelo menos 2 tasks na 1ª criação | Grupo novo precisa de 2+ itens. Atualizações podem ter 1+. |
sequence obrigatório |
Cada item precisa de sequence (inteiro ≥1) — ordena itens dentro do grupo, não a activity. |
| Fluxo recomendado | Crie uma vez via POST /task-groups; referencie com integration_id simples em cada activity. Criação inline na activity só quando o grupo é novo. |
| Template em branco | O grupo guarda só estrutura (chaves de catálogo, ordem, flags). expected_start_date / expected_duration_seconds / duration_* por activity vão no payload da activity (task_group_items[].task), não no template do grupo. |
| Anexar via activity | Você pode adicionar novos task_group_items a um template existente a partir de um payload de activity. Campos existentes do template (name, detalhes do item) são imutáveis via activity — divergências são ignoradas. Altere o template compartilhado via /task-groups. |
| Expandir = materializar linhas | Referência simples ou expandida cria linhas activity_tasks faltantes na activity; linhas existentes são somente preenchimento (veja regras de atualização de activity). |
Exemplo (grupo novo mínimo):
{
"integration_id": "GRP-MORNING",
"name": "Rota da manhã",
"task_group_items": [
{
"integration_id": "TASK-CHECK",
"sequence": 1,
"task": {
"integration_id": "GRP-ITEM-1",
"name": "Checar equipamento",
"status_photo": false,
"status_obs": false
}
},
{
"integration_id": "TASK-CLEAN",
"sequence": 2,
"task": {
"integration_id": "GRP-ITEM-2",
"name": "Limpeza",
"status_photo": true,
"status_obs": false
}
}
]
}
Quando uma activity referencia este grupo (Forma B referência simples), cada item do grupo vira uma linha activity_tasks; linhas expandidas usam os ids internos do grupo (GRP-ITEM-1, …) como activity_tasks.integration_id e têm is_free_task: false.
Usuários do domain (GET /users)
Por quê: mapear colaboradores ERP para usuários Puzl e escolher qual token de integração usar ao criar activities. A escrita de activities não aceita responsible_user_id no corpo — o dono do token autenticado vira o responsável.
curl -s "https://puzltask-api.puzl.place/api/v1/public/users" \
-H "X-Integration-Key: puzl_usr_seu_token"
Resposta (sem paginação — lista completa):
{
"message": "",
"data": [
{ "user_id": "abc-123...", "user_name": "Maria Silva" }
]
}
| Campo | Uso |
|---|---|
user_id |
Mapeamento ERP; filtro opcional em GET /activities (parâmetro responsible_user_id) |
user_name |
Somente exibição |
Não há campo de código ERP em users — mantenha uma tabela de mapeamento no seu sistema (colaborador ERP → user_id) se necessário.
O que o colaborador vê no celular
| Etapa | O que acontece |
|---|---|
| Você cria a activity | Job aparece na Agenda no horário agendado |
| Colaborador toca em Iniciar | Status → In progress; start_at registrado |
| Colaborador conclui etapas | Fotos, notas, etc. conforme configuração da task |
| Colaborador finaliza | Status → Finished; finished_at registrado |
Coluna de horário na agenda:
| Estado do job | Horário exibido à esquerda |
|---|---|
| Pending | Horário agendado (schedule_date) |
| Iniciado ou concluído | Horário real de início (start_at) |
O responsável na activity deve corresponder ao usuário logado no app, senão não verá o job (ou não poderá retomá-lo).
Para planejamento por linha exibido ao usuário, priorize expected_start_date + expected_duration_seconds. Continue enviando expected_finish_date só quando sua integração precisar de consistência de validação/armazenamento no backend; não assuma que ele é exibido de forma proeminente na UI mobile.
Criar vs atualizar (POST e PUT)
Crítico — não crie uma activity nova por engano.
A API Pública mira um registro pelo seuintegration_idda activity (ex.:ACT-MANUAL-1).
Se esse código estiver ausente no corpo de umPOST, toda chamada cria uma nova activity.
Para atualizar o mesmo job de novo (preencher linhas, anexar tasks), você deve:
POST /activitiescom o mesmointegration_idno corpo → 200 OK, ouPUT /activities/{mesmo-codigo}(o path identifica o registro; não envieintegration_idno corpo).
POST = criar ou atualizar pelo seu código
Mesma URL POST /activities, chave de upsert = integration_id do corpo:
integration_id no corpo |
Resultado | HTTP |
|---|---|---|
| Presente e novo | Cria essa activity | 201 Created |
| Presente e já existe | Atualiza essa activity | 200 OK |
| Ausente / null | Sempre cria uma nova activity (não é atualização) | 201 Created |
Sempre envie um integration_id estável na criação se precisar atualizar esse job depois (retentativas, preencher campos, anexar linhas).
PUT = atualizar um registro conhecido
URL contém seu código: PUT /activities/ACT-MANUAL-1
| Regra | Detalhe |
|---|---|
| Path | Deve ser o integration_id da activity que você criou antes |
integration_id no corpo |
Proibido — só o path define o registro |
| Mesmo job | Usar outro código no path atualiza (ou 404) uma activity diferente — não anexa à que você queria |
O que a API Pública sobrescreve e o que não sobrescreve
Edições são permitidas só enquanto a activity está Pending. Depois que o colaborador toca em Iniciar, atualizações retornam 422.
| Alvo | Atualizado pelo payload da activity? | Notas |
|---|---|---|
Cabeçalho da activity (name, description, schedule_date, …) |
Sim | Enviado em todo POST / PUT. schedule_date aceita qualquer datetime válido (UTC ao gravar). |
Linha activity_tasks existente — duration_seconds, duration_paused_seconds, duration_finished_seconds |
Sim (somente preenchimento) | Casamento por activity_tasks.integration_id. Na Forma B, envie em task_group_items[] (nível do task interno ou do item). |
Linha existente — expected_start_date, expected_duration_seconds |
Sim (somente preenchimento) | Mesmas regras de casamento que durações. Estes campos dirigem o planejamento no app móvel. |
Linha existente — expected_finish_date |
Não | Estrutural na atualização → 422 se tentar alterar numa linha existente. |
Linha existente — task_id, sequence, task_group_id, is_free_task |
Não | Estrutural → 422. |
Linha existente — is_finished, start_at |
Não | is_finished é proibido; conclusão e início real são só do app. |
| Linhas existentes omitidas | Mantidas | O payload não apaga linhas da checklist que já existem. |
| Novas linhas no payload | Criadas | Novos valores de activity_tasks.integration_id são anexados à activity. |
Template de task group (name, ordem, name do catálogo, status_photo, …) via activity |
Não | Divergências são ignoradas; só novos task_group_items são anexados. Altere o template compartilhado via /task-groups. |
Task de catálogo (tabela tasks) via activity |
Não | Find-or-create só na primeira vez; campos existentes do catálogo não são atualizados a partir de payloads de activity. |
Casamento de linha Forma B: linhas expandidas usam inner task.integration_id (ex.: GRP-ITEM-3) como activity_tasks.integration_id, não a chave de catálogo externa (CAT-GRP-STEP-3). Envie campos de preenchimento na entrada task_group_items[].task correspondente.
Todos os endpoints
Base: /api/v1/public
| O que você quer | Método | Caminho |
|---|---|---|
| Verificar se a API está no ar | GET | /health (sem chave) |
| Verificar seu token | GET | /ping |
| Listar colaboradores | GET | /users |
| Listar / criar tasks | GET, POST | /tasks |
| Uma task | GET, PUT, DELETE | /tasks/{seu-codigo} |
| Listar / criar grupos | GET, POST | /task-groups |
| Um grupo | GET, PUT, DELETE | /task-groups/{seu-codigo} |
| Reordenar grupos | PUT | /task-groups/sequences |
| Listar / criar custom fields | GET, POST | /custom-fields |
| Um custom field | GET, PUT, DELETE | /custom-fields/{seu-codigo} |
| Excluir opção de field | DELETE | /custom-fields/{cf}/items/{item} |
| Listar / criar activities | GET, POST | /activities |
| Uma activity | GET, PUT, DELETE | /activities/{seu-codigo} |
Referência de campos
Task — campos obrigatórios
| Campo | Obrigatório | Notas |
|---|---|---|
integration_id |
Recomendado | Seu código |
name |
Sim | |
status_photo |
Sim | boolean — permitir fotos? |
status_obs |
Sim | boolean — permitir notas? |
is_free_task |
Sim | boolean — etapa avulsa? |
description |
Não | |
is_active, is_favorite |
Não | padrão true / false |
Não envie: id, status_status (removido), task_category_id.
Activity — campos obrigatórios
| Campo | Obrigatório | Notas |
|---|---|---|
integration_id |
Fortemente recomendado | Seu código estável do job. Obrigatório na prática se for atualizar depois. Omitir → todo POST cria uma nova activity. |
name |
Sim | |
description |
Sim | |
schedule_date |
Sim | ISO-8601; normalizado para UTC; horários no passado permitidos |
responsible_user_id |
Não (proibido) | Definido pelo servidor a partir do token; UUID interno opcional só no filtro de GET /activities |
activity_tasks |
Não* | *Normalmente você envia linhas da checklist |
Não envie: start_at, status, finished_at, id, token, short_url.
Opcional: schedule_finish, comments, campos GPS, activity_custom_fields.
Linhas da activity (activity_tasks[])
Cada entrada é Forma A (linha avulsa) ou Forma B (task_group). Não envie task_id, task_group_id nem is_free_task. sequence externo é opcional (inteiro >= 1); omita para a API definir a ordem pelo array.
| Forma | Campos obrigatórios | expected_* / duration_* por activity? |
|---|---|---|
| A — linha avulsa | Outer integration_id (catálogo), inner task com integration_id (id de linha), name, status_photo, status_obs |
Sim — no task interno (criação e atualização somente preenchimento) |
| B — grupo simples | Só task_group.integration_id (grupo deve existir → senão 404) |
Não na referência simples sozinha — só expansão |
| B — grupo inline (criar, anexar ou preencher) | task_group com integration_id; name + task_group_items[] opcionais |
Sim — em task_group_items[].task (expected_start_date, expected_duration_seconds, duration_*; expected_finish_date só na criação, não na atualização de linha existente) |
Agenda no task interno (gravada em activity_tasks, não no catálogo): expected_start_date, expected_finish_date, expected_duration_seconds (inteiro ≥ 0). Veja O que a API Pública sobrescreve e o que não sobrescreve para regras de preenchimento vs estrutural na atualização.
Custom fields — criar o grupo primeiro (POST /custom-fields)
Um custom field é um grupo reutilizável de campos. Crie-o uma vez com POST /custom-fields e depois anexe-o a activities pelo integration_id. Corpo:
{
"integration_id": "CF-VEHICLE",
"name": "Dados do veículo",
"is_active": true,
"custom_field_items": [
{ "integration_id": "ITEM-PLATE", "type": "PLATE", "name": "Placa", "is_required": true },
{ "integration_id": "ITEM-NOTES", "type": "LONG_TEXT", "name": "Observações", "is_required": false }
]
}
| Campo | Obrigatório | Notas |
|---|---|---|
integration_id |
Recomendado | Seu código; permite que activities referenciem o grupo e torna a chamada idempotente |
name |
Sim | |
is_active |
Não | Padrão true |
custom_field_items |
Sim | Pelo menos 1; cada um precisa de type (ver tipos abaixo) e name; is_required padrão false |
POST faz upsert por integration_id (201 novo, 200 se já existe). Um grupo por requisição — envie uma chamada por grupo (ex.: CF-VEHICLE, depois CF-CHECKLIST). Atualizar um grupo substitui sua lista de itens, então envie todos os itens que deseja manter.
Custom fields — grupos são atômicos
Um custom field é, na verdade, um grupo de campos (ex.: “Dados do veículo” → placa, observações). Grupos são atômicos: ao anexar um a uma activity, todos os seus campos são adicionados automaticamente para o operador preencher — você não escolhe um subconjunto, e o flag obrigatório/opcional de cada campo vem da definição. Por isso, para anexar um grupo existente, basta referenciá-lo — você não lista os campos:
{
"activity_custom_fields": [
{ "custom_field": { "integration_id": "CF-1" } }
]
}
Todos os campos de CF-1 aparecem em branco na tela do operador. Você só descreve campos ao criar um grupo novo (envie custom_field.name + custom_field_items[]); fazer isso para um grupo que já existe é rejeitado com 422 (gerencie grupos existentes pelos endpoints /custom-fields).
Custom fields — jeito fácil (atalho custom_fields) para pré-preencher
Se você já sabe algumas respostas e quer pré-preenchê-las, envie um objeto plano com chave por integration_id. O grupo inteiro continua sendo anexado de forma atômica; os valores enviados preenchem os campos correspondentes, o restante fica em branco:
{
"custom_fields": {
"CF-1": { "ITEM-1": "Resposta", "ITEM-2": "Outra" },
"CF-2": { "ITEM-PLATE": "ABC-1234" }
}
}
Cada chave pode ser o integration_id do recurso ou o id interno (ULID) — então campos criados no app Puzl (que não têm integration_id) também funcionam: use o ULID de GET /custom-fields como chave. Chave externa = grupo de custom field; chave interna = item; valor = a resposta. Só referencia grupos existentes (use a forma aninhada activity_custom_fields[] com custom_field.name para criar um grupo inline), valores são gravados como string e a resposta do GET continua no formato aninhado.
Tipos de item de custom field
TEXT, INT, DECIMAL, EIN, PLATE, DATE, PHONE, EMAIL, LONG_TEXT
Endpoints de listagem — paginação
Estes suportam limit (máx. 100), page, paginate_type, filtros:
GET /tasksGET /task-groupsGET /custom-fieldsGET /activities
Formato de resposta de listagem:
{
"data": {
"items": [ ... ],
"page": 1,
"limit": 15,
"total": 42
}
}
GET /users retorna array simples em data — sem páginas.
Sincronização incremental: GET /activities?timestamp=1716200000000 (milissegundos).
Quando algo dá errado
Correções rápidas
| Problema | Causa provável | Correção |
|---|---|---|
| 401 em tudo | Token ruim ou header ausente | Verifique X-Integration-Key (deve ser token puzl_usr_... válido) |
| Job não na agenda | Só criou uma task | Crie uma activity |
| Job não na agenda | Responsável errado | Use o token puzl_usr_... desse colaborador em X-Integration-Key ao criar a activity |
| 422 schedule_date | Datetime ausente ou inválido | Envie schedule_date ISO-8601 obrigatório |
| 422 responsible_user_id | Enviou no corpo da escrita | Remova — o responsável vem do token |
| 422 start_at / finished_at | Você enviou um deles | Remova — são definidos pelo app |
| 404 ao buscar task | Task ausente | POST /tasks antes ou crie inline |
| 422 editando activity | Colaborador já iniciou | Aguarde concluir; crie novo job se necessário |
| Activity nova a cada POST | integration_id ausente no corpo, ou código diferente a cada vez |
Reutilize o mesmo integration_id da activity: corpo no POST ou path no PUT |
| 422 linha da activity | Alteração estrutural em linha existente | Casamento por activity_tasks.integration_id; só campos de preenchimento podem mudar: duration_*, expected_start_date, expected_duration_seconds. Não envie is_finished. |
| 422 activity_tasks | Formato de linha errado | Forma A: id de catálogo externo + task interno; Forma B: só task_group. Sem task_id ou task_group_id |
| 422 activity_tasks ids | Id de linha no campo errado | Outer = código do catálogo; inner task.integration_id = seu id de linha (ex.: 6566-1) |
| Nome do grupo / campos de item diferem no re-POST | Esperado se o app renomeou o template | A activity ainda sucede; campos do template não são sobrescritos. Para alterar o template compartilhado, use PUT /task-groups |
| 404 task group na activity | Referência simples, grupo ausente | Mensagem: Task group with integration_id "…" was not found in this domain. — crie via POST /task-groups ou definição inline primeiro |
| Horários planejados não atualizam (Forma B) | Só referência simples ou id de linha errado | Inclua task_group_items[].task com campos de preenchimento; casamento pelo inner task.integration_id (ex.: GRP-ITEM-3), não pelo id de catálogo externo (CAT-GRP-STEP-3) |
| 422 expected_finish_date na atualização | Tentou alterar término em linha existente | expected_finish_date é estrutural na atualização — defina só na criação, ou use Forma A |
| 422 expected_finish_date | Término anterior ao início na mesma linha | Corrija datetimes no task interno |
| 422 expected_duration_seconds | Valor negativo | Use inteiro ≥ 0 ou omita |
| 422 task group | Criação exige ≥2 itens | Adicione outro item em task_group_items, cada um com sequence |
GET /users vazio |
Sem membros | Adicione usuários no admin PuzlTask |
FAQ
Posso usar Bearer token em vez da chave de integração?
Não. A API Pública usa somente X-Integration-Key.
Qual token de integração devo usar?
Use o token por usuário puzl_usr_... do colaborador que deve ver e executar o job — no header X-Integration-Key em toda escrita. Gere no admin PuzlTask (Cadastros → Usuários → Token). Não coloque o token (nem user_id) em responsible_user_id na escrita de activities — esse campo é proibido.
Posso usar nosso código de colaborador ERP como responsible_user_id?
Não. Mapeie ERP → user_id via GET /users, depois autentique escritas de activities com o token puzl_usr_... dessa pessoa em X-Integration-Key.
Posso definir quando o colaborador iniciou?
Não. start_at é definido pelo app móvel.
Qual a diferença entre 200 e 201 no POST?
201 = registro novo. 200 = integration_id existente atualizado.
Posso atualizar um job depois que o colaborador iniciou?
Não. Atualizações só funcionam com status Pending.
Antes enviávamos ids de linha no outer integration_id e códigos de catálogo dentro de task — ainda vale?
Não. Era o contrato anterior. Inverta: outer = código da task no catálogo (reutilizado entre activities), inner task.integration_id = id de linha único por activity. sequence externo é opcional agora — omita para a ordem vir do array, ou envie para definir explicitamente.
Task groups compartilham tasks do catálogo?
Sim. POST /task-groups standalone e itens inline usam mapeamento outer/inner: outer find-or-create no catálogo; inner é o id da linha dentro do grupo (task_group_items.integration_id = inner task.integration_id). Planejamento por activity é enviado no payload da activity, não armazenado no template do grupo.
Nosso ERP envia horários planejados em todo passo de toda entrega — devemos usar task group?
Use Forma A quando cada etapa precisa de seu próprio outer id de catálogo e agenda completa (incluindo expected_finish_date) sem template compartilhado. Use Forma B quando o mesmo template de checklist é reutilizado e você só precisa de expected_start_date + expected_duration_seconds (e duration_*) nas linhas da activity — envie em task_group_items[].task ao criar ou atualizar a activity.
Devemos criar task groups em POST /activities ou em POST /task-groups?
Melhor prática: POST /task-groups uma vez, depois { "task_group": { "integration_id": "..." } } simples em toda activity. Você pode anexar novos itens de template a partir de uma activity depois; campos existentes do template não são sobrescritos via activity (divergências do ERP são ignoradas). Para renomear ou editar itens existentes, use /task-groups.
Segurança
- Guarde o token de integração em um gerenciador de segredos, não no git ou apps móveis.
- Use tokens separados para staging e produção.
- Rotacionar o token invalida o anterior imediatamente.
- Tokens ficam somente no seu servidor — nunca em app de celular.
Códigos HTTP (lista resumida)
| Código | Significado |
|---|---|
| 200 | OK / atualizado |
| 201 | Criado |
| 204 | Excluído |
| 401 | Problema com o token |
| 404 | Não encontrado no seu domain |
| 422 | Dados inválidos ou regra de negócio |
OpenAPI (para ferramentas como Postman)
Gerar:
docker exec puzl-task-api php artisan l5-swagger:generate public
Navegar: /api/documentation/public
Adicione o header X-Integration-Key ao testar requisições no Swagger.
Documentos relacionados
| Documento | Finalidade |
|---|---|
| public-api-v1.md | Lista de endpoints em uma página (download) |
| Entenda o produto | Conceitos de domain, tasks e activities |
API Pública v1 — autenticação por token por usuário (puzl_usr_ em X-Integration-Key), mapeamento outer/inner de integration_id em activity_tasks, espelho em task groups, lista de usuários, start_at/finished_at proibidos na escrita, regras de atualização fill-only, comportamento da agenda no mobile.