PuzlTask

Language Português English

Public API — Integration Guide

Easy-to-read guide for developers, project managers, and support teams connecting an ERP (or any external system) to PuzlTask.

PuzlTask Public API — Integration Guide

Easy-to-read guide for developers, project managers, and support teams connecting an ERP (or any external system) to PuzlTask.

API address: https://YOUR-SERVER/api/v1/public/...
Interactive docs (Swagger): https://YOUR-SERVER/api/documentation/public
Short cheat sheet: public-api-v1.md


Read this first (2 minutes)

What PuzlTask does

PuzlTask sends jobs to field workers’ phones. Your system creates the job; the worker completes it in the mobile app.

The one thing people get wrong

You create… Shows on the phone?
A task (checklist template) No — it is only a reusable template
An activity (scheduled job + assignee + date) Yes — this appears on the agenda

Remember: To put work on someone’s schedule, always call POST /activities, not only POST /tasks.

The five rules that matter most

  1. Auth: every request (except health) needs header X-Integration-Key: puzl_usr_... (per-user token) — not Bearer/JWT.
  2. Your codes: use integration_id for your order/visit codes (e.g. ACT-SO-8842). Never send PuzlTask internal id in create/update bodies.
  3. Assignee: on activity writes, do not send responsible_user_id — the API sets it to the authenticated token user.
  4. Schedule time: schedule_date is required (ISO-8601, normalized to UTC). The Public API does not reject past times — your integration owns that choice.
  5. Start time: do not send start_at — the mobile app sets it when the worker taps Start.

Typical flow (picture)

Your ERP                          PuzlTask API                    Mobile app
   |                                   |                              |
   |-- GET /users -------------------->|  Who can I assign?           |
   |<-- user_id, user_name -------------|                              |
   |                                   |                              |
   |-- POST /activities -------------->|  Save scheduled job          |
   |                                   |----------------------------->|  Shows on agenda
   |                                   |                              |  Worker starts & finishes
   |-- GET /activities/ACT-123 ------->|  Status: FINISHED            |

Who should read which part?

Role Read
New to PuzlTask Understand the productHands-on tutorial
Building the integration AuthenticationSend a job from your ERPActivity checklist lines
Support / debugging When something goes wrong
Need every field All endpointsField reference

Understand the product

Words we use

Word Plain meaning
Domain Your company’s workspace in PuzlTask. The user token resolves to one domain.
Task A checklist step template (“Take photo”, “Get signature”). Stored in a catalog.
Activity A scheduled job for one worker on one date/time. This is what they see on the phone.
Activity task One line in the job’s checklist (step 1, step 2, …).
Task group A reusable blank template: which catalog steps run, in which order. Not where you send per-visit scheduling or filled-in line data.
Integration key Per-user secret for the API (puzl_usr_...). Sent in X-Integration-Key.
integration_id Your code for a record (ERP order number, visit id).
id PuzlTask internal code — read only, do not send on create.

How things nest

Domain (your company workspace)
 ├── Users          → people with the app
 ├── Tasks          → templates (catalog)
 ├── Task groups    → bundled templates
 ├── Custom fields  → extra questions (optional)
 └── Activities     → scheduled jobs ⭐ main integration target
      └── Activity tasks → checklist lines inside the job

Public API vs mobile app API

Public API (this guide) Mobile / admin API
Used by ERP, partners PuzlTask app
Login X-Integration-Key JWT Bearer token
URL /api/v1/public/... /api/activities, etc.

Integrators should only use the Public API unless PuzlTask tells you otherwise.


Before you start (checklist)

