Developer docs · v2.0 · October 2026
Integration specification
This page describes how Journeez exchanges data with a client system (CRM, ERP or WMS): how tasks are pulled, how fields are mapped, how imported tasks are handled, and which webhooks Journeez sends when the schedule changes.
GET /tasks is the only mandatory endpoint.
Required per task, in the client’s own field names.
Delivered to a single POST URL.
Optional signing, with a timestamp and an idempotency key on every delivery.
Overview
The integration has three parts. All of them are configured per integration in the Journeez dashboard.
Pull
On a configured schedule, Journeez calls the client’s task endpoint and fetches tasks changed since the last run. The client system can also trigger an immediate sync through an inbound notification.
Map
Each item is mapped to Journeez fields using JSONPath expressions, transforms, and translation tables for statuses and skills.
Write back
When a worker’s day is approved, changed or released, or a task is updated, Journeez sends a webhook to the client system.
Connectors
An integration is created from a catalog template: Generic REST, OData or GraphQL, or a native connector for Priority ERP, monday.com or SAP Business One. Alternatively, Set up with AI reads a sample response and proposes a mapping for review.
Handled by Journeez
- Polling schedule and sync cursor.
- Geocoding of task addresses.
- Status translation, configured once in a table.
Pull API
Journeez initiates every request; the client system responds to GET /tasks. The path is configurable. Optional /employees and /locations endpoints are used to match workers and known sites.
| Setting | Supported values |
|---|---|
| Authentication | Bearer token, Basic, API key header, OAuth2 client credentials, or HMAC-signed requests (sha256=<hex> over <timestamp>.). |
| Pagination | offset / limit, page number, or cursor. Default page size 50, maximum 200. |
| Incremental filter | Required. The time of the last successful sync is sent in a configurable query parameter (default updatedAfter). Format is ISO-8601 UTC by default, or Unix seconds / milliseconds. Each call overlaps the previous one by 5 minutes. |
| Pull mode | Only what changed since the last pull (recommended): changed tasks only. All active tasks every time: a full snapshot on every call; required for closing tasks that disappear from the source. |
| Frequency | Cron expression per integration. Templates default to every 15 minutes. |
| Import tasks created from | Optional. Tasks created before this date are not imported. Defaults to the day the integration is connected; clear it to import the full history. Requires the item’s creation-date field (JSONPath, e.g. $.createdAt). If the API can filter by creation date (e.g. ?createdAfter=), set that parameter name too and older tasks are not requested at all. |
| Timeout | 30 seconds per page. |
Minimum valid item
Contains only the required fields. Field names are the client’s own and are mapped in configuration.
{
"items": [
{
"id": 12345,
"type": "AC unit replacement",
"skill": "MECHANIC",
"location": { "street": "Rothschild 12", "city": "Tel Aviv" },
"updatedAt": "2026-06-01T08:14:22Z"
}
]
}Full item
Adds the recommended fields: status, planned times, duration, assignee, customer and coordinates.
{
"items": [
{
"id": 12345,
"type": "AC unit replacement",
"status": "APPROVED",
"scheduledStartTime": "2026-06-04T09:00:00Z",
"scheduledEndTime": "2026-06-04T12:00:00Z",
"duration": 180,
"assignedToId": 22,
"skill": "MECHANIC",
"customer": "Acme Facilities",
"location": {
"street": "Rothschild 12",
"city": "Tel Aviv",
"lat": 32.0628,
"lng": 34.7716
},
"updatedAt": "2026-06-01T08:14:22Z"
}
],
"nextCursor": "10481"
}Field mapping
Each Journeez field is defined by a JSONPath into the source item, an optional transform and an optional default. The item array is located by a root path (default $.items[*]). A task missing any required field is not imported.
| Journeez field | Type | Level | Notes |
|---|---|---|---|
originId | string | Required | Stable, unique task ID in the client system. Used for deduplication and returned in every webhook. |
name | string | Required | Task label shown to dispatchers, typically the job type. |
roles | string[] | Required | Required skills, translated through the role table. A single value can be wrapped into an array. |
startAddress | object | Required | startAddress.city is the minimum. Add street, houseNumber and position.lat / position.lon for a precise location. |
updatedAt | ISO-8601 UTC | Required | Last modification time in the client system. Used by the incremental filter. |
date / startHour | date / timestamp | Recommended | Target day and planned start. If no date is mapped, the current day is used. |
duration | number (hours) | Recommended | Stored in hours; apply minutesToHours to minute values. A missing or zero duration raises a data flag that blocks scheduling until corrected. Can also be derived by a rule. |
endHour | timestamp | Recommended | Planned end. If absent, computed as start plus duration. |
providerOriginId | string | Recommended | Client-side ID of the assigned worker. Used when importing pre-scheduled tasks. |
clientName | string | Optional | Customer name. Matched to existing clients case-insensitively; see automatic client creation below. |
priority, description | string | Optional | Priority is Normal, High or Critical after translation. |
Transforms
Convert a source value into the type a Journeez field expects: dates, times, numbers, strings and arrays. One transform per rule, chosen from a fixed set of eleven.
Translation tables
Map client values to Journeez values: statuses (Status value mapping) and skills (Role / skill value mapping). Lookups are exact; status rows can apply inbound, outbound or both.
Derivation rules
Fill in duration, qualifications or location that the source system doesn’t have (for example, job type X → 2 hours). The first matching rule applies, top to bottom; if none match, a default is used or the row is flagged for review.
Mapping rules
Each Journeez field is configured with a rule made of the parts below. All parts are optional, but a rule needs at least one of from, template or default.
| Part | Behaviour | Example |
|---|---|---|
from | JSONPath into the source item. A wildcard path ([*]) returns an array. | $.location.street |
template | Builds one string from several paths. Each {{path}} is resolved by JSONPath, null renders as empty, and the result is trimmed. Takes precedence over from. | {{type}} - {{customer}} |
default | Used when the extracted value is missing, null, an empty string or an empty array. Without from or template, it sets a constant. | "Normal" |
transform | Converts the value. One per rule; see the reference below. | minutesToHours |
translateWith | Looks the value up in a translation table (statuses, roles). Arrays are translated element by element. | roleTable |
Processing order
- Extract the value with
template, or elsefrom. - If the value is empty, apply
default. - Apply
transform. For a wildcard path, it runs on each element. - Apply
translateWith, skipped if the transform failed.
If a transform cannot parse its input, the field is left empty and the error is recorded on the row with the field name and reason. It is visible in the sync run and in the test-mode preview. If the field is required, the task is not imported.
Example
"fields": {
"originId": { "from": "$.id", "transform": "toString" },
"name": { "template": "{{type}} - {{customer}}" },
"roles": { "from": "$.skill", "transform": "wrapArray",
"translateWith": "roleTable" },
"date": { "from": "$.scheduledStartTime",
"transform": "isoDateOrNow" },
"startHour": { "from": "$.scheduledStartTime",
"transform": "extractHHmm" },
"duration": { "from": "$.duration",
"transform": "minutesToHours" },
"startAddress.position.lat":
{ "from": "$.location.lat", "transform": "toFloat" },
"priority": { "default": "Normal" }
}Translation tables
A translation table maps the values a client system sends to Journeez values. A field uses a table through translateWith. In the dashboard, the two tables are Status value mapping and Role / skill value mapping.
How a lookup works
Keys are matched exactly and are case-sensitive: "Approved" and "APPROVED" are different keys. When the value is an array, each element is looked up on its own.
| Value received | Result |
|---|---|
| Listed in the table | The mapped value. |
Not listed, table has _default | The _default value. |
Not listed, no _default | null. |
Not listed, table has _missing: "ERROR" | A mapping error is recorded on the row for that field. _default is not used. |
null or missing | _default if set, otherwise null. |
Status value mapping
- Each row has a direction: → inbound (client → Journeez), ← outbound (Journeez → client) or ↔ both. Inbound rows form the forward table and outbound rows form a reverse table (
statusReverse). If several rows map to the same Journeez status outbound, the first row is used. - Without a usable
_default, the table decides which tasks are imported. A new task whose status is not listed is not created. An existing task that moves to an unlisted status is flagged for the dispatcher (see Import behaviour). - With a
_default, every unlisted status resolves to that value and the task is imported. - Without a status table, the source value is passed through unchanged, so it must already be one of the six Journeez statuses.
- Outbound rows are used when Journeez writes a status back through a native connector or an outbound mapping. A status with no outbound row is never guessed: the delivery fails visibly in the delivery history. The raw webhook payload always carries the untranslated Journeez status.
Role / skill value mapping
- Inbound only. Maps the client’s skill codes to Journeez roles. When the source sends a single skill, apply
wrapArrayfirst. - A value that resolves to a role the organization doesn’t have does not block the import: the task is imported, keeps any roles that did resolve, and is flagged with the unmatched names. It cannot be scheduled until the role is added or the row is fixed.
- A task left with no roles (for example, every value unlisted and no
_default) is flagged as missing roles. - The dashboard lists values found in the sample that have no row, and can add them in one step.
Example
"translations": {
"statusTable": {
"APPROVED": "Open",
"DISPATCHED": "Scheduled",
"IN_PROGRESS": "InProgress",
"COMPLETED": "Closed",
"CANCELLED": "Closed"
},
"statusReverse": {
"Scheduled": "DISPATCHED",
"Closed": "COMPLETED"
},
"roleTable": {
"MECHANIC": "Mechanic",
"ELEC": "Electrician",
"_default": "General technician"
}
}Transform reference
These are the only transforms available. An unknown name is a configuration error. Unless noted, null, missing and empty-string inputs return null without an error.
| Transform | Behaviour | Input → output | Typical use |
|---|---|---|---|
isoDate | Parses a date or datetime and returns ISO-8601 UTC. Fails on unparseable input. | "2026-06-04 09:00" → "2026-06-04T09:00:00.000Z" | Non-standard date strings. |
unixToIso | Converts a Unix timestamp (number or numeric string) to ISO-8601 UTC. Values of 10¹¹ and above are treated as milliseconds, smaller values as seconds. | "1775773020000" → "2026-04-09T22:17:00.000Z" | Epoch timestamps, which isoDate does not accept as strings. |
isoDateOrNow | Returns the calendar day of the input in the organization’s time zone, at midnight UTC. Empty or whitespace input becomes today. | null → today, e.g. "2026-10-21T00:00:00.000Z" | date when unscheduled work should appear on today’s board. |
extractHHmm | Returns the time of day as HH:mm, taken as written in the source. Offsets are not converted to UTC. | "2026-05-31T08:15:00+03:00" → "08:15" | startHour / endHour from a datetime field. |
toFloat | Converts to a number, keeping decimals. Fails on non-numeric input. | "32.0628" → 32.0628 | Coordinates sent as strings. |
toInt | Converts to an integer, dropping any fractional part. Fails on non-numeric input. | "90.7" → 90 | Counts and numeric IDs sent as strings. |
minutesToHours | Divides by 60. | 90 → 1.5 | duration supplied in minutes. |
toString | Converts a value to a string. | 12345 → "12345" | Numeric IDs, e.g. originId. |
concat | Joins an array with single spaces, skipping empty elements. A non-array is returned as a string; null becomes "". | ["Herzl", null, "5"] → "Herzl 5" | Building one value from a wildcard path. |
splitArrayItem | Returns the first element of an array. A non-array is returned unchanged. | ["MECHANIC"] → "MECHANIC" | A wildcard path that yields a single value. |
wrapArray | Wraps a single value in an array. Arrays pass through unchanged; empty input becomes []. | "MECHANIC" → ["MECHANIC"] | roles when the source has one skill. Combine with translateWith. |
Values that need a shape not listed here, such as unit conversions other than minutes to hours, should be prepared in the client system before the response is returned.
Import behaviour
Test mode
Enabled by default on new integrations. Data is fetched and mapped, but nothing is written. Use the connection test and the live preview, then turn test mode off to go live.
Status filtering
Without a _default in the status table, a new task is created only if its status is listed. If an existing task moves to an unlisted status (for example, on hold), it is flagged for the dispatcher. Optionally, Delete tasks with an unmapped status removes such tasks while they are still open and unscheduled.
Closing tasks
Terminal status (recommended): return the task with a completed or cancelled status mapped to Closed. Close tasks that disappear from the source: available only with “All active tasks every time”; tasks no longer returned are closed. In both cases the assignment history is retained.
Pre-scheduled tasks
When a client status is mapped to Scheduled and the item includes a worker, a date, a start time and an end or duration, the task is placed on that worker’s day and excluded from automatic optimization. Conflicting placements are applied and flagged for review. When one of those values is missing, Keep scheduled tasks that can’t be placed keeps the task and flags it; otherwise it is skipped.
Manual edits
A field corrected by a dispatcher in Journeez is not overwritten by later syncs unless the source value changes.
Auto-create customers
Off by default. When on, a clientName that doesn’t match an existing customer creates a new customer record on import.
Task statuses
Journeez uses six statuses. Client statuses are translated through the status table. Closed is the only terminal status and covers both completion and cancellation; the client system remains the source of truth for that distinction.
- 01OpenNot placed on a worker’s day.
- 02ScheduledPlaced on a worker’s day.
- 03OnRouteWorker is travelling to the site.
- 04OnSiteWorker has arrived.
- 05InProgressWork has started.
- 06ClosedCompleted or cancelled.
OnRoute, OnSite and InProgress are set by the Journeez mobile app.
Actions and events
| Action | Event | Journeez status | Typical client status |
|---|---|---|---|
| Task created in the client system | — (pulled) | Open | Waiting to be scheduled |
| Task placed on an approved day | schedule.upserted | Scheduled | Scheduled; worker and time stored |
| Time moved or worker replaced | schedule.upserted (day re-sent) | Scheduled | Unchanged; worker and time updated |
| Task removed from an approved day | schedule.deleted | Open | Worker and time cleared; back to waiting |
| Task edited, status changed, or closed in the field | task.updated | e.g. Closed | e.g. pending manager approval |
| Task deleted in Journeez | task.deleted | — | Client-defined |
Outbound webhooks
All events are sent as POST requests to one URL. Schedule events carry assignments (worker, day, times). Task events carry task data only and never include assignment information.
| Event | Dashboard label | Sent when |
|---|---|---|
schedule.upserted | Schedule confirmed | A worker’s day is approved, or an approved day (or the current day) is saved. |
schedule.deleted | Schedule cleared | An approved day is un-approved, cleared or deleted. The listed bookings should be released. |
task.updated | Task changed | A task is created, edited, changes status or is closed. |
task.deleted | Task removed | A task is deleted in Journeez. |
Schedule delivery
Only when confirmed (default): schedule events are sent for approved days and for the current day. Unapproved future days are not sent. Every update: unapproved days are sent as well. The mode applies to schedule events only; task events are always sent.
Receiver requirements
- Return
2xxwithin 15 seconds. - Deduplicate on
X-Idempotency-Key. - Apply each body as current state, not as a delta.
- On task events, treat
nullas “no data” and leave the field unchanged. - Return
200and write nothing whenx-journeez-webhook-test: trueis present.
Headers
| Header | Value | Sent when |
|---|---|---|
X-Journeez-Signature | sha256=<hex> | An HMAC secret is configured |
X-Journeez-Timestamp | Unix seconds | An HMAC secret is configured |
X-Idempotency-Key | outbox-<n> | Always |
Authorization | Bearer <token> | A bearer token is configured |
X-API-Key (name configurable) | API key | An API key is configured |
x-journeez-webhook-test | true | Connectivity tests only |
At least one of bearer token, API key or HMAC secret must be configured before webhooks can be enabled.
Payloads
schedule.upserted
One worker, one day. confirmedDate is present only when the event was produced by approving the day.
{
"version": 1,
"event": "schedule.upserted",
"orgId": 63,
"confirmedDate": "2026-07-23",
"schedule": {
"date": "2026-07-23",
"endDate": "2026-07-23",
"providerId": 773,
"providerOriginId": "97",
"resourceId": null,
"isPlanned": true,
"tasks": [
{
"id": 31798,
"originId": "82600020",
"startHour": "2026-07-23T08:00:00.000",
"endHour": "2026-07-23T09:00:00.000",
"providerOriginId": "97"
}
]
}
}schedule.deleted
Same shape as schedule.upserted. null times and worker indicate a released booking; originId identifies it.
{
"version": 1,
"event": "schedule.deleted",
"orgId": 63,
"schedule": {
"date": "2026-07-23",
"providerId": 773,
"providerOriginId": "97",
"resourceId": null,
"isPlanned": true,
"tasks": [
{
"id": 31798,
"originId": "82600020",
"startHour": null,
"endHour": null,
"providerOriginId": null
}
]
}
}task.updated
duration is in minutes. status is the untranslated Journeez status. Fields can be restricted per integration; id and originId are always included. task.deleted uses the same envelope.
{
"version": 2,
"event": "task.updated",
"orgId": 63,
"task": {
"id": 4711,
"originId": "12345",
"status": "Closed",
"duration": 90,
"name": "Brake inspection",
"description": "Rear axle noise reported",
"priority": "Normal",
"clientId": 812,
"startAddress": {
"displayName": "Herzl 5, Tel Aviv",
"street": "Herzl",
"houseNumber": "5",
"city": "Tel Aviv",
"position": { "lat": 32.0668, "lon": 34.7647 }
},
"endAddress": null
}
}Verifying the signature
HMAC-SHA256 of <timestamp>.<raw body>, computed over the raw body before JSON parsing.
import { createHmac, timingSafeEqual } from "crypto";
function verify(rawBody, headers, secret) {
const ts = headers["x-journeez-timestamp"];
const received = headers["x-journeez-signature"];
// Replay protection: reject more than 5 minutes of clock skew.
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = "sha256=" + createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
const a = Buffer.from(received ?? "");
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}Errors and retries
Pull requests
| Response | Journeez behaviour |
|---|---|
200 | Page applied; cursor advanced. |
400 / 422 / other 4xx | Run fails. An {"error": "..."} body is shown in the dashboard. |
401 / 403 | Run fails; not retried. After 3 consecutive permanent failures, pulling is paused until the configuration is fixed. |
429 | Retried on the next scheduled run. |
5xx, timeout, network | Retried in the same run after 1, 2 and 4 seconds, then on the next scheduled run. |
Webhook deliveries
| Response | Journeez behaviour |
|---|---|
2xx | Marked as delivered. |
400 401 403 409 410 422 | Permanent failure. Moved to the dead-letter queue without retry; can be replayed from the dashboard. |
429 / 503 | Retried, honouring Retry-After up to 1 hour. |
404, other 5xx, timeout | Retried after 1s, 5s, 30s, 2m, 10m and 1h (7 attempts in total), then dead-lettered. |
When several pending events exist for the same day or task, only the most recent is delivered.
Edge cases
Addresses without a street
Common for construction sites and new developments. When only a city is provided, the task is geocoded to the city centre, the address is marked approximate, and a dispatcher is asked to confirm it.
Team tasks
A task can require several workers with different skills at the same site. All team members share the same start and end time. Pre-scheduled team tasks are not placed on import; they are flagged for the dispatcher.
Vehicle and asset schedules
A schedule for a vehicle or asset has providerOriginId: null, the same as a released booking. Use resourceId to distinguish the two.
Go-live checklist
GET /tasksreturns the five required fields and honours the incremental filter parameter.- The connection test succeeds and the mapped-row preview is correct in test mode.
- The status and role tables cover every value the client system returns.
- The webhook receiver returns
200without writing data whenx-journeez-webhook-test: trueis present (the Send test event button). - The receiver deduplicates on
X-Idempotency-Keyand, if an HMAC secret is configured, verifies the signature. - The receiver handles all four events, including
schedule.deletedwithnullvalues. - Test mode is disabled.