PJP (Permanent Journey Plan) API
Digitize, optimize, and enforce recurring field sales beats, retail visit schedules, customer check-ins, and territory coverage for field teams.
Overview & Solution
Managing distributed field forces across thousands of retail outlets requires systematic beat planning. The PJP (Permanent Journey Plan) API enables enterprise managers to design weekly/monthly beat plans, assign outlets to sales representatives, and verify check-in authenticity with GPS geofencing.
Industry Use Cases
1. FMCG & Pharma Sales Force Automation (SFA)
- Beat Management: Set recurring Monday-Saturday outlet visit schedules for medical reps and retail territory managers.
2. Banking, Microfinance & Loan Collections
- Daily Field Collection Routes: Plan customer loan recovery visits along optimized spatial paths, tracking completed vs. missed appointments in real time.
3. Telecom & Distributor Merchandising
- Retail Visibility Auditing: Track promotional banner placements and merchandising compliance during scheduled distributor visits.
API reference
Field Force Mobility Execution APIs · Requires Intelomatic tenant
Field definitions: Plan, Visit
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST | /api/app-base/plans/create | Create a plan (with nested visits) |
GET | /api/app-base/plans/{plan_guid} | Get a plan |
POST | /api/app-base/plans/search | Search plans |
PUT | /api/app-base/plans/{plan_guid} | Update a plan |
DELETE | /api/app-base/plans/{plan_guid} | Delete a plan |
GET | /api/app-base/visits/{visit_guid} | Get a visit |
POST | /api/app-base/visits/search | Search visits |
PUT | /api/app-base/visits/{visit_guid} | Update a visit |
DELETE | /api/app-base/visits/{visit_guid} | Delete a visit |
All requests require a bearer token and the api-key query parameter:
Authorization: Bearer <access_token>
?api-key=<apiKey>
Create a plan (with nested visits)
POST /api/app-base/plans/create
Request Parameters
| Parameter | In | Type | Required | Description | Example |
|---|---|---|---|---|---|
plan_assignee_username | body | string | No | ||
plan_name | body | string | No | Name for the plan. | |
visits | body | array | No | ||
visits[] | body | object | No | ||
visits[].visit_target_guid | body | string | No | Must belong to a customer already assigned to visit_assignee_username (directly or via an ALL assignment). Customers assigned to another user cannot be planned for this assignee. | |
visits[].visit_assignee_username | body | string | No | The plan's own assignee - sent as a username here (contrast with the standalone POST /visits, where this same field name takes an employee_guid instead). | |
visits[].visit_date | body | string | No | Must be today or later (UTC). | |
visits[].visit_priority | body | string | No | Priority, sent as a code string (the example sends "3"). | |
visits[].visit_estimate_time | body | string | No | Estimated visit duration, sent as a string (the example sends "30"). | |
visits[].visit_objective | body | string | No | Purpose of the visit, free text. | |
visits[].visit_order | body | integer | No | Position of the visit in the day's sequence, starting at 0. | |
plan_date | body | string | No | Must be today or later (UTC). | |
config | body | object | No | ||
config.timeZoneOffset | body | integer | No | Client time-zone offset in minutes (the example -420 is UTC+7). |
Request body
{
"plan_assignee_username": "apidemo_newuser_9",
"plan_name": "test plan",
"visits": [
{
"visit_target_guid": "6aa0bb299521e3582ef555f8",
"visit_assignee_username": "apidemo_newuser_9",
"visit_date": "2026-09-25T16:59:59.999Z",
"visit_priority": "3",
"visit_estimate_time": "30",
"visit_objective": "test",
"visit_order": 0
},
{
"visit_target_guid": "6aa3d2b3d1d2d6ccfde198cd",
"visit_assignee_username": "apidemo_newuser_9",
"visit_date": "2026-09-25T16:59:59.999Z",
"visit_priority": "3",
"visit_estimate_time": "30",
"visit_objective": "test",
"visit_order": 1
}
],
"plan_date": "2026-09-25T16:59:59.999Z",
"config": {
"timeZoneOffset": -420
}
}
Response Schema
200 OK
{
"plan_guid": "6ab386e8e2e38f375be67f45",
"plan_assignee_username": "apidemo_newuser_9",
"plan_name": "test plan",
"visits": [
{
"visit_target_guid": "6aa0bb299521e3582ef555f8",
"visit_assignee_username": "apidemo_newuser_9",
"visit_date": "2026-09-25T16:59:59.999Z",
"visit_priority": "3",
"visit_estimate_time": 30,
"visit_objective": "test",
"visit_order": 0,
"__customerInfo": {
"customer_guid": "6aa0bb299521e3582ef555f8",
"customer_full_name": "My Customer",
"Title": "My Customer",
"customer_address": "Mulshi, Pune, MAHARASHTRA - 412108",
"customer_postal_code": "412108",
"customer_category": "Default",
"customer_is_active": 1,
"customer_assigned_team": "2",
"customer_assignee": "all"
},
"Title": "Visit 1790150375739",
"visit_status": 1,
"visit_contact_full_name": "My Customer",
"visit_contact_phone_number": "919000000002",
"visit_address": "Mulshi, Pune, MAHARASHTRA - 412108",
"visit_administrative_1": "33",
"visit_administrative_2": "138",
"visit_administrative_3": "14204",
"visit_postal_code": "412108",
"visit_customer_category": "Default",
"visit_location": "{\n \"type\": \"Point\",\n \"coordinates\": [\n 73.55986487231496,\n 18.462925230845542\n ]\n}",
"visit_boundary": null,
"validated": true,
"notified": true,
"__isValid": true,
"__dataErrors": [],
"visit_plan_guid": "6ab386e8e2e38f375be67f45",
"visit_created_from": 2,
"visit_last_modified_from": 2,
"Id": "f3fc45bd-3790-4c97-b795-2dc59931a919",
"visit_guid": "6ab386eae2e38f35d9e67f75",
"isSuccess": true
},
{
"visit_target_guid": "6aa3d2b3d1d2d6ccfde198cd",
"visit_assignee_username": "apidemo_newuser_9",
"visit_date": "2026-09-25T16:59:59.999Z",
"visit_priority": "3",
"visit_estimate_time": 30,
"visit_objective": "test",
"visit_order": 1,
"__customerInfo": {
"customer_guid": "6aa3d2b3d1d2d6ccfde198cd",
"customer_full_name": "New customer",
"Title": "New customer",
"customer_address": "ABC",
"customer_postal_code": "451331",
"customer_category": null,
"customer_is_active": 1,
"customer_assigned_team": "2",
"customer_assignee": "all"
},
"Title": "Visit 1790150375740",
"visit_status": 1,
"visit_contact_full_name": "New customer",
"visit_contact_phone_number": null,
"visit_contact_email": null,
"visit_address": "ABC",
"visit_administrative_1": "26",
"visit_administrative_2": "570",
"visit_administrative_3": "13563",
"visit_postal_code": "451331",
"visit_customer_category": null,
"visit_location": "{\n \"type\": \"Point\",\n \"coordinates\": [\n 75.97974198425027,\n 21.888548035168114\n ]\n}",
"visit_boundary": null,
"validated": true,
"notified": true,
"__isValid": true,
"__dataErrors": [],
"visit_plan_guid": "6ab386e8e2e38f375be67f45",
"Id": "6fc95dff-c53b-4ec5-b34d-99c48f3637e7",
"visit_guid": "6ab386ede2e38f44f7e67f76",
"isSuccess": true
}
],
"plan_date": "2026-09-25T16:59:59.999Z",
"plan_created_from": 2,
"plan_last_modified_from": 2,
"Id": "4837cc76-5ae0-4b6a-ba46-5e0c7cf4f827",
"Path": "/<internal storage path>"
}
| Field | Type | Description |
|---|---|---|
plan_guid | string | Identifier of the plan just created. Use it to read, update or delete the plan. |
plan_assignee_username | string | The field user this plan belongs to. |
plan_name | string | The plan name as stored - the value you sent in plan_name. |
visits | array | One entry per visit created with the plan, each expanded with the customer details resolved from visit_target_guid. |
visits[] | object | |
visits[].visit_target_guid | string | |
visits[].visit_assignee_username | string | |
visits[].visit_date | string | |
visits[].visit_priority | string | |
visits[].visit_estimate_time | integer | Estimated visit duration - sent as a string, returned as a number. |
visits[].visit_objective | string | |
visits[].visit_order | integer | Position of the visit in the day's sequence, as sent. |
visits[].__customerInfo | object | The full customer record resolved from visit_target_guid (shape: Customer). Trimmed to its identifying fields in the example above for brevity. |
visits[].__customerInfo.customer_guid | string | |
visits[].__customerInfo.customer_full_name | string | |
visits[].__customerInfo.Title | string | |
visits[].__customerInfo.customer_address | string | |
visits[].__customerInfo.customer_postal_code | string | |
visits[].__customerInfo.customer_category | string | |
visits[].__customerInfo.customer_is_active | integer | |
visits[].__customerInfo.customer_assigned_team | string | |
visits[].__customerInfo.customer_assignee | string | |
visits[].Title | string | Generated by the platform (Visit <timestamp>) - do not send. |
visits[].visit_status | integer | Numeric status code - 1 = New. |
visits[].visit_contact_full_name | string | Copied from the target customer's contact details, along with the other visit_contact_* fields. |
visits[].visit_contact_phone_number | string | |
visits[].visit_contact_email | string | |
visits[].visit_address | string | Copied from the target customer. |
visits[].visit_administrative_1 | string | Copied from the target customer's administrative codes (visit_administrative_1..3). |
visits[].visit_administrative_2 | string | |
visits[].visit_administrative_3 | string | |
visits[].visit_postal_code | string | |
visits[].visit_customer_category | string | |
visits[].visit_location | string | Copied from the target customer, as a GeoJSON Point string. |
visits[].visit_boundary | string | |
visits[].validated | boolean | Whether this visit has been validated. |
visits[].notified | boolean | Whether the assignee has been notified of this visit. |
visits[].__isValid | boolean | Whether this visit passed validation. See __dataErrors for what failed when false. |
visits[].__dataErrors | array | What failed validation for this visit, if __isValid is false. Empty when the visit is valid. |
visits[].visit_plan_guid | string | The plan this visit belongs to. |
visits[].visit_created_from | integer | |
visits[].visit_last_modified_from | integer | |
visits[].Id | string | |
visits[].visit_guid | string | Identifier of this visit. Generated by the platform - do not send it on create. |
visits[].isSuccess | boolean | Whether this individual visit was created successfully. The plan call can return 200 OK while one visit fails - check this flag per visit. |
plan_date | string | The day the plan covers. |
plan_created_from | integer | Channel the plan was created from: 1 Mobile, 2 Web, 3 Others. |
plan_last_modified_from | integer | Channel of the last change. |
Id | string | Record ID of the plan. The plan endpoints address it by plan_guid, not by this value. |
Path | string | Internal storage location. Read-only. |
Response - some visits are invalid (errorCode 3001)
{
"result": -1,
"resultId": "gx5isi129d",
"data": {
"plan_assignee_username": "apidemo_newuser_9",
"plan_date": "2026-09-23T16:59:59.999Z",
"plan_created_from": 2,
"plan_last_modified_from": 2,
"plan_name": "Plan for apidemo_newuser_9 created on 23/09/2026",
"Path": "/<internal storage path>"
},
"errorMessage": "Some visits are invalid.",
"message": "Some visits are invalid.",
"errorDetails": [
{
"data": {
"visit_target_category": "Audit",
"visit_target_guid": "6aa3d2b3d1d2d6ccfde198cd",
"visit_assignee_username": "apidemo_newuser_9",
"visit_date": "2026-09-23T16:59:59.999Z",
"visit_status": 1,
"visit_priority": "Medium",
"visit_objective": "Audit",
"visit_estimate_time": 10,
"__customerInfo": {
"customer_guid": "6aa3d2b3d1d2d6ccfde198cd",
"customer_full_name": "New customer",
"Title": "New customer",
"customer_address": "ABC",
"customer_postal_code": "451331",
"customer_assignee": "all",
"customer_assigned_team": "2"
},
"Title": "Visit 1790149244060",
"visit_contact_full_name": "New customer",
"visit_address": "ABC",
"visit_postal_code": "451331",
"visit_location": "{\"type\":\"Point\",\"coordinates\":[75.97974198425027,21.888548035168114]}",
"validated": true,
"notified": true,
"__isValid": true,
"__dataErrors": []
},
"isValid": false,
"isSuccess": false,
"message": "A visit already exists for this customer on this date",
"details": [
{
"visit_date": "A visit already exists for this customer on this date"
}
]
}
],
"errorCode": 3001
}
| Field | Type | Description |
|---|---|---|
result | integer | -1 when the plan could not be created as sent. |
resultId | string | Correlation ID for this call. Quote it when reporting a problem to support. |
data | object | The plan as the platform prepared it. No plan_guid is returned in this case. |
data.plan_assignee_username | string | |
data.plan_date | string | |
data.plan_created_from | integer | |
data.plan_last_modified_from | integer | |
data.plan_name | string | |
data.Path | string | |
errorMessage | string | Summary of the failure. |
message | string | Same text as errorMessage. |
errorDetails | array | One entry per visit that failed. |
errorDetails[] | object | |
errorDetails[].data | object | The visit as the platform resolved it, including the customer details from visit_target_guid. |
errorDetails[].data.visit_target_category | string | |
errorDetails[].data.visit_target_guid | string | |
errorDetails[].data.visit_assignee_username | string | |
errorDetails[].data.visit_date | string | |
errorDetails[].data.visit_status | integer | |
errorDetails[].data.visit_priority | string | |
errorDetails[].data.visit_objective | string | |
errorDetails[].data.visit_estimate_time | integer | |
errorDetails[].data.__customerInfo | object | The target customer record (trimmed to identifying fields in this example). |
errorDetails[].data.__customerInfo.customer_guid | string | |
errorDetails[].data.__customerInfo.customer_full_name | string | |
errorDetails[].data.__customerInfo.Title | string | |
errorDetails[].data.__customerInfo.customer_address | string | |
errorDetails[].data.__customerInfo.customer_postal_code | string | |
errorDetails[].data.__customerInfo.customer_assignee | string | |
errorDetails[].data.__customerInfo.customer_assigned_team | string | |
errorDetails[].data.Title | string | |
errorDetails[].data.visit_contact_full_name | string | |
errorDetails[].data.visit_address | string | |
errorDetails[].data.visit_postal_code | string | |
errorDetails[].data.visit_location | string | |
errorDetails[].data.validated | boolean | |
errorDetails[].data.notified | boolean | |
errorDetails[].data.__isValid | boolean | |
errorDetails[].data.__dataErrors | array | |
errorDetails[].isValid | boolean | false for a visit that failed. |
errorDetails[].isSuccess | boolean | false - the visit was not created. |
errorDetails[].message | string | Why the visit failed. |
errorDetails[].details | array | Field-level reasons, keyed by field name. |
errorDetails[].details[] | object | |
errorDetails[].details[].visit_date | string | |
errorCode | integer | Numeric error code. 3001 = some visits are invalid. |
Get a plan
GET /api/app-base/plans/{plan_guid}
Request Parameters
| Parameter | In | Type | Required | Description | Example |
|---|---|---|---|---|---|
plan_guid | path | string | Yes | Plan GUID | 6aa13d2e620f0c4efa094477 |
Response Schema
200 OK
{
"plan_guid": "6aa13d2e620f0c4efa094477",
"Id": "f3d2c4b9-0d46-426d-ac3f-7400eedc94fd",
"Layer": "PLAN",
"Path": "/<internal storage path>",
"ParentPath": "/<internal storage path>",
"CreatedDate": "2026-09-09T11:04:14Z",
"CreatedUser": "apidemo_ss",
"ModifiedDate": "2026-09-09T11:04:14Z",
"ModifiedUser": "apidemo_ss",
"Title": "ZFLCFX_PLAN",
"plan_assignee_username": "apidemo_ss",
"plan_date": "2026-09-10T00:00:59Z",
"plan_name": "Plan for apidemo_ss created on 09/09/2026",
"plan_cancel_status": "Empty",
"plan_is_repeated": false
}
| Field | Type | Description |
|---|---|---|
plan_guid | string | The plan you requested - same value you sent in the path. |
Id | string | Record ID of the plan in the PLAN layer. Not used by the plan endpoints, which address the plan by plan_guid. |
Layer | string | Layer the record lives in, always PLAN. |
Path | string | Internal storage location. Read-only. |
ParentPath | string | Internal storage location of the parent node. Read-only. |
CreatedDate | string | When the plan was created, UTC. |
CreatedUser | string | Account that created the plan. |
ModifiedDate | string | When the plan was last changed, UTC. |
ModifiedUser | string | Account that last changed the plan. |
Title | string | Returned as the internal layer name rather than a per-record label. |
plan_assignee_username | string | The field user this plan belongs to. |
plan_date | string | The day the plan covers. |
plan_name | string | Label generated by the platform from the assignee and the plan date. |
plan_cancel_status | string | Cancellation state of the plan. Empty means it has not been cancelled. |
plan_is_repeated | boolean | Whether this is a repeating plan (plan_repeat/plan_cycle) rather than a single date. |
This call does not return the plan's visits. It returns the plan record only - the visit_guids returned on create are what you have to read each visit's detail.
Search plans
POST /api/app-base/plans/search
Request Parameters
| Parameter | In | Type | Required | Description | Example |
|---|---|---|---|---|---|
skip | body | integer | No | ||
take | body | integer | No | ||
plan_assignee_username | body | string | No | ||
sortBy | body | array | No | ||
sortBy[] | body | object | No | ||
sortBy[].Field | body | string | No | ||
sortBy[].Direction | body | string | No |
Request body
{
"skip": 0,
"take": 20,
"plan_assignee_username": "<username>",
"sortBy": [
{
"Field": "CreatedDate_pdt",
"Direction": "DESC"
}
]
}
skip and take are paging properties:
skip- number of records to skip before the page starts.0is the first page.take- maximum number of records returned in this page.-1returns every match, which is only safe on small datasets.
The response reports total, the number of records matching the query regardless of paging - keep requesting pages until skip + take reaches it.
Response Schema
200 OK
{
"data": [
{
"Id": "f3d2c4b9-0d46-426d-ac3f-7400eedc94fd",
"Layer": "PLAN",
"Path": "/<internal storage path>",
"ParentPath": "/<internal storage path>",
"CreatedDate": "2026-09-09T11:04:14Z",
"CreatedUser": "apidemo_ss",
"ModifiedDate": "2026-09-09T11:04:14Z",
"ModifiedUser": "apidemo_ss",
"Title": "ZFLCFX_PLAN",
"plan_guid": "6aa13d2e620f0c4efa094477",
"plan_assignee_username": "apidemo_ss",
"plan_date": "2026-09-10T00:00:59Z",
"plan_name": "Plan for apidemo_ss created on 09/09/2026",
"plan_cancel_status": "Empty",
"plan_is_repeated": false
}
],
"total": 2
}
| Field | Type | Description |
|---|---|---|
data | array | The page of matching plans. Each entry carries the same fields as GET /plans/{plan_guid} - use plan_guid to address one of them. |
data[] | object | |
data[].Id | string | |
data[].Layer | string | |
data[].Path | string | |
data[].ParentPath | string | |
data[].CreatedDate | string | |
data[].CreatedUser | string | |
data[].ModifiedDate | string | |
data[].ModifiedUser | string | |
data[].Title | string | |
data[].plan_guid | string | |
data[].plan_assignee_username | string | |
data[].plan_date | string | |
data[].plan_name | string | |
data[].plan_cancel_status | string | |
data[].plan_is_repeated | boolean | |
total | integer | Total number of plans matching the query, ignoring skip and take - use it to drive paging. |
Search results do not include visits, the same as GET /plans/{plan_guid} - each entry is the plan record only.
Update a plan
PUT /api/app-base/plans/{plan_guid}
Request Parameters
| Parameter | In | Type | Required | Description | Example |
|---|---|---|---|---|---|
plan_guid | path | string | Yes | Plan GUID | 6aa13d2e620f0c4efa094477 |
plan_name | body | string | No |
Request body
{
"plan_name": "Updated plan name"
}
Delete a plan
DELETE /api/app-base/plans/{plan_guid}
Request Parameters
| Parameter | In | Type | Required | Description | Example |
|---|---|---|---|---|---|
plan_guid | path | string | Yes | Plan GUID |
Response Schema
200 OK
{
"count": 1
}
| Field | Type | Description |
|---|---|---|
count | integer | Number of records actually deleted. 1 = the record was found and removed, 0 = nothing matched the ID. The call returns 200 OK either way, so check this value to confirm the delete. |
Get a visit
GET /api/app-base/visits/{visit_guid}
Request Parameters
| Parameter | In | Type | Required | Description | Example |
|---|---|---|---|---|---|
visit_guid | path | string | Yes | Visit GUID |
Search visits
POST /api/app-base/visits/search
Request Parameters
| Parameter | In | Type | Required | Description | Example |
|---|---|---|---|---|---|
skip | body | integer | No | ||
take | body | integer | No | ||
visit_assignee_username | body | string | No | ||
visit_date | body | object | No | ||
visit_date.between | body | array | No | ||
visit_date.between[] | body | string | No | ||
searchConfig | body | object | No | ||
searchConfig.includeCustomer | body | boolean | No | ||
searchConfig.customerFields | body | array | No | ||
searchConfig.customerFields[] | body | string | No |
Request body
{
"skip": 0,
"take": -1,
"visit_assignee_username": "<username>",
"visit_date": {
"between": [
"2026-07-25T17:00:00.000Z",
"2026-08-01T16:59:59.999Z"
]
},
"searchConfig": {
"includeCustomer": true,
"customerFields": [
"customer_full_name",
"customer_phone_number",
"customer_email",
"customer_address",
"customer_guid"
]
}
}
skip and take are paging properties:
skip- number of records to skip before the page starts.0is the first page.take- maximum number of records returned in this page.-1returns every match, which is only safe on small datasets.
The response reports total, the number of records matching the query regardless of paging - keep requesting pages until skip + take reaches it.
Update a visit
PUT /api/app-base/visits/{visit_guid}
Request Parameters
| Parameter | In | Type | Required | Description | Example |
|---|---|---|---|---|---|
visit_guid | path | string | Yes | Visit GUID |
Delete a visit
DELETE /api/app-base/visits/{visit_guid}
Request Parameters
| Parameter | In | Type | Required | Description | Example |
|---|---|---|---|---|---|
visit_guid | path | string | Yes | Visit GUID |
Response Schema
200 OK
{
"count": 1
}
| Field | Type | Description |
|---|---|---|
count | integer | Number of records actually deleted. 1 = the record was found and removed, 0 = nothing matched the ID. The call returns 200 OK either way, so check this value to confirm the delete. |
Example
curl -X POST '$BASE_URL/api/app-base/plans/create?api-key=$API_KEY' \
-H 'Authorization: Bearer $TOKEN' \
-H 'Content-Type: application/json' \
-d '{"plan_assignee_username": "apidemo_newuser_9", "plan_name": "test plan", "visits": [{"visit_target_guid": "6aa0bb299521e3582ef555f8", "visit_assignee_username": "apidemo_newuser_9", "visit_date": "2026-09-25T16:59:59.999Z", "visit_priority": "3", "visit_estimate_time": "30", "visit_objective": "test", "visit_order": 0}, {"visit_target_guid": "6aa3d2b3d1d2d6ccfde198cd", "visit_assignee_username": "apidemo_newuser_9", "visit_date": "2026-09-25T16:59:59.999Z", "visit_priority": "3", "visit_estimate_time": "30", "visit_objective": "test", "visit_order": 1}], "plan_date": "2026-09-25T16:59:59.999Z", "config": {"timeZoneOffset": -420}}'