Skip to content

Visits API

The Visits API handles the complete visit lifecycle for field force operations, including visit creation, assignment, check-in/check-out, form submissions, and approval workflows.

1. Create Visit (Admin/Auto) → 2. Assign Users → 3. Check-In → 4. Submit Forms → 5. Check-Out → 6. Approve/Reject
EndpointMethodPlatformDescription
/office/v1/visitsPOSTAdminCreate a new visit
/office/v1/visits/:id/assignPOSTAdminAssign users to visit
/app/v1/visits/selfPOSTMobileCreate self-visit
/app/v1/visits/:id/check-inPOSTMobileCheck in to visit
/app/v1/visits/:id/submit-formPOSTMobileSubmit form (legacy)
/app/v1/visits/:id/submit-form-v2POSTMobileSubmit form (v2)
/app/v1/visits/:id/check-outPOSTMobileCheck out from visit
/app/v1/visits/meGETMobileGet my visits
/office/v1/visitsGETAdminList all visits
/office/v1/visits/:idGETBothGet visit details
/app/v1/visits/me/:idGETMobileGet my visit details
/app/v1/visits/activeGETMobileGet active visit
/office/v1/visits/:id/force-closePUTAdminForce close visit
/office/v1/visits/:id/cancelPUTAdminCancel visit

Create a new visit and assign it to outlets and users.

POST /office/v1/visits

Request Body:

{
"outlet_id": 123,
"project_id": 45,
"team_id": 67,
"visit_date": "2024-02-20",
"user_ids": [10, 11, 12],
"submissions": [
{
"form_id": 5,
"is_required": true,
"order": 1
}
]
}

Response:

{
"code": "VISIT_CREATED",
"message": "Visit created successfully",
"data": {
"id": 789,
"outlet": { "id": 123, "name": "Store ABC" },
"project": { "id": 45, "name": "Q1 Campaign" },
"status": "scheduled",
"visit_date": "2024-02-20",
"assignees": [
{ "user_id": 10, "name": "John Doe" }
]
}
}

Add or update user assignments for a visit.

POST /office/v1/visits/:id/assign

Request Body:

{
"user_ids": [10, 11, 12]
}

Field users can create their own visit when at an outlet.

POST /app/v1/visits/self

Request Body:

{
"outlet_id": 123,
"project_id": 45,
"notes": "Unscheduled visit for urgent restocking"
}

Example (React Native):

import { apiClient } from '@core/services';
async function createSelfVisit(outletId, projectId, notes) {
const response = await apiClient.post('/app/v1/visits/self', {
outlet_id: outletId,
project_id: projectId,
notes: notes
});
return response.data;
}

Start a visit by checking in with GPS location and photo.

POST /app/v1/visits/:id/check-in

Request Body:

{
"latitude": -6.2088,
"longitude": 106.8456,
"check_in_photo": "base64_encoded_image_or_url",
"notes": "Arrived at store"
}

Response:

{
"code": "VISIT_CHECKED_IN",
"message": "Check-in successful",
"data": {
"id": 789,
"status": "in_progress",
"checked_in_at": "2024-02-20T09:15:00Z",
"check_in_location": {
"latitude": -6.2088,
"longitude": 106.8456
}
}
}

Example (React Native):

import { apiClient } from '@core/services';
import * as Location from 'expo-location';
async function checkInToVisit(visitId, photo) {
// Get current location
const location = await Location.getCurrentPositionAsync({});
const response = await apiClient.post(`/app/v1/visits/${visitId}/check-in`, {
latitude: location.coords.latitude,
longitude: location.coords.longitude,
check_in_photo: photo,
notes: 'Checked in successfully'
});
return response.data;
}

Complete a visit by checking out with GPS location and photo.

POST /app/v1/visits/:id/check-out

Request Body:

{
"latitude": -6.2088,
"longitude": 106.8456,
"check_out_photo": "base64_encoded_image_or_url",
"notes": "Visit completed"
}

Submit a form with answers during a visit. This is the current recommended version.

POST /app/v1/visits/:id/submit-form-v2

Request Body:

{
"form_id": 5,
"items": [
{
"submission_id": 101,
"status": "completed",
"answers": {
"field_1": "Answer text",
"field_2": ["option_a", "option_b"],
"field_3": 42
}
}
]
}

Example (React Native):

import { apiClient } from '@core/services';
async function submitVisitForm(visitId, formId, answers) {
const response = await apiClient.post(
`/app/v1/visits/${visitId}/submit-form-v2`,
{
form_id: formId,
items: [
{
submission_id: null, // or existing submission ID for updates
status: 'completed',
answers: answers
}
]
}
);
return response.data;
}

Retrieve visits assigned to the current user.

GET /app/v1/visits/me

Query Parameters:

  • status - Filter by status: scheduled, in_progress, completed, cancelled
  • outlet_id - Filter by outlet ID
  • project_id - Filter by project ID
  • date_from - Filter from date (YYYY-MM-DD)
  • date_to - Filter to date (YYYY-MM-DD)
  • page - Page number (default: 1)
  • limit - Items per page (default: 10)

Example:

GET /app/v1/visits/me?status=scheduled&page=1&limit=20

Example (React Native with TanStack Query):

import { useQuery } from '@tanstack/react-query';
import { apiClient } from '@core/services';
function useMyVisits(filters) {
return useQuery({
queryKey: ['visits', 'me', filters],
queryFn: async () => {
const response = await apiClient.get('/app/v1/visits/me', {
params: filters
});
return response.data;
},
networkMode: 'offlineFirst' // Offline-first for mobile
});
}
// Usage in component
function VisitsScreen() {
const { data, isLoading } = useMyVisits({
status: 'scheduled',
page: 1,
limit: 20
});
// Render visits list...
}

