PuzlTask

API Pública — 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.

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 apenas POST /tasks.

As cinco regras mais importantes

  1. Autenticação: toda requisição (exceto health) precisa do header X-Integration-Key: puzl_usr_... (token por usuário) — não Bearer/JWT.
  2. Seus códigos: use integration_id para seus códigos de pedido/visita (ex.: ACT-SO-8842). Nunca envie o id interno do PuzlTask nos corpos de criação/atualização.
  3. Responsável: na escrita de activities, não envie responsible_user_id — a API define com base no usuário do token autenticado.
  4. 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.
  5. 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 produtoTutorial prático
Montando a integração AutenticaçãoEnviar um job do seu ERPLinhas da checklist da activity
Suporte / depuração Quando algo dá errado
Precisa de todos os campos Todos os endpointsReferê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

  1. Abra o PuzlTask como o usuário atribuído.
  2. Vá em Agenda no dia agendado.
  3. Você deve ver “Meu primeiro job via API”.
  4. Inicie e conclua no app.
  5. Chame GET /activities/TUTORIAL-ACT-1 de 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-groups uma vez, depois { "task_group": { "integration_id": "..." } } em cada activity. Criação inline de grupo dentro da activity só na primeira vez que aquele integration_id aparece.

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 token puzl_usr_... do colaborador que deve executar o job (não responsible_user_id no corpo)
  • [ ] schedule_date é um datetime ISO-8601 válido (armazenado em UTC)
  • [ ] Outer integration_id em 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-MORNING existe 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, status ou finished_at nas linhas da activity (sequence externo é 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-groups standalone ou criação inline na activity) define quais tasks do catálogo aparecem e em qual ordem. O template do grupo não armazena expected_* por activity — esses valores ficam em cada linha activity_tasks quando 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 via task_group_items. Campos existentes do template (name, sequence do 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 atualizar expected_start_date + expected_duration_seconds (e duration_*) nas linhas da activity — envie em task_group_items[].task com o inner task.integration_id correspondente (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_date ainda é 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 qualquer status enviado e sempre cria a task com status: 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 envietrue 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 seu integration_id da activity (ex.: ACT-MANUAL-1).
Se esse código estiver ausente no corpo de um POST, toda chamada cria uma nova activity.
Para atualizar o mesmo job de novo (preencher linhas, anexar tasks), você deve:

  1. POST /activities com o mesmo integration_id no corpo → 200 OK, ou
  2. PUT /activities/{mesmo-codigo} (o path identifica o registro; não envie integration_id no 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 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 /tasks
  • GET /task-groups
  • GET /custom-fields
  • GET /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.