Ask your PuzlTask administrator for:

  • [ ] API base URL (example: https://api.puzltask.com)
  • [ ] Per-user integration token (puzl_usr_ + random characters) for the responsible teammate
  • [ ] At least one active user in the domain (someone who can test on the phone)

Getting a token: an admin generates it in PuzlTask (Records → Users → Edit user → Token → Create, or internal API PUT /api/domains/{userId}/users/token). The plain token is shown once — save it in a password manager. Regenerating turns off the old token.

You will need on your side:

  • [ ] A tool to send HTTPS + JSON (curl, Postman, or your ERP HTTP client)
  • [ ] Accurate server clock (schedule validation uses UTC time)

Authentication

Every protected call needs:

X-Integration-Key: puzl_usr_your_user_token_here
Content-Type: application/json

(on POST and PUT)

Test the key:

curl -s "https://YOUR-SERVER/api/v1/public/ping" \
  -H "X-Integration-Key: puzl_usr_your_user_token_here"

Good response:

{ "message": "", "data": { "status": "pong" } }

Wrong or missing key → 401 Unauthorized.

Do not use Authorization: Bearer ... on public routes.


How responses look

Success:

{
  "message": "",
  "data": { ... }
}

Validation problem → 422, details usually here:

{
  "message": "The given data was invalid.",
  "data": {
    "name": ["The name field is required."]
  }
}

Delete success → 204 with empty body.


Hands-on tutorial

Replace YOUR-SERVER and YOUR-KEY below.

Step 1 — Is the API online?

curl -s "https://YOUR-SERVER/api/v1/public/health"

Expect: "status": "ok"

Step 2 — Is your key valid?

curl -s "https://YOUR-SERVER/api/v1/public/ping" \
  -H "X-Integration-Key: YOUR-KEY"

Expect: "status": "pong"

Step 3 — Who can I assign work to?

curl -s "https://YOUR-SERVER/api/v1/public/users" \
  -H "X-Integration-Key: YOUR-KEY"

Example answer:

{
  "message": "",
  "data": [
    { "user_id": "550e8400-e29b-41d4-a716-446655440000", "user_name": "Maria Silva" }
  ]
}

Copy user_id for ERP ↔ Puzl mapping. On activity writes, do not send responsible_user_id — authenticate with that user’s puzl_usr_... token in X-Integration-Key instead.

Empty list? No active users in the domain — add users in PuzlTask first.

Step 4 — Create a task template (optional but good for learning)

curl -s -X POST "https://YOUR-SERVER/api/v1/public/tasks" \
  -H "X-Integration-Key: YOUR-KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "integration_id": "TUTORIAL-TASK-1",
    "name": "Confirm arrival",
    "description": "Mark arrival at site",
    "status_photo": true,
    "status_obs": true,
    "is_free_task": true
  }'

Expect 201 Created. This task is not on anyone’s phone yet.

Step 5 — Create the scheduled job (activity)

Pick a schedule_date (UTC). Use the integration key of the worker who should see the job in X-Integration-Key (Maria’s puzl_usr_... token — not her user_id in the body).

curl -s -X POST "https://YOUR-SERVER/api/v1/public/activities" \
  -H "X-Integration-Key: puzl_usr_MARIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "integration_id": "TUTORIAL-ACT-1",
    "name": "My first API job",
    "description": "Test from integration guide",
    "schedule_date": "2026-12-01T14:00:00Z",
    "activity_tasks": [
      {
        "integration_id": "TUTORIAL-TASK-1",
        "task": {
          "integration_id": "TUTORIAL-LINE-1",
          "name": "Tutorial step",
          "status_photo": false,
          "status_obs": false
        }
      }
    ]
  }'

Expect 201 Created, "status": 0 (pending).

Step 6 — Check on the phone

  1. Open PuzlTask as the assigned user.
  2. Go to Agenda / Schedule on the scheduled day.
  3. You should see “My first API job”.
  4. Start and finish it in the app.
  5. Call GET /activities/TUTORIAL-ACT-1 again — status becomes finished; start_at is filled by the app, not by you.

Send a job from your ERP

Real-world example: ERP order SO-8842 → field visit for technician Maria.

Order of API calls

1. GET  /users              → map ERP employee → user_id (pick whose token to use)
2. POST /tasks              → optional: create catalog tasks
3. POST /task-groups        → recommended: create blank route templates once
4. POST /activities         → dispatch the job ⭐ (use assignee's token in X-Integration-Key)
5. GET  /activities/ACT-... → later: check if finished

Task groups: prefer POST /task-groups once, then { "task_group": { "integration_id": "..." } } on each activity. Inline group create inside an activity is allowed only the first time that integration_id is seen.

Full example body (copy and adapt)

{
  "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
      }
    }
  ]
}

Before sending, check:

  • [ ] X-Integration-Key is the puzl_usr_... token of the worker who should execute the job (not responsible_user_id in the body)
  • [ ] schedule_date is a valid ISO-8601 datetime (stored in UTC)
  • [ ] Outer integration_id on each free-task line points to the catalog task (find-or-create in your domain)
  • [ ] Inner task.integration_id is unique per activity line
  • [ ] GRP-MORNING exists if you reference it by integration_id only (or send a full create-only definition when the group does not exist yet)
  • [ ] You did not include start_at, status, or finished_at on activity lines (outer sequence is optional)

Activity checklist lines (activity_tasks[])

Each item in activity_tasks[] is either a free-task line (Pattern A) or a task-group reference (Pattern B). You may send an optional outer sequence (integer >= 1) on a Pattern A line to set the row order explicitly; when omitted, the API assigns sequence from the array order.

Form A vs Form B — choose the right shape

Need Use Why
Full per-step planning at create — every step with its own catalog code, expected_finish_date, or line ids that are not tied to a shared group template Pattern A (free-task line) Simplest shape when each step is independent in the payload.
Same checklist structure every time — reuse a group template; optionally set or update planned start/duration per step on the activity Pattern B (task group) Expands the shared template into activity_tasks rows. Send expected_start_date / expected_duration_seconds on task_group_items[].task when creating or updating lines (fill-only on update).

Task groups are shared structure, not shared scheduling storage.
A task group (standalone POST /task-groups or inline create on an activity) defines which catalog tasks appear and in what order. The group template itself does not store per-activity expected_* — those live on each activity_tasks row when you send them on an activity payload.

When you reference an existing group on an activity ({ "task_group": { "integration_id": "..." } }), the API expands the template into activity lines. You may append new items to the shared template via task_group_items. Existing template fields (name, item sequence, catalog link, task name) are immutable via the activity endpoint — ERP mismatches are ignored (activity succeeds; app/template data is not overwritten).

Pattern A is still the best fit when every step needs its own catalog outer id and full scheduling (including expected_finish_date) at create time with no shared group. Pattern B fits when the same checklist template is reused and you only need to set or update expected_start_date + expected_duration_seconds (and execution duration_*) on the activity lines — send them on task_group_items[].task matching inner task.integration_id (e.g. GRP-ITEM-3).

Two integration ids on every free-task line (Pattern A)

Field you send Stored on Meaning
Outer activity_tasks[].integration_id tasks.integration_id (catalog) Domain-scoped catalog task key; find-or-create before the activity line is saved.
Inner task.integration_id activity_tasks.integration_id Unique id for this occurrence inside the activity (your line id, e.g. 6566-1).

Common mistake: sending the line id on the outside and the catalog code on the inside. Swap them to match the table above.

Pattern A — Free-task line (use when sending per-activity data)

Use Pattern A whenever the ERP needs to send data for this specific activity: planned schedule, unique line ids per visit, or any value that is not identical on every job that shares the same catalog task.

{
  "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
  }
}

Required on the inner task: integration_id, name, status_photo, status_obs.

Optional scheduling fields on the inner task (stored on the activity_tasks line, not the catalog task):

Field Notes
expected_start_date ISO-8601 datetime; stored in UTC.
expected_finish_date ISO-8601 datetime; stored in UTC. Must not be earlier than expected_start_date when both are sent.
expected_duration_seconds Integer ≥ 0. Omitted or null → stored as 0.

Mobile visibility caveat: in the current app flow, workers primarily see planned start and planned duration. expected_finish_date is still accepted, validated, and stored by the API, but should not be treated as the primary UI-visible planning field.

Naming caveat: expected_start_date is planned start from ERP. start_at is actual execution start set by the mobile app when the worker taps Start.

activity_tasks[].status (occurrence flag). This boolean is the task’s occurrence flag, not a completion flag: true = no occurrence (normal), false = an occurrence is raised (the red flag on the task card in the app). Occurrences can only be opened by a worker tapping the flag button in the app. The public API ignores any status you send and always creates the task with status: true (no occurrence).

Pattern B — Task group reference (shared checklist template)

Use Pattern B when the checklist structure is shared across jobs via a task group. The mobile app still records actual start/finish when the worker runs the job.

What Pattern B does: expands the saved group into activity_tasks rows (catalog task_id, group order, is_free_task: false). Per-activity overlays: when you include task_group_items[], you may send expected_start_date, expected_duration_seconds, and duration_* on each item’s inner task — stored on the matching activity_tasks row (matched by inner task.integration_id, e.g. GRP-ITEM-1). On update, only those fill fields may change on existing lines; expected_finish_date cannot be changed on an existing line (422). A bare reference { "task_group": { "integration_id": "..." } } alone does not carry fill fields — include task_group_items to set planning or durations.

Recommended workflow (best practice)

1. POST /api/v1/public/task-groups     → create the blank template once (201)
2. POST /api/v1/public/activities      → reference it with a bare task_group on every job

Step 1 — create the group once (POST /task-groups):

{
  "integration_id": "GRP-MORNING",
  "name": "Morning routine",
  "task_group_items": [ "... at least 2 items ..." ]
}

Step 2 — reference it on each activity (only integration_id; no name, no task_group_items):

{
  "activity_tasks": [
    { "task_group": { "integration_id": "GRP-MORNING" } }
  ]
}

Alternative: inline create on the first activity only

You may embed the full group definition inside activity_tasks[] when the group does not exist yet (same shape as POST /task-groups). The API creates the group and expands it in one call. Use this for quick prototypes or one-off setups.

On every later activity, use the bare reference, or send task_group_items to append new steps to the shared template. If the payload repeats an existing item (or a different group name), those fields are left as stored in the app — mismatches are ignored; only brand-new item integration_ids are appended.

To change existing template items after the group exists, use POST /task-groups (upsert) or PUT /task-groups/{integration_id} — not an activity payload.

Bare reference when the group already exists:

{
  "task_group": { "integration_id": "GRP-MORNING" }
}

If the group does not exist and you send only the bare reference (no inline definition), you get 404:

{
  "message": "Task group with integration_id \"GRP-MORNING\" was not found in this domain."
}

Create-only inline definition (first time only; 422 if the group already exists; needs ≥2 items):

{
  "task_group": {
    "integration_id": "GRP-MORNING",
    "name": "Morning routine",
    "task_group_items": [
      {
        "integration_id": "TASK-CHECK",
        "sequence": 1,
        "task": {
          "integration_id": "GRP-ITEM-1",
          "name": "Equipment check",
          "status_photo": false,
          "status_obs": false
        }
      },
      {
        "integration_id": "TASK-CLEAN",
        "sequence": 2,
        "task": {
          "integration_id": "GRP-ITEM-2",
          "name": "Cleanup",
          "status_photo": true,
          "status_obs": false
        }
      }
    ]
  }
}

Prohibited on activity lines: task_id, task_group_id, is_free_task, is_finished.

Fill planning on existing Form B lines (update)

Re-POST or PUT the activity with the same inline group and send fill fields on the step you want to update. Match by inner task.integration_id (activity line id), not the outer catalog id:

{
  "integration_id": "ACT-GROUP-FLOW-4",
  "name": "Afternoon job",
  "description": "Update planned time on last step",
  "schedule_date": "2026-07-08T19:45:00Z",
  "activity_tasks": [
    {
      "task_group": {
        "integration_id": "GRP-MORNING-TEST",
        "name": "Morning routine",
        "task_group_items": [
          {
            "integration_id": "CAT-GRP-STEP-3",
            "sequence": 3,
            "task": {
              "integration_id": "GRP-ITEM-3",
              "name": "Sign off",
              "status_photo": true,
              "status_obs": false,
              "expected_start_date": "2026-07-08T19:45:00Z",
              "expected_duration_seconds": 900
            }
          }
        ]
      }
    }
  ]
}

You may repeat all group items in the payload (mismatches on name/flags are ignored) or only the items where you send fill fields — omitted existing activity lines are not deleted.

Real-world example — multiple free-task lines (concrete delivery)

Same mapping on every line: outer = catalog code (PMIX-CHARGING-*), inner = your line id (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
      }
    }
  ]
}

Partial scheduling is allowed: you may send only expected_start_date, only expected_finish_date, or omit both on a line. Omitted expected_duration_seconds is stored as 0.

What the API fills in for you

Field What happens
uuid Generated if missing (needed by mobile)
sequence Running counter by array order
is_next_task First line defaults to true
is_free_task on the line Do not sendtrue for Pattern A; false for group-expanded rows

Rules for activities (must follow)

Topic Rule
Status Always created as Pending (0). Ignore status in your JSON.
Scheduled Always true. You cannot create unscheduled jobs via public API.
Assignee responsible_user_id prohibited on writes — set from the authenticated user token.
Schedule time schedule_date required. Stored in UTC. Any valid datetime is accepted (including past times).
Start / finish time start_at forbidden on write. App sets when worker starts.
Editing Can change only while Pending. After worker starts → 422 on update.
Activity tasks (update) Existing lines matched by activity_tasks.integration_id. New lines are created; existing lines accept only fill fields (duration_seconds, duration_paused_seconds, duration_finished_seconds, expected_start_date, expected_duration_seconds). Structural changes or deletions → 422. Omitted lines are not deleted. is_finished is prohibited (edits only while Pending; completion is set by the mobile app).
Transactions All task lines resolved in one go — partial save does not happen.

Schedule time and timezones

Send ISO-8601 with timezone, for example:

  • 2026-06-11T14:00:00Z (UTC)
  • 2026-06-11T11:00:00-03:00 (Brazil, converted to UTC by API)

The API does not enforce a “must be in the future” or “5 minutes in the past” rule on Public API writes. If you send a time in the past, it is stored as sent (UTC) — agenda placement in the app is your integration’s responsibility.

Activity status (for polling)

Status Number Meaning
Pending 0 On agenda, not started
In progress 1 Worker started
Finished 2 Done
Expired 3 Expired
Opened 4 Needs attention

Poll with: GET /activities/YOUR-INTEGRATION-ID


Rules for task groups (POST /task-groups and inline create on activities)

Task groups are reusable blank templates. They store the checklist blueprint (catalog task keys, step order, photo/note flags on the catalog task). They are not the place to send per-activity scheduling or filled-in execution data — use Pattern A free-task lines on POST /activities for that.

Task groups use the same outer/inner id mapping as activity free-task lines:

Field you send Stored on Meaning
Outer task_group_items[].integration_id tasks.integration_id (catalog) Catalog task key; find-or-create in the domain.
Inner task.integration_id task_group_items.integration_id Unique id for this row inside the group.
Rule Explanation
Catalog + line ids Outer = catalog key; inner task.integration_id = row id (mirror of activity Pattern A).
Inline task fields Inner task must include name, status_photo, and status_obs.
At least 2 tasks on first create New group needs 2+ items. Updates can have 1+.
sequence required Each item needs sequence (integer ≥1) — this orders items inside the group, not the activity.
Recommended workflow Create once via POST /task-groups; reference with bare integration_id on each activity. Inline create on activity allowed only when the group is new.
Blank template The group stores structure only (catalog keys, order, flags). Per-activity expected_start_date / expected_duration_seconds / duration_* are sent on the activity payload (task_group_items[].task), not stored on the group template.
Append via activity You may add new task_group_items to an existing template from an activity payload. Existing template fields (name, item details) are immutable via activity — mismatches are ignored. Change the shared template via /task-groups.
Expand = materialize lines Bare or expanded group reference creates missing activity_tasks lines on the activity; existing lines are fill-only (see activity update rules).

Example (minimum new group):

{
  "integration_id": "GRP-MORNING",
  "name": "Morning route",
  "task_group_items": [
    {
      "integration_id": "TASK-CHECK",
      "sequence": 1,
      "task": {
        "integration_id": "GRP-ITEM-1",
        "name": "Equipment check",
        "status_photo": false,
        "status_obs": false
      }
    },
    {
      "integration_id": "TASK-CLEAN",
      "sequence": 2,
      "task": {
        "integration_id": "GRP-ITEM-2",
        "name": "Cleanup",
        "status_photo": true,
        "status_obs": false
      }
    }
  ]
}

When an activity references this group (Pattern B bare reference), each group item expands to one activity_tasks row; expanded rows use the group’s inner ids (GRP-ITEM-1, …) as activity_tasks.integration_id and have is_free_task: false.


Domain users (GET /users)

Why: map ERP employees to Puzl users and choose which integration token to use when creating activities. Activity writes do not accept responsible_user_id in the body — the authenticated token owner becomes the responsible.

curl -s "https://YOUR-SERVER/api/v1/public/users" \
  -H "X-Integration-Key: YOUR-KEY"

Response (no pagination — full list):

{
  "message": "",
  "data": [
    { "user_id": "abc-123...", "user_name": "Maria Silva" }
  ]
}
Field Use
user_id ERP mapping; optional filter on GET /activities (responsible_user_id query param)
user_name Display only

There is no ERP code field on users — maintain a mapping table in your system (ERP employee → user_id) if needed.


What the worker sees on the phone

Stage What happens
You create activity Job appears on Agenda at scheduled time
Worker taps Start Status → In progress; start_at recorded
Worker completes steps Photos, notes, etc. per task settings
Worker finishes Status → Finished; finished_at recorded

Agenda time column:

Job state Time shown on the left
Pending Scheduled time (schedule_date)
Started or finished Actual start time (start_at)

The assignee on the activity must match the logged-in app user, or they will not see the job (or cannot resume it).

For planned line timing shown to users, prioritize expected_start_date + expected_duration_seconds. Keep sending expected_finish_date only when your integration needs backend validation/storage consistency; do not assume it is prominently shown in mobile UI.


Create vs update (POST and PUT)

Critical — do not create a new activity by accident.
The Public API targets a record by your activity integration_id (e.g. ACT-MANUAL-1).
If that code is missing from a POST body, every call creates a new activity.
To update the same job again (fill lines, append tasks), you must either:

  1. POST /activities with the same body integration_id200 OK, or
  2. PUT /activities/{that-same-code} (path identifies the record; do not send integration_id in the body).

POST = create or update by your code

Same URL POST /activities, upsert key = body integration_id:

Body integration_id Result HTTP
Present and new Creates that activity 201 Created
Present and already exists Updates that activity 200 OK
Missing / null Always creates a new activity (not an update) 201 Created

Always send a stable integration_id on create if you ever need to update that job later (retries, fill fields, append lines).

PUT = update one known record

URL contains your code: PUT /activities/ACT-MANUAL-1

Rule Detail
Path Must be the activity’s integration_id you created earlier
Body integration_id Prohibited — path alone targets the record
Same job Using a different path code updates (or 404s) a different activity — it does not attach to the one you meant

What Public API will and won’t overwrite

Edits are allowed only while the activity is Pending. After the worker taps Start, updates return 422.

Target Updated from activity payload? Notes
Activity header (name, description, schedule_date, …) Yes Sent on every POST / PUT. schedule_date accepts any valid datetime (UTC on save).
Existing activity_tasks line — duration_seconds, duration_paused_seconds, duration_finished_seconds Yes (fill-only) Match line by activity_tasks.integration_id. On Form B, send on task_group_items[] (inner task or item level).
Existing activity_tasks line — expected_start_date, expected_duration_seconds Yes (fill-only) Same matching rules as durations. These drive planned timing in the mobile app.
Existing activity_tasks line — expected_finish_date No Structural on update → 422 if you try to change it on an existing line.
Existing line — task_id, sequence, task_group_id, is_free_task No Structural → 422.
Existing line — is_finished, start_at No is_finished is prohibited; completion and actual start are app-only.
Omitted existing lines Kept Payload does not delete checklist lines that already exist.
New lines in payload Created New activity_tasks.integration_id values append to the activity.
Task group template (name, item order, catalog name, status_photo, …) via activity No Mismatches are ignored; only new task_group_items append. Change the shared template via /task-groups.
Catalog task (tasks table) via activity No Find-or-create on first sight only; existing catalog fields are not updated from activity payloads.

Form B line matching: expanded rows use inner task.integration_id (e.g. GRP-ITEM-3) as activity_tasks.integration_id, not the outer catalog key (CAT-GRP-STEP-3). Send fill fields on the matching task_group_items[].task entry.


All endpoints

Base: /api/v1/public

What you want Method Path
Check API is up GET /health (no key)
Check your key GET /ping
List workers GET /users
List / create tasks GET, POST /tasks
One task GET, PUT, DELETE /tasks/{your-code}
List / create groups GET, POST /task-groups
One group GET, PUT, DELETE /task-groups/{your-code}
Reorder groups PUT /task-groups/sequences
List / create custom fields GET, POST /custom-fields
One custom field GET, PUT, DELETE /custom-fields/{your-code}
Delete one field option DELETE /custom-fields/{cf}/items/{item}
List / create activities GET, POST /activities
One activity GET, PUT, DELETE /activities/{your-code}

Field reference

Task — required fields

Field Required Notes
integration_id Recommended Your code
name Yes
status_photo Yes boolean — allow photos?
status_obs Yes boolean — allow notes?
is_free_task Yes boolean — standalone step?
description No
is_active, is_favorite No default true / false

Do not send: id, status_status (removed), task_category_id.

Activity — required fields

Field Required Notes
integration_id Strongly recommended Your stable job code. Required in practice if you will update later. Omit → every POST creates a new activity.
name Yes
description Yes
schedule_date Yes ISO-8601; normalized to UTC; past times allowed
responsible_user_id No (prohibited) Server-set from token; optional internal UUID on GET /activities filter only
activity_tasks No* *Usually you send checklist lines

Do not send: start_at, status, finished_at, id, token, short_url.

Optional: schedule_finish, comments, GPS fields, activity_custom_fields.

Activity task lines (activity_tasks[])

Each entry is Pattern A (free task) or Pattern B (task_group). Do not send task_id, task_group_id, or is_free_task. Outer sequence is optional (integer >= 1); omit it to let the API assign order from the array.

Pattern Required fields Per-activity expected_* / duration_*?
A — free task Outer integration_id (catalog), inner task with integration_id (line id), name, status_photo, status_obs Yes — on inner task (create and fill-only update)
B — bare group task_group.integration_id only (group must exist → else 404) No in the bare reference alone — expand only
B — inline group (create, append, or fill) task_group with integration_id; optional name + task_group_items[] Yes — on task_group_items[].task (expected_start_date, expected_duration_seconds, duration_*; expected_finish_date only on create, not on update of existing line)

Scheduling on inner task (stored on activity_tasks, not catalog): expected_start_date, expected_finish_date, expected_duration_seconds (integer ≥ 0). See What Public API will and won’t overwrite for fill vs structural rules on update.

Custom fields — create a group first (POST /custom-fields)

A custom field is a reusable group of fields. Create it once with POST /custom-fields, then attach it to activities by its integration_id. Body:

{
  "integration_id": "CF-VEHICLE",
  "name": "Vehicle info",
  "is_active": true,
  "custom_field_items": [
    { "integration_id": "ITEM-PLATE", "type": "PLATE", "name": "License plate", "is_required": true },
    { "integration_id": "ITEM-NOTES", "type": "LONG_TEXT", "name": "Notes", "is_required": false }
  ]
}
Field Required Notes
integration_id Recommended Your code; lets activities reference the group and makes the call idempotent
name Yes
is_active No Defaults to true
custom_field_items Yes At least 1; each needs type (see types below) and name; is_required defaults to false

POST upserts by integration_id (201 new, 200 if it already exists). One group per request — send one call per group (e.g. CF-VEHICLE, then CF-CHECKLIST). Updating a group replaces its item list, so send all the items you want to keep.

Custom fields — groups are atomic

A custom field is really a group of fields (e.g. “Vehicle info” → plate, notes). Groups are atomic: when you attach one to an activity, all of its fields are added automatically for the worker to fill — you cannot pick a subset, and each field’s required/optional flag comes from the field’s definition. So to attach an existing group, just reference it — you do not list its fields:

{
  "activity_custom_fields": [
    { "custom_field": { "integration_id": "CF-1" } }
  ]
}

All of CF-1’s fields show up blank on the worker’s screen. You only spell out fields when you are creating a brand-new group (send custom_field.name + custom_field_items[]); doing that for a group that already exists is rejected with 422 (manage existing groups via the /custom-fields endpoints).

Custom fields — easy way (custom_fields shortcut) to pre-fill values

If you already know some answers and want to pre-fill them, send a flat object keyed by integration_id. The whole group is still attached atomically; the values you send fill the matching fields, the rest stay blank:

{
  "custom_fields": {
    "CF-1": { "ITEM-1": "Answer", "ITEM-2": "Another" },
    "CF-2": { "ITEM-PLATE": "ABC-1234" }
  }
}

Each key can be the resource integration_id or its internal id (ULID) — so fields created in the Puzl app (which have no integration_id) also work: just use the ULID from GET /custom-fields as the key. Outer key = custom field group; inner key = item; value = the answer. It references existing groups only (use the nested activity_custom_fields[] form with custom_field.name to create a new group inline), values are stored as strings, and the GET response stays in the nested shape. See the API reference for full details.

Custom field item types

TEXT, INT, DECIMAL, EIN, PLATE, DATE, PHONE, EMAIL, LONG_TEXT

List endpoints — pagination

These support limit (max 100), page, paginate_type, filters:

  • GET /tasks
  • GET /task-groups
  • GET /custom-fields
  • GET /activities

List response shape:

{
  "data": {
    "items": [ ... ],
    "page": 1,
    "limit": 15,
    "total": 42
  }
}

GET /users returns a plain array in data — no pages.

Incremental sync: GET /activities?timestamp=1716200000000 (milliseconds).


When something goes wrong

Quick fixes

Problem Likely cause Fix
401 everywhere Bad key or missing header Check X-Integration-Key
Job not on phone Only created a task Create an activity
Job not on phone Wrong assignee Use that worker’s puzl_usr_... token in X-Integration-Key when creating the activity
422 schedule_date Missing or invalid datetime Send required ISO-8601 schedule_date
422 responsible_user_id Sent on write body Remove it — responsible comes from the token
422 start_at You sent start_at Remove it
404 on task lookup Task missing POST /tasks first or inline create
422 editing activity Worker already started Wait until finished; create new job if needed
New activity every time instead of update Missing body integration_id on POST, or PUT/POST targeting a different code Always reuse the same activity integration_id: body on POST, or path on PUT
422 activity task line Structural change on existing line Match by activity_tasks.integration_id; only fill fields may change: duration_*, expected_start_date, expected_duration_seconds. Do not send is_finished.
422 activity_tasks Wrong line shape Pattern A: outer catalog id + inner task; Pattern B: task_group only. No task_id or task_group_id
422 activity_tasks ids Line id on wrong field Outer = catalog code; inner task.integration_id = your line id (e.g. 6566-1)
Group name / item fields differ on re-POST Expected if the app renamed the template Activity still succeeds; template fields are not overwritten. To change the shared template, use PUT /task-groups
404 task group on activity Bare reference, group missing Message: Task group with integration_id "…" was not found in this domain. — create via POST /task-groups or inline definition first
Planned times not updating (Form B) Bare group reference only, or wrong line id Include task_group_items[].task with fill fields; match inner task.integration_id (e.g. GRP-ITEM-3), not outer catalog id (CAT-GRP-STEP-3)
422 expected_finish_date on update Tried to change finish on existing line expected_finish_date is structural on update — set at create only, or use Pattern A
422 expected_finish_date Finish before start on same line (validation) Fix datetimes on inner task
422 expected_duration_seconds Negative value Use integer ≥ 0 or omit
422 task group Creation needs ≥2 items Add another task_group_items entry, each with sequence
Empty GET /users No members Add users in PuzlTask admin

FAQ

Can I use Bearer token instead of the integration key?
No. Public API uses X-Integration-Key only.

Which integration key should I use?
Use the puzl_usr_... per-user token of the worker who should see and run the job — in the X-Integration-Key header on every write. Generate it in PuzlTask admin (Cadastros → Usuários → Token). Do not put the token (or user_id) in responsible_user_id on activity writes — that field is prohibited.

Can I use our ERP employee code as responsible_user_id?
No. Map ERP → user_id via GET /users, then authenticate activity writes with that person’s puzl_usr_... token in X-Integration-Key.

Can I set when the worker started?
No. start_at is set by the mobile app.

What’s the difference between 200 and 201 on POST?
201 = new record. 200 = updated existing integration_id.

Can I update a job after the worker started?
No. Updates only work while status is Pending.

We used to send line ids on the outer integration_id and catalog codes inside task — is that still valid?
No. That was the previous contract. Swap them: outer = catalog task code (reused across activities), inner task.integration_id = unique line id per activity. Outer sequence is optional now — omit it to let the array define order, or send it to set the order explicitly.

Do task groups share catalog tasks?
Yes. Standalone POST /task-groups and inline group items use outer/inner mapping: outer id find-or-creates the catalog task; inner id is the row id inside the group (task_group_items.integration_id = inner task.integration_id). Per-activity planning is sent on the activity payload, not stored on the group template.

Our ERP sends planned times for every step on every delivery — should we use a task group?
Use Pattern A when each step needs its own catalog outer id and full scheduling (including expected_finish_date) with no shared template. Use Pattern B when the same checklist template is reused and you only need expected_start_date + expected_duration_seconds (and duration_*) on activity lines — send them on task_group_items[].task when creating or updating the activity.

Should we create task groups on POST /activities or on POST /task-groups?
Best practice: POST /task-groups once, then bare { "task_group": { "integration_id": "..." } } on every activity. You may append new template items from an activity later; existing template fields are not overwritten via activity (ERP mismatches are ignored). To rename or edit existing items, use /task-groups.


Security

  • Store the integration key in a secrets manager, not in git or mobile apps.
  • Use separate keys for staging and production.
  • Rotating the key invalidates the old one immediately.
  • Keys belong on your server only — never in a phone app.

HTTP status codes (short list)

Code Meaning
200 OK / updated
201 Created
204 Deleted
401 Key problem
404 Not found in your domain
422 Invalid data or business rule

OpenAPI (for tools like Postman)

Generate:

docker exec puzl-task-api php artisan l5-swagger:generate public

Browse: /api/documentation/public

Add header X-Integration-Key when trying requests in Swagger.


Document Purpose
public-api-v1.md One-page endpoint list
domain-context.md Internal API domain behavior

Public API v1 — integration key auth, activity_tasks outer/inner integration_id mapping, task group mirror mapping, users list, mandatory assignee, prohibited start_at/finished_at on writes, mobile agenda behavior.