Retrieve all visits with comprehensive filtering.

GET /office/v1/visits

Query Parameters:

  • status - Filter by status
  • outlet_id - Filter by outlet ID
  • project_id - Filter by project ID
  • team_id - Filter by team ID
  • user_id - Filter by assigned user ID
  • date_from - Filter from date
  • date_to - Filter to date
  • page - Page number
  • limit - Items per page

Retrieve detailed information about a specific visit.

GET /office/v1/visits/:id
GET /app/v1/visits/me/:id

Response:

{
"code": "SUCCESS",
"message": "Visit retrieved successfully",
"data": {
"id": 789,
"outlet": {
"id": 123,
"name": "Store ABC",
"address": "Jl. Example No. 123"
},
"project": {
"id": 45,
"name": "Q1 Campaign"
},
"status": "completed",
"visit_date": "2024-02-20",
"checked_in_at": "2024-02-20T09:15:00Z",
"checked_out_at": "2024-02-20T11:30:00Z",
"assignees": [
{
"user_id": 10,
"name": "John Doe",
"email": "john@example.com"
}
],
"submissions": [
{
"id": 101,
"form_id": 5,
"title": "Product Audit Form",
"status": "completed",
"approval_status": "approved",
"submitted_at": "2024-02-20T10:00:00Z"
}
],
"logs": [
{
"action": "check_in",
"timestamp": "2024-02-20T09:15:00Z",
"user": "John Doe"
}
]
}
}

Get the currently active (in-progress) visit for the authenticated user.

GET /app/v1/visits/active

Response:

{
"code": "SUCCESS",
"message": "Active visit retrieved",
"data": {
"id": 789,
"outlet": { "id": 123, "name": "Store ABC" },
"status": "in_progress",
"checked_in_at": "2024-02-20T09:15:00Z"
}
}

Example (React Native):

import { useQuery } from '@tanstack/react-query';
import { apiClient } from '@core/services';
function useActiveVisit() {
return useQuery({
queryKey: ['visits', 'active'],
queryFn: async () => {
const response = await apiClient.get('/app/v1/visits/active');
return response.data.data;
},
networkMode: 'offlineFirst',
staleTime: 30000 // 30 seconds
});
}

Retrieve all submissions for a specific visit.

GET /office/v1/visits/:id/submissions
GET /app/v1/visits/me/:id/submissions

Response:

{
"code": "SUCCESS",
"message": "Submissions retrieved",
"data": [
{
"id": 101,
"form_id": 5,
"title": "Product Audit Form",
"status": "completed",
"approval_status": "pending",
"is_required": true,
"submitted_at": "2024-02-20T10:00:00Z",
"answers": {
"field_1": "Answer",
"field_2": ["option_a"]
}
}
]
}

Retrieve detailed information about a specific submission.

GET /office/v1/visits/:visit_id/submissions/:submission_id
GET /app/v1/visits/me/:visit_id/submissions/:submission_id

Approve a pending submission (admin only).

POST /office/v1/visits/:visit_id/submissions/:submission_id/approve

Request Body:

{
"notes": "Looks good, approved"
}

Add an additional form submission to an existing visit.

POST /office/v1/visits/:id/submissions

Request Body:

{
"form_id": 8,
"is_required": false
}

Remove a submission from a visit (if not yet submitted).

DELETE /office/v1/visits/:visit_id/submissions/:submission_id

Administratively close a visit that’s stuck in progress.

PUT /office/v1/visits/:id/force-close

Request Body:

{
"reason": "User forgot to check out, visit confirmed complete"
}

Cancel a scheduled or in-progress visit.

PUT /office/v1/visits/:id/cancel

Request Body:

{
"reason": "Outlet temporarily closed"
}
scheduled → in_progress → completed
↓ ↓
cancelled force_closed
  • scheduled: Visit created but not yet started
  • in_progress: User has checked in
  • completed: User has checked out normally
  • cancelled: Admin cancelled the visit
  • force_closed: Admin force-closed stuck visit
// 1. Get today's scheduled visits
const { data: visits } = useMyVisits({
status: 'scheduled',
date_from: today,
date_to: today
});
// 2. Check in when arriving
await checkInToVisit(visitId, checkInPhoto);
// 3. Submit required forms
await submitVisitForm(visitId, formId, answers);
// 4. Check out when leaving
await checkOutFromVisit(visitId, checkOutPhoto);
// Get in-progress visits for a team
const { data } = useVisitsQuery({
team_id: 5,
status: 'in_progress',
page: 1,
limit: 50
});
// Check submission approval queue
const { data: submissions } = useSubmissionsQuery({
approval_status: 'pending',
team_id: 5
});
CodeDescriptionSolution
VISIT_NOT_FOUNDVisit ID doesn’t existVerify visit ID
VISIT_ALREADY_CHECKED_INAlready checked inCheck visit status
VISIT_NOT_STARTEDCannot check out before check-inCheck in first
SUBMISSION_REQUIREDRequired forms not submittedComplete required forms
INVALID_VISIT_STATUSAction not allowed in current statusCheck visit workflow
LOCATION_REQUIREDGPS location missingEnable location services
  1. Offline Support: Use offlineFirst network mode for mobile queries
  2. Location Accuracy: Request high accuracy for check-in/check-out GPS
  3. Photo Compression: Compress check-in/check-out photos before upload
  4. Form Validation: Validate form answers client-side before submission
  5. Active Visit Check: Always check for active visit before creating new
  6. Status Polling: Poll active visit status every 30 seconds
  7. Submission Caching: Cache submission drafts in MMKV for offline editing

Platform Support: Web (Admin) + Mobile (Field Users)
Authentication: Required (BearerAuth)
RBAC Permissions: visit:create, visit:view, submission:approve