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 onlyPOST /tasks.
The five rules that matter most
- Auth: every request (except health) needs header
X-Integration-Key: puzl_usr_...(per-user token) — not Bearer/JWT. - Your codes: use
integration_idfor your order/visit codes (e.g.ACT-SO-8842). Never send PuzlTask internalidin create/update bodies. - Assignee: on activity writes, do not send
responsible_user_id— the API sets it to the authenticated token user. - Schedule time:
schedule_dateis required (ISO-8601, normalized to UTC). The Public API does not reject past times — your integration owns that choice. - 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 product → Hands-on tutorial |
| Building the integration | Authentication → Send a job from your ERP → Activity checklist lines |
| Support / debugging | When something goes wrong |
| Need every field | All endpoints → Field 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
- Open PuzlTask as the assigned user.
- Go to Agenda / Schedule on the scheduled day.
- You should see “My first API job”.
- Start and finish it in the app.
- Call
GET /activities/TUTORIAL-ACT-1again — status becomes finished;start_atis 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-groupsonce, then{ "task_group": { "integration_id": "..." } }on each activity. Inline group create inside an activity is allowed only the first time thatintegration_idis 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-Keyis thepuzl_usr_...token of the worker who should execute the job (notresponsible_user_idin the body) - [ ]
schedule_dateis a valid ISO-8601 datetime (stored in UTC) - [ ] Outer
integration_idon each free-task line points to the catalog task (find-or-create in your domain) - [ ] Inner
task.integration_idis unique per activity line - [ ]
GRP-MORNINGexists 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, orfinished_aton activity lines (outersequenceis 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 (standalonePOST /task-groupsor inline create on an activity) defines which catalog tasks appear and in what order. The group template itself does not store per-activityexpected_*— those live on eachactivity_tasksrow 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 viatask_group_items. Existing template fields (name, itemsequence, 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 updateexpected_start_date+expected_duration_seconds(and executionduration_*) on the activity lines — send them ontask_group_items[].taskmatching innertask.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_dateis still accepted, validated, and stored by the API, but should not be treated as the primary UI-visible planning field.Naming caveat:
expected_start_dateis planned start from ERP.start_atis 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 anystatusyou send and always creates the task withstatus: 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 send — true 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 activityintegration_id(e.g.ACT-MANUAL-1).
If that code is missing from aPOSTbody, every call creates a new activity.
To update the same job again (fill lines, append tasks), you must either:
POST /activitieswith the same bodyintegration_id→ 200 OK, orPUT /activities/{that-same-code}(path identifies the record; do not sendintegration_idin 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 /tasksGET /task-groupsGET /custom-fieldsGET /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.
Related documents
| 